]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
LP1904036 Verify patron credentials
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Actor.pm
1 package OpenILS::Application::Actor;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
5 use Data::Dumper;
6 $Data::Dumper::Indent = 0;
7 use OpenILS::Event;
8
9 use Digest::MD5 qw(md5_hex);
10
11 use OpenSRF::EX qw(:try);
12 use OpenILS::Perm;
13
14 use OpenILS::Application::AppUtils;
15
16 use OpenILS::Utils::Fieldmapper;
17 use OpenILS::Utils::ModsParser;
18 use OpenSRF::Utils::Logger qw/$logger/;
19 use OpenILS::Utils::DateTime qw/:datetime/;
20 use OpenSRF::Utils::SettingsClient;
21
22 use OpenSRF::Utils::Cache;
23
24 use OpenSRF::Utils::JSON;
25 use DateTime;
26 use DateTime::Format::ISO8601;
27 use OpenILS::Const qw/:const/;
28
29 use OpenILS::Application::Actor::Carousel;
30 use OpenILS::Application::Actor::Container;
31 use OpenILS::Application::Actor::ClosedDates;
32 use OpenILS::Application::Actor::UserGroups;
33 use OpenILS::Application::Actor::Friends;
34 use OpenILS::Application::Actor::Stage;
35 use OpenILS::Application::Actor::Settings;
36
37 use OpenILS::Utils::CStoreEditor qw/:funcs/;
38 use OpenILS::Utils::Penalty;
39 use OpenILS::Utils::BadContact;
40 use List::Util qw/max reduce/;
41
42 use UUID::Tiny qw/:std/;
43
44 sub initialize {
45     OpenILS::Application::Actor::Container->initialize();
46     OpenILS::Application::Actor::UserGroups->initialize();
47     OpenILS::Application::Actor::ClosedDates->initialize();
48 }
49
50 my $apputils = "OpenILS::Application::AppUtils";
51 my $U = $apputils;
52
53 sub _d { warn "Patron:\n" . Dumper(shift()); }
54
55 my $cache;
56 my $set_user_settings;
57 my $set_ou_settings;
58
59
60 #__PACKAGE__->register_method(
61 #   method  => "allowed_test",
62 #   api_name    => "open-ils.actor.allowed_test",
63 #);
64 #sub allowed_test {
65 #    my($self, $conn, $auth, $orgid, $permcode) = @_;
66 #    my $e = new_editor(authtoken => $auth);
67 #    return $e->die_event unless $e->checkauth;
68 #
69 #    return {
70 #        orgid => $orgid,
71 #        permcode => $permcode,
72 #        result => $e->allowed($permcode, $orgid)
73 #    };
74 #}
75
76 __PACKAGE__->register_method(
77     method  => "update_user_setting",
78     api_name    => "open-ils.actor.patron.settings.update",
79 );
80 sub update_user_setting {
81     my($self, $conn, $auth, $user_id, $settings) = @_;
82     my $e = new_editor(xact => 1, authtoken => $auth);
83     return $e->die_event unless $e->checkauth;
84
85     $user_id = $e->requestor->id unless defined $user_id;
86
87     unless($e->requestor->id == $user_id) {
88         my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
89         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
90     }
91
92     for my $name (keys %$settings) {
93         my $val = $$settings{$name};
94         my $set = $e->search_actor_user_setting({usr => $user_id, name => $name})->[0];
95
96         if(defined $val) {
97             $val = OpenSRF::Utils::JSON->perl2JSON($val);
98             if($set) {
99                 $set->value($val);
100                 $e->update_actor_user_setting($set) or return $e->die_event;
101             } else {
102                 $set = Fieldmapper::actor::user_setting->new;
103                 $set->usr($user_id);
104                 $set->name($name);
105                 $set->value($val);
106                 $e->create_actor_user_setting($set) or return $e->die_event;
107             }
108         } elsif($set) {
109             $e->delete_actor_user_setting($set) or return $e->die_event;
110         }
111     }
112
113     $e->commit;
114     return 1;
115 }
116
117
118 __PACKAGE__->register_method(
119     method    => "update_privacy_waiver",
120     api_name  => "open-ils.actor.patron.privacy_waiver.update",
121     signature => {
122         desc => "Replaces any existing privacy waiver entries for the patron with the supplied values.",
123         params => [
124             {desc => 'Authentication token', type => 'string'},
125             {desc => 'User ID', type => 'number'},
126             {desc => 'Arrayref of privacy waiver entries', type => 'object'}
127         ],
128         return => {desc => '1 on success, Event on error'}
129     }
130 );
131 sub update_privacy_waiver {
132     my($self, $conn, $auth, $user_id, $waiver) = @_;
133     my $e = new_editor(xact => 1, authtoken => $auth);
134     return $e->die_event unless $e->checkauth;
135
136     $user_id = $e->requestor->id unless defined $user_id;
137
138     unless($e->requestor->id == $user_id) {
139         my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
140         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
141     }
142
143     foreach my $w (@$waiver) {
144         $w->{usr} = $user_id unless $w->{usr};
145         if ($w->{id} && $w->{id} ne 'new') {
146             my $existing_rows = $e->search_actor_usr_privacy_waiver({usr => $user_id, id => $w->{id}});
147             if ($existing_rows) {
148                 my $existing = $existing_rows->[0];
149                 # delete existing if name is empty
150                 if (!$w->{name} or $w->{name} =~ /^\s*$/) {
151                     $e->delete_actor_usr_privacy_waiver($existing) or return $e->die_event;
152
153                 # delete existing if none of the boxes were checked
154                 } elsif (!$w->{place_holds} && !$w->{pickup_holds} && !$w->{checkout_items} && !$w->{view_history}) {
155                     $e->delete_actor_usr_privacy_waiver($existing) or return $e->die_event;
156
157                 # otherwise, update existing waiver entry
158                 } else {
159                     $existing->name($w->{name});
160                     $existing->place_holds($w->{place_holds});
161                     $existing->pickup_holds($w->{pickup_holds});
162                     $existing->checkout_items($w->{checkout_items});
163                     $existing->view_history($w->{view_history});
164                     $e->update_actor_usr_privacy_waiver($existing) or return $e->die_event;
165                 }
166             } else {
167                 $logger->warn("No privacy waiver entry found for user $user_id with ID " . $w->{id});
168             }
169
170         } else {
171             # ignore new entries with empty name or with no boxes checked
172             next if (!$w->{name} or $w->{name} =~ /^\s*$/);
173             next if (!$w->{place_holds} && !$w->{pickup_holds} && !$w->{checkout_items} && !$w->{view_history});
174             my $new = Fieldmapper::actor::usr_privacy_waiver->new;
175             $new->usr($w->{usr});
176             $new->name($w->{name});
177             $new->place_holds($w->{place_holds});
178             $new->pickup_holds($w->{pickup_holds});
179             $new->checkout_items($w->{checkout_items});
180             $new->view_history($w->{view_history});
181             $e->create_actor_usr_privacy_waiver($new) or return $e->die_event;
182         }
183     }
184
185     $e->commit;
186     return 1;
187 }
188
189 __PACKAGE__->register_method(
190     method    => "get_ou_setting_history",
191     api_name  => "open-ils.actor.org_unit.settings.history.retrieve",
192     signature => {
193         desc => "Retrieves the history of an Org Unit Setting.  The permission to retrieve "          .
194                 "an org unit setting's history is dependant on a specific permission specified "       .
195                 "in the view_perm column of the config.org_unit_setting_type " .
196                 "table's row corresponding to the setting being changed." ,
197         params => [
198             {desc => 'Authentication token',        type => 'string'},
199             {desc => 'Org Unit ID',                 type => 'number'},
200             {desc => 'Setting Type Name',           type => 'string'}
201         ],
202         return => {desc => 'History IDL Object'}
203     }
204 );
205
206 sub get_ou_setting_history {
207     my( $self, $client, $auth, $setting, $orgid ) = @_;
208     my $e = new_editor(authtoken => $auth, xact => 1);
209     return $e->die_event unless $e->checkauth;
210
211     return $U->ou_ancestor_setting_log(
212         $orgid, $setting, $e, $auth
213     );
214
215 }
216
217 __PACKAGE__->register_method(
218     method    => "set_ou_settings",
219     api_name  => "open-ils.actor.org_unit.settings.update",
220     signature => {
221         desc => "Updates the value for a given org unit setting.  The permission to update "          .
222                 "an org unit setting is either the UPDATE_ORG_UNIT_SETTING_ALL, or a specific "       .
223                 "permission specified in the update_perm column of the config.org_unit_setting_type " .
224                 "table's row corresponding to the setting being changed." ,
225         params => [
226             {desc => 'Authentication token',             type => 'string'},
227             {desc => 'Org unit ID',                      type => 'number'},
228             {desc => 'Hash of setting name-value pairs', type => 'object'}
229         ],
230         return => {desc => '1 on success, Event on error'}
231     }
232 );
233
234 sub set_ou_settings {
235     my( $self, $client, $auth, $org_id, $settings ) = @_;
236
237     my $e = new_editor(authtoken => $auth, xact => 1);
238     return $e->die_event unless $e->checkauth;
239
240     my $all_allowed = $e->allowed("UPDATE_ORG_UNIT_SETTING_ALL", $org_id);
241
242     for my $name (keys %$settings) {
243         my $val = $$settings{$name};
244
245         my $type = $e->retrieve_config_org_unit_setting_type([
246             $name,
247             {flesh => 1, flesh_fields => {'coust' => ['update_perm']}}
248         ]) or return $e->die_event;
249         my $set = $e->search_actor_org_unit_setting({org_unit => $org_id, name => $name})->[0];
250
251         # If there is no relevant permission, the default assumption will
252         # be, "no, the caller cannot change that value."
253         return $e->die_event unless ($all_allowed ||
254             ($type->update_perm && $e->allowed($type->update_perm->code, $org_id)));
255
256         if(defined $val) {
257             $val = OpenSRF::Utils::JSON->perl2JSON($val);
258             if($set) {
259                 $set->value($val);
260                 $e->update_actor_org_unit_setting($set) or return $e->die_event;
261             } else {
262                 $set = Fieldmapper::actor::org_unit_setting->new;
263                 $set->org_unit($org_id);
264                 $set->name($name);
265                 $set->value($val);
266                 $e->create_actor_org_unit_setting($set) or return $e->die_event;
267             }
268         } elsif($set) {
269             $e->delete_actor_org_unit_setting($set) or return $e->die_event;
270         }
271     }
272
273     $e->commit;
274     return 1;
275 }
276
277 __PACKAGE__->register_method(
278     method    => "fetch_visible_ou_settings_log",
279     api_name  => "open-ils.actor.org_unit.settings.history.visible.retrieve",
280     signature => {
281         desc => "Retrieves the log entries for the specified OU setting. " .
282                 "If the setting has a view permission, the results are limited " .
283                 "to entries at the OUs that the user has the view permission. ",
284         params => [
285             {desc => 'Authentication token', type => 'string'},
286             {desc => 'Setting name',         type => 'string'}
287         ],
288         return => {desc => 'List of fieldmapper objects of the log entries, Event on error'}
289     }
290 );
291
292 sub fetch_visible_ou_settings_log {
293     my( $self, $client, $auth, $setting ) = @_;
294
295     my $e = new_editor(authtoken => $auth);
296     return $e->event unless $e->checkauth;
297     return $e->die_event unless $e->allowed("STAFF_LOGIN");
298     return OpenILS::Event->new('BAD_PARAMS') unless defined($setting);
299
300     my $type = $e->retrieve_config_org_unit_setting_type([
301         $setting,
302         {flesh => 1, flesh_fields => {coust => ['view_perm']}}
303     ]);
304     return OpenILS::Event->new('BAD_PARAMS', note => 'setting type not found')
305         unless $type;
306
307     my $query = { field_name => $setting };
308     if ($type->view_perm) {
309         $query->{org} = $U->user_has_work_perm_at($e, $type->view_perm->code, {descendants => 1});
310         if (scalar @{ $query->{org} } == 0) {
311             # user doesn't have the view permission anywhere, so return nothing
312             return [];
313         }
314     }
315
316     my $results = $e->search_config_org_unit_setting_type_log([$query, {'order_by' => 'date_applied ASC'}])
317         or return $e->die_event;
318     return $results;
319 }
320
321 __PACKAGE__->register_method(
322     method   => "user_settings",
323     authoritative => 1,
324     api_name => "open-ils.actor.patron.settings.retrieve",
325 );
326 sub user_settings {
327     my( $self, $client, $auth, $user_id, $setting ) = @_;
328
329     my $e = new_editor(authtoken => $auth);
330     return $e->event unless $e->checkauth;
331     $user_id = $e->requestor->id unless defined $user_id;
332
333     my $patron = $e->retrieve_actor_user($user_id) or return $e->event;
334     if($e->requestor->id != $user_id) {
335         return $e->event unless $e->allowed('VIEW_USER', $patron->home_ou);
336     }
337
338     sub get_setting {
339         my($e, $user_id, $setting) = @_;
340         my $val = $e->search_actor_user_setting({usr => $user_id, name => $setting})->[0];
341         return undef unless $val; # XXX this should really return undef, but needs testing
342         return OpenSRF::Utils::JSON->JSON2perl($val->value);
343     }
344
345     if($setting) {
346         if(ref $setting eq 'ARRAY') {
347             my %settings;
348             $settings{$_} = get_setting($e, $user_id, $_) for @$setting;
349             return \%settings;
350         } else {
351             return get_setting($e, $user_id, $setting);
352         }
353     } else {
354         my $s = $e->search_actor_user_setting({usr => $user_id});
355         return { map { ( $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) ) } @$s };
356     }
357 }
358
359
360 __PACKAGE__->register_method(
361     method    => "ranged_ou_settings",
362     api_name  => "open-ils.actor.org_unit_setting.values.ranged.retrieve",
363     signature => {
364         desc   => "Retrieves all org unit settings for the given org_id, up to whatever limit " .
365                 "is implied for retrieving OU settings by the authenticated users' permissions.",
366         params => [
367             {desc => 'Authentication token',   type => 'string'},
368             {desc => 'Org unit ID',            type => 'number'},
369         ],
370         return => {desc => 'A hashref of "ranged" settings, event on error'}
371     }
372 );
373 sub ranged_ou_settings {
374     my( $self, $client, $auth, $org_id ) = @_;
375
376     my $e = new_editor(authtoken => $auth);
377     return $e->event unless $e->checkauth;
378
379     my %ranged_settings;
380     my $org_list = $U->get_org_ancestors($org_id);
381     my $settings = $e->search_actor_org_unit_setting({org_unit => $org_list});
382     $org_list = [ reverse @$org_list ];
383
384     # start at the context org and capture the setting value
385     # without clobbering settings we've already captured
386     for my $this_org_id (@$org_list) {
387
388         my @sets = grep { $_->org_unit == $this_org_id } @$settings;
389
390         for my $set (@sets) {
391             my $type = $e->retrieve_config_org_unit_setting_type([
392                 $set->name,
393                 {flesh => 1, flesh_fields => {coust => ['view_perm']}}
394             ]);
395
396             # If there is no relevant permission, the default assumption will
397             # be, "yes, the caller can have that value."
398             if ($type && $type->view_perm) {
399                 next if not $e->allowed($type->view_perm->code, $org_id);
400             }
401
402             $ranged_settings{$set->name} = OpenSRF::Utils::JSON->JSON2perl($set->value)
403                 unless defined $ranged_settings{$set->name};
404         }
405     }
406
407     return \%ranged_settings;
408 }
409
410
411
412 __PACKAGE__->register_method(
413     api_name  => 'open-ils.actor.ou_setting.ancestor_default',
414     method    => 'ou_ancestor_setting',
415     signature => {
416         desc => 'Get the org unit setting value associated with the setting name as seen from the specified org unit.  ' .
417                 'This method will make sure that the given user has permission to view that setting, if there is a '     .
418                 'permission associated with the setting.  If a permission is required and no authtoken is given, or '     .
419                 'the user lacks the permisssion, undef will be returned.'       ,
420         params => [
421             { desc => 'Org unit ID',          type => 'number' },
422             { desc => 'setting name',         type => 'string' },
423             { desc => 'authtoken (optional)', type => 'string' }
424         ],
425         return => {desc => 'A value for the org unit setting, or undef'}
426     }
427 );
428
429 # ------------------------------------------------------------------
430 # Attempts to find the org setting value for a given org.  if not
431 # found at the requested org, searches up the org tree until it
432 # finds a parent that has the requested setting.
433 # when found, returns { org => $id, value => $value }
434 # otherwise, returns NULL
435 # ------------------------------------------------------------------
436 sub ou_ancestor_setting {
437     my( $self, $client, $orgid, $name, $auth ) = @_;
438     # Make sure $auth is set to something if not given.
439     $auth ||= -1;
440     return $U->ou_ancestor_setting($orgid, $name, undef, $auth);
441 }
442
443 __PACKAGE__->register_method(
444     api_name  => 'open-ils.actor.ou_setting.ancestor_default.batch',
445     method    => 'ou_ancestor_setting_batch',
446     signature => {
447         desc => 'Get org unit setting name => value pairs for a list of names, as seen from the specified org unit.  ' .
448                 'This method will make sure that the given user has permission to view that setting, if there is a '     .
449                 'permission associated with the setting.  If a permission is required and no authtoken is given, or '     .
450                 'the user lacks the permisssion, undef will be returned.'       ,
451         params => [
452             { desc => 'Org unit ID',          type => 'number' },
453             { desc => 'setting name list',    type => 'array'  },
454             { desc => 'authtoken (optional)', type => 'string' }
455         ],
456         return => {desc => 'A hash with name => value pairs for the org unit settings'}
457     }
458 );
459 sub ou_ancestor_setting_batch {
460     my( $self, $client, $orgid, $name_list, $auth ) = @_;
461
462     # splitting the list of settings to fetch values
463     # so that ones that *don't* require view_perm checks
464     # can be fetched in one fell swoop, which is
465     # significantly faster in cases where a large
466     # number of settings need to be fetched.
467     my %perm_check_required = ();
468     my @perm_check_not_required = ();
469
470     # Note that ->ou_ancestor_setting also can check
471     # to see if the setting has a view_perm, but testing
472     # suggests that the redundant checks do not significantly
473     # increase the time it takes to fetch the values of
474     # permission-controlled settings.
475     my $e = new_editor();
476     my $res = $e->search_config_org_unit_setting_type({
477         name      => $name_list,
478         view_perm => { "!=" => undef },
479     });
480     %perm_check_required = map { $_->name() => 1 } @$res;
481     foreach my $setting (@$name_list) {
482         push @perm_check_not_required, $setting
483             unless exists($perm_check_required{$setting});
484     }
485
486     my %values;
487     if (@perm_check_not_required) {
488         %values = $U->ou_ancestor_setting_batch_insecure($orgid, \@perm_check_not_required);
489     }
490     $values{$_} = $U->ou_ancestor_setting(
491         $orgid, $_, undef,
492         ($auth ? $auth : -1)
493     ) for keys(%perm_check_required);
494     return \%values;
495 }
496
497
498
499 __PACKAGE__->register_method(
500     method   => "update_patron",
501     api_name => "open-ils.actor.patron.update",
502     signature => {
503         desc   => q/
504             Update an existing user, or create a new one.  Related objects,
505             like cards, addresses, survey responses, and stat cats,
506             can be updated by attaching them to the user object in their
507             respective fields.  For examples, the billing address object
508             may be inserted into the 'billing_address' field, etc.  For each
509             attached object, indicate if the object should be created,
510             updated, or deleted using the built-in 'isnew', 'ischanged',
511             and 'isdeleted' fields on the object.
512         /,
513         params => [
514             { desc => 'Authentication token', type => 'string' },
515             { desc => 'Patron data object',   type => 'object' }
516         ],
517         return => {desc => 'A fleshed user object, event on error'}
518     }
519 );
520
521 sub update_patron {
522     my( $self, $client, $auth, $patron ) = @_;
523
524     my $e = new_editor(xact => 1, authtoken => $auth);
525     return $e->event unless $e->checkauth;
526
527     $logger->info($patron->isnew ? "Creating new patron..." :
528         "Updating Patron: " . $patron->id);
529
530     my $evt = check_group_perm($e, $e->requestor, $patron);
531     return $evt if $evt;
532
533     # $new_patron is the patron in progress.  $patron is the original patron
534     # passed in with the method.  new_patron will change as the components
535     # of patron are added/updated.
536
537     my $new_patron;
538
539     # unflesh the real items on the patron
540     $patron->card( $patron->card->id ) if(ref($patron->card));
541     $patron->billing_address( $patron->billing_address->id )
542         if(ref($patron->billing_address));
543     $patron->mailing_address( $patron->mailing_address->id )
544         if(ref($patron->mailing_address));
545
546     # create/update the patron first so we can use his id
547
548     # $patron is the obj from the client (new data) and $new_patron is the
549     # patron object properly built for db insertion, so we need a third variable
550     # if we want to represent the old patron.
551
552     my $old_patron;
553     my $barred_hook = '';
554     my $renew_hook = '';
555
556     if($patron->isnew()) {
557         ( $new_patron, $evt ) = _add_patron($e, _clone_patron($patron));
558         return $evt if $evt;
559         if($U->is_true($patron->barred)) {
560             return $e->die_event unless
561                 $e->allowed('BAR_PATRON', $patron->home_ou);
562         }
563         if(($patron->photo_url)) {
564             return $e->die_event unless
565                 $e->allowed('UPDATE_USER_PHOTO_URL', $patron->home_ou);
566         }
567     } else {
568         $new_patron = $patron;
569
570         # Did auth checking above already.
571         $old_patron = $e->retrieve_actor_user($patron->id) or
572             return $e->die_event;
573
574         $renew_hook = 'au.renewed' if ($old_patron->expire_date ne $new_patron->expire_date);
575
576         if($U->is_true($old_patron->barred) != $U->is_true($new_patron->barred)) {
577             my $perm = $U->is_true($old_patron->barred) ? 'UNBAR_PATRON' : 'BAR_PATRON';
578             return $e->die_event unless $e->allowed($perm, $patron->home_ou);
579
580             $barred_hook = $U->is_true($new_patron->barred) ?
581                 'au.barred' : 'au.unbarred';
582         }
583
584         if($old_patron->photo_url ne $new_patron->photo_url) {
585             my $perm = 'UPDATE_USER_PHOTO_URL';
586             return $e->die_event unless $e->allowed($perm, $patron->home_ou);
587         }
588
589         # update the password by itself to avoid the password protection magic
590         if ($patron->passwd && $patron->passwd ne $old_patron->passwd) {
591             modify_migrated_user_password($e, $patron->id, $patron->passwd);
592             $new_patron->passwd(''); # subsequent update will set
593                                      # actor.usr.passwd to MD5('')
594         }
595     }
596
597     ( $new_patron, $evt ) = _add_update_addresses($e, $patron, $new_patron);
598     return $evt if $evt;
599
600     ( $new_patron, $evt ) = _add_update_cards($e, $patron, $new_patron);
601     return $evt if $evt;
602
603     ( $new_patron, $evt ) = _add_update_waiver_entries($e, $patron, $new_patron);
604     return $evt if $evt;
605
606     ( $new_patron, $evt ) = _add_survey_responses($e, $patron, $new_patron);
607     return $evt if $evt;
608
609     # re-update the patron if anything has happened to him during this process
610     if($new_patron->ischanged()) {
611         ( $new_patron, $evt ) = _update_patron($e, $new_patron);
612         return $evt if $evt;
613     }
614
615     ( $new_patron, $evt ) = _clear_badcontact_penalties($e, $old_patron, $new_patron);
616     return $evt if $evt;
617
618     ($new_patron, $evt) = _create_stat_maps($e, $patron, $new_patron);
619     return $evt if $evt;
620
621     ($new_patron, $evt) = _create_perm_maps($e, $patron, $new_patron);
622     return $evt if $evt;
623
624     $evt = apply_invalid_addr_penalty($e, $patron);
625     return $evt if $evt;
626
627     $e->commit;
628
629     my $tses = OpenSRF::AppSession->create('open-ils.trigger');
630     if($patron->isnew) {
631         $tses->request('open-ils.trigger.event.autocreate',
632             'au.created', $new_patron, $new_patron->home_ou);
633     } else {
634         $tses->request('open-ils.trigger.event.autocreate',
635             'au.updated', $new_patron, $new_patron->home_ou);
636
637         $tses->request('open-ils.trigger.event.autocreate', $renew_hook,
638             $new_patron, $new_patron->home_ou) if $renew_hook;
639
640         $tses->request('open-ils.trigger.event.autocreate', $barred_hook,
641             $new_patron, $new_patron->home_ou) if $barred_hook;
642     }
643
644     $e->xact_begin; # $e->rollback is called in new_flesh_user
645     return flesh_user($new_patron->id(), $e);
646 }
647
648 sub apply_invalid_addr_penalty {
649     my $e = shift;
650     my $patron = shift;
651
652     # grab the invalid address penalty if set
653     my $penalties = OpenILS::Utils::Penalty->retrieve_usr_penalties($e, $patron->id, $patron->home_ou);
654
655     my ($addr_penalty) = grep
656         { $_->standing_penalty->name eq 'INVALID_PATRON_ADDRESS' } @$penalties;
657
658     # do we enforce invalid address penalty
659     my $enforce = $U->ou_ancestor_setting_value(
660         $patron->home_ou, 'circ.patron_invalid_address_apply_penalty') || 0;
661
662     my $addrs = $e->search_actor_user_address(
663         {usr => $patron->id, valid => 'f', id => {'>' => 0}}, {idlist => 1});
664     my $addr_count = scalar(@$addrs);
665
666     if($addr_count == 0 and $addr_penalty) {
667
668         # regardless of any settings, remove the penalty when the user has no invalid addresses
669         $e->delete_actor_user_standing_penalty($addr_penalty) or return $e->die_event;
670         $e->commit;
671
672     } elsif($enforce and $addr_count > 0 and !$addr_penalty) {
673
674         my $ptype = $e->retrieve_config_standing_penalty(29) or return $e->die_event;
675         my $depth = $ptype->org_depth;
676         my $ctx_org = $U->org_unit_ancestor_at_depth($patron->home_ou, $depth) if defined $depth;
677         $ctx_org = $patron->home_ou unless defined $ctx_org;
678
679         my $penalty = Fieldmapper::actor::user_standing_penalty->new;
680         $penalty->usr($patron->id);
681         $penalty->org_unit($ctx_org);
682         $penalty->standing_penalty(OILS_PENALTY_INVALID_PATRON_ADDRESS);
683
684         $e->create_actor_user_standing_penalty($penalty) or return $e->die_event;
685     }
686
687     return undef;
688 }
689
690
691 sub flesh_user {
692     my $id = shift;
693     my $e = shift;
694     my $home_ou = shift;
695
696     my $fields = [
697         "cards",
698         "card",
699         "standing_penalties",
700         "settings",
701         "addresses",
702         "billing_address",
703         "mailing_address",
704         "stat_cat_entries",
705         "waiver_entries",
706         "settings",
707         "usr_activity"
708     ];
709     push @$fields, "home_ou" if $home_ou;
710     return new_flesh_user($id, $fields, $e );
711 }
712
713
714
715
716
717
718 # clone and clear stuff that would break the database
719 sub _clone_patron {
720     my $patron = shift;
721
722     my $new_patron = $patron->clone;
723     # clear these
724     $new_patron->clear_billing_address();
725     $new_patron->clear_mailing_address();
726     $new_patron->clear_addresses();
727     $new_patron->clear_card();
728     $new_patron->clear_cards();
729     $new_patron->clear_id();
730     $new_patron->clear_isnew();
731     $new_patron->clear_ischanged();
732     $new_patron->clear_isdeleted();
733     $new_patron->clear_stat_cat_entries();
734     $new_patron->clear_waiver_entries();
735     $new_patron->clear_permissions();
736     $new_patron->clear_standing_penalties();
737
738     return $new_patron;
739 }
740
741
742 sub _add_patron {
743
744     my $e          = shift;
745     my $patron      = shift;
746
747     return (undef, $e->die_event) unless
748         $e->allowed('CREATE_USER', $patron->home_ou);
749
750     my $ex = $e->search_actor_user(
751         {usrname => $patron->usrname}, {idlist => 1});
752     return (undef, OpenILS::Event->new('USERNAME_EXISTS')) if @$ex;
753
754     $logger->info("Creating new user in the DB with username: ".$patron->usrname());
755
756     # do a dance to get the password hashed securely
757     my $saved_password = $patron->passwd;
758     $patron->passwd('');
759     $e->create_actor_user($patron) or return (undef, $e->die_event);
760     modify_migrated_user_password($e, $patron->id, $saved_password);
761
762     my $id = $patron->id; # added by CStoreEditor
763
764     $logger->info("Successfully created new user [$id] in DB");
765     return ($e->retrieve_actor_user($id), undef);
766 }
767
768
769 sub check_group_perm {
770     my( $e, $requestor, $patron ) = @_;
771     my $evt;
772
773     # first let's see if the requestor has
774     # priveleges to update this user in any way
775     if( ! $patron->isnew ) {
776         my $p = $e->retrieve_actor_user($patron->id);
777
778         # If we are the requestor (trying to update our own account)
779         # and we are not trying to change our profile, we're good
780         if( $p->id == $requestor->id and
781                 $p->profile == $patron->profile ) {
782             return undef;
783         }
784
785
786         $evt = group_perm_failed($e, $requestor, $p);
787         return $evt if $evt;
788     }
789
790     # They are allowed to edit this patron.. can they put the
791     # patron into the group requested?
792     $evt = group_perm_failed($e, $requestor, $patron);
793     return $evt if $evt;
794     return undef;
795 }
796
797
798 sub group_perm_failed {
799     my( $e, $requestor, $patron ) = @_;
800
801     my $perm;
802     my $grp;
803     my $grpid = $patron->profile;
804
805     do {
806
807         $logger->debug("user update looking for group perm for group $grpid");
808         $grp = $e->retrieve_permission_grp_tree($grpid);
809
810     } while( !($perm = $grp->application_perm) and ($grpid = $grp->parent) );
811
812     $logger->info("user update checking perm $perm on user ".
813         $requestor->id." for update/create on user username=".$patron->usrname);
814
815     return $e->allowed($perm, $patron->home_ou) ? undef : $e->die_event;
816 }
817
818
819
820 sub _update_patron {
821     my( $e, $patron, $noperm) = @_;
822
823     $logger->info("Updating patron ".$patron->id." in DB");
824
825     my $evt;
826
827     if(!$noperm) {
828         return (undef, $e->die_event)
829             unless $e->allowed('UPDATE_USER', $patron->home_ou);
830     }
831
832     if(!$patron->ident_type) {
833         $patron->clear_ident_type;
834         $patron->clear_ident_value;
835     }
836
837     $evt = verify_last_xact($e, $patron);
838     return (undef, $evt) if $evt;
839
840     $e->update_actor_user($patron) or return (undef, $e->die_event);
841
842     # re-fetch the user to pick up the latest last_xact_id value
843     # to avoid collisions.
844     $patron = $e->retrieve_actor_user($patron->id);
845
846     return ($patron);
847 }
848
849 sub verify_last_xact {
850     my( $e, $patron ) = @_;
851     return undef unless $patron->id and $patron->id > 0;
852     my $p = $e->retrieve_actor_user($patron->id);
853     my $xact = $p->last_xact_id;
854     return undef unless $xact;
855     $logger->info("user xact = $xact, saving with xact " . $patron->last_xact_id);
856     return OpenILS::Event->new('XACT_COLLISION')
857         if $xact ne $patron->last_xact_id;
858     return undef;
859 }
860
861
862 sub _check_dup_ident {
863     my( $session, $patron ) = @_;
864
865     return undef unless $patron->ident_value;
866
867     my $search = {
868         ident_type  => $patron->ident_type,
869         ident_value => $patron->ident_value,
870     };
871
872     $logger->debug("patron update searching for dup ident values: " .
873         $patron->ident_type . ':' . $patron->ident_value);
874
875     $search->{id} = {'!=' => $patron->id} if $patron->id and $patron->id > 0;
876
877     my $dups = $session->request(
878         'open-ils.storage.direct.actor.user.search_where.atomic', $search )->gather(1);
879
880
881     return OpenILS::Event->new('PATRON_DUP_IDENT1', payload => $patron )
882         if $dups and @$dups;
883
884     return undef;
885 }
886
887
888 sub _add_update_addresses {
889
890     my $e = shift;
891     my $patron = shift;
892     my $new_patron = shift;
893
894     my $evt;
895
896     my $current_id; # id of the address before creation
897
898     my $addresses = $patron->addresses();
899
900     for my $address (@$addresses) {
901
902         next unless ref $address;
903         $current_id = $address->id();
904
905         if( $patron->billing_address() and
906             $patron->billing_address() == $current_id ) {
907             $logger->info("setting billing addr to $current_id");
908             $new_patron->billing_address($address->id());
909             $new_patron->ischanged(1);
910         }
911
912         if( $patron->mailing_address() and
913             $patron->mailing_address() == $current_id ) {
914             $new_patron->mailing_address($address->id());
915             $logger->info("setting mailing addr to $current_id");
916             $new_patron->ischanged(1);
917         }
918
919
920         if($address->isnew()) {
921
922             $address->usr($new_patron->id());
923
924             ($address, $evt) = _add_address($e,$address);
925             return (undef, $evt) if $evt;
926
927             # we need to get the new id
928             if( $patron->billing_address() and
929                     $patron->billing_address() == $current_id ) {
930                 $new_patron->billing_address($address->id());
931                 $logger->info("setting billing addr to $current_id");
932                 $new_patron->ischanged(1);
933             }
934
935             if( $patron->mailing_address() and
936                     $patron->mailing_address() == $current_id ) {
937                 $new_patron->mailing_address($address->id());
938                 $logger->info("setting mailing addr to $current_id");
939                 $new_patron->ischanged(1);
940             }
941
942         } elsif($address->ischanged() ) {
943
944             ($address, $evt) = _update_address($e, $address);
945             return (undef, $evt) if $evt;
946
947         } elsif($address->isdeleted() ) {
948
949             if( $address->id() == $new_patron->mailing_address() ) {
950                 $new_patron->clear_mailing_address();
951                 ($new_patron, $evt) = _update_patron($e, $new_patron);
952                 return (undef, $evt) if $evt;
953             }
954
955             if( $address->id() == $new_patron->billing_address() ) {
956                 $new_patron->clear_billing_address();
957                 ($new_patron, $evt) = _update_patron($e, $new_patron);
958                 return (undef, $evt) if $evt;
959             }
960
961             $evt = _delete_address($e, $address);
962             return (undef, $evt) if $evt;
963         }
964     }
965
966     return ( $new_patron, undef );
967 }
968
969
970 # adds an address to the db and returns the address with new id
971 sub _add_address {
972     my($e, $address) = @_;
973     $address->clear_id();
974
975     $logger->info("Creating new address at street ".$address->street1);
976
977     # put the address into the database
978     $e->create_actor_user_address($address) or return (undef, $e->die_event);
979     return ($address, undef);
980 }
981
982
983 sub _update_address {
984     my( $e, $address ) = @_;
985
986     $logger->info("Updating address ".$address->id." in the DB");
987
988     $e->update_actor_user_address($address) or return (undef, $e->die_event);
989
990     return ($address, undef);
991 }
992
993
994
995 sub _add_update_cards {
996
997     my $e = shift;
998     my $patron = shift;
999     my $new_patron = shift;
1000
1001     my $evt;
1002
1003     my $virtual_id; #id of the card before creation
1004
1005     my $card_changed = 0;
1006     my $cards = $patron->cards();
1007     for my $card (@$cards) {
1008
1009         $card->usr($new_patron->id());
1010
1011         if(ref($card) and $card->isnew()) {
1012
1013             $virtual_id = $card->id();
1014             ( $card, $evt ) = _add_card($e, $card);
1015             return (undef, $evt) if $evt;
1016
1017             #if(ref($patron->card)) { $patron->card($patron->card->id); }
1018             if($patron->card() == $virtual_id) {
1019                 $new_patron->card($card->id());
1020                 $new_patron->ischanged(1);
1021             }
1022             $card_changed++;
1023
1024         } elsif( ref($card) and $card->ischanged() ) {
1025             $evt = _update_card($e, $card);
1026             return (undef, $evt) if $evt;
1027             $card_changed++;
1028         }
1029     }
1030
1031     $U->create_events_for_hook('au.barcode_changed', $new_patron, $e->requestor->ws_ou)
1032         if $card_changed;
1033
1034     return ( $new_patron, undef );
1035 }
1036
1037
1038 # adds an card to the db and returns the card with new id
1039 sub _add_card {
1040     my( $e, $card ) = @_;
1041     $card->clear_id();
1042
1043     $logger->info("Adding new patron card ".$card->barcode);
1044
1045     $e->create_actor_card($card) or return (undef, $e->die_event);
1046
1047     return ( $card, undef );
1048 }
1049
1050
1051 # returns event on error.  returns undef otherwise
1052 sub _update_card {
1053     my( $e, $card ) = @_;
1054     $logger->info("Updating patron card ".$card->id);
1055
1056     $e->update_actor_card($card) or return $e->die_event;
1057     return undef;
1058 }
1059
1060
1061 sub _add_update_waiver_entries {
1062     my $e = shift;
1063     my $patron = shift;
1064     my $new_patron = shift;
1065     my $evt;
1066
1067     my $waiver_entries = $patron->waiver_entries();
1068     for my $waiver (@$waiver_entries) {
1069         next unless ref $waiver;
1070         $waiver->usr($new_patron->id());
1071         if ($waiver->isnew()) {
1072             next if (!$waiver->name() or $waiver->name() =~ /^\s*$/);
1073             next if (!$waiver->place_holds() && !$waiver->pickup_holds() && !$waiver->checkout_items() && !$waiver->view_history());
1074             $logger->info("Adding new patron waiver entry");
1075             $waiver->clear_id();
1076             $e->create_actor_usr_privacy_waiver($waiver) or return (undef, $e->die_event);
1077         } elsif ($waiver->ischanged()) {
1078             $logger->info("Updating patron waiver entry " . $waiver->id);
1079             $e->update_actor_usr_privacy_waiver($waiver) or return (undef, $e->die_event);
1080         } elsif ($waiver->isdeleted()) {
1081             $logger->info("Deleting patron waiver entry " . $waiver->id);
1082             $e->delete_actor_usr_privacy_waiver($waiver) or return (undef, $e->die_event);
1083         }
1084     }
1085     return ($new_patron, undef);
1086 }
1087
1088
1089 # returns event on error.  returns undef otherwise
1090 sub _delete_address {
1091     my( $e, $address ) = @_;
1092
1093     $logger->info("Deleting address ".$address->id." from DB");
1094
1095     $e->delete_actor_user_address($address) or return $e->die_event;
1096     return undef;
1097 }
1098
1099
1100
1101 sub _add_survey_responses {
1102     my ($e, $patron, $new_patron) = @_;
1103
1104     $logger->info( "Updating survey responses for patron ".$new_patron->id );
1105
1106     my $responses = $patron->survey_responses;
1107
1108     if($responses) {
1109
1110         $_->usr($new_patron->id) for (@$responses);
1111
1112         my $evt = $U->simplereq( "open-ils.circ",
1113             "open-ils.circ.survey.submit.user_id", $responses );
1114
1115         return (undef, $evt) if defined($U->event_code($evt));
1116
1117     }
1118
1119     return ( $new_patron, undef );
1120 }
1121
1122 sub _clear_badcontact_penalties {
1123     my ($e, $old_patron, $new_patron) = @_;
1124
1125     return ($new_patron, undef) unless $old_patron;
1126
1127     my $PNM = $OpenILS::Utils::BadContact::PENALTY_NAME_MAP;
1128
1129     # This ignores whether the caller of update_patron has any permission
1130     # to remove penalties, but these penalties no longer make sense
1131     # if an email address field (for example) is changed (and the caller must
1132     # have perms to do *that*) so there's no reason not to clear the penalties.
1133
1134     my $bad_contact_penalties = $e->search_actor_user_standing_penalty([
1135         {
1136             "+csp" => {"name" => [values(%$PNM)]},
1137             "+ausp" => {"stop_date" => undef, "usr" => $new_patron->id}
1138         }, {
1139             "join" => {"csp" => {}},
1140             "flesh" => 1,
1141             "flesh_fields" => {"ausp" => ["standing_penalty"]}
1142         }
1143     ]) or return (undef, $e->die_event);
1144
1145     return ($new_patron, undef) unless @$bad_contact_penalties;
1146
1147     my @penalties_to_clear;
1148     my ($field, $penalty_name);
1149
1150     # For each field that might have an associated bad contact penalty,
1151     # check for such penalties and add them to the to-clear list if that
1152     # field has changed.
1153     while (($field, $penalty_name) = each(%$PNM)) {
1154         if ($old_patron->$field ne $new_patron->$field) {
1155             push @penalties_to_clear, grep {
1156                 $_->standing_penalty->name eq $penalty_name
1157             } @$bad_contact_penalties;
1158         }
1159     }
1160
1161     foreach (@penalties_to_clear) {
1162         # Note that this "archives" penalties, in the terminology of the staff
1163         # client, instead of just deleting them.  This may assist reporting,
1164         # or preserving old contact information when it is still potentially
1165         # of interest.
1166         $_->standing_penalty($_->standing_penalty->id); # deflesh
1167         $_->stop_date('now');
1168         $e->update_actor_user_standing_penalty($_) or return (undef, $e->die_event);
1169     }
1170
1171     return ($new_patron, undef);
1172 }
1173
1174
1175 sub _create_stat_maps {
1176
1177     my($e, $patron, $new_patron) = @_;
1178
1179     my $maps = $patron->stat_cat_entries();
1180
1181     for my $map (@$maps) {
1182
1183         my $method = "update_actor_stat_cat_entry_user_map";
1184
1185         if ($map->isdeleted()) {
1186             $method = "delete_actor_stat_cat_entry_user_map";
1187
1188         } elsif ($map->isnew()) {
1189             $method = "create_actor_stat_cat_entry_user_map";
1190             $map->clear_id;
1191         }
1192
1193
1194         $map->target_usr($new_patron->id);
1195
1196         $logger->info("Updating stat entry with method $method and map $map");
1197
1198         $e->$method($map) or return (undef, $e->die_event);
1199     }
1200
1201     return ($new_patron, undef);
1202 }
1203
1204 sub _create_perm_maps {
1205
1206     my($e, $patron, $new_patron) = @_;
1207
1208     my $maps = $patron->permissions;
1209
1210     for my $map (@$maps) {
1211
1212         my $method = "update_permission_usr_perm_map";
1213         if ($map->isdeleted()) {
1214             $method = "delete_permission_usr_perm_map";
1215         } elsif ($map->isnew()) {
1216             $method = "create_permission_usr_perm_map";
1217             $map->clear_id;
1218         }
1219
1220         $map->usr($new_patron->id);
1221
1222         $logger->info( "Updating permissions with method $method and map $map" );
1223
1224         $e->$method($map) or return (undef, $e->die_event);
1225     }
1226
1227     return ($new_patron, undef);
1228 }
1229
1230
1231 __PACKAGE__->register_method(
1232     method   => "set_user_work_ous",
1233     api_name => "open-ils.actor.user.work_ous.update",
1234 );
1235
1236 sub set_user_work_ous {
1237     my $self   = shift;
1238     my $client = shift;
1239     my $ses    = shift;
1240     my $maps   = shift;
1241
1242     my( $requestor, $evt ) = $apputils->checksesperm( $ses, 'ASSIGN_WORK_ORG_UNIT' );
1243     return $evt if $evt;
1244
1245     my $session = $apputils->start_db_session();
1246     $apputils->set_audit_info($session, $ses, $requestor->id, $requestor->wsid);
1247
1248     for my $map (@$maps) {
1249
1250         my $method = "open-ils.storage.direct.permission.usr_work_ou_map.update";
1251         if ($map->isdeleted()) {
1252             $method = "open-ils.storage.direct.permission.usr_work_ou_map.delete";
1253         } elsif ($map->isnew()) {
1254             $method = "open-ils.storage.direct.permission.usr_work_ou_map.create";
1255             $map->clear_id;
1256         }
1257
1258         #warn( "Updating permissions with method $method and session $ses and map $map" );
1259         $logger->info( "Updating work_ou map with method $method and map $map" );
1260
1261         my $stat = $session->request($method, $map)->gather(1);
1262         $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
1263
1264     }
1265
1266     $apputils->commit_db_session($session);
1267
1268     return scalar(@$maps);
1269 }
1270
1271
1272 __PACKAGE__->register_method(
1273     method   => "set_user_perms",
1274     api_name => "open-ils.actor.user.permissions.update",
1275 );
1276
1277 sub set_user_perms {
1278     my $self = shift;
1279     my $client = shift;
1280     my $ses = shift;
1281     my $maps = shift;
1282
1283     my $session = $apputils->start_db_session();
1284
1285     my( $user_obj, $evt ) = $U->checkses($ses);
1286     return $evt if $evt;
1287     $apputils->set_audit_info($session, $ses, $user_obj->id, $user_obj->wsid);
1288
1289     my $perms = $session->request('open-ils.storage.permission.user_perms.atomic', $user_obj->id)->gather(1);
1290
1291     my $all = undef;
1292     $all = 1 if ($U->is_true($user_obj->super_user()));
1293     $all = 1 unless ($U->check_perms($user_obj->id, $user_obj->home_ou, 'EVERYTHING'));
1294
1295     for my $map (@$maps) {
1296
1297         my $method = "open-ils.storage.direct.permission.usr_perm_map.update";
1298         if ($map->isdeleted()) {
1299             $method = "open-ils.storage.direct.permission.usr_perm_map.delete";
1300         } elsif ($map->isnew()) {
1301             $method = "open-ils.storage.direct.permission.usr_perm_map.create";
1302             $map->clear_id;
1303         }
1304
1305         next if (!$all and !grep { $_->perm eq $map->perm and $U->is_true($_->grantable) and $_->depth <= $map->depth } @$perms);
1306         #warn( "Updating permissions with method $method and session $ses and map $map" );
1307         $logger->info( "Updating permissions with method $method and map $map" );
1308
1309         my $stat = $session->request($method, $map)->gather(1);
1310         $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
1311
1312     }
1313
1314     $apputils->commit_db_session($session);
1315
1316     return scalar(@$maps);
1317 }
1318
1319
1320 __PACKAGE__->register_method(
1321     method  => "user_retrieve_by_barcode",
1322     authoritative => 1,
1323     api_name    => "open-ils.actor.user.fleshed.retrieve_by_barcode",);
1324
1325 sub user_retrieve_by_barcode {
1326     my($self, $client, $auth, $barcode, $flesh_home_ou) = @_;
1327
1328     my $e = new_editor(authtoken => $auth);
1329     return $e->event unless $e->checkauth;
1330
1331     my $card = $e->search_actor_card({barcode => $barcode})->[0]
1332         or return $e->event;
1333
1334     my $user = flesh_user($card->usr, $e, $flesh_home_ou);
1335     return $e->event unless $e->allowed(
1336         "VIEW_USER", $flesh_home_ou ? $user->home_ou->id : $user->home_ou
1337     );
1338     return $user;
1339 }
1340
1341
1342
1343 __PACKAGE__->register_method(
1344     method        => "get_user_by_id",
1345     authoritative => 1,
1346     api_name      => "open-ils.actor.user.retrieve",
1347 );
1348
1349 sub get_user_by_id {
1350     my ($self, $client, $auth, $id) = @_;
1351     my $e = new_editor(authtoken=>$auth);
1352     return $e->event unless $e->checkauth;
1353     my $user = $e->retrieve_actor_user($id) or return $e->event;
1354     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
1355     return $user;
1356 }
1357
1358
1359 __PACKAGE__->register_method(
1360     method   => "get_org_types",
1361     api_name => "open-ils.actor.org_types.retrieve",
1362 );
1363 sub get_org_types {
1364     return $U->get_org_types();
1365 }
1366
1367
1368 __PACKAGE__->register_method(
1369     method   => "get_user_ident_types",
1370     api_name => "open-ils.actor.user.ident_types.retrieve",
1371 );
1372 my $ident_types;
1373 sub get_user_ident_types {
1374     return $ident_types if $ident_types;
1375     return $ident_types =
1376         new_editor()->retrieve_all_config_identification_type();
1377 }
1378
1379
1380 __PACKAGE__->register_method(
1381     method   => "get_org_unit",
1382     api_name => "open-ils.actor.org_unit.retrieve",
1383 );
1384
1385 sub get_org_unit {
1386     my( $self, $client, $user_session, $org_id ) = @_;
1387     my $e = new_editor(authtoken => $user_session);
1388     if(!$org_id) {
1389         return $e->event unless $e->checkauth;
1390         $org_id = $e->requestor->ws_ou;
1391     }
1392     my $o = $e->retrieve_actor_org_unit($org_id)
1393         or return $e->event;
1394     return $o;
1395 }
1396
1397 __PACKAGE__->register_method(
1398     method   => "search_org_unit",
1399     api_name => "open-ils.actor.org_unit_list.search",
1400 );
1401
1402 sub search_org_unit {
1403
1404     my( $self, $client, $field, $value ) = @_;
1405
1406     my $list = OpenILS::Application::AppUtils->simple_scalar_request(
1407         "open-ils.cstore",
1408         "open-ils.cstore.direct.actor.org_unit.search.atomic",
1409         { $field => $value } );
1410
1411     return $list;
1412 }
1413
1414
1415 # build the org tree
1416
1417 __PACKAGE__->register_method(
1418     method  => "get_org_tree",
1419     api_name    => "open-ils.actor.org_tree.retrieve",
1420     argc        => 0,
1421     note        => "Returns the entire org tree structure",
1422 );
1423
1424 sub get_org_tree {
1425     my $self = shift;
1426     my $client = shift;
1427     return $U->get_org_tree($client->session->session_locale);
1428 }
1429
1430
1431 __PACKAGE__->register_method(
1432     method  => "get_org_descendants",
1433     api_name    => "open-ils.actor.org_tree.descendants.retrieve"
1434 );
1435
1436 # depth is optional.  org_unit is the id
1437 sub get_org_descendants {
1438     my( $self, $client, $org_unit, $depth ) = @_;
1439
1440     if(ref $org_unit eq 'ARRAY') {
1441         $depth ||= [];
1442         my @trees;
1443         for my $i (0..scalar(@$org_unit)-1) {
1444             my $list = $U->simple_scalar_request(
1445                 "open-ils.storage",
1446                 "open-ils.storage.actor.org_unit.descendants.atomic",
1447                 $org_unit->[$i], $depth->[$i] );
1448             push(@trees, $U->build_org_tree($list));
1449         }
1450         return \@trees;
1451
1452     } else {
1453         my $orglist = $apputils->simple_scalar_request(
1454                 "open-ils.storage",
1455                 "open-ils.storage.actor.org_unit.descendants.atomic",
1456                 $org_unit, $depth );
1457         return $U->build_org_tree($orglist);
1458     }
1459 }
1460
1461
1462 __PACKAGE__->register_method(
1463     method  => "get_org_ancestors",
1464     api_name    => "open-ils.actor.org_tree.ancestors.retrieve"
1465 );
1466
1467 # depth is optional.  org_unit is the id
1468 sub get_org_ancestors {
1469     my( $self, $client, $org_unit, $depth ) = @_;
1470     my $orglist = $apputils->simple_scalar_request(
1471             "open-ils.storage",
1472             "open-ils.storage.actor.org_unit.ancestors.atomic",
1473             $org_unit, $depth );
1474     return $U->build_org_tree($orglist);
1475 }
1476
1477
1478 __PACKAGE__->register_method(
1479     method  => "get_standings",
1480     api_name    => "open-ils.actor.standings.retrieve"
1481 );
1482
1483 my $user_standings;
1484 sub get_standings {
1485     return $user_standings if $user_standings;
1486     return $user_standings =
1487         $apputils->simple_scalar_request(
1488             "open-ils.cstore",
1489             "open-ils.cstore.direct.config.standing.search.atomic",
1490             { id => { "!=" => undef } }
1491         );
1492 }
1493
1494
1495 __PACKAGE__->register_method(
1496     method   => "get_my_org_path",
1497     api_name => "open-ils.actor.org_unit.full_path.retrieve"
1498 );
1499
1500 sub get_my_org_path {
1501     my( $self, $client, $auth, $org_id ) = @_;
1502     my $e = new_editor(authtoken=>$auth);
1503     return $e->event unless $e->checkauth;
1504     $org_id = $e->requestor->ws_ou unless defined $org_id;
1505
1506     return $apputils->simple_scalar_request(
1507         "open-ils.storage",
1508         "open-ils.storage.actor.org_unit.full_path.atomic",
1509         $org_id );
1510 }
1511
1512 __PACKAGE__->register_method(
1513     method   => "retrieve_coordinates",
1514     api_name => "open-ils.actor.geo.retrieve_coordinates",
1515     signature => {
1516         params => [
1517             {desc => 'Authentication token', type => 'string' },
1518             {type => 'number', desc => 'Context Organizational Unit'},
1519             {type => 'string', desc => 'Address to look-up as a text string'}
1520         ],
1521         return => { desc => 'Hash/object containing latitude and longitude for the provided address.'}
1522     }
1523 );
1524
1525 sub retrieve_coordinates {
1526     my( $self, $client, $auth, $org_id, $addr_string ) = @_;
1527     my $e = new_editor(authtoken=>$auth);
1528     return $e->event unless $e->checkauth;
1529     $org_id = $e->requestor->ws_ou unless defined $org_id;
1530
1531     return $apputils->simple_scalar_request(
1532         "open-ils.geo",
1533         "open-ils.geo.retrieve_coordinates",
1534         $org_id, $addr_string );
1535 }
1536
1537 __PACKAGE__->register_method(
1538     method   => "get_my_org_ancestor_at_depth",
1539     api_name => "open-ils.actor.org_unit.ancestor_at_depth.retrieve"
1540 );
1541
1542 sub get_my_org_ancestor_at_depth {
1543     my( $self, $client, $auth, $org_id, $depth ) = @_;
1544     my $e = new_editor(authtoken=>$auth);
1545     return $e->event unless $e->checkauth;
1546     $org_id = $e->requestor->ws_ou unless defined $org_id;
1547
1548     return $apputils->org_unit_ancestor_at_depth( $org_id, $depth );
1549 }
1550
1551 __PACKAGE__->register_method(
1552     method   => "patron_adv_search",
1553     api_name => "open-ils.actor.patron.search.advanced"
1554 );
1555
1556 __PACKAGE__->register_method(
1557     method   => "patron_adv_search",
1558     api_name => "open-ils.actor.patron.search.advanced.fleshed",
1559     stream => 1,
1560     # Flush the response stream at most 5 patrons in for UI responsiveness.
1561     max_bundle_count => 5,
1562     signature => {
1563         desc => q/Returns a stream of fleshed user objects instead of
1564             a pile of identifiers/
1565     }
1566 );
1567
1568 sub patron_adv_search {
1569     my( $self, $client, $auth, $search_hash, $search_limit,
1570         $search_sort, $include_inactive, $search_ou, $flesh_fields, $offset) = @_;
1571
1572     # API params sanity checks.
1573     # Exit early with empty result if no filter exists.
1574     # .fleshed call is streaming.  Non-fleshed is effectively atomic.
1575     my $fleshed = ($self->api_name =~ /fleshed/);
1576     return ($fleshed ? undef : []) unless (ref $search_hash ||'') eq 'HASH';
1577     my $search_ok = 0;
1578     for my $key (keys %$search_hash) {
1579         next if $search_hash->{$key}{value} =~ /^\s*$/; # empty filter
1580         $search_ok = 1;
1581         last;
1582     }
1583     return ($fleshed ? undef : []) unless $search_ok;
1584
1585     my $e = new_editor(authtoken=>$auth);
1586     return $e->event unless $e->checkauth;
1587     return $e->event unless $e->allowed('VIEW_USER');
1588
1589     # depth boundary outside of which patrons must opt-in, default to 0
1590     my $opt_boundary = 0;
1591     $opt_boundary = $U->ou_ancestor_setting_value($e->requestor->ws_ou,'org.patron_opt_boundary') if user_opt_in_enabled($self);
1592
1593     if (not defined $search_ou) {
1594         my $depth = $U->ou_ancestor_setting_value(
1595             $e->requestor->ws_ou,
1596             'circ.patron_edit.duplicate_patron_check_depth'
1597         );
1598
1599         if (defined $depth) {
1600             $search_ou = $U->org_unit_ancestor_at_depth(
1601                 $e->requestor->ws_ou, $depth
1602             );
1603         }
1604     }
1605
1606     my $ids = $U->storagereq(
1607         "open-ils.storage.actor.user.crazy_search", $search_hash,
1608         $search_limit, $search_sort, $include_inactive,
1609         $e->requestor->ws_ou, $search_ou, $opt_boundary, $offset);
1610
1611     return $ids unless $self->api_name =~ /fleshed/;
1612
1613     $client->respond(new_flesh_user($_, $flesh_fields, $e)) for @$ids;
1614
1615     return;
1616 }
1617
1618
1619 # A migrated (main) password has the form:
1620 # CRYPT( MD5( pw_salt || MD5(real_password) ), pw_salt )
1621 sub modify_migrated_user_password {
1622     my ($e, $user_id, $passwd) = @_;
1623
1624     # new password gets a new salt
1625     my $new_salt = $e->json_query({
1626         from => ['actor.create_salt', 'main']})->[0];
1627     $new_salt = $new_salt->{'actor.create_salt'};
1628
1629     $e->json_query({
1630         from => [
1631             'actor.set_passwd',
1632             $user_id,
1633             'main',
1634             md5_hex($new_salt . md5_hex($passwd)),
1635             $new_salt
1636         ]
1637     });
1638 }
1639
1640
1641
1642 __PACKAGE__->register_method(
1643     method    => "update_passwd",
1644     api_name  => "open-ils.actor.user.password.update",
1645     signature => {
1646         desc   => "Update the operator's password",
1647         params => [
1648             { desc => 'Authentication token', type => 'string' },
1649             { desc => 'New password',         type => 'string' },
1650             { desc => 'Current password',     type => 'string' }
1651         ],
1652         return => {desc => '1 on success, Event on error or incorrect current password'}
1653     }
1654 );
1655
1656 __PACKAGE__->register_method(
1657     method    => "update_passwd",
1658     api_name  => "open-ils.actor.user.username.update",
1659     signature => {
1660         desc   => "Update the operator's username",
1661         params => [
1662             { desc => 'Authentication token', type => 'string' },
1663             { desc => 'New username',         type => 'string' },
1664             { desc => 'Current password',     type => 'string' }
1665         ],
1666         return => {desc => '1 on success, Event on error or incorrect current password'}
1667     }
1668 );
1669
1670 __PACKAGE__->register_method(
1671     method    => "update_passwd",
1672     api_name  => "open-ils.actor.user.email.update",
1673     signature => {
1674         desc   => "Update the operator's email address",
1675         params => [
1676             { desc => 'Authentication token', type => 'string' },
1677             { desc => 'New email address',    type => 'string' },
1678             { desc => 'Current password',     type => 'string' }
1679         ],
1680         return => {desc => '1 on success, Event on error or incorrect current password'}
1681     }
1682 );
1683
1684 __PACKAGE__->register_method(
1685     method    => "update_passwd",
1686     api_name  => "open-ils.actor.user.locale.update",
1687     signature => {
1688         desc   => "Update the operator's i18n locale",
1689         params => [
1690             { desc => 'Authentication token', type => 'string' },
1691             { desc => 'New locale',           type => 'string' },
1692             { desc => 'Current password',     type => 'string' }
1693         ],
1694         return => {desc => '1 on success, Event on error or incorrect current password'}
1695     }
1696 );
1697
1698 sub update_passwd {
1699     my( $self, $conn, $auth, $new_val, $orig_pw ) = @_;
1700     my $e = new_editor(xact=>1, authtoken=>$auth);
1701     return $e->die_event unless $e->checkauth;
1702
1703     my $db_user = $e->retrieve_actor_user($e->requestor->id)
1704         or return $e->die_event;
1705     my $api = $self->api_name;
1706
1707     if (!$U->verify_migrated_user_password($e, $db_user->id, $orig_pw)) {
1708         $e->rollback;
1709         return new OpenILS::Event('INCORRECT_PASSWORD');
1710     }
1711
1712     my $at_event = 0;
1713     if( $api =~ /password/o ) {
1714         # NOTE: with access to the plain text password we could crypt
1715         # the password without the extra MD5 pre-hashing.  Other changes
1716         # would be required.  Noting here for future reference.
1717         modify_migrated_user_password($e, $db_user->id, $new_val);
1718         $db_user->passwd('');
1719
1720     } else {
1721
1722         # if we don't clear the password, the user will be updated with
1723         # a hashed version of the hashed version of their password
1724         $db_user->clear_passwd;
1725
1726         if( $api =~ /username/o ) {
1727
1728             # make sure no one else has this username
1729             my $exist = $e->search_actor_user({usrname=>$new_val},{idlist=>1});
1730             if (@$exist) {
1731                 $e->rollback;
1732                 return new OpenILS::Event('USERNAME_EXISTS');
1733             }
1734             $db_user->usrname($new_val);
1735             $at_event++;
1736
1737         } elsif( $api =~ /email/o ) {
1738             $db_user->email($new_val);
1739             $at_event++;
1740
1741         } elsif( $api =~ /locale/o ) {
1742             $db_user->locale($new_val);
1743             $at_event++;
1744         }
1745     }
1746
1747     $e->update_actor_user($db_user) or return $e->die_event;
1748     $e->commit;
1749
1750     $U->create_events_for_hook('au.updated', $db_user, $e->requestor->ws_ou)
1751         if $at_event;
1752
1753     # update the cached user to pick up these changes
1754     $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1);
1755     return 1;
1756 }
1757
1758
1759
1760 __PACKAGE__->register_method(
1761     method   => "check_user_perms",
1762     api_name => "open-ils.actor.user.perm.check",
1763     notes    => <<"    NOTES");
1764     Takes a login session, user id, an org id, and an array of perm type strings.  For each
1765     perm type, if the user does *not* have the given permission it is added
1766     to a list which is returned from the method.  If all permissions
1767     are allowed, an empty list is returned
1768     if the logged in user does not match 'user_id', then the logged in user must
1769     have VIEW_PERMISSION priveleges.
1770     NOTES
1771
1772 sub check_user_perms {
1773     my( $self, $client, $login_session, $user_id, $org_id, $perm_types ) = @_;
1774
1775     my( $staff, $evt ) = $apputils->checkses($login_session);
1776     return $evt if $evt;
1777
1778     if($staff->id ne $user_id) {
1779         if( $evt = $apputils->check_perms(
1780             $staff->id, $org_id, 'VIEW_PERMISSION') ) {
1781             return $evt;
1782         }
1783     }
1784
1785     my @not_allowed;
1786     for my $perm (@$perm_types) {
1787         if($apputils->check_perms($user_id, $org_id, $perm)) {
1788             push @not_allowed, $perm;
1789         }
1790     }
1791
1792     return \@not_allowed
1793 }
1794
1795 __PACKAGE__->register_method(
1796     method  => "check_user_perms2",
1797     api_name    => "open-ils.actor.user.perm.check.multi_org",
1798     notes       => q/
1799         Checks the permissions on a list of perms and orgs for a user
1800         @param authtoken The login session key
1801         @param user_id The id of the user to check
1802         @param orgs The array of org ids
1803         @param perms The array of permission names
1804         @return An array of  [ orgId, permissionName ] arrays that FAILED the check
1805         if the logged in user does not match 'user_id', then the logged in user must
1806         have VIEW_PERMISSION priveleges.
1807     /);
1808
1809 sub check_user_perms2 {
1810     my( $self, $client, $authtoken, $user_id, $orgs, $perms ) = @_;
1811
1812     my( $staff, $target, $evt ) = $apputils->checkses_requestor(
1813         $authtoken, $user_id, 'VIEW_PERMISSION' );
1814     return $evt if $evt;
1815
1816     my @not_allowed;
1817     for my $org (@$orgs) {
1818         for my $perm (@$perms) {
1819             if($apputils->check_perms($user_id, $org, $perm)) {
1820                 push @not_allowed, [ $org, $perm ];
1821             }
1822         }
1823     }
1824
1825     return \@not_allowed
1826 }
1827
1828
1829 __PACKAGE__->register_method(
1830     method => 'check_user_perms3',
1831     api_name    => 'open-ils.actor.user.perm.highest_org',
1832     notes       => q/
1833         Returns the highest org unit id at which a user has a given permission
1834         If the requestor does not match the target user, the requestor must have
1835         'VIEW_PERMISSION' rights at the home org unit of the target user
1836         @param authtoken The login session key
1837         @param userid The id of the user in question
1838         @param perm The permission to check
1839         @return The org unit highest in the org tree within which the user has
1840         the requested permission
1841     /);
1842
1843 sub check_user_perms3 {
1844     my($self, $client, $authtoken, $user_id, $perm) = @_;
1845     my $e = new_editor(authtoken=>$authtoken);
1846     return $e->event unless $e->checkauth;
1847
1848     my $tree = $U->get_org_tree();
1849
1850     unless($e->requestor->id == $user_id) {
1851         my $user = $e->retrieve_actor_user($user_id)
1852             or return $e->event;
1853         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1854         return $U->find_highest_perm_org($perm, $user_id, $user->home_ou, $tree );
1855     }
1856
1857     return $U->find_highest_perm_org($perm, $user_id, $e->requestor->ws_ou, $tree);
1858 }
1859
1860 __PACKAGE__->register_method(
1861     method => 'user_has_work_perm_at',
1862     api_name    => 'open-ils.actor.user.has_work_perm_at',
1863     authoritative => 1,
1864     signature => {
1865         desc => q/
1866             Returns a set of org unit IDs which represent the highest orgs in
1867             the org tree where the user has the requested permission.  The
1868             purpose of this method is to return the smallest set of org units
1869             which represent the full expanse of the user's ability to perform
1870             the requested action.  The user whose perms this method should
1871             check is implied by the authtoken. /,
1872         params => [
1873             {desc => 'authtoken', type => 'string'},
1874             {desc => 'permission name', type => 'string'},
1875             {desc => q/user id, optional.  If present, check perms for
1876                 this user instead of the logged in user/, type => 'number'},
1877         ],
1878         return => {desc => 'An array of org IDs'}
1879     }
1880 );
1881
1882 sub user_has_work_perm_at {
1883     my($self, $conn, $auth, $perm, $user_id) = @_;
1884     my $e = new_editor(authtoken=>$auth);
1885     return $e->event unless $e->checkauth;
1886     if(defined $user_id) {
1887         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1888         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1889     }
1890     return $U->user_has_work_perm_at($e, $perm, undef, $user_id);
1891 }
1892
1893 __PACKAGE__->register_method(
1894     method => 'user_has_work_perm_at_batch',
1895     api_name    => 'open-ils.actor.user.has_work_perm_at.batch',
1896     authoritative => 1,
1897 );
1898
1899 sub user_has_work_perm_at_batch {
1900     my($self, $conn, $auth, $perms, $user_id) = @_;
1901     my $e = new_editor(authtoken=>$auth);
1902     return $e->event unless $e->checkauth;
1903     if(defined $user_id) {
1904         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1905         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1906     }
1907     my $map = {};
1908     $map->{$_} = $U->user_has_work_perm_at($e, $_) for @$perms;
1909     return $map;
1910 }
1911
1912
1913
1914 __PACKAGE__->register_method(
1915     method => 'check_user_perms4',
1916     api_name    => 'open-ils.actor.user.perm.highest_org.batch',
1917     notes       => q/
1918         Returns the highest org unit id at which a user has a given permission
1919         If the requestor does not match the target user, the requestor must have
1920         'VIEW_PERMISSION' rights at the home org unit of the target user
1921         @param authtoken The login session key
1922         @param userid The id of the user in question
1923         @param perms An array of perm names to check
1924         @return An array of orgId's  representing the org unit
1925         highest in the org tree within which the user has the requested permission
1926         The arrah of orgId's has matches the order of the perms array
1927     /);
1928
1929 sub check_user_perms4 {
1930     my( $self, $client, $authtoken, $userid, $perms ) = @_;
1931
1932     my( $staff, $target, $org, $evt );
1933
1934     ( $staff, $target, $evt ) = $apputils->checkses_requestor(
1935         $authtoken, $userid, 'VIEW_PERMISSION' );
1936     return $evt if $evt;
1937
1938     my @arr;
1939     return [] unless ref($perms);
1940     my $tree = $U->get_org_tree();
1941
1942     for my $p (@$perms) {
1943         push( @arr, $U->find_highest_perm_org( $p, $userid, $target->home_ou, $tree ) );
1944     }
1945     return \@arr;
1946 }
1947
1948
1949 __PACKAGE__->register_method(
1950     method        => "user_fines_summary",
1951     api_name      => "open-ils.actor.user.fines.summary",
1952     authoritative => 1,
1953     signature     => {
1954         desc   => 'Returns a short summary of the users total open fines, '  .
1955                 'excluding voided fines Params are login_session, user_id' ,
1956         params => [
1957             {desc => 'Authentication token', type => 'string'},
1958             {desc => 'User ID',              type => 'string'}  # number?
1959         ],
1960         return => {
1961             desc => "a 'mous' object, event on error",
1962         }
1963     }
1964 );
1965
1966 sub user_fines_summary {
1967     my( $self, $client, $auth, $user_id ) = @_;
1968
1969     my $e = new_editor(authtoken=>$auth);
1970     return $e->event unless $e->checkauth;
1971
1972     if( $user_id ne $e->requestor->id ) {
1973         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1974         return $e->event unless
1975             $e->allowed('VIEW_USER_FINES_SUMMARY', $user->home_ou);
1976     }
1977
1978     return $e->search_money_open_user_summary({usr => $user_id})->[0];
1979 }
1980
1981
1982 __PACKAGE__->register_method(
1983     method        => "user_opac_vitals",
1984     api_name      => "open-ils.actor.user.opac.vital_stats",
1985     argc          => 1,
1986     authoritative => 1,
1987     signature     => {
1988         desc   => 'Returns a short summary of the users vital stats, including '  .
1989                 'identification information, accumulated balance, number of holds, ' .
1990                 'and current open circulation stats' ,
1991         params => [
1992             {desc => 'Authentication token',                          type => 'string'},
1993             {desc => 'Optional User ID, for use in the staff client', type => 'number'}  # number?
1994         ],
1995         return => {
1996             desc => "An object with four properties: user, fines, checkouts and holds."
1997         }
1998     }
1999 );
2000
2001 sub user_opac_vitals {
2002     my( $self, $client, $auth, $user_id ) = @_;
2003
2004     my $e = new_editor(authtoken=>$auth);
2005     return $e->event unless $e->checkauth;
2006
2007     $user_id ||= $e->requestor->id;
2008
2009     my $user = $e->retrieve_actor_user( $user_id );
2010
2011     my ($fines) = $self
2012         ->method_lookup('open-ils.actor.user.fines.summary')
2013         ->run($auth => $user_id);
2014     return $fines if (defined($U->event_code($fines)));
2015
2016     if (!$fines) {
2017         $fines = new Fieldmapper::money::open_user_summary ();
2018         $fines->balance_owed(0.00);
2019         $fines->total_owed(0.00);
2020         $fines->total_paid(0.00);
2021         $fines->usr($user_id);
2022     }
2023
2024     my ($holds) = $self
2025         ->method_lookup('open-ils.actor.user.hold_requests.count')
2026         ->run($auth => $user_id);
2027     return $holds if (defined($U->event_code($holds)));
2028
2029     my ($out) = $self
2030         ->method_lookup('open-ils.actor.user.checked_out.count')
2031         ->run($auth => $user_id);
2032     return $out if (defined($U->event_code($out)));
2033
2034     $out->{"total_out"} = reduce { $a + $out->{$b} } 0, qw/out overdue/;
2035
2036     my $unread_msgs = $e->search_actor_usr_message([
2037         {usr => $user_id, read_date => undef, deleted => 'f',
2038             'pub' => 't', # this is for the unread message count in the opac
2039             #'-or' => [ # Hiding Archived messages are for staff UI, not this
2040             #    {stop_date => undef},
2041             #    {stop_date => {'>' => 'now'}}
2042             #],
2043         },
2044         {idlist => 1}
2045     ]);
2046
2047     return {
2048         user => {
2049             first_given_name  => $user->first_given_name,
2050             second_given_name => $user->second_given_name,
2051             family_name       => $user->family_name,
2052             alias             => $user->alias,
2053             usrname           => $user->usrname
2054         },
2055         fines => $fines->to_bare_hash,
2056         checkouts => $out,
2057         holds => $holds,
2058         messages => { unread => scalar(@$unread_msgs) }
2059     };
2060 }
2061
2062
2063 ##### a small consolidation of related method registrations
2064 my $common_params = [
2065     { desc => 'Authentication token', type => 'string' },
2066     { desc => 'User ID',              type => 'string' },
2067     { desc => 'Transactions type (optional, defaults to all)', type => 'string' },
2068     { desc => 'Options hash.  May contain limit and offset for paged results.', type => 'object' },
2069 ];
2070 my %methods = (
2071     'open-ils.actor.user.transactions'                      => '',
2072     'open-ils.actor.user.transactions.fleshed'              => '',
2073     'open-ils.actor.user.transactions.have_charge'          => ' that have an initial charge',
2074     'open-ils.actor.user.transactions.have_charge.fleshed'  => ' that have an initial charge',
2075     'open-ils.actor.user.transactions.have_balance'         => ' that have an outstanding balance',
2076     'open-ils.actor.user.transactions.have_balance.fleshed' => ' that have an outstanding balance',
2077 );
2078
2079 foreach (keys %methods) {
2080     my %args = (
2081         method    => "user_transactions",
2082         api_name  => $_,
2083         signature => {
2084             desc   => 'For a given user, retrieve a list of '
2085                     . (/\.fleshed/ ? 'fleshed ' : '')
2086                     . 'transactions' . $methods{$_}
2087                     . ' optionally limited to transactions of a given type.',
2088             params => $common_params,
2089             return => {
2090                 desc => "List of objects, or event on error.  Each object is a hash containing: transaction, circ, record. "
2091                     . 'These represent the relevant (mbts) transaction, attached circulation and title pointed to in the circ, respectively.',
2092             }
2093         }
2094     );
2095     $args{authoritative} = 1;
2096     __PACKAGE__->register_method(%args);
2097 }
2098
2099 # Now for the counts
2100 %methods = (
2101     'open-ils.actor.user.transactions.count'              => '',
2102     'open-ils.actor.user.transactions.have_charge.count'  => ' that have an initial charge',
2103     'open-ils.actor.user.transactions.have_balance.count' => ' that have an outstanding balance',
2104 );
2105
2106 foreach (keys %methods) {
2107     my %args = (
2108         method    => "user_transactions",
2109         api_name  => $_,
2110         signature => {
2111             desc   => 'For a given user, retrieve a count of open '
2112                     . 'transactions' . $methods{$_}
2113                     . ' optionally limited to transactions of a given type.',
2114             params => $common_params,
2115             return => { desc => "Integer count of transactions, or event on error" }
2116         }
2117     );
2118     /\.have_balance/ and $args{authoritative} = 1;     # FIXME: I don't know why have_charge isn't authoritative
2119     __PACKAGE__->register_method(%args);
2120 }
2121
2122 __PACKAGE__->register_method(
2123     method        => "user_transactions",
2124     api_name      => "open-ils.actor.user.transactions.have_balance.total",
2125     authoritative => 1,
2126     signature     => {
2127         desc   => 'For a given user, retrieve the total balance owed for open transactions,'
2128                 . ' optionally limited to transactions of a given type.',
2129         params => $common_params,
2130         return => { desc => "Decimal balance value, or event on error" }
2131     }
2132 );
2133
2134
2135 sub user_transactions {
2136     my( $self, $client, $auth, $user_id, $type, $options ) = @_;
2137     $options ||= {};
2138
2139     my $e = new_editor(authtoken => $auth);
2140     return $e->event unless $e->checkauth;
2141
2142     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
2143
2144     return $e->event unless
2145         $e->requestor->id == $user_id or
2146         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
2147
2148     my $api = $self->api_name();
2149
2150     my $filter = ($api =~ /have_balance/o) ?
2151         { 'balance_owed' => { '<>' => 0 } }:
2152         { 'total_owed' => { '>' => 0 } };
2153
2154     my $method = 'open-ils.actor.user.transactions.history.still_open';
2155     $method = "$method.authoritative" if $api =~ /authoritative/;
2156     my ($trans) = $self->method_lookup($method)->run($auth, $user_id, $type, $filter, $options);
2157
2158     if($api =~ /total/o) {
2159         my $total = 0.0;
2160         $total += $_->balance_owed for @$trans;
2161         return $total;
2162     }
2163
2164     ($api =~ /count/o  ) and return scalar @$trans;
2165     ($api !~ /fleshed/o) and return $trans;
2166
2167     my @resp;
2168     for my $t (@$trans) {
2169
2170         if( $t->xact_type ne 'circulation' ) {
2171             push @resp, {transaction => $t};
2172             next;
2173         }
2174
2175         my $circ_data = flesh_circ($e, $t->id);
2176         push @resp, {transaction => $t, %$circ_data};
2177     }
2178
2179     return \@resp;
2180 }
2181
2182
2183 __PACKAGE__->register_method(
2184     method   => "user_transaction_retrieve",
2185     api_name => "open-ils.actor.user.transaction.fleshed.retrieve",
2186     argc     => 1,
2187     authoritative => 1,
2188     notes    => "Returns a fleshed transaction record"
2189 );
2190
2191 __PACKAGE__->register_method(
2192     method   => "user_transaction_retrieve",
2193     api_name => "open-ils.actor.user.transaction.retrieve",
2194     argc     => 1,
2195     authoritative => 1,
2196     notes    => "Returns a transaction record"
2197 );
2198
2199 sub user_transaction_retrieve {
2200     my($self, $client, $auth, $bill_id) = @_;
2201
2202     my $e = new_editor(authtoken => $auth);
2203     return $e->event unless $e->checkauth;
2204
2205     my $trans = $e->retrieve_money_billable_transaction_summary(
2206         [$bill_id, {flesh => 1, flesh_fields => {mbts => ['usr']}}]) or return $e->event;
2207
2208     return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $trans->usr->home_ou);
2209
2210     $trans->usr($trans->usr->id); # de-flesh for backwards compat
2211
2212     return $trans unless $self->api_name =~ /flesh/;
2213     return {transaction => $trans} if $trans->xact_type ne 'circulation';
2214
2215     my $circ_data = flesh_circ($e, $trans->id, 1);
2216
2217     return {transaction => $trans, %$circ_data};
2218 }
2219
2220 sub flesh_circ {
2221     my $e = shift;
2222     my $circ_id = shift;
2223     my $flesh_copy = shift;
2224
2225     my $circ = $e->retrieve_action_circulation([
2226         $circ_id, {
2227             flesh => 3,
2228             flesh_fields => {
2229                 circ => ['target_copy'],
2230                 acp => ['call_number'],
2231                 acn => ['record']
2232             }
2233         }
2234     ]);
2235
2236     my $mods;
2237     my $copy = $circ->target_copy;
2238
2239     if($circ->target_copy->call_number->id == OILS_PRECAT_CALL_NUMBER) {
2240         $mods = new Fieldmapper::metabib::virtual_record;
2241         $mods->doc_id(OILS_PRECAT_RECORD);
2242         $mods->title($copy->dummy_title);
2243         $mods->author($copy->dummy_author);
2244
2245     } else {
2246         $mods = $U->record_to_mvr($circ->target_copy->call_number->record);
2247     }
2248
2249     # more de-fleshiing
2250     $circ->target_copy($circ->target_copy->id);
2251     $copy->call_number($copy->call_number->id);
2252
2253     return {circ => $circ, record => $mods, copy => ($flesh_copy) ? $copy : undef };
2254 }
2255
2256
2257 __PACKAGE__->register_method(
2258     method        => "hold_request_count",
2259     api_name      => "open-ils.actor.user.hold_requests.count",
2260     authoritative => 1,
2261     argc          => 1,
2262     notes         => q/
2263         Returns hold ready vs. total counts.
2264         If a context org unit is provided, a third value
2265         is returned with key 'behind_desk', which reports
2266         how many holds are ready at the pickup library
2267         with the behind_desk flag set to true.
2268     /
2269 );
2270
2271 sub hold_request_count {
2272     my( $self, $client, $authtoken, $user_id, $ctx_org ) = @_;
2273     my $e = new_editor(authtoken => $authtoken);
2274     return $e->event unless $e->checkauth;
2275
2276     $user_id = $e->requestor->id unless defined $user_id;
2277
2278     if($e->requestor->id ne $user_id) {
2279         my $user = $e->retrieve_actor_user($user_id);
2280         return $e->event unless $e->allowed('VIEW_HOLD', $user->home_ou);
2281     }
2282
2283     my $holds = $e->json_query({
2284         select => {ahr => ['pickup_lib', 'current_shelf_lib', 'behind_desk']},
2285         from => 'ahr',
2286         where => {
2287             usr => $user_id,
2288             fulfillment_time => {"=" => undef },
2289             cancel_time => undef,
2290         }
2291     });
2292
2293     my @ready = grep {
2294         $_->{current_shelf_lib} and # avoid undef warnings
2295         $_->{pickup_lib} eq $_->{current_shelf_lib}
2296     } @$holds;
2297
2298     my $resp = {
2299         total => scalar(@$holds),
2300         ready => int(scalar(@ready))
2301     };
2302
2303     if ($ctx_org) {
2304         # count of holds ready at pickup lib with behind_desk true.
2305         $resp->{behind_desk} = int(scalar(
2306             grep {
2307                 $_->{pickup_lib} == $ctx_org and
2308                 $U->is_true($_->{behind_desk})
2309             } @ready
2310         ));
2311     }
2312
2313     return $resp;
2314 }
2315
2316 __PACKAGE__->register_method(
2317     method        => "checked_out",
2318     api_name      => "open-ils.actor.user.checked_out",
2319     authoritative => 1,
2320     argc          => 2,
2321     signature     => {
2322         desc => "For a given user, returns a structure of circulations objects sorted by out, overdue, lost, claims_returned, long_overdue. "
2323             . "A list of IDs are returned of each type.  Circs marked lost, long_overdue, and claims_returned will not be 'finished' "
2324             . "(i.e., outstanding balance or some other pending action on the circ). "
2325             . "The .count method also includes a 'total' field which sums all open circs.",
2326         params => [
2327             { desc => 'Authentication Token', type => 'string'},
2328             { desc => 'User ID',              type => 'string'},
2329         ],
2330         return => {
2331             desc => 'Returns event on error, or an object with ID lists, like: '
2332                 . '{"out":[12552,451232], "claims_returned":[], "long_overdue":[23421] "overdue":[], "lost":[]}'
2333         },
2334     }
2335 );
2336
2337 __PACKAGE__->register_method(
2338     method        => "checked_out",
2339     api_name      => "open-ils.actor.user.checked_out.count",
2340     authoritative => 1,
2341     argc          => 2,
2342     signature     => q/@see open-ils.actor.user.checked_out/
2343 );
2344
2345 sub checked_out {
2346     my( $self, $conn, $auth, $userid ) = @_;
2347
2348     my $e = new_editor(authtoken=>$auth);
2349     return $e->event unless $e->checkauth;
2350
2351     if( $userid ne $e->requestor->id ) {
2352         my $user = $e->retrieve_actor_user($userid) or return $e->event;
2353         unless($e->allowed('VIEW_CIRCULATIONS', $user->home_ou)) {
2354
2355             # see if there is a friend link allowing circ.view perms
2356             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
2357                 $e, $userid, $e->requestor->id, 'circ.view');
2358             return $e->event unless $allowed;
2359         }
2360     }
2361
2362     my $count = $self->api_name =~ /count/;
2363     return _checked_out( $count, $e, $userid );
2364 }
2365
2366 sub _checked_out {
2367     my( $iscount, $e, $userid ) = @_;
2368
2369     my %result = (
2370         out => [],
2371         overdue => [],
2372         lost => [],
2373         claims_returned => [],
2374         long_overdue => []
2375     );
2376     my $meth = 'retrieve_action_open_circ_';
2377
2378     if ($iscount) {
2379         $meth .= 'count';
2380         %result = (
2381             out => 0,
2382             overdue => 0,
2383             lost => 0,
2384             claims_returned => 0,
2385             long_overdue => 0
2386         );
2387     } else {
2388         $meth .= 'list';
2389     }
2390
2391     my $data = $e->$meth($userid);
2392
2393     if ($data) {
2394         if ($iscount) {
2395             $result{$_} += $data->$_() for (keys %result);
2396             $result{total} += $data->$_() for (keys %result);
2397         } else {
2398             for my $k (keys %result) {
2399                 $result{$k} = [ grep { $_ > 0 } split( ',', $data->$k()) ];
2400             }
2401         }
2402     }
2403
2404     return \%result;
2405 }
2406
2407
2408
2409 __PACKAGE__->register_method(
2410     method        => "checked_in_with_fines",
2411     api_name      => "open-ils.actor.user.checked_in_with_fines",
2412     authoritative => 1,
2413     argc          => 2,
2414     signature     => q/@see open-ils.actor.user.checked_out/
2415 );
2416
2417 sub checked_in_with_fines {
2418     my( $self, $conn, $auth, $userid ) = @_;
2419
2420     my $e = new_editor(authtoken=>$auth);
2421     return $e->event unless $e->checkauth;
2422
2423     if( $userid ne $e->requestor->id ) {
2424         return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2425     }
2426
2427     # money is owed on these items and they are checked in
2428     my $open = $e->search_action_circulation(
2429         {
2430             usr             => $userid,
2431             xact_finish     => undef,
2432             checkin_time    => { "!=" => undef },
2433         }
2434     );
2435
2436
2437     my( @lost, @cr, @lo );
2438     for my $c (@$open) {
2439         push( @lost, $c->id ) if ($c->stop_fines eq 'LOST');
2440         push( @cr, $c->id ) if $c->stop_fines eq 'CLAIMSRETURNED';
2441         push( @lo, $c->id ) if $c->stop_fines eq 'LONGOVERDUE';
2442     }
2443
2444     return {
2445         lost        => \@lost,
2446         claims_returned => \@cr,
2447         long_overdue        => \@lo
2448     };
2449 }
2450
2451
2452 sub _sigmaker {
2453     my ($api, $desc, $auth) = @_;
2454     $desc = $desc ? (" " . $desc) : '';
2455     my $ids = ($api =~ /ids$/) ? 1 : 0;
2456     my @sig = (
2457         argc      => 1,
2458         method    => "user_transaction_history",
2459         api_name  => "open-ils.actor.user.transactions.$api",
2460         signature => {
2461             desc   => "For a given User ID, returns a list of billable transaction" .
2462                     ($ids ? " id" : '') .
2463                     "s$desc, optionally filtered by type and/or fields in money.billable_xact_summary.  " .
2464                     "The VIEW_USER_TRANSACTIONS permission is required to view another user's transactions",
2465             params => [
2466                 {desc => 'Authentication token',        type => 'string'},
2467                 {desc => 'User ID',                     type => 'number'},
2468                 {desc => 'Transaction type (optional)', type => 'number'},
2469                 {desc => 'Hash of Billable Transaction Summary filters (optional)', type => 'object'}
2470             ],
2471             return => {
2472                 desc => 'List of transaction' . ($ids ? " id" : '') . 's, Event on error'
2473             },
2474         }
2475     );
2476     $auth and push @sig, (authoritative => 1);
2477     return @sig;
2478 }
2479
2480 my %auth_hist_methods = (
2481     'history'             => '',
2482     'history.have_charge' => 'that have an initial charge',
2483     'history.still_open'  => 'that are not finished',
2484     'history.have_balance'         => 'that have a balance',
2485     'history.have_bill'            => 'that have billings',
2486     'history.have_bill_or_payment' => 'that have non-zero-sum billings or at least 1 payment',
2487     'history.have_payment' => 'that have at least 1 payment',
2488 );
2489
2490 foreach (keys %auth_hist_methods) {
2491     __PACKAGE__->register_method(_sigmaker($_,       $auth_hist_methods{$_}, 1));
2492     __PACKAGE__->register_method(_sigmaker("$_.ids", $auth_hist_methods{$_}, 1));
2493     __PACKAGE__->register_method(_sigmaker("$_.fleshed", $auth_hist_methods{$_}, 1));
2494 }
2495
2496 sub user_transaction_history {
2497     my( $self, $conn, $auth, $userid, $type, $filter, $options ) = @_;
2498     $filter ||= {};
2499     $options ||= {};
2500
2501     my $e = new_editor(authtoken=>$auth);
2502     return $e->die_event unless $e->checkauth;
2503
2504     if ($e->requestor->id ne $userid) {
2505         return $e->die_event unless $e->allowed('VIEW_USER_TRANSACTIONS');
2506     }
2507
2508     my $api = $self->api_name;
2509     my @xact_finish  = (xact_finish => undef ) if ($api =~ /history\.still_open$/);     # What about history.still_open.ids?
2510
2511     if(defined($type)) {
2512         $filter->{'xact_type'} = $type;
2513     }
2514
2515     if($api =~ /have_bill_or_payment/o) {
2516
2517         # transactions that have a non-zero sum across all billings or at least 1 payment
2518         $filter->{'-or'} = {
2519             'balance_owed' => { '<>' => 0 },
2520             'last_payment_ts' => { '<>' => undef }
2521         };
2522
2523     } elsif($api =~ /have_payment/) {
2524
2525         $filter->{last_payment_ts} ||= {'<>' => undef};
2526
2527     } elsif( $api =~ /have_balance/o) {
2528
2529         # transactions that have a non-zero overall balance
2530         $filter->{'balance_owed'} = { '<>' => 0 };
2531
2532     } elsif( $api =~ /have_charge/o) {
2533
2534         # transactions that have at least 1 billing, regardless of whether it was voided
2535         $filter->{'last_billing_ts'} = { '<>' => undef };
2536
2537     } elsif( $api =~ /have_bill/o) {    # needs to be an elsif, or we double-match have_bill_or_payment!
2538
2539         # transactions that have non-zero sum across all billings.  This will exclude
2540         # xacts where all billings have been voided
2541         $filter->{'total_owed'} = { '<>' => 0 };
2542     }
2543
2544     my $options_clause = { order_by => { mbt => 'xact_start DESC' } };
2545     $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'};
2546     $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'};
2547
2548     my $mbts = $e->search_money_billable_transaction_summary(
2549         [   { usr => $userid, @xact_finish, %$filter },
2550             $options_clause
2551         ]
2552     );
2553
2554     return [map {$_->id} @$mbts] if $api =~ /\.ids/;
2555     return $mbts unless $api =~ /fleshed/;
2556
2557     my @resp;
2558     for my $t (@$mbts) {
2559
2560         if( $t->xact_type ne 'circulation' ) {
2561             push @resp, {transaction => $t};
2562             next;
2563         }
2564
2565         my $circ_data = flesh_circ($e, $t->id);
2566         push @resp, {transaction => $t, %$circ_data};
2567     }
2568
2569     return \@resp;
2570 }
2571
2572
2573
2574 __PACKAGE__->register_method(
2575     method   => "user_perms",
2576     api_name => "open-ils.actor.permissions.user_perms.retrieve",
2577     argc     => 1,
2578     notes    => "Returns a list of permissions"
2579 );
2580
2581 sub user_perms {
2582     my( $self, $client, $authtoken, $user ) = @_;
2583
2584     my( $staff, $evt ) = $apputils->checkses($authtoken);
2585     return $evt if $evt;
2586
2587     $user ||= $staff->id;
2588
2589     if( $user != $staff->id and $evt = $apputils->check_perms( $staff->id, $staff->home_ou, 'VIEW_PERMISSION') ) {
2590         return $evt;
2591     }
2592
2593     return $apputils->simple_scalar_request(
2594         "open-ils.storage",
2595         "open-ils.storage.permission.user_perms.atomic",
2596         $user);
2597 }
2598
2599 __PACKAGE__->register_method(
2600     method   => "retrieve_perms",
2601     api_name => "open-ils.actor.permissions.retrieve",
2602     notes    => "Returns a list of permissions"
2603 );
2604 sub retrieve_perms {
2605     my( $self, $client ) = @_;
2606     return $apputils->simple_scalar_request(
2607         "open-ils.cstore",
2608         "open-ils.cstore.direct.permission.perm_list.search.atomic",
2609         { id => { '!=' => undef } }
2610     );
2611 }
2612
2613 __PACKAGE__->register_method(
2614     method   => "retrieve_groups",
2615     api_name => "open-ils.actor.groups.retrieve",
2616     notes    => "Returns a list of user groups"
2617 );
2618 sub retrieve_groups {
2619     my( $self, $client ) = @_;
2620     return new_editor()->retrieve_all_permission_grp_tree();
2621 }
2622
2623 __PACKAGE__->register_method(
2624     method  => "retrieve_org_address",
2625     api_name    => "open-ils.actor.org_unit.address.retrieve",
2626     notes        => <<'    NOTES');
2627     Returns an org_unit address by ID
2628     @param An org_address ID
2629     NOTES
2630 sub retrieve_org_address {
2631     my( $self, $client, $id ) = @_;
2632     return $apputils->simple_scalar_request(
2633         "open-ils.cstore",
2634         "open-ils.cstore.direct.actor.org_address.retrieve",
2635         $id
2636     );
2637 }
2638
2639 __PACKAGE__->register_method(
2640     method   => "retrieve_groups_tree",
2641     api_name => "open-ils.actor.groups.tree.retrieve",
2642     notes    => "Returns a list of user groups"
2643 );
2644
2645 sub retrieve_groups_tree {
2646     my( $self, $client ) = @_;
2647     return new_editor()->search_permission_grp_tree(
2648         [
2649             { parent => undef},
2650             {
2651                 flesh               => -1,
2652                 flesh_fields    => { pgt => ["children"] },
2653                 order_by            => { pgt => 'name'}
2654             }
2655         ]
2656     )->[0];
2657 }
2658
2659
2660 __PACKAGE__->register_method(
2661     method   => "add_user_to_groups",
2662     api_name => "open-ils.actor.user.set_groups",
2663     notes    => "Adds a user to one or more permission groups"
2664 );
2665
2666 sub add_user_to_groups {
2667     my( $self, $client, $authtoken, $userid, $groups ) = @_;
2668
2669     my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2670         $authtoken, $userid, 'CREATE_USER_GROUP_LINK' );
2671     return $evt if $evt;
2672
2673     ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2674         $authtoken, $userid, 'REMOVE_USER_GROUP_LINK' );
2675     return $evt if $evt;
2676
2677     $apputils->simplereq(
2678         'open-ils.storage',
2679         'open-ils.storage.direct.permission.usr_grp_map.mass_delete', { usr => $userid } );
2680
2681     for my $group (@$groups) {
2682         my $link = Fieldmapper::permission::usr_grp_map->new;
2683         $link->grp($group);
2684         $link->usr($userid);
2685
2686         my $id = $apputils->simplereq(
2687             'open-ils.storage',
2688             'open-ils.storage.direct.permission.usr_grp_map.create', $link );
2689     }
2690
2691     return 1;
2692 }
2693
2694 __PACKAGE__->register_method(
2695     method   => "get_user_perm_groups",
2696     api_name => "open-ils.actor.user.get_groups",
2697     notes    => "Retrieve a user's permission groups."
2698 );
2699
2700
2701 sub get_user_perm_groups {
2702     my( $self, $client, $authtoken, $userid ) = @_;
2703
2704     my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2705         $authtoken, $userid, 'VIEW_PERM_GROUPS' );
2706     return $evt if $evt;
2707
2708     return $apputils->simplereq(
2709         'open-ils.cstore',
2710         'open-ils.cstore.direct.permission.usr_grp_map.search.atomic', { usr => $userid } );
2711 }
2712
2713
2714 __PACKAGE__->register_method(
2715     method   => "get_user_work_ous",
2716     api_name => "open-ils.actor.user.get_work_ous",
2717     notes    => "Retrieve a user's work org units."
2718 );
2719
2720 __PACKAGE__->register_method(
2721     method   => "get_user_work_ous",
2722     api_name => "open-ils.actor.user.get_work_ous.ids",
2723     notes    => "Retrieve a user's work org units."
2724 );
2725
2726 sub get_user_work_ous {
2727     my( $self, $client, $auth, $userid ) = @_;
2728     my $e = new_editor(authtoken=>$auth);
2729     return $e->event unless $e->checkauth;
2730     $userid ||= $e->requestor->id;
2731
2732     if($e->requestor->id != $userid) {
2733         my $user = $e->retrieve_actor_user($userid)
2734             or return $e->event;
2735         return $e->event unless $e->allowed('ASSIGN_WORK_ORG_UNIT', $user->home_ou);
2736     }
2737
2738     return $e->search_permission_usr_work_ou_map({usr => $userid})
2739         unless $self->api_name =~ /.ids$/;
2740
2741     # client just wants a list of org IDs
2742     return $U->get_user_work_ou_ids($e, $userid);
2743 }
2744
2745
2746
2747 __PACKAGE__->register_method(
2748     method    => 'register_workstation',
2749     api_name  => 'open-ils.actor.workstation.register.override',
2750     signature => q/@see open-ils.actor.workstation.register/
2751 );
2752
2753 __PACKAGE__->register_method(
2754     method    => 'register_workstation',
2755     api_name  => 'open-ils.actor.workstation.register',
2756     signature => q/
2757         Registers a new workstion in the system
2758         @param authtoken The login session key
2759         @param name The name of the workstation id
2760         @param owner The org unit that owns this workstation
2761         @return The workstation id on success, WORKSTATION_NAME_EXISTS
2762         if the name is already in use.
2763     /
2764 );
2765
2766 sub register_workstation {
2767     my( $self, $conn, $authtoken, $name, $owner, $oargs ) = @_;
2768
2769     my $e = new_editor(authtoken=>$authtoken, xact=>1);
2770     return $e->die_event unless $e->checkauth;
2771     return $e->die_event unless $e->allowed('REGISTER_WORKSTATION', $owner);
2772     my $existing = $e->search_actor_workstation({name => $name})->[0];
2773     $oargs = { all => 1 } unless defined $oargs;
2774
2775     if( $existing ) {
2776
2777         if( $self->api_name =~ /override/o && ($oargs->{all} || grep { $_ eq 'WORKSTATION_NAME_EXISTS' } @{$oargs->{events}}) ) {
2778             # workstation with the given name exists.
2779
2780             if($owner ne $existing->owning_lib) {
2781                 # if necessary, update the owning_lib of the workstation
2782
2783                 $logger->info("changing owning lib of workstation ".$existing->id.
2784                     " from ".$existing->owning_lib." to $owner");
2785                 return $e->die_event unless
2786                     $e->allowed('UPDATE_WORKSTATION', $existing->owning_lib);
2787
2788                 return $e->die_event unless $e->allowed('UPDATE_WORKSTATION', $owner);
2789
2790                 $existing->owning_lib($owner);
2791                 return $e->die_event unless $e->update_actor_workstation($existing);
2792
2793                 $e->commit;
2794
2795             } else {
2796                 $logger->info(
2797                     "attempt to register an existing workstation.  returning existing ID");
2798             }
2799
2800             return $existing->id;
2801
2802         } else {
2803             return OpenILS::Event->new('WORKSTATION_NAME_EXISTS')
2804         }
2805     }
2806
2807     my $ws = Fieldmapper::actor::workstation->new;
2808     $ws->owning_lib($owner);
2809     $ws->name($name);
2810     $e->create_actor_workstation($ws) or return $e->die_event;
2811     $e->commit;
2812     return $ws->id; # note: editor sets the id on the new object for us
2813 }
2814
2815 __PACKAGE__->register_method(
2816     method    => 'workstation_list',
2817     api_name  => 'open-ils.actor.workstation.list',
2818     signature => q/
2819         Returns a list of workstations registered at the given location
2820         @param authtoken The login session key
2821         @param ids A list of org_unit.id's for the workstation owners
2822     /
2823 );
2824
2825 sub workstation_list {
2826     my( $self, $conn, $authtoken, @orgs ) = @_;
2827
2828     my $e = new_editor(authtoken=>$authtoken);
2829     return $e->event unless $e->checkauth;
2830     my %results;
2831
2832     for my $o (@orgs) {
2833         return $e->event
2834             unless $e->allowed('REGISTER_WORKSTATION', $o);
2835         $results{$o} = $e->search_actor_workstation({owning_lib=>$o});
2836     }
2837     return \%results;
2838 }
2839
2840 __PACKAGE__->register_method(
2841     method        => 'fetch_patron_messages',
2842     api_name      => 'open-ils.actor.message.retrieve',
2843     authoritative => 1,
2844     signature     => q/
2845         Returns a list of notes for a given user, not
2846         including ones marked deleted
2847         @param authtoken The login session key
2848         @param patronid patron ID
2849         @param options hash containing optional limit and offset
2850     /
2851 );
2852
2853 sub fetch_patron_messages {
2854     my( $self, $conn, $auth, $patronid, $options ) = @_;
2855
2856     $options ||= {};
2857
2858     my $e = new_editor(authtoken => $auth);
2859     return $e->die_event unless $e->checkauth;
2860
2861     if ($e->requestor->id ne $patronid) {
2862         return $e->die_event unless $e->allowed('VIEW_USER');
2863     }
2864
2865     my $select_clause = { usr => $patronid };
2866     my $options_clause = { order_by => { aum => 'create_date DESC' } };
2867     $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'};
2868     $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'};
2869
2870     my $aum = $e->search_actor_usr_message([ $select_clause, $options_clause ]);
2871     return $aum;
2872 }
2873
2874
2875 __PACKAGE__->register_method(
2876     method    => 'usrname_exists',
2877     api_name  => 'open-ils.actor.username.exists',
2878     signature => {
2879         desc  => 'Check if a username is already taken (by an undeleted patron)',
2880         param => [
2881             {desc => 'Authentication token', type => 'string'},
2882             {desc => 'Username',             type => 'string'}
2883         ],
2884         return => {
2885             desc => 'id of existing user if username exists, undef otherwise.  Event on error'
2886         },
2887     }
2888 );
2889
2890 sub usrname_exists {
2891     my( $self, $conn, $auth, $usrname ) = @_;
2892     my $e = new_editor(authtoken=>$auth);
2893     return $e->event unless $e->checkauth;
2894     my $a = $e->search_actor_user({usrname => $usrname}, {idlist=>1});
2895     return $$a[0] if $a and @$a;
2896     return undef;
2897 }
2898
2899 __PACKAGE__->register_method(
2900     method        => 'barcode_exists',
2901     api_name      => 'open-ils.actor.barcode.exists',
2902     authoritative => 1,
2903     signature     => 'Returns 1 if the requested barcode exists, returns 0 otherwise'
2904 );
2905
2906 sub barcode_exists {
2907     my( $self, $conn, $auth, $barcode ) = @_;
2908     my $e = new_editor(authtoken=>$auth);
2909     return $e->event unless $e->checkauth;
2910     my $card = $e->search_actor_card({barcode => $barcode});
2911     if (@$card) {
2912         return 1;
2913     } else {
2914         return 0;
2915     }
2916     #return undef unless @$card;
2917     #return $card->[0]->usr;
2918 }
2919
2920
2921 __PACKAGE__->register_method(
2922     method   => 'retrieve_net_levels',
2923     api_name => 'open-ils.actor.net_access_level.retrieve.all',
2924 );
2925
2926 sub retrieve_net_levels {
2927     my( $self, $conn, $auth ) = @_;
2928     my $e = new_editor(authtoken=>$auth);
2929     return $e->event unless $e->checkauth;
2930     return $e->retrieve_all_config_net_access_level();
2931 }
2932
2933 # Retain the old typo API name just in case
2934 __PACKAGE__->register_method(
2935     method   => 'fetch_org_by_shortname',
2936     api_name => 'open-ils.actor.org_unit.retrieve_by_shorname',
2937 );
2938 __PACKAGE__->register_method(
2939     method   => 'fetch_org_by_shortname',
2940     api_name => 'open-ils.actor.org_unit.retrieve_by_shortname',
2941 );
2942 sub fetch_org_by_shortname {
2943     my( $self, $conn, $sname ) = @_;
2944     my $e = new_editor();
2945     my $org = $e->search_actor_org_unit({ shortname => uc($sname)})->[0];
2946     return $e->event unless $org;
2947     return $org;
2948 }
2949
2950
2951 __PACKAGE__->register_method(
2952     method   => 'session_home_lib',
2953     api_name => 'open-ils.actor.session.home_lib',
2954 );
2955
2956 sub session_home_lib {
2957     my( $self, $conn, $auth ) = @_;
2958     my $e = new_editor(authtoken=>$auth);
2959     return undef unless $e->checkauth;
2960     my $org = $e->retrieve_actor_org_unit($e->requestor->home_ou);
2961     return $org->shortname;
2962 }
2963
2964 __PACKAGE__->register_method(
2965     method    => 'session_safe_token',
2966     api_name  => 'open-ils.actor.session.safe_token',
2967     signature => q/
2968         Returns a hashed session ID that is safe for export to the world.
2969         This safe token will expire after 1 hour of non-use.
2970         @param auth Active authentication token
2971     /
2972 );
2973
2974 sub session_safe_token {
2975     my( $self, $conn, $auth ) = @_;
2976     my $e = new_editor(authtoken=>$auth);
2977     return undef unless $e->checkauth;
2978
2979     my $safe_token = md5_hex($auth);
2980
2981     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
2982
2983     # add more user fields as needed
2984     $cache->put_cache(
2985         "safe-token-user-$safe_token", {
2986             id => $e->requestor->id,
2987             home_ou_shortname => $e->retrieve_actor_org_unit(
2988                 $e->requestor->home_ou)->shortname,
2989         },
2990         60 * 60
2991     );
2992
2993     return $safe_token;
2994 }
2995
2996
2997 __PACKAGE__->register_method(
2998     method    => 'safe_token_home_lib',
2999     api_name  => 'open-ils.actor.safe_token.home_lib.shortname',
3000     signature => q/
3001         Returns the home library shortname from the session
3002         asscociated with a safe token from generated by
3003         open-ils.actor.session.safe_token.
3004         @param safe_token Active safe token
3005         @param who Optional user activity "ewho" value
3006     /
3007 );
3008
3009 sub safe_token_home_lib {
3010     my( $self, $conn, $safe_token, $who ) = @_;
3011     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
3012
3013     my $blob = $cache->get_cache("safe-token-user-$safe_token");
3014     return unless $blob;
3015
3016     $U->log_user_activity($blob->{id}, $who, 'verify');
3017     return $blob->{home_ou_shortname};
3018 }
3019
3020
3021 __PACKAGE__->register_method(
3022     method   => "update_penalties",
3023     api_name => "open-ils.actor.user.penalties.update"
3024 );
3025
3026 sub update_penalties {
3027     my($self, $conn, $auth, $user_id) = @_;
3028     my $e = new_editor(authtoken=>$auth, xact => 1);
3029     return $e->die_event unless $e->checkauth;
3030     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3031     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3032     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $e->requestor->ws_ou);
3033     return $evt if $evt;
3034     $e->commit;
3035     return 1;
3036 }
3037
3038
3039 __PACKAGE__->register_method(
3040     method   => "apply_penalty",
3041     api_name => "open-ils.actor.user.penalty.apply"
3042 );
3043
3044 sub apply_penalty {
3045     my($self, $conn, $auth, $penalty, $msg) = @_;
3046
3047     $msg ||= {};
3048
3049     my $e = new_editor(authtoken=>$auth, xact => 1);
3050     return $e->die_event unless $e->checkauth;
3051
3052     my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
3053     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3054
3055     my $ptype = $e->retrieve_config_standing_penalty($penalty->standing_penalty) or return $e->die_event;
3056
3057     my $ctx_org = $penalty->org_unit; # csp org_depth is now considered in the UI for the org drop-down menu
3058
3059     if (($msg->{title} || $msg->{message}) && ($msg->{title} ne '' || $msg->{message} ne '')) {
3060         my $aum = Fieldmapper::actor::usr_message->new;
3061
3062         $aum->create_date('now');
3063         $aum->sending_lib($e->requestor->ws_ou);
3064         $aum->title($msg->{title});
3065         $aum->usr($penalty->usr);
3066         $aum->message($msg->{message});
3067         $aum->pub($msg->{pub});
3068
3069         $aum = $e->create_actor_usr_message($aum)
3070             or return $e->die_event;
3071
3072         $penalty->usr_message($aum->id);
3073     }
3074
3075     $penalty->org_unit($ctx_org);
3076     $penalty->staff($e->requestor->id);
3077     $e->create_actor_user_standing_penalty($penalty) or return $e->die_event;
3078
3079     $e->commit;
3080     return $penalty->id;
3081 }
3082
3083 __PACKAGE__->register_method(
3084     method   => "modify_penalty",
3085     api_name => "open-ils.actor.user.penalty.modify"
3086 );
3087
3088 sub modify_penalty {
3089     my($self, $conn, $auth, $penalty, $usr_msg) = @_;
3090
3091     my $e = new_editor(authtoken=>$auth, xact => 1);
3092     return $e->die_event unless $e->checkauth;
3093
3094     my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
3095     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3096
3097     $usr_msg->editor($e->requestor->id);
3098     $usr_msg->edit_date('now');
3099
3100     if ($usr_msg->isnew) {
3101         $usr_msg = $e->create_actor_usr_message($usr_msg)
3102             or return $e->die_event;
3103         $penalty->usr_message($usr_msg->id);
3104     } else {
3105         $usr_msg = $e->update_actor_usr_message($usr_msg)
3106             or return $e->die_event;
3107     }
3108
3109     if ($penalty->isnew) {
3110         $penalty = $e->create_actor_user_standing_penalty($penalty)
3111             or return $e->die_event;
3112     } else {
3113         $penalty = $e->update_actor_user_standing_penalty($penalty)
3114             or return $e->die_event;
3115     }
3116
3117     $e->commit;
3118     return 1;
3119 }
3120
3121 __PACKAGE__->register_method(
3122     method   => "remove_penalty",
3123     api_name => "open-ils.actor.user.penalty.remove"
3124 );
3125
3126 sub remove_penalty {
3127     my($self, $conn, $auth, $penalty) = @_;
3128     my $e = new_editor(authtoken=>$auth, xact => 1);
3129     return $e->die_event unless $e->checkauth;
3130     my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
3131     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3132
3133     $e->delete_actor_user_standing_penalty($penalty) or return $e->die_event;
3134     $e->commit;
3135     return 1;
3136 }
3137
3138 __PACKAGE__->register_method(
3139     method   => "update_penalty_note",
3140     api_name => "open-ils.actor.user.penalty.note.update"
3141 );
3142
3143 sub update_penalty_note {
3144     my($self, $conn, $auth, $penalty_ids, $note) = @_;
3145     my $e = new_editor(authtoken=>$auth, xact => 1);
3146     return $e->die_event unless $e->checkauth;
3147     for my $penalty_id (@$penalty_ids) {
3148         my $penalty = $e->search_actor_user_standing_penalty([
3149             { id => $penalty_id },
3150             {   flesh => 1,
3151                 flesh_fields => {aum => ['usr_message']}
3152             }
3153         ])->[0];
3154         if (! $penalty ) { return $e->die_event; }
3155         my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
3156         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3157
3158         my $aum = $penalty->usr_message();
3159         if (!$aum) {
3160             $aum = Fieldmapper::actor::usr_message->new;
3161
3162             $aum->create_date('now');
3163             $aum->sending_lib($e->requestor->ws_ou);
3164             $aum->title('');
3165             $aum->usr($penalty->usr);
3166             $aum->message($note);
3167             $aum->pub(0);
3168             $aum->isnew(1);
3169
3170             $aum = $e->create_actor_usr_message($aum)
3171                 or return $e->die_event;
3172
3173             $penalty->usr_message($aum->id);
3174             $penalty->ischanged(1);
3175             $e->update_actor_user_standing_penalty($penalty) or return $e->die_event;
3176         } else {
3177             $aum = $e->retrieve_actor_usr_message($aum) or return $e->die_event;
3178             $aum->message($note); $aum->ischanged(1);
3179             $e->update_actor_usr_message($aum) or return $e->die_event;
3180         }
3181     }
3182     $e->commit;
3183     return 1;
3184 }
3185
3186 __PACKAGE__->register_method(
3187     method   => "ranged_penalty_thresholds",
3188     api_name => "open-ils.actor.grp_penalty_threshold.ranged.retrieve",
3189     stream   => 1
3190 );
3191
3192 sub ranged_penalty_thresholds {
3193     my($self, $conn, $auth, $context_org) = @_;
3194     my $e = new_editor(authtoken=>$auth);
3195     return $e->event unless $e->checkauth;
3196     return $e->event unless $e->allowed('VIEW_GROUP_PENALTY_THRESHOLD', $context_org);
3197     my $list = $e->search_permission_grp_penalty_threshold([
3198         {org_unit => $U->get_org_ancestors($context_org)},
3199         {order_by => {pgpt => 'id'}}
3200     ]);
3201     $conn->respond($_) for @$list;
3202     return undef;
3203 }
3204
3205
3206
3207 __PACKAGE__->register_method(
3208     method        => "user_retrieve_fleshed_by_id",
3209     authoritative => 1,
3210     api_name      => "open-ils.actor.user.fleshed.retrieve",
3211 );
3212
3213 sub user_retrieve_fleshed_by_id {
3214     my( $self, $client, $auth, $user_id, $fields ) = @_;
3215     my $e = new_editor(authtoken => $auth);
3216     return $e->event unless $e->checkauth;
3217
3218     if( $e->requestor->id != $user_id ) {
3219         return $e->event unless $e->allowed('VIEW_USER');
3220     }
3221
3222     $fields ||= [
3223         "cards",
3224         "card",
3225         "groups",
3226         "standing_penalties",
3227         "settings",
3228         "addresses",
3229         "billing_address",
3230         "mailing_address",
3231         "stat_cat_entries",
3232         "waiver_entries",
3233         "usr_activity" ];
3234     return new_flesh_user($user_id, $fields, $e);
3235 }
3236
3237
3238 sub new_flesh_user {
3239
3240     my $id = shift;
3241     my $fields = shift || [];
3242     my $e = shift;
3243
3244     my $fetch_penalties = 0;
3245     if(grep {$_ eq 'standing_penalties'} @$fields) {
3246         $fields = [grep {$_ ne 'standing_penalties'} @$fields];
3247         $fetch_penalties = 1;
3248     }
3249
3250     my $fetch_notes = 0;
3251     if(grep {$_ eq 'notes'} @$fields) {
3252         $fields = [grep {$_ ne 'notes'} @$fields];
3253         $fetch_notes = 1;
3254     }
3255
3256     my $fetch_usr_act = 0;
3257     if(grep {$_ eq 'usr_activity'} @$fields) {
3258         $fields = [grep {$_ ne 'usr_activity'} @$fields];
3259         $fetch_usr_act = 1;
3260     }
3261
3262     my $user = $e->retrieve_actor_user(
3263     [
3264         $id,
3265         {
3266             "flesh"             => 1,
3267             "flesh_fields" =>  { "au" => $fields }
3268         }
3269     ]
3270     ) or return $e->die_event;
3271
3272
3273     if( grep { $_ eq 'addresses' } @$fields ) {
3274
3275         $user->addresses([]) unless @{$user->addresses};
3276         # don't expose "replaced" addresses by default
3277         $user->addresses([grep {$_->id >= 0} @{$user->addresses}]);
3278
3279         if( ref $user->billing_address ) {
3280             unless( grep { $user->billing_address->id == $_->id } @{$user->addresses} ) {
3281                 push( @{$user->addresses}, $user->billing_address );
3282             }
3283         }
3284
3285         if( ref $user->mailing_address ) {
3286             unless( grep { $user->mailing_address->id == $_->id } @{$user->addresses} ) {
3287                 push( @{$user->addresses}, $user->mailing_address );
3288             }
3289         }
3290     }
3291
3292     if($fetch_penalties) {
3293         # grab the user penalties ranged for this location
3294         $user->standing_penalties(
3295             $e->search_actor_user_standing_penalty([
3296                 {   usr => $id,
3297                     '-or' => [
3298                         {stop_date => undef},
3299                         {stop_date => {'>' => 'now'}}
3300                     ],
3301                     org_unit => $U->get_org_full_path($e->requestor->ws_ou)
3302                 },
3303                 {   flesh => 1,
3304                     flesh_fields => {ausp => ['standing_penalty','usr_message']}
3305                 }
3306             ])
3307         );
3308     }
3309
3310     if($fetch_notes) {
3311         # grab notes (now actor.usr_message_penalty) that have not hit their stop_date
3312         # NOTE: This is a view that already filters out deleted messages that are not
3313         # attached to a penalty
3314         $user->notes([
3315             @{ $e->search_actor_usr_message_penalty([
3316                 {   usr => $id,
3317                     '-or' => [
3318                         {stop_date => undef},
3319                         {stop_date => {'>' => 'now'}}
3320                     ],
3321                 }, {}
3322             ]) }
3323         ]);
3324     }
3325
3326     # retrieve the most recent usr_activity entry
3327     if ($fetch_usr_act) {
3328
3329         # max number to return for simple patron fleshing
3330         my $limit = $U->ou_ancestor_setting_value(
3331             $e->requestor->ws_ou,
3332             'circ.patron.usr_activity_retrieve.max');
3333
3334         my $opts = {
3335             flesh => 1,
3336             flesh_fields => {auact => ['etype']},
3337             order_by => {auact => 'event_time DESC'},
3338         };
3339
3340         # 0 == none, <0 == return all
3341         $limit = 1 unless defined $limit;
3342         $opts->{limit} = $limit if $limit > 0;
3343
3344         $user->usr_activity(
3345             ($limit == 0) ?
3346                 [] : # skip the DB call
3347                 $e->search_actor_usr_activity([{usr => $user->id}, $opts])
3348         );
3349     }
3350
3351     $e->rollback;
3352     $user->clear_passwd();
3353     return $user;
3354 }
3355
3356
3357
3358
3359 __PACKAGE__->register_method(
3360     method   => "user_retrieve_parts",
3361     api_name => "open-ils.actor.user.retrieve.parts",
3362 );
3363
3364 sub user_retrieve_parts {
3365     my( $self, $client, $auth, $user_id, $fields ) = @_;
3366     my $e = new_editor(authtoken => $auth);
3367     return $e->event unless $e->checkauth;
3368     $user_id ||= $e->requestor->id;
3369     if( $e->requestor->id != $user_id ) {
3370         return $e->event unless $e->allowed('VIEW_USER');
3371     }
3372     my @resp;
3373     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3374     push(@resp, $user->$_()) for(@$fields);
3375     return \@resp;
3376 }
3377
3378
3379
3380 __PACKAGE__->register_method(
3381     method    => 'user_opt_in_enabled',
3382     api_name  => 'open-ils.actor.user.org_unit_opt_in.enabled',
3383     signature => '@return 1 if user opt-in is globally enabled, 0 otherwise.'
3384 );
3385
3386 sub user_opt_in_enabled {
3387     my($self, $conn) = @_;
3388     my $sc = OpenSRF::Utils::SettingsClient->new;
3389     return 1 if lc($sc->config_value(share => user => 'opt_in')) eq 'true';
3390     return 0;
3391 }
3392
3393
3394 __PACKAGE__->register_method(
3395     method    => 'user_opt_in_at_org',
3396     api_name  => 'open-ils.actor.user.org_unit_opt_in.check',
3397     signature => q/
3398         @param $auth The auth token
3399         @param user_id The ID of the user to test
3400         @return 1 if the user has opted in at the specified org,
3401             2 if opt-in is disallowed for the user's home org,
3402             event on error, and 0 otherwise. /
3403 );
3404 sub user_opt_in_at_org {
3405     my($self, $conn, $auth, $user_id) = @_;
3406
3407     # see if we even need to enforce the opt-in value
3408     return 1 unless user_opt_in_enabled($self);
3409
3410     my $e = new_editor(authtoken => $auth);
3411     return $e->event unless $e->checkauth;
3412
3413     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3414     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3415
3416     my $ws_org = $e->requestor->ws_ou;
3417     # user is automatically opted-in if they are from the local org
3418     return 1 if $user->home_ou eq $ws_org;
3419
3420     # get the boundary setting
3421     my $opt_boundary = $U->ou_ancestor_setting_value($e->requestor->ws_ou,'org.patron_opt_boundary');
3422
3423     # auto opt in if user falls within the opt boundary
3424     my $opt_orgs = $U->get_org_descendants($ws_org, $opt_boundary);
3425
3426     return 1 if grep $_ eq $user->home_ou, @$opt_orgs;
3427
3428     # check whether opt-in is restricted at the user's home library
3429     my $opt_restrict_depth = $U->ou_ancestor_setting_value($user->home_ou, 'org.restrict_opt_to_depth');
3430     if ($opt_restrict_depth) {
3431         my $restrict_ancestor = $U->org_unit_ancestor_at_depth($user->home_ou, $opt_restrict_depth);
3432         my $unrestricted_orgs = $U->get_org_descendants($restrict_ancestor);
3433
3434         # opt-in is disallowed unless the workstation org is within the home
3435         # library's opt-in scope
3436         return 2 unless grep $_ eq $e->requestor->ws_ou, @$unrestricted_orgs;
3437     }
3438
3439     my $vals = $e->search_actor_usr_org_unit_opt_in(
3440         {org_unit=>$opt_orgs, usr=>$user_id},{idlist=>1});
3441
3442     return 1 if @$vals;
3443     return 0;
3444 }
3445
3446 __PACKAGE__->register_method(
3447     method    => 'create_user_opt_in_at_org',
3448     api_name  => 'open-ils.actor.user.org_unit_opt_in.create',
3449     signature => q/
3450         @param $auth The auth token
3451         @param user_id The ID of the user to test
3452         @return The ID of the newly created object, event on error./
3453 );
3454
3455 sub create_user_opt_in_at_org {
3456     my($self, $conn, $auth, $user_id, $org_id) = @_;
3457
3458     my $e = new_editor(authtoken => $auth, xact=>1);
3459     return $e->die_event unless $e->checkauth;
3460
3461     # if a specific org unit wasn't passed in, get one based on the defaults;
3462     if(!$org_id){
3463         my $wsou = $e->requestor->ws_ou;
3464         # get the default opt depth
3465         my $opt_depth = $U->ou_ancestor_setting_value($wsou,'org.patron_opt_default');
3466         # get the org unit at that depth
3467         my $org = $e->json_query({
3468             from => [ 'actor.org_unit_ancestor_at_depth', $wsou, $opt_depth ]})->[0];
3469         $org_id = $org->{id};
3470     }
3471     if (!$org_id) {
3472         # fall back to the workstation OU, the pre-opt-in-boundary way
3473         $org_id = $e->requestor->ws_ou;
3474     }
3475
3476     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3477     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3478
3479     my $opt_in = Fieldmapper::actor::usr_org_unit_opt_in->new;
3480
3481     $opt_in->org_unit($org_id);
3482     $opt_in->usr($user_id);
3483     $opt_in->staff($e->requestor->id);
3484     $opt_in->opt_in_ts('now');
3485     $opt_in->opt_in_ws($e->requestor->wsid);
3486
3487     $opt_in = $e->create_actor_usr_org_unit_opt_in($opt_in)
3488         or return $e->die_event;
3489
3490     $e->commit;
3491
3492     return $opt_in->id;
3493 }
3494
3495
3496 __PACKAGE__->register_method (
3497     method      => 'retrieve_org_hours',
3498     api_name    => 'open-ils.actor.org_unit.hours_of_operation.retrieve',
3499     signature   => q/
3500         Returns the hours of operation for a specified org unit
3501         @param authtoken The login session key
3502         @param org_id The org_unit ID
3503     /
3504 );
3505
3506 sub retrieve_org_hours {
3507     my($self, $conn, $auth, $org_id) = @_;
3508     my $e = new_editor(authtoken => $auth);
3509     return $e->die_event unless $e->checkauth;
3510     $org_id ||= $e->requestor->ws_ou;
3511     return $e->retrieve_actor_org_unit_hours_of_operation($org_id);
3512 }
3513
3514
3515 __PACKAGE__->register_method (
3516     method      => 'verify_user_password',
3517     api_name    => 'open-ils.actor.verify_user_password',
3518     signature   => q/
3519         Given a barcode or username and the MD5 encoded password,
3520         The password can also be passed without the MD5 hashing.
3521         returns 1 if the password is correct.  Returns 0 otherwise.
3522     /
3523 );
3524
3525 sub verify_user_password {
3526     my($self, $conn, $auth, $barcode, $username, $password, $pass_nohash) = @_;
3527     my $e = new_editor(authtoken => $auth);
3528     return $e->die_event unless $e->checkauth;
3529     my $user;
3530     my $user_by_barcode;
3531     my $user_by_username;
3532     if($barcode) {
3533         my $card = $e->search_actor_card([
3534             {barcode => $barcode},
3535             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0] or return 0;
3536         $user_by_barcode = $card->usr;
3537         $user = $user_by_barcode;
3538     }
3539     if ($username) {
3540         $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return 0;
3541         $user = $user_by_username;
3542     }
3543     return 0 if (!$user || $U->is_true($user->deleted));
3544     return 0 if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3545     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3546
3547     if ($pass_nohash) {
3548         return $U->verify_migrated_user_password($e, $user->id, $pass_nohash);
3549     } else {
3550         return $U->verify_migrated_user_password($e, $user->id, $password, 1);
3551     }
3552 }
3553
3554 __PACKAGE__->register_method (
3555     method      => 'retrieve_usr_id_via_barcode_or_usrname',
3556     api_name    => "open-ils.actor.user.retrieve_id_by_barcode_or_username",
3557     signature   => q/
3558         Given a barcode or username returns the id for the user or
3559         a failure event.
3560     /
3561 );
3562
3563 sub retrieve_usr_id_via_barcode_or_usrname {
3564     my($self, $conn, $auth, $barcode, $username) = @_;
3565     my $e = new_editor(authtoken => $auth);
3566     return $e->die_event unless $e->checkauth;
3567     my $id_as_barcode= OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.actor' => app_settings => 'id_as_barcode');
3568     my $user;
3569     my $user_by_barcode;
3570     my $user_by_username;
3571     $logger->info("$id_as_barcode is the ID as BARCODE");
3572     if($barcode) {
3573         my $card = $e->search_actor_card([
3574             {barcode => $barcode},
3575             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
3576         if ($id_as_barcode =~ /^t/i) {
3577             if (!$card) {
3578                 $user = $e->retrieve_actor_user($barcode);
3579                 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$user);
3580             }else {
3581                 $user_by_barcode = $card->usr;
3582                 $user = $user_by_barcode;
3583             }
3584         }else {
3585             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$card);
3586             $user_by_barcode = $card->usr;
3587             $user = $user_by_barcode;
3588         }
3589     }
3590
3591     if ($username) {
3592         $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return OpenILS::Event->new( 'ACTOR_USR_NOT_FOUND' );
3593
3594         $user = $user_by_username;
3595     }
3596     return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if (!$user);
3597     return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3598     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3599     return $user->id;
3600 }
3601
3602
3603 __PACKAGE__->register_method (
3604     method      => 'merge_users',
3605     api_name    => 'open-ils.actor.user.merge',
3606     signature   => {
3607         desc => q/
3608             Given a list of source users and destination user, transfer all data from the source
3609             to the dest user and delete the source user.  All user related data is
3610             transferred, including circulations, holds, bookbags, etc.
3611         /
3612     }
3613 );
3614
3615 sub merge_users {
3616     my($self, $conn, $auth, $master_id, $user_ids, $options) = @_;
3617     my $e = new_editor(xact => 1, authtoken => $auth);
3618     return $e->die_event unless $e->checkauth;
3619
3620     # disallow the merge if any subordinate accounts are in collections
3621     my $colls = $e->search_money_collections_tracker({usr => $user_ids}, {idlist => 1});
3622     return OpenILS::Event->new('MERGED_USER_IN_COLLECTIONS', payload => $user_ids) if @$colls;
3623
3624     return OpenILS::Event->new('MERGE_SELF_NOT_ALLOWED')
3625         if $master_id == $e->requestor->id;
3626
3627     my $master_user = $e->retrieve_actor_user($master_id) or return $e->die_event;
3628     my $evt = group_perm_failed($e, $e->requestor, $master_user);
3629     return $evt if $evt;
3630
3631     my $del_addrs = ($U->ou_ancestor_setting_value(
3632         $master_user->home_ou, 'circ.user_merge.delete_addresses', $e)) ? 't' : 'f';
3633     my $del_cards = ($U->ou_ancestor_setting_value(
3634         $master_user->home_ou, 'circ.user_merge.delete_cards', $e)) ? 't' : 'f';
3635     my $deactivate_cards = ($U->ou_ancestor_setting_value(
3636         $master_user->home_ou, 'circ.user_merge.deactivate_cards', $e)) ? 't' : 'f';
3637
3638     for my $src_id (@$user_ids) {
3639
3640         my $src_user = $e->retrieve_actor_user($src_id) or return $e->die_event;
3641         my $evt = group_perm_failed($e, $e->requestor, $src_user);
3642         return $evt if $evt;
3643
3644         return OpenILS::Event->new('MERGE_SELF_NOT_ALLOWED')
3645             if $src_id == $e->requestor->id;
3646
3647         return $e->die_event unless $e->allowed('MERGE_USERS', $src_user->home_ou);
3648         if($src_user->home_ou ne $master_user->home_ou) {
3649             return $e->die_event unless $e->allowed('MERGE_USERS', $master_user->home_ou);
3650         }
3651
3652         return $e->die_event unless
3653             $e->json_query({from => [
3654                 'actor.usr_merge',
3655                 $src_id,
3656                 $master_id,
3657                 $del_addrs,
3658                 $del_cards,
3659                 $deactivate_cards
3660             ]});
3661     }
3662
3663     $e->commit;
3664     return 1;
3665 }
3666
3667
3668 __PACKAGE__->register_method (
3669     method      => 'approve_user_address',
3670     api_name    => 'open-ils.actor.user.pending_address.approve',
3671     signature   => {
3672         desc => q/
3673         /
3674     }
3675 );
3676
3677 sub approve_user_address {
3678     my($self, $conn, $auth, $addr) = @_;
3679     my $e = new_editor(xact => 1, authtoken => $auth);
3680     return $e->die_event unless $e->checkauth;
3681     if(ref $addr) {
3682         # if the caller passes an address object, assume they want to
3683         # update it first before approving it
3684         $e->update_actor_user_address($addr) or return $e->die_event;
3685     } else {
3686         $addr = $e->retrieve_actor_user_address($addr) or return $e->die_event;
3687     }
3688     my $user = $e->retrieve_actor_user($addr->usr);
3689     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3690     my $result = $e->json_query({from => ['actor.approve_pending_address', $addr->id]})->[0]
3691         or return $e->die_event;
3692     $e->commit;
3693     return [values %$result]->[0];
3694 }
3695
3696
3697 __PACKAGE__->register_method (
3698     method      => 'retrieve_friends',
3699     api_name    => 'open-ils.actor.friends.retrieve',
3700     signature   => {
3701         desc => q/
3702             returns { confirmed: [], pending_out: [], pending_in: []}
3703             pending_out are users I'm requesting friendship with
3704             pending_in are users requesting friendship with me
3705         /
3706     }
3707 );
3708
3709 sub retrieve_friends {
3710     my($self, $conn, $auth, $user_id, $options) = @_;
3711     my $e = new_editor(authtoken => $auth);
3712     return $e->event unless $e->checkauth;
3713     $user_id ||= $e->requestor->id;
3714
3715     if($user_id != $e->requestor->id) {
3716         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3717         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3718     }
3719
3720     return OpenILS::Application::Actor::Friends->retrieve_friends(
3721         $e, $user_id, $options);
3722 }
3723
3724
3725
3726 __PACKAGE__->register_method (
3727     method      => 'apply_friend_perms',
3728     api_name    => 'open-ils.actor.friends.perms.apply',
3729     signature   => {
3730         desc => q/
3731         /
3732     }
3733 );
3734 sub apply_friend_perms {
3735     my($self, $conn, $auth, $user_id, $delegate_id, @perms) = @_;
3736     my $e = new_editor(authtoken => $auth, xact => 1);
3737     return $e->die_event unless $e->checkauth;
3738
3739     if($user_id != $e->requestor->id) {
3740         my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3741         return $e->die_event unless $e->allowed('VIEW_USER', $user->home_ou);
3742     }
3743
3744     for my $perm (@perms) {
3745         my $evt =
3746             OpenILS::Application::Actor::Friends->apply_friend_perm(
3747                 $e, $user_id, $delegate_id, $perm);
3748         return $evt if $evt;
3749     }
3750
3751     $e->commit;
3752     return 1;
3753 }
3754
3755
3756 __PACKAGE__->register_method (
3757     method      => 'update_user_pending_address',
3758     api_name    => 'open-ils.actor.user.address.pending.cud'
3759 );
3760
3761 sub update_user_pending_address {
3762     my($self, $conn, $auth, $addr) = @_;
3763     my $e = new_editor(authtoken => $auth, xact => 1);
3764     return $e->die_event unless $e->checkauth;
3765
3766     my $user = $e->retrieve_actor_user($addr->usr) or return $e->die_event;
3767     if($addr->usr != $e->requestor->id) {
3768         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3769     }
3770
3771     if($addr->isnew) {
3772         $e->create_actor_user_address($addr) or return $e->die_event;
3773     } elsif($addr->isdeleted) {
3774         $e->delete_actor_user_address($addr) or return $e->die_event;
3775     } else {
3776         $e->update_actor_user_address($addr) or return $e->die_event;
3777     }
3778
3779     $e->commit;
3780     $U->create_events_for_hook('au.updated', $user, $e->requestor->ws_ou);
3781
3782     return $addr->id;
3783 }
3784
3785
3786 __PACKAGE__->register_method (
3787     method      => 'user_events',
3788     api_name    => 'open-ils.actor.user.events.circ',
3789     stream      => 1,
3790 );
3791 __PACKAGE__->register_method (
3792     method      => 'user_events',
3793     api_name    => 'open-ils.actor.user.events.ahr',
3794     stream      => 1,
3795 );
3796
3797 sub user_events {
3798     my($self, $conn, $auth, $user_id, $filters) = @_;
3799     my $e = new_editor(authtoken => $auth);
3800     return $e->event unless $e->checkauth;
3801
3802     (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3803     my $user_field = 'usr';
3804
3805     $filters ||= {};
3806     $filters->{target} = {
3807         select => { $obj_type => ['id'] },
3808         from => $obj_type,
3809         where => {usr => $user_id}
3810     };
3811
3812     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3813     if($e->requestor->id != $user_id) {
3814         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3815     }
3816
3817     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3818     my $req = $ses->request('open-ils.trigger.events_by_target',
3819         $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3820
3821     while(my $resp = $req->recv) {
3822         my $val = $resp->content;
3823         my $tgt = $val->target;
3824
3825         if($obj_type eq 'circ') {
3826             $tgt->target_copy($e->retrieve_asset_copy($tgt->target_copy));
3827
3828         } elsif($obj_type eq 'ahr') {
3829             $tgt->current_copy($e->retrieve_asset_copy($tgt->current_copy))
3830                 if $tgt->current_copy;
3831         }
3832
3833         $conn->respond($val) if $val;
3834     }
3835
3836     return undef;
3837 }
3838
3839 __PACKAGE__->register_method (
3840     method      => 'copy_events',
3841     api_name    => 'open-ils.actor.copy.events.circ',
3842     stream      => 1,
3843 );
3844 __PACKAGE__->register_method (
3845     method      => 'copy_events',
3846     api_name    => 'open-ils.actor.copy.events.ahr',
3847     stream      => 1,
3848 );
3849
3850 sub copy_events {
3851     my($self, $conn, $auth, $copy_id, $filters) = @_;
3852     my $e = new_editor(authtoken => $auth);
3853     return $e->event unless $e->checkauth;
3854
3855     (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3856
3857     my $copy = $e->retrieve_asset_copy($copy_id) or return $e->event;
3858
3859     my $copy_field = 'target_copy';
3860     $copy_field = 'current_copy' if $obj_type eq 'ahr';
3861
3862     $filters ||= {};
3863     $filters->{target} = {
3864         select => { $obj_type => ['id'] },
3865         from => $obj_type,
3866         where => {$copy_field => $copy_id}
3867     };
3868
3869
3870     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3871     my $req = $ses->request('open-ils.trigger.events_by_target',
3872         $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3873
3874     while(my $resp = $req->recv) {
3875         my $val = $resp->content;
3876         my $tgt = $val->target;
3877
3878         my $user = $e->retrieve_actor_user($tgt->usr);
3879         if($e->requestor->id != $user->id) {
3880             return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3881         }
3882
3883         $tgt->$copy_field($copy);
3884
3885         $tgt->usr($user);
3886         $conn->respond($val) if $val;
3887     }
3888
3889     return undef;
3890 }
3891
3892
3893 __PACKAGE__->register_method (
3894     method      => 'get_itemsout_notices',
3895     api_name    => 'open-ils.actor.user.itemsout.notices',
3896     stream      => 1,
3897     argc        => 2,
3898     signature   => {
3899         desc => q/Summary counts of circulat notices/,
3900         params => [
3901             {desc => 'authtoken', type => 'string'},
3902             {desc => 'circulation identifiers', type => 'array of numbers'}
3903         ],
3904         return => q/Stream of summary objects/
3905     }
3906 );
3907
3908 sub get_itemsout_notices {
3909     my ($self, $client, $auth, $circ_ids) = @_;
3910
3911     my $e = new_editor(authtoken => $auth);
3912     return $e->event unless $e->checkauth;
3913
3914     $circ_ids = [$circ_ids] unless ref $circ_ids eq 'ARRAY';
3915
3916     for my $circ_id (@$circ_ids) {
3917         my $resp = get_itemsout_notices_impl($e, $circ_id);
3918
3919         if ($U->is_event($resp)) {
3920             $client->respond($resp);
3921             return;
3922         }
3923
3924         $client->respond({circ_id => $circ_id, %$resp});
3925     }
3926
3927     return undef;
3928 }
3929
3930
3931
3932 sub get_itemsout_notices_impl {
3933     my ($e, $circId) = @_;
3934
3935     my $requestorId = $e->requestor->id;
3936
3937     my $circ = $e->retrieve_action_circulation($circId) or return $e->event;
3938
3939     my $patronId = $circ->usr;
3940
3941     if( $patronId ne $requestorId ){
3942         my $user = $e->retrieve_actor_user($requestorId) or return $e->event;
3943         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
3944     }
3945
3946     #my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3947     #my $req = $ses->request('open-ils.trigger.events_by_target',
3948     #   'circ', {target => [$circId], event=> {state=>'complete'}});
3949     # ^ Above removed in favor of faster json_query.
3950     #
3951     # SQL:
3952     # select complete_time
3953     # from action_trigger.event atev
3954     #     JOIN action_trigger.event_definition def ON (def.id = atev.event_def)
3955     #     JOIN action_trigger.hook athook ON (athook.key = def.hook)
3956     # where hook = 'checkout.due' AND state = 'complete' and target = <circId>;
3957     #
3958
3959     my $ctx_loc = $e->requestor->ws_ou;
3960     my $exclude_courtesy_notices = $U->ou_ancestor_setting_value(
3961         $ctx_loc, 'webstaff.circ.itemsout_notice_count_excludes_courtesies');
3962
3963     my $query = {
3964             select => { atev => ["complete_time"] },
3965             from => {
3966                     atev => {
3967                             atevdef => { field => "id",fkey => "event_def"}
3968                     }
3969             },
3970             where => {
3971             "+atevdef" => { active => 't', hook => 'checkout.due' },
3972             "+atev" => { target => $circId, state => 'complete' }
3973         }
3974     };
3975
3976     if ($exclude_courtesy_notices){
3977         $query->{"where"}->{"+atevdef"}->{validator} = { "<>" => "CircIsOpen"};
3978     }
3979
3980     my %resblob = ( numNotices => 0, lastDt => undef );
3981
3982     my $res = $e->json_query($query);
3983     for my $ndate (@$res) {
3984         $resblob{numNotices}++;
3985         if( !defined $resblob{lastDt}){
3986             $resblob{lastDt} = $$ndate{complete_time};
3987         }
3988
3989         if ($resblob{lastDt} lt $$ndate{complete_time}){
3990            $resblob{lastDt} = $$ndate{complete_time};
3991         }
3992    }
3993
3994     return \%resblob;
3995 }
3996
3997 __PACKAGE__->register_method (
3998     method      => 'update_events',
3999     api_name    => 'open-ils.actor.user.event.cancel.batch',
4000     stream      => 1,
4001 );
4002 __PACKAGE__->register_method (
4003     method      => 'update_events',
4004     api_name    => 'open-ils.actor.user.event.reset.batch',
4005     stream      => 1,
4006 );
4007
4008 sub update_events {
4009     my($self, $conn, $auth, $event_ids) = @_;
4010     my $e = new_editor(xact => 1, authtoken => $auth);
4011     return $e->die_event unless $e->checkauth;
4012
4013     my $x = 1;
4014     for my $id (@$event_ids) {
4015
4016         # do a little dance to determine what user we are ultimately affecting
4017         my $event = $e->retrieve_action_trigger_event([
4018             $id,
4019             {   flesh => 2,
4020                 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
4021             }
4022         ]) or return $e->die_event;
4023
4024         my $user_id;
4025         if($event->event_def->hook->core_type eq 'circ') {
4026             $user_id = $e->retrieve_action_circulation($event->target)->usr;
4027         } elsif($event->event_def->hook->core_type eq 'ahr') {
4028             $user_id = $e->retrieve_action_hold_request($event->target)->usr;
4029         } else {
4030             return 0;
4031         }
4032
4033         my $user = $e->retrieve_actor_user($user_id);
4034         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
4035
4036         if($self->api_name =~ /cancel/) {
4037             $event->state('invalid');
4038         } elsif($self->api_name =~ /reset/) {
4039             $event->clear_start_time;
4040             $event->clear_update_time;
4041             $event->state('pending');
4042         }
4043
4044         $e->update_action_trigger_event($event) or return $e->die_event;
4045         $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
4046     }
4047
4048     $e->commit;
4049     return {complete => 1};
4050 }
4051
4052
4053 __PACKAGE__->register_method (
4054     method      => 'really_delete_user',
4055     api_name    => 'open-ils.actor.user.delete.override',
4056     signature   => q/@see open-ils.actor.user.delete/
4057 );
4058
4059 __PACKAGE__->register_method (
4060     method      => 'really_delete_user',
4061     api_name    => 'open-ils.actor.user.delete',
4062     signature   => q/
4063         It anonymizes all personally identifiable information in actor.usr. By calling actor.usr_purge_data()
4064         it also purges related data from other tables, sometimes by transferring it to a designated destination user.
4065         The usrname field (along with first_given_name and family_name) is updated to id '-PURGED-' now().
4066         dest_usr_id is only required when deleting a user that performs staff functions.
4067     /
4068 );
4069
4070 sub really_delete_user {
4071     my($self, $conn, $auth, $user_id, $dest_user_id, $oargs) = @_;
4072     my $e = new_editor(authtoken => $auth, xact => 1);
4073     return $e->die_event unless $e->checkauth;
4074     $oargs = { all => 1 } unless defined $oargs;
4075
4076     # Find all unclosed billings for for user $user_id, thereby, also checking for open circs
4077     my $open_bills = $e->json_query({
4078         select => { mbts => ['id'] },
4079         from => 'mbts',
4080         where => {
4081             xact_finish => { '=' => undef },
4082             usr => { '=' => $user_id },
4083         }
4084     }) or return $e->die_event;
4085
4086     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
4087
4088     # No deleting patrons with open billings or checked out copies, unless perm-enabled override
4089     if (@$open_bills) {
4090         return $e->die_event(OpenILS::Event->new('ACTOR_USER_DELETE_OPEN_XACTS'))
4091         unless $self->api_name =~ /override/o && ($oargs->{all} || grep { $_ eq 'ACTOR_USER_DELETE_OPEN_XACTS' } @{$oargs->{events}})
4092         && $e->allowed('ACTOR_USER_DELETE_OPEN_XACTS.override', $user->home_ou);
4093     }
4094     # No deleting yourself - UI is supposed to stop you first, though.
4095     return $e->die_event unless $e->requestor->id != $user->id;
4096     return $e->die_event unless $e->allowed('DELETE_USER', $user->home_ou);
4097     # Check if you are allowed to mess with this patron permission group at all
4098     my $evt = group_perm_failed($e, $e->requestor, $user);
4099     return $e->die_event($evt) if $evt;
4100     my $stat = $e->json_query(
4101         {from => ['actor.usr_delete', $user_id, $dest_user_id]})->[0]
4102         or return $e->die_event;
4103     $e->commit;
4104     return 1;
4105 }
4106
4107
4108 __PACKAGE__->register_method (
4109     method      => 'user_payments',
4110     api_name    => 'open-ils.actor.user.payments.retrieve',
4111     stream => 1,
4112     signature   => q/
4113         Returns all payments for a given user.  Default order is newest payments first.
4114         @param auth Authentication token
4115         @param user_id The user ID
4116         @param filters An optional hash of filters, including limit, offset, and order_by definitions
4117     /
4118 );
4119
4120 sub user_payments {
4121     my($self, $conn, $auth, $user_id, $filters) = @_;
4122     $filters ||= {};
4123
4124     my $e = new_editor(authtoken => $auth);
4125     return $e->die_event unless $e->checkauth;
4126
4127     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4128     return $e->event unless
4129         $e->requestor->id == $user_id or
4130         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
4131
4132     # Find all payments for all transactions for user $user_id
4133     my $query = {
4134         select => {mp => ['id']},
4135         from => 'mp',
4136         where => {
4137             xact => {
4138                 in => {
4139                     select => {mbt => ['id']},
4140                     from => 'mbt',
4141                     where => {usr => $user_id}
4142                 }
4143             }
4144         },
4145         order_by => [
4146             { # by default, order newest payments first
4147                 class => 'mp',
4148                 field => 'payment_ts',
4149                 direction => 'desc'
4150             }, {
4151                 # secondary sort in ID as a tie-breaker, since payments created
4152                 # within the same transaction will have identical payment_ts's
4153                 class => 'mp',
4154                 field => 'id'
4155             }
4156         ]
4157     };
4158
4159     for (qw/order_by limit offset/) {
4160         $query->{$_} = $filters->{$_} if defined $filters->{$_};
4161     }
4162
4163     if(defined $filters->{where}) {
4164         foreach (keys %{$filters->{where}}) {
4165             # don't allow the caller to expand the result set to other users
4166             $query->{where}->{$_} = $filters->{where}->{$_} unless $_ eq 'xact';
4167         }
4168     }
4169
4170     my $payment_ids = $e->json_query($query);
4171     for my $pid (@$payment_ids) {
4172         my $pay = $e->retrieve_money_payment([
4173             $pid->{id},
4174             {   flesh => 6,
4175                 flesh_fields => {
4176                     mp => ['xact'],
4177                     mbt => ['summary', 'circulation', 'grocery'],
4178                     circ => ['target_copy'],
4179                     acp => ['call_number'],
4180                     acn => ['record']
4181                 }
4182             }
4183         ]);
4184
4185         my $resp = {
4186             mp => $pay,
4187             xact_type => $pay->xact->summary->xact_type,
4188             last_billing_type => $pay->xact->summary->last_billing_type,
4189         };
4190
4191         if($pay->xact->summary->xact_type eq 'circulation') {
4192             $resp->{barcode} = $pay->xact->circulation->target_copy->barcode;
4193             $resp->{title} = $U->record_to_mvr($pay->xact->circulation->target_copy->call_number->record)->title;
4194         }
4195
4196         $pay->xact($pay->xact->id); # de-flesh
4197         $conn->respond($resp);
4198     }
4199
4200     return undef;
4201 }
4202
4203
4204
4205 __PACKAGE__->register_method (
4206     method      => 'negative_balance_users',
4207     api_name    => 'open-ils.actor.users.negative_balance',
4208     stream => 1,
4209     signature   => q/
4210         Returns all users that have an overall negative balance
4211         @param auth Authentication token
4212         @param org_id The context org unit as an ID or list of IDs.  This will be the home
4213         library of the user.  If no org_unit is specified, no org unit filter is applied
4214     /
4215 );
4216
4217 sub negative_balance_users {
4218     my($self, $conn, $auth, $org_id, $options) = @_;
4219
4220     $options ||= {};
4221     $options->{limit} = 1000 unless $options->{limit};
4222     $options->{offset} = 0 unless $options->{offset};
4223
4224     my $e = new_editor(authtoken => $auth);
4225     return $e->die_event unless $e->checkauth;
4226     return $e->die_event unless $e->allowed('VIEW_USER', $org_id);
4227
4228     my $query = {
4229         select => {
4230             mous => ['usr', 'balance_owed'],
4231             au => ['home_ou'],
4232             mbts => [
4233                 {column => 'last_billing_ts', transform => 'max', aggregate => 1},
4234                 {column => 'last_payment_ts', transform => 'max', aggregate => 1},
4235             ]
4236         },
4237         from => {
4238             mous => {
4239                 au => {
4240                     fkey => 'usr',
4241                     field => 'id',
4242                     join => {
4243                         mbts => {
4244                             key => 'id',
4245                             field => 'usr'
4246                         }
4247                     }
4248                 }
4249             }
4250         },
4251         where => {'+mous' => {balance_owed => {'<' => 0}}},
4252         offset => $options->{offset},
4253         limit => $options->{limit},
4254         order_by => [{class => 'mous', field => 'usr'}]
4255     };
4256
4257     $org_id = $U->get_org_descendants($org_id) if $options->{org_descendants};
4258
4259     $query->{from}->{mous}->{au}->{filter}->{home_ou} = $org_id if $org_id;
4260
4261     my $list = $e->json_query($query, {timeout => 600});
4262
4263     for my $data (@$list) {
4264         $conn->respond({
4265             usr => $e->retrieve_actor_user([$data->{usr}, {flesh => 1, flesh_fields => {au => ['card']}}]),
4266             balance_owed => $data->{balance_owed},
4267             last_billing_activity => max($data->{last_billing_ts}, $data->{last_payment_ts})
4268         });
4269     }
4270
4271     return undef;
4272 }
4273
4274 __PACKAGE__->register_method(
4275     method  => "request_password_reset",
4276     api_name    => "open-ils.actor.patron.password_reset.request",
4277     signature   => {
4278         desc => "Generates a UUID token usable with the open-ils.actor.patron.password_reset.commit " .
4279                 "method for changing a user's password.  The UUID token is distributed via A/T "      .
4280                 "templates (i.e. email to the user).",
4281         params => [
4282             { desc => 'user_id_type', type => 'string' },
4283             { desc => 'user_id', type => 'string' },
4284             { desc => 'optional (based on library setting) matching email address for authorizing request', type => 'string' },
4285         ],
4286         return => {desc => '1 on success, Event on error'}
4287     }
4288 );
4289 sub request_password_reset {
4290     my($self, $conn, $user_id_type, $user_id, $email) = @_;
4291
4292     # Check to see if password reset requests are already being throttled:
4293     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
4294
4295     my $e = new_editor(xact => 1);
4296     my $user;
4297
4298     # Get the user, if any, depending on the input value
4299     if ($user_id_type eq 'username') {
4300         $user = $e->search_actor_user({usrname => $user_id})->[0];
4301         if (!$user) {
4302             $e->die_event;
4303             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
4304         }
4305     } elsif ($user_id_type eq 'barcode') {
4306         my $card = $e->search_actor_card([
4307             {barcode => $user_id},
4308             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
4309         if (!$card) {
4310             $e->die_event;
4311             return OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
4312         }
4313         $user = $card->usr;
4314     }
4315
4316     # If the user doesn't have an email address, we can't help them
4317     if (!$user->email) {
4318         $e->die_event;
4319         return OpenILS::Event->new('PATRON_NO_EMAIL_ADDRESS');
4320     }
4321
4322     my $email_must_match = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_requires_matching_email');
4323     if ($email_must_match) {
4324         if (lc($user->email) ne lc($email)) {
4325             return OpenILS::Event->new('EMAIL_VERIFICATION_FAILED');
4326         }
4327     }
4328
4329     _reset_password_request($conn, $e, $user);
4330 }
4331
4332 # Once we have the user, we can issue the password reset request
4333 # XXX Add a wrapper method that accepts barcode + email input
4334 sub _reset_password_request {
4335     my ($conn, $e, $user) = @_;
4336
4337     # 1. Get throttle threshold and time-to-live from OU_settings
4338     my $aupr_throttle = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_throttle') || 1000;
4339     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4340
4341     my $threshold_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl)->iso8601();
4342
4343     # 2. Get time of last request and number of active requests (num_active)
4344     my $active_requests = $e->json_query({
4345         from => 'aupr',
4346         select => {
4347             aupr => [
4348                 {
4349                     column => 'uuid',
4350                     transform => 'COUNT'
4351                 },
4352                 {
4353                     column => 'request_time',
4354                     transform => 'MAX'
4355                 }
4356             ]
4357         },
4358         where => {
4359             has_been_reset => { '=' => 'f' },
4360             request_time => { '>' => $threshold_time }
4361         }
4362     });
4363
4364     # Guard against no active requests
4365     if ($active_requests->[0]->{'request_time'}) {
4366         my $last_request = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($active_requests->[0]->{'request_time'}));
4367         my $now = DateTime::Format::ISO8601->new();
4368
4369         # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute)
4370         if (($active_requests->[0]->{'usr'} > $aupr_throttle) &&
4371             ($last_request->add_duration('1 minute') > $now)) {
4372             $cache->put_cache('open-ils.actor.password.throttle', DateTime::Format::ISO8601->new(), 60);
4373             $e->die_event;
4374             return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4375         }
4376     }
4377
4378     # TODO Check to see if the user is in a password-reset-restricted group
4379
4380     # Otherwise, go ahead and try to get the user.
4381
4382     # Check the number of active requests for this user
4383     $active_requests = $e->json_query({
4384         from => 'aupr',
4385         select => {
4386             aupr => [
4387                 {
4388                     column => 'usr',
4389                     transform => 'COUNT'
4390                 }
4391             ]
4392         },
4393         where => {
4394             usr => { '=' => $user->id },
4395             has_been_reset => { '=' => 'f' },
4396             request_time => { '>' => $threshold_time }
4397         }
4398     });
4399
4400     $logger->info("User " . $user->id . " has " . $active_requests->[0]->{'usr'} . " active password reset requests.");
4401
4402     # if less than or equal to per-user threshold, proceed; otherwise, return event
4403     my $aupr_per_user_limit = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_per_user_limit') || 3;
4404     if ($active_requests->[0]->{'usr'} > $aupr_per_user_limit) {
4405         $e->die_event;
4406         return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4407     }
4408
4409     # Create the aupr object and insert into the database
4410     my $reset_request = Fieldmapper::actor::usr_password_reset->new;
4411     my $uuid = create_uuid_as_string(UUID_V4);
4412     $reset_request->uuid($uuid);
4413     $reset_request->usr($user->id);
4414
4415     my $aupr = $e->create_actor_usr_password_reset($reset_request) or return $e->die_event;
4416     $e->commit;
4417
4418     # Create an event to notify user of the URL to reset their password
4419
4420     # Can we stuff this in the user_data param for trigger autocreate?
4421     my $hostname = $U->ou_ancestor_setting_value($user->home_ou, 'lib.hostname') || 'localhost';
4422
4423     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
4424     $ses->request('open-ils.trigger.event.autocreate', 'password.reset_request', $aupr, $user->home_ou);
4425
4426     # Trunk only
4427     # $U->create_trigger_event('password.reset_request', $aupr, $user->home_ou);
4428
4429     return 1;
4430 }
4431
4432 __PACKAGE__->register_method(
4433     method  => "commit_password_reset",
4434     api_name    => "open-ils.actor.patron.password_reset.commit",
4435     signature   => {
4436         desc => "Checks a UUID token generated by the open-ils.actor.patron.password_reset.request method for " .
4437                 "validity, and if valid, uses it as authorization for changing the associated user's password " .
4438                 "with the supplied password.",
4439         params => [
4440             { desc => 'uuid', type => 'string' },
4441             { desc => 'password', type => 'string' },
4442         ],
4443         return => {desc => '1 on success, Event on error'}
4444     }
4445 );
4446 sub commit_password_reset {
4447     my($self, $conn, $uuid, $password) = @_;
4448
4449     # Check to see if password reset requests are already being throttled:
4450     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
4451     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
4452     my $throttle = $cache->get_cache('open-ils.actor.password.throttle') || undef;
4453     if ($throttle) {
4454         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4455     }
4456
4457     my $e = new_editor(xact => 1);
4458
4459     my $aupr = $e->search_actor_usr_password_reset({
4460         uuid => $uuid,
4461         has_been_reset => 0
4462     });
4463
4464     if (!$aupr->[0]) {
4465         $e->die_event;
4466         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4467     }
4468     my $user_id = $aupr->[0]->usr;
4469     my $user = $e->retrieve_actor_user($user_id);
4470
4471     # Ensure we're still within the TTL for the request
4472     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4473     my $threshold = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($aupr->[0]->request_time))->add(seconds => $aupr_ttl);
4474     if ($threshold < DateTime->now(time_zone => 'local')) {
4475         $e->die_event;
4476         $logger->info("Password reset request needed to be submitted before $threshold");
4477         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4478     }
4479
4480     # Check complexity of password against OU-defined regex
4481     my $pw_regex = $U->ou_ancestor_setting_value($user->home_ou, 'global.password_regex');
4482
4483     my $is_strong = 0;
4484     if ($pw_regex) {
4485         # Calling JSON2perl on the $pw_regex causes failure, even before the fancy Unicode regex
4486         # ($pw_regex = OpenSRF::Utils::JSON->JSON2perl($pw_regex)) =~ s/\\u([0-9a-fA-F]{4})/\\x{$1}/gs;
4487         $is_strong = check_password_strength_custom($password, $pw_regex);
4488     } else {
4489         $is_strong = check_password_strength_default($password);
4490     }
4491
4492     if (!$is_strong) {
4493         $e->die_event;
4494         return OpenILS::Event->new('PATRON_PASSWORD_WAS_NOT_STRONG');
4495     }
4496
4497     # All is well; update the password
4498     modify_migrated_user_password($e, $user->id, $password);
4499
4500     # And flag that this password reset request has been honoured
4501     $aupr->[0]->has_been_reset('t');
4502     $e->update_actor_usr_password_reset($aupr->[0]);
4503     $e->commit;
4504
4505     return 1;
4506 }
4507
4508 sub check_password_strength_default {
4509     my $password = shift;
4510     # Use the default set of checks
4511     if ( (length($password) < 7) or
4512             ($password !~ m/.*\d+.*/) or
4513             ($password !~ m/.*[A-Za-z]+.*/)
4514     ) {
4515         return 0;
4516     }
4517     return 1;
4518 }
4519
4520 sub check_password_strength_custom {
4521     my ($password, $pw_regex) = @_;
4522
4523     $pw_regex = qr/$pw_regex/;
4524     if ($password !~  /$pw_regex/) {
4525         return 0;
4526     }
4527     return 1;
4528 }
4529
4530 __PACKAGE__->register_method(
4531     method    => "fire_test_notification",
4532     api_name  => "open-ils.actor.event.test_notification"
4533 );
4534
4535 sub fire_test_notification {
4536     my($self, $conn, $auth, $args) = @_;
4537     my $e = new_editor(authtoken => $auth);
4538     return $e->event unless $e->checkauth;
4539     if ($e->requestor->id != $$args{target}) {
4540         my $home_ou = $e->retrieve_actor_user($$args{target})->home_ou;
4541         return $e->die_event unless $home_ou && $e->allowed('VIEW_USER', $home_ou);
4542     }
4543
4544     my $event_hook = $$args{hook} or return $e->event;
4545     return $e->event unless ($event_hook eq 'au.email.test' or $event_hook eq 'au.sms_text.test');
4546
4547     my $usr = $e->retrieve_actor_user($$args{target});
4548     return $e->event unless $usr;
4549
4550     return $U->fire_object_event(undef, $event_hook, $usr, $e->requestor->ws_ou);
4551 }
4552
4553
4554 __PACKAGE__->register_method(
4555     method    => "event_def_opt_in_settings",
4556     api_name  => "open-ils.actor.event_def.opt_in.settings",
4557     stream => 1,
4558     signature => {
4559         desc   => 'Streams the set of "cust" objects that are used as opt-in settings for event definitions',
4560         params => [
4561             { desc => 'Authentication token',  type => 'string'},
4562             {
4563                 desc => 'Org Unit ID.  (optional).  If no org ID is present, the home_ou of the requesting user is used',
4564                 type => 'number'
4565             },
4566         ],
4567         return => {
4568             desc => q/set of "cust" objects that are used as opt-in settings for event definitions at the specified org unit/,
4569             type => 'object',
4570             class => 'cust'
4571         }
4572     }
4573 );
4574
4575 sub event_def_opt_in_settings {
4576     my($self, $conn, $auth, $org_id) = @_;
4577     my $e = new_editor(authtoken => $auth);
4578     return $e->event unless $e->checkauth;
4579
4580     if(defined $org_id and $org_id != $e->requestor->home_ou) {
4581         return $e->event unless
4582             $e->allowed(['VIEW_USER_SETTING_TYPE', 'ADMIN_USER_SETTING_TYPE'], $org_id);
4583     } else {
4584         $org_id = $e->requestor->home_ou;
4585     }
4586
4587     # find all config.user_setting_type's related to event_defs for the requested org unit
4588     my $types = $e->json_query({
4589         select => {cust => ['name']},
4590         from => {atevdef => 'cust'},
4591         where => {
4592             '+atevdef' => {
4593                 owner => $U->get_org_ancestors($org_id), # context org plus parents
4594                 active => 't'
4595             }
4596         }
4597     });
4598
4599     if(@$types) {
4600         $conn->respond($_) for
4601             @{$e->search_config_usr_setting_type({name => [map {$_->{name}} @$types]})};
4602     }
4603
4604     return undef;
4605 }
4606
4607
4608 __PACKAGE__->register_method(
4609     method    => "user_circ_history",
4610     api_name  => "open-ils.actor.history.circ",
4611     stream => 1,
4612     authoritative => 1,
4613     signature => {
4614         desc   => 'Returns user circ history objects for the calling user',
4615         params => [
4616             { desc => 'Authentication token',  type => 'string'},
4617             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4618         ],
4619         return => {
4620             desc => q/Stream of 'auch' circ history objects/,
4621             type => 'object',
4622         }
4623     }
4624 );
4625
4626 __PACKAGE__->register_method(
4627     method    => "user_circ_history",
4628     api_name  => "open-ils.actor.history.circ.clear",
4629     stream => 1,
4630     signature => {
4631         desc   => 'Delete all user circ history entries for the calling user',
4632         params => [
4633             { desc => 'Authentication token',  type => 'string'},
4634             { desc => "Options hash. 'circ_ids' is an arrayref of circulation IDs to delete", type => 'object' },
4635         ],
4636         return => {
4637             desc => q/1 on success, event on error/,
4638             type => 'object',
4639         }
4640     }
4641 );
4642
4643 __PACKAGE__->register_method(
4644     method    => "user_circ_history",
4645     api_name  => "open-ils.actor.history.circ.print",
4646     stream => 1,
4647     signature => {
4648         desc   => q/Returns printable output for the caller's circ history objects/,
4649         params => [
4650             { desc => 'Authentication token',  type => 'string'},
4651             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4652         ],
4653         return => {
4654             desc => q/An action_trigger.event object or error event./,
4655             type => 'object',
4656         }
4657     }
4658 );
4659
4660 __PACKAGE__->register_method(
4661     method    => "user_circ_history",
4662     api_name  => "open-ils.actor.history.circ.email",
4663     stream => 1,
4664     signature => {
4665         desc   => q/Emails the caller's circ history/,
4666         params => [
4667             { desc => 'Authentication token',  type => 'string'},
4668             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4669             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4670         ],
4671         return => {
4672             desc => q/undef, or event on error/
4673         }
4674     }
4675 );
4676
4677 sub user_circ_history {
4678     my ($self, $conn, $auth, $options) = @_;
4679     $options ||= {};
4680
4681     my $for_print = ($self->api_name =~ /print/);
4682     my $for_email = ($self->api_name =~ /email/);
4683     my $for_clear = ($self->api_name =~ /clear/);
4684
4685     # No perm check is performed.  Caller may only access his/her own
4686     # circ history entries.
4687     my $e = new_editor(authtoken => $auth);
4688     return $e->event unless $e->checkauth;
4689
4690     my %limits = ();
4691     if (!$for_clear) { # clear deletes all
4692         $limits{offset} = $options->{offset} if defined $options->{offset};
4693         $limits{limit} = $options->{limit} if defined $options->{limit};
4694     }
4695
4696     my %circ_id_filter = $options->{circ_ids} ?
4697         (id => $options->{circ_ids}) : ();
4698
4699     my $circs = $e->search_action_user_circ_history([
4700         {   usr => $e->requestor->id,
4701             %circ_id_filter
4702         },
4703         {   # order newest to oldest by default
4704             order_by => {auch => 'xact_start DESC'},
4705             %limits
4706         },
4707         {substream => 1} # could be a large list
4708     ]);
4709
4710     if ($for_print) {
4711         return $U->fire_object_event(undef,
4712             'circ.format.history.print', $circs, $e->requestor->home_ou);
4713     }
4714
4715     $e->xact_begin if $for_clear;
4716     $conn->respond_complete(1) if $for_email;  # no sense in waiting
4717
4718     for my $circ (@$circs) {
4719
4720         if ($for_email) {
4721             # events will be fired from action_trigger_runner
4722             $U->create_events_for_hook('circ.format.history.email',
4723                 $circ, $e->editor->home_ou, undef, undef, 1);
4724
4725         } elsif ($for_clear) {
4726
4727             $e->delete_action_user_circ_history($circ)
4728                 or return $e->die_event;
4729
4730         } else {
4731             $conn->respond($circ);
4732         }
4733     }
4734
4735     if ($for_clear) {
4736         $e->commit;
4737         return 1;
4738     }
4739
4740     return undef;
4741 }
4742
4743
4744 __PACKAGE__->register_method(
4745     method    => "user_visible_holds",
4746     api_name  => "open-ils.actor.history.hold.visible",
4747     stream => 1,
4748     signature => {
4749         desc   => 'Returns the set of opt-in visible holds',
4750         params => [
4751             { desc => 'Authentication token',  type => 'string'},
4752             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4753             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4754         ],
4755         return => {
4756             desc => q/An object with 1 field: "hold"/,
4757             type => 'object',
4758         }
4759     }
4760 );
4761
4762 __PACKAGE__->register_method(
4763     method    => "user_visible_holds",
4764     api_name  => "open-ils.actor.history.hold.visible.print",
4765     stream => 1,
4766     signature => {
4767         desc   => 'Returns printable output for the set of opt-in visible holds',
4768         params => [
4769             { desc => 'Authentication token',  type => 'string'},
4770             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4771             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4772         ],
4773         return => {
4774             desc => q/An action_trigger.event object or error event./,
4775             type => 'object',
4776         }
4777     }
4778 );
4779
4780 __PACKAGE__->register_method(
4781     method    => "user_visible_holds",
4782     api_name  => "open-ils.actor.history.hold.visible.email",
4783     stream => 1,
4784     signature => {
4785         desc   => 'Emails the set of opt-in visible holds to the requestor',
4786         params => [
4787             { desc => 'Authentication token',  type => 'string'},
4788             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4789             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4790         ],
4791         return => {
4792             desc => q/undef, or event on error/
4793         }
4794     }
4795 );
4796
4797 sub user_visible_holds {
4798     my($self, $conn, $auth, $user_id, $options) = @_;
4799
4800     my $is_hold = 1;
4801     my $for_print = ($self->api_name =~ /print/);
4802     my $for_email = ($self->api_name =~ /email/);
4803     my $e = new_editor(authtoken => $auth);
4804     return $e->event unless $e->checkauth;
4805
4806     $user_id ||= $e->requestor->id;
4807     $options ||= {};
4808     $options->{limit} ||= 50;
4809     $options->{offset} ||= 0;
4810
4811     if($user_id != $e->requestor->id) {
4812         my $perm = ($is_hold) ? 'VIEW_HOLD' : 'VIEW_CIRCULATIONS';
4813         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4814         return $e->event unless $e->allowed($perm, $user->home_ou);
4815     }
4816
4817     my $db_func = ($is_hold) ? 'action.usr_visible_holds' : 'action.usr_visible_circs';
4818
4819     my $data = $e->json_query({
4820         from => [$db_func, $user_id],
4821         limit => $$options{limit},
4822         offset => $$options{offset}
4823
4824         # TODO: I only want IDs. code below didn't get me there
4825         # {"select":{"au":[{"column":"id", "result_field":"id",
4826         # "transform":"action.usr_visible_circs"}]}, "where":{"id":10}, "from":"au"}
4827     },{
4828         substream => 1
4829     });
4830
4831     return undef unless @$data;
4832
4833     if ($for_print) {
4834
4835         # collect the batch of objects
4836
4837         if($is_hold) {
4838
4839             my $hold_list = $e->search_action_hold_request({id => [map { $_->{id} } @$data]});
4840             return $U->fire_object_event(undef, 'ahr.format.history.print', $hold_list, $$hold_list[0]->request_lib);
4841
4842         } else {
4843
4844             my $circ_list = $e->search_action_circulation({id => [map { $_->{id} } @$data]});
4845             return $U->fire_object_event(undef, 'circ.format.history.print', $circ_list, $$circ_list[0]->circ_lib);
4846         }
4847
4848     } elsif ($for_email) {
4849
4850         $conn->respond_complete(1) if $for_email;  # no sense in waiting
4851
4852         foreach (@$data) {
4853
4854             my $id = $_->{id};
4855
4856             if($is_hold) {
4857
4858                 my $hold = $e->retrieve_action_hold_request($id);
4859                 $U->create_events_for_hook('ahr.format.history.email', $hold, $hold->request_lib, undef, undef, 1);
4860                 # events will be fired from action_trigger_runner
4861
4862             } else {
4863
4864                 my $circ = $e->retrieve_action_circulation($id);
4865                 $U->create_events_for_hook('circ.format.history.email', $circ, $circ->circ_lib, undef, undef, 1);
4866                 # events will be fired from action_trigger_runner
4867             }
4868         }
4869
4870     } else { # just give me the data please
4871
4872         foreach (@$data) {
4873
4874             my $id = $_->{id};
4875
4876             if($is_hold) {
4877
4878                 my $hold = $e->retrieve_action_hold_request($id);
4879                 $conn->respond({hold => $hold});
4880
4881             } else {
4882
4883                 my $circ = $e->retrieve_action_circulation($id);
4884                 $conn->respond({
4885                     circ => $circ,
4886                     summary => $U->create_circ_chain_summary($e, $id)
4887                 });
4888             }
4889         }
4890     }
4891
4892     return undef;
4893 }
4894
4895 __PACKAGE__->register_method(
4896     method     => "user_saved_search_cud",
4897     api_name   => "open-ils.actor.user.saved_search.cud",
4898     stream     => 1,
4899     signature  => {
4900         desc   => 'Create/Update/Delete Access to user saved searches',
4901         params => [
4902             { desc => 'Authentication token', type => 'string' },
4903             { desc => 'Saved Search Object', type => 'object', class => 'auss' }
4904         ],
4905         return => {
4906             desc   => q/The retrieved or updated saved search object, or id of a deleted object; Event on error/,
4907             class  => 'auss'
4908         }
4909     }
4910 );
4911
4912 __PACKAGE__->register_method(
4913     method     => "user_saved_search_cud",
4914     api_name   => "open-ils.actor.user.saved_search.retrieve",
4915     stream     => 1,
4916     signature  => {
4917         desc   => 'Retrieve a saved search object',
4918         params => [
4919             { desc => 'Authentication token', type => 'string' },
4920             { desc => 'Saved Search ID', type => 'number' }
4921         ],
4922         return => {
4923             desc   => q/The saved search object, Event on error/,
4924             class  => 'auss'
4925         }
4926     }
4927 );
4928
4929 sub user_saved_search_cud {
4930     my( $self, $client, $auth, $search ) = @_;
4931     my $e = new_editor( authtoken=>$auth );
4932     return $e->die_event unless $e->checkauth;
4933
4934     my $o_search;      # prior version of the object, if any
4935     my $res;           # to be returned
4936
4937     # branch on the operation type
4938
4939     if( $self->api_name =~ /retrieve/ ) {                    # Retrieve
4940
4941         # Get the old version, to check ownership
4942         $o_search = $e->retrieve_actor_usr_saved_search( $search )
4943             or return $e->die_event;
4944
4945         # You can't read somebody else's search
4946         return OpenILS::Event->new('BAD_PARAMS')
4947             unless $o_search->owner == $e->requestor->id;
4948
4949         $res = $o_search;
4950
4951     } else {
4952
4953         $e->xact_begin;               # start an editor transaction
4954
4955         if( $search->isnew ) {                               # Create
4956
4957             # You can't create a search for somebody else
4958             return OpenILS::Event->new('BAD_PARAMS')
4959                 unless $search->owner == $e->requestor->id;
4960
4961             $e->create_actor_usr_saved_search( $search )
4962                 or return $e->die_event;
4963
4964             $res = $search->id;
4965
4966         } elsif( $search->ischanged ) {                      # Update
4967
4968             # You can't change ownership of a search
4969             return OpenILS::Event->new('BAD_PARAMS')
4970                 unless $search->owner == $e->requestor->id;
4971
4972             # Get the old version, to check ownership
4973             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4974                 or return $e->die_event;
4975
4976             # You can't update somebody else's search
4977             return OpenILS::Event->new('BAD_PARAMS')
4978                 unless $o_search->owner == $e->requestor->id;
4979
4980             # Do the update
4981             $e->update_actor_usr_saved_search( $search )
4982                 or return $e->die_event;
4983
4984             $res = $search;
4985
4986         } elsif( $search->isdeleted ) {                      # Delete
4987
4988             # Get the old version, to check ownership
4989             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4990                 or return $e->die_event;
4991
4992             # You can't delete somebody else's search
4993             return OpenILS::Event->new('BAD_PARAMS')
4994                 unless $o_search->owner == $e->requestor->id;
4995
4996             # Do the delete
4997             $e->delete_actor_usr_saved_search( $o_search )
4998                 or return $e->die_event;
4999
5000             $res = $search->id;
5001         }
5002
5003         $e->commit;
5004     }
5005
5006     return $res;
5007 }
5008
5009 __PACKAGE__->register_method(
5010     method   => "get_barcodes",
5011     api_name => "open-ils.actor.get_barcodes"
5012 );
5013
5014 sub get_barcodes {
5015     my( $self, $client, $auth, $org_id, $context, $barcode ) = @_;
5016     my $e = new_editor(authtoken => $auth);
5017     return $e->event unless $e->checkauth;
5018     return $e->event unless $e->allowed('STAFF_LOGIN', $org_id);
5019
5020     my $db_result = $e->json_query(
5021         {   from => [
5022                 'evergreen.get_barcodes',
5023                 $org_id, $context, $barcode,
5024             ]
5025         }
5026     );
5027     if($context =~ /actor/) {
5028         my $filter_result = ();
5029         my $patron;
5030         foreach my $result (@$db_result) {
5031             if($result->{type} eq 'actor') {
5032                 if($e->requestor->id != $result->{id}) {
5033                     $patron = $e->retrieve_actor_user($result->{id});
5034                     if(!$patron) {
5035                         push(@$filter_result, $e->event);
5036                         next;
5037                     }
5038                     if($e->allowed('VIEW_USER', $patron->home_ou)) {
5039                         push(@$filter_result, $result);
5040                     }
5041                     else {
5042                         push(@$filter_result, $e->event);
5043                     }
5044                 }
5045                 else {
5046                     push(@$filter_result, $result);
5047                 }
5048             }
5049             else {
5050                 push(@$filter_result, $result);
5051             }
5052         }
5053         return $filter_result;
5054     }
5055     else {
5056         return $db_result;
5057     }
5058 }
5059 __PACKAGE__->register_method(
5060     method   => 'address_alert_test',
5061     api_name => 'open-ils.actor.address_alert.test',
5062     signature => {
5063         desc => "Tests a set of address fields to determine if they match with an address_alert",
5064         params => [
5065             {desc => 'Authentication token', type => 'string'},
5066             {desc => 'Org Unit',             type => 'number'},
5067             {desc => 'Fields',               type => 'hash'},
5068         ],
5069         return => {desc => 'List of matching address_alerts'}
5070     }
5071 );
5072
5073 sub address_alert_test {
5074     my ($self, $client, $auth, $org_unit, $fields) = @_;
5075     return [] unless $fields and grep {$_} values %$fields;
5076
5077     my $e = new_editor(authtoken => $auth);
5078     return $e->event unless $e->checkauth;
5079     return $e->event unless $e->allowed('CREATE_USER', $org_unit);
5080     $org_unit ||= $e->requestor->ws_ou;
5081
5082     my $alerts = $e->json_query({
5083         from => [
5084             'actor.address_alert_matches',
5085             $org_unit,
5086             $$fields{street1},
5087             $$fields{street2},
5088             $$fields{city},
5089             $$fields{county},
5090             $$fields{state},
5091             $$fields{country},
5092             $$fields{post_code},
5093             $$fields{mailing_address},
5094             $$fields{billing_address}
5095         ]
5096     });
5097
5098     # map the json_query hashes to real objects
5099     return [
5100         map {$e->retrieve_actor_address_alert($_)}
5101             (map {$_->{id}} @$alerts)
5102     ];
5103 }
5104
5105 __PACKAGE__->register_method(
5106     method   => "mark_users_contact_invalid",
5107     api_name => "open-ils.actor.invalidate.email",
5108     signature => {
5109         desc => "Given a patron or email address, clear the email field for one patron or all patrons with that email address and put the old email address into a note and/or create a standing penalty, depending on OU settings",
5110         params => [
5111             {desc => "Authentication token", type => "string"},
5112             {desc => "Patron ID (optional if Email address specified)", type => "number"},
5113             {desc => "Additional note text (optional)", type => "string"},
5114             {desc => "penalty org unit ID (optional)", type => "number"},
5115             {desc => "Email address (optional)", type => "string"}
5116         ],
5117         return => {desc => "Event describing success or failure", type => "object"}
5118     }
5119 );
5120
5121 __PACKAGE__->register_method(
5122     method   => "mark_users_contact_invalid",
5123     api_name => "open-ils.actor.invalidate.day_phone",
5124     signature => {
5125         desc => "Given a patron or phone number, clear the day_phone field for one patron or all patrons with that day_phone number and put the old day_phone into a note and/or create a standing penalty, depending on OU settings",
5126         params => [
5127             {desc => "Authentication token", type => "string"},
5128             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
5129             {desc => "Additional note text (optional)", type => "string"},
5130             {desc => "penalty org unit ID (optional)", type => "number"},
5131             {desc => "Phone Number (optional)", type => "string"}
5132         ],
5133         return => {desc => "Event describing success or failure", type => "object"}
5134     }
5135 );
5136
5137 __PACKAGE__->register_method(
5138     method   => "mark_users_contact_invalid",
5139     api_name => "open-ils.actor.invalidate.evening_phone",
5140     signature => {
5141         desc => "Given a patron or phone number, clear the evening_phone field for one patron or all patrons with that evening_phone number and put the old evening_phone into a note and/or create a standing penalty, depending on OU settings",
5142         params => [
5143             {desc => "Authentication token", type => "string"},
5144             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
5145             {desc => "Additional note text (optional)", type => "string"},
5146             {desc => "penalty org unit ID (optional)", type => "number"},
5147             {desc => "Phone Number (optional)", type => "string"}
5148         ],
5149         return => {desc => "Event describing success or failure", type => "object"}
5150     }
5151 );
5152
5153 __PACKAGE__->register_method(
5154     method   => "mark_users_contact_invalid",
5155     api_name => "open-ils.actor.invalidate.other_phone",
5156     signature => {
5157         desc => "Given a patron or phone number, clear the other_phone field for one patron or all patrons with that other_phone number and put the old other_phone into a note and/or create a standing penalty, depending on OU settings",
5158         params => [
5159             {desc => "Authentication token", type => "string"},
5160             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
5161             {desc => "Additional note text (optional)", type => "string"},
5162             {desc => "penalty org unit ID (optional, default to top of org tree)",
5163                 type => "number"},
5164             {desc => "Phone Number (optional)", type => "string"}
5165         ],
5166         return => {desc => "Event describing success or failure", type => "object"}
5167     }
5168 );
5169
5170 sub mark_users_contact_invalid {
5171     my ($self, $conn, $auth, $patron_id, $addl_note, $penalty_ou, $contact) = @_;
5172
5173     # This method invalidates an email address or a phone_number which
5174     # removes the bad email address or phone number, copying its contents
5175     # to a patron note, and institutes a standing penalty for "bad email"
5176     # or "bad phone number" which is cleared when the user is saved or
5177     # optionally only when the user is saved with an email address or
5178     # phone number (or staff manually delete the penalty).
5179
5180     my $contact_type = ($self->api_name =~ /invalidate.(\w+)(\.|$)/)[0];
5181
5182     my $e = new_editor(authtoken => $auth, xact => 1);
5183     return $e->die_event unless $e->checkauth;
5184     
5185     my $howfind = {};
5186     if (defined $patron_id && $patron_id ne "") {
5187         $howfind = {usr => $patron_id};
5188     } elsif (defined $contact && $contact ne "") {
5189         $howfind = {$contact_type => $contact};
5190     } else {
5191         # Error out if no patron id set or no contact is set.
5192         return OpenILS::Event->new('BAD_PARAMS');
5193     }
5194  
5195     return OpenILS::Utils::BadContact->mark_users_contact_invalid(
5196         $e, $contact_type, $howfind,
5197         $addl_note, $penalty_ou, $e->requestor->id
5198     );
5199 }
5200
5201 # Putting the following method in open-ils.actor is a bad fit, except in that
5202 # it serves an interface that lives under 'actor' in the templates directory,
5203 # and in that there's nowhere else obvious to put it (open-ils.trigger is
5204 # private).
5205 __PACKAGE__->register_method(
5206     api_name => "open-ils.actor.action_trigger.reactors.all_in_use",
5207     method   => "get_all_at_reactors_in_use",
5208     api_level=> 1,
5209     argc     => 1,
5210     signature=> {
5211         params => [
5212             { name => 'authtoken', type => 'string' }
5213         ],
5214         return => {
5215             desc => 'list of reactor names', type => 'array'
5216         }
5217     }
5218 );
5219
5220 sub get_all_at_reactors_in_use {
5221     my ($self, $conn, $auth) = @_;
5222
5223     my $e = new_editor(authtoken => $auth);
5224     $e->checkauth or return $e->die_event;
5225     return $e->die_event unless $e->allowed('VIEW_TRIGGER_EVENT_DEF');
5226
5227     my $reactors = $e->json_query({
5228         select => {
5229             atevdef => [{column => "reactor", transform => "distinct"}]
5230         },
5231         from => {atevdef => {}}
5232     });
5233
5234     return $e->die_event unless ref $reactors eq "ARRAY";
5235     $e->disconnect;
5236
5237     return [ map { $_->{reactor} } @$reactors ];
5238 }
5239
5240 __PACKAGE__->register_method(
5241     method   => "filter_group_entry_crud",
5242     api_name => "open-ils.actor.filter_group_entry.crud",
5243     signature => {
5244         desc => q/
5245             Provides CRUD access to filter group entry objects.  These are not full accessible
5246             via PCRUD, since they requre "asq" objects for storing the query, and "asq" objects
5247             are not accessible via PCRUD (because they have no fields against which to link perms)
5248             /,
5249         params => [
5250             {desc => "Authentication token", type => "string"},
5251             {desc => "Entry ID / Entry Object", type => "number"},
5252             {desc => "Additional note text (optional)", type => "string"},
5253             {desc => "penalty org unit ID (optional, default to top of org tree)",
5254                 type => "number"}
5255         ],
5256         return => {
5257             desc => "Entry fleshed with query on Create, Retrieve, and Uupdate.  1 on Delete",
5258             type => "object"
5259         }
5260     }
5261 );
5262
5263 sub filter_group_entry_crud {
5264     my ($self, $conn, $auth, $arg) = @_;
5265
5266     return OpenILS::Event->new('BAD_PARAMS') unless $arg;
5267     my $e = new_editor(authtoken => $auth, xact => 1);
5268     return $e->die_event unless $e->checkauth;
5269
5270     if (ref $arg) {
5271
5272         if ($arg->isnew) {
5273
5274             my $grp = $e->retrieve_actor_search_filter_group($arg->grp)
5275                 or return $e->die_event;
5276
5277             return $e->die_event unless $e->allowed(
5278                 'ADMIN_SEARCH_FILTER_GROUP', $grp->owner);
5279
5280             my $query = $arg->query;
5281             $query = $e->create_actor_search_query($query) or return $e->die_event;
5282             $arg->query($query->id);
5283             my $entry = $e->create_actor_search_filter_group_entry($arg) or return $e->die_event;
5284             $entry->query($query);
5285
5286             $e->commit;
5287             return $entry;
5288
5289         } elsif ($arg->ischanged) {
5290
5291             my $entry = $e->retrieve_actor_search_filter_group_entry([
5292                 $arg->id, {
5293                     flesh => 1,
5294                     flesh_fields => {asfge => ['grp']}
5295                 }
5296             ]) or return $e->die_event;
5297
5298             return $e->die_event unless $e->allowed(
5299                 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
5300
5301             my $query = $e->update_actor_search_query($arg->query) or return $e->die_event;
5302             $arg->query($arg->query->id);
5303             $e->update_actor_search_filter_group_entry($arg) or return $e->die_event;
5304             $arg->query($query);
5305
5306             $e->commit;
5307             return $arg;
5308
5309         } elsif ($arg->isdeleted) {
5310
5311             my $entry = $e->retrieve_actor_search_filter_group_entry([
5312                 $arg->id, {
5313                     flesh => 1,
5314                     flesh_fields => {asfge => ['grp', 'query']}
5315                 }
5316             ]) or return $e->die_event;
5317
5318             return $e->die_event unless $e->allowed(
5319                 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
5320
5321             $e->delete_actor_search_filter_group_entry($entry) or return $e->die_event;
5322             $e->delete_actor_search_query($entry->query) or return $e->die_event;
5323
5324             $e->commit;
5325             return 1;
5326
5327         } else {
5328
5329             $e->rollback;
5330             return undef;
5331         }
5332
5333     } else {
5334
5335         my $entry = $e->retrieve_actor_search_filter_group_entry([
5336             $arg, {
5337                 flesh => 1,
5338                 flesh_fields => {asfge => ['grp', 'query']}
5339             }
5340         ]) or return $e->die_event;
5341
5342         return $e->die_event unless $e->allowed(
5343             ['ADMIN_SEARCH_FILTER_GROUP', 'VIEW_SEARCH_FILTER_GROUP'],
5344             $entry->grp->owner);
5345
5346         $e->rollback;
5347         $entry->grp($entry->grp->id); # for consistency
5348         return $entry;
5349     }
5350 }
5351
5352
5353 __PACKAGE__->register_method(
5354     method        => 'user_billing_xacts',
5355     api_name      => 'open-ils.actor.user.transactions.for_billing',
5356     signature     => {
5357         desc   => q/Returns a stream of user billing data appropriate for
5358             display in the user bills UI.  API is natively "authoritative"./,
5359         params => [
5360             {desc => 'Authentication token', type => 'string'},
5361             {desc => 'User ID', type => 'number'},
5362             {desc => 'Xact IDs.  Optionally limit to specific transactions', 
5363              type => 'array'}
5364         ],
5365         return => {
5366             desc => q/First response is the user money summary, following
5367                 responses are fleshed billable transactions/
5368         }
5369     }
5370 );
5371
5372 sub user_billing_xacts {
5373     my ($self, $client, $auth, $user_id, $xact_ids) = @_;
5374
5375     my $e = new_editor(authtoken => $auth, xact => 1);
5376     return $e->die_event unless $e->checkauth;
5377
5378     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
5379
5380     return $e->die_event unless 
5381         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
5382
5383     # Start with the user summary.
5384     $client->respond($e->retrieve_money_user_summary($user_id));
5385
5386     # Even if xact_ids are specified, run this query to confirm the
5387     # provided IDs are linked to the specified user and have a balance.
5388     $xact_ids = $e->json_query({
5389         select => {mbts => ['id']},
5390         from => 'mbts',
5391         where => {
5392             usr => $user_id,
5393             balance_owed => {'<>' => 0},
5394             $xact_ids ? (id => $xact_ids) : ()
5395         },
5396         order_by => {mbts => {xact_start => 'asc'}}
5397     });
5398
5399     for my $xact_id (map { $_->{id} } @$xact_ids) {
5400
5401         my $xact = $e->retrieve_money_billable_transaction([
5402             $xact_id, {
5403                 flesh => 5,
5404                 flesh_fields => {
5405                     mbt => [qw/summary circulation grocery/],
5406                     circ => [qw/
5407                         target_copy 
5408                         workstation 
5409                         checkin_workstation 
5410                     /],
5411                     acp =>  [qw/
5412                         call_number
5413                         holds_count
5414                         status
5415                         circ_lib
5416                         location
5417                         floating
5418                         age_protect
5419                         parts
5420                     /],
5421                     acpm => [qw/part/],
5422                     acn =>  [qw/record owning_lib prefix suffix/],
5423                     bre =>  [qw/wide_display_entry/]
5424                 },
5425                 # Avoid adding the MARXML
5426                 # Fleshed fields are implicitly included.
5427                 select => {bre => ['id']} 
5428             }
5429         ]);
5430
5431         $client->respond($xact);
5432     }
5433
5434     $e->rollback;
5435
5436     return undef;
5437 }
5438
5439 1;