]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
LP1904036 Billing continued
[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         returns 1 if the password is correct.  Returns 0 otherwise.
3521     /
3522 );
3523
3524 sub verify_user_password {
3525     my($self, $conn, $auth, $barcode, $username, $password) = @_;
3526     my $e = new_editor(authtoken => $auth);
3527     return $e->die_event unless $e->checkauth;
3528     my $user;
3529     my $user_by_barcode;
3530     my $user_by_username;
3531     if($barcode) {
3532         my $card = $e->search_actor_card([
3533             {barcode => $barcode},
3534             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0] or return 0;
3535         $user_by_barcode = $card->usr;
3536         $user = $user_by_barcode;
3537     }
3538     if ($username) {
3539         $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return 0;
3540         $user = $user_by_username;
3541     }
3542     return 0 if (!$user || $U->is_true($user->deleted));
3543     return 0 if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3544     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3545     return $U->verify_migrated_user_password($e, $user->id, $password, 1);
3546 }
3547
3548 __PACKAGE__->register_method (
3549     method      => 'retrieve_usr_id_via_barcode_or_usrname',
3550     api_name    => "open-ils.actor.user.retrieve_id_by_barcode_or_username",
3551     signature   => q/
3552         Given a barcode or username returns the id for the user or
3553         a failure event.
3554     /
3555 );
3556
3557 sub retrieve_usr_id_via_barcode_or_usrname {
3558     my($self, $conn, $auth, $barcode, $username) = @_;
3559     my $e = new_editor(authtoken => $auth);
3560     return $e->die_event unless $e->checkauth;
3561     my $id_as_barcode= OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.actor' => app_settings => 'id_as_barcode');
3562     my $user;
3563     my $user_by_barcode;
3564     my $user_by_username;
3565     $logger->info("$id_as_barcode is the ID as BARCODE");
3566     if($barcode) {
3567         my $card = $e->search_actor_card([
3568             {barcode => $barcode},
3569             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
3570         if ($id_as_barcode =~ /^t/i) {
3571             if (!$card) {
3572                 $user = $e->retrieve_actor_user($barcode);
3573                 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$user);
3574             }else {
3575                 $user_by_barcode = $card->usr;
3576                 $user = $user_by_barcode;
3577             }
3578         }else {
3579             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$card);
3580             $user_by_barcode = $card->usr;
3581             $user = $user_by_barcode;
3582         }
3583     }
3584
3585     if ($username) {
3586         $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return OpenILS::Event->new( 'ACTOR_USR_NOT_FOUND' );
3587
3588         $user = $user_by_username;
3589     }
3590     return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if (!$user);
3591     return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3592     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3593     return $user->id;
3594 }
3595
3596
3597 __PACKAGE__->register_method (
3598     method      => 'merge_users',
3599     api_name    => 'open-ils.actor.user.merge',
3600     signature   => {
3601         desc => q/
3602             Given a list of source users and destination user, transfer all data from the source
3603             to the dest user and delete the source user.  All user related data is
3604             transferred, including circulations, holds, bookbags, etc.
3605         /
3606     }
3607 );
3608
3609 sub merge_users {
3610     my($self, $conn, $auth, $master_id, $user_ids, $options) = @_;
3611     my $e = new_editor(xact => 1, authtoken => $auth);
3612     return $e->die_event unless $e->checkauth;
3613
3614     # disallow the merge if any subordinate accounts are in collections
3615     my $colls = $e->search_money_collections_tracker({usr => $user_ids}, {idlist => 1});
3616     return OpenILS::Event->new('MERGED_USER_IN_COLLECTIONS', payload => $user_ids) if @$colls;
3617
3618     return OpenILS::Event->new('MERGE_SELF_NOT_ALLOWED')
3619         if $master_id == $e->requestor->id;
3620
3621     my $master_user = $e->retrieve_actor_user($master_id) or return $e->die_event;
3622     my $evt = group_perm_failed($e, $e->requestor, $master_user);
3623     return $evt if $evt;
3624
3625     my $del_addrs = ($U->ou_ancestor_setting_value(
3626         $master_user->home_ou, 'circ.user_merge.delete_addresses', $e)) ? 't' : 'f';
3627     my $del_cards = ($U->ou_ancestor_setting_value(
3628         $master_user->home_ou, 'circ.user_merge.delete_cards', $e)) ? 't' : 'f';
3629     my $deactivate_cards = ($U->ou_ancestor_setting_value(
3630         $master_user->home_ou, 'circ.user_merge.deactivate_cards', $e)) ? 't' : 'f';
3631
3632     for my $src_id (@$user_ids) {
3633
3634         my $src_user = $e->retrieve_actor_user($src_id) or return $e->die_event;
3635         my $evt = group_perm_failed($e, $e->requestor, $src_user);
3636         return $evt if $evt;
3637
3638         return OpenILS::Event->new('MERGE_SELF_NOT_ALLOWED')
3639             if $src_id == $e->requestor->id;
3640
3641         return $e->die_event unless $e->allowed('MERGE_USERS', $src_user->home_ou);
3642         if($src_user->home_ou ne $master_user->home_ou) {
3643             return $e->die_event unless $e->allowed('MERGE_USERS', $master_user->home_ou);
3644         }
3645
3646         return $e->die_event unless
3647             $e->json_query({from => [
3648                 'actor.usr_merge',
3649                 $src_id,
3650                 $master_id,
3651                 $del_addrs,
3652                 $del_cards,
3653                 $deactivate_cards
3654             ]});
3655     }
3656
3657     $e->commit;
3658     return 1;
3659 }
3660
3661
3662 __PACKAGE__->register_method (
3663     method      => 'approve_user_address',
3664     api_name    => 'open-ils.actor.user.pending_address.approve',
3665     signature   => {
3666         desc => q/
3667         /
3668     }
3669 );
3670
3671 sub approve_user_address {
3672     my($self, $conn, $auth, $addr) = @_;
3673     my $e = new_editor(xact => 1, authtoken => $auth);
3674     return $e->die_event unless $e->checkauth;
3675     if(ref $addr) {
3676         # if the caller passes an address object, assume they want to
3677         # update it first before approving it
3678         $e->update_actor_user_address($addr) or return $e->die_event;
3679     } else {
3680         $addr = $e->retrieve_actor_user_address($addr) or return $e->die_event;
3681     }
3682     my $user = $e->retrieve_actor_user($addr->usr);
3683     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3684     my $result = $e->json_query({from => ['actor.approve_pending_address', $addr->id]})->[0]
3685         or return $e->die_event;
3686     $e->commit;
3687     return [values %$result]->[0];
3688 }
3689
3690
3691 __PACKAGE__->register_method (
3692     method      => 'retrieve_friends',
3693     api_name    => 'open-ils.actor.friends.retrieve',
3694     signature   => {
3695         desc => q/
3696             returns { confirmed: [], pending_out: [], pending_in: []}
3697             pending_out are users I'm requesting friendship with
3698             pending_in are users requesting friendship with me
3699         /
3700     }
3701 );
3702
3703 sub retrieve_friends {
3704     my($self, $conn, $auth, $user_id, $options) = @_;
3705     my $e = new_editor(authtoken => $auth);
3706     return $e->event unless $e->checkauth;
3707     $user_id ||= $e->requestor->id;
3708
3709     if($user_id != $e->requestor->id) {
3710         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3711         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3712     }
3713
3714     return OpenILS::Application::Actor::Friends->retrieve_friends(
3715         $e, $user_id, $options);
3716 }
3717
3718
3719
3720 __PACKAGE__->register_method (
3721     method      => 'apply_friend_perms',
3722     api_name    => 'open-ils.actor.friends.perms.apply',
3723     signature   => {
3724         desc => q/
3725         /
3726     }
3727 );
3728 sub apply_friend_perms {
3729     my($self, $conn, $auth, $user_id, $delegate_id, @perms) = @_;
3730     my $e = new_editor(authtoken => $auth, xact => 1);
3731     return $e->die_event unless $e->checkauth;
3732
3733     if($user_id != $e->requestor->id) {
3734         my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3735         return $e->die_event unless $e->allowed('VIEW_USER', $user->home_ou);
3736     }
3737
3738     for my $perm (@perms) {
3739         my $evt =
3740             OpenILS::Application::Actor::Friends->apply_friend_perm(
3741                 $e, $user_id, $delegate_id, $perm);
3742         return $evt if $evt;
3743     }
3744
3745     $e->commit;
3746     return 1;
3747 }
3748
3749
3750 __PACKAGE__->register_method (
3751     method      => 'update_user_pending_address',
3752     api_name    => 'open-ils.actor.user.address.pending.cud'
3753 );
3754
3755 sub update_user_pending_address {
3756     my($self, $conn, $auth, $addr) = @_;
3757     my $e = new_editor(authtoken => $auth, xact => 1);
3758     return $e->die_event unless $e->checkauth;
3759
3760     my $user = $e->retrieve_actor_user($addr->usr) or return $e->die_event;
3761     if($addr->usr != $e->requestor->id) {
3762         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3763     }
3764
3765     if($addr->isnew) {
3766         $e->create_actor_user_address($addr) or return $e->die_event;
3767     } elsif($addr->isdeleted) {
3768         $e->delete_actor_user_address($addr) or return $e->die_event;
3769     } else {
3770         $e->update_actor_user_address($addr) or return $e->die_event;
3771     }
3772
3773     $e->commit;
3774     $U->create_events_for_hook('au.updated', $user, $e->requestor->ws_ou);
3775
3776     return $addr->id;
3777 }
3778
3779
3780 __PACKAGE__->register_method (
3781     method      => 'user_events',
3782     api_name    => 'open-ils.actor.user.events.circ',
3783     stream      => 1,
3784 );
3785 __PACKAGE__->register_method (
3786     method      => 'user_events',
3787     api_name    => 'open-ils.actor.user.events.ahr',
3788     stream      => 1,
3789 );
3790
3791 sub user_events {
3792     my($self, $conn, $auth, $user_id, $filters) = @_;
3793     my $e = new_editor(authtoken => $auth);
3794     return $e->event unless $e->checkauth;
3795
3796     (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3797     my $user_field = 'usr';
3798
3799     $filters ||= {};
3800     $filters->{target} = {
3801         select => { $obj_type => ['id'] },
3802         from => $obj_type,
3803         where => {usr => $user_id}
3804     };
3805
3806     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3807     if($e->requestor->id != $user_id) {
3808         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3809     }
3810
3811     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3812     my $req = $ses->request('open-ils.trigger.events_by_target',
3813         $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3814
3815     while(my $resp = $req->recv) {
3816         my $val = $resp->content;
3817         my $tgt = $val->target;
3818
3819         if($obj_type eq 'circ') {
3820             $tgt->target_copy($e->retrieve_asset_copy($tgt->target_copy));
3821
3822         } elsif($obj_type eq 'ahr') {
3823             $tgt->current_copy($e->retrieve_asset_copy($tgt->current_copy))
3824                 if $tgt->current_copy;
3825         }
3826
3827         $conn->respond($val) if $val;
3828     }
3829
3830     return undef;
3831 }
3832
3833 __PACKAGE__->register_method (
3834     method      => 'copy_events',
3835     api_name    => 'open-ils.actor.copy.events.circ',
3836     stream      => 1,
3837 );
3838 __PACKAGE__->register_method (
3839     method      => 'copy_events',
3840     api_name    => 'open-ils.actor.copy.events.ahr',
3841     stream      => 1,
3842 );
3843
3844 sub copy_events {
3845     my($self, $conn, $auth, $copy_id, $filters) = @_;
3846     my $e = new_editor(authtoken => $auth);
3847     return $e->event unless $e->checkauth;
3848
3849     (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3850
3851     my $copy = $e->retrieve_asset_copy($copy_id) or return $e->event;
3852
3853     my $copy_field = 'target_copy';
3854     $copy_field = 'current_copy' if $obj_type eq 'ahr';
3855
3856     $filters ||= {};
3857     $filters->{target} = {
3858         select => { $obj_type => ['id'] },
3859         from => $obj_type,
3860         where => {$copy_field => $copy_id}
3861     };
3862
3863
3864     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3865     my $req = $ses->request('open-ils.trigger.events_by_target',
3866         $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3867
3868     while(my $resp = $req->recv) {
3869         my $val = $resp->content;
3870         my $tgt = $val->target;
3871
3872         my $user = $e->retrieve_actor_user($tgt->usr);
3873         if($e->requestor->id != $user->id) {
3874             return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3875         }
3876
3877         $tgt->$copy_field($copy);
3878
3879         $tgt->usr($user);
3880         $conn->respond($val) if $val;
3881     }
3882
3883     return undef;
3884 }
3885
3886
3887 __PACKAGE__->register_method (
3888     method      => 'get_itemsout_notices',
3889     api_name    => 'open-ils.actor.user.itemsout.notices',
3890     stream      => 1,
3891     argc        => 2,
3892     signature   => {
3893         desc => q/Summary counts of circulat notices/,
3894         params => [
3895             {desc => 'authtoken', type => 'string'},
3896             {desc => 'circulation identifiers', type => 'array of numbers'}
3897         ],
3898         return => q/Stream of summary objects/
3899     }
3900 );
3901
3902 sub get_itemsout_notices {
3903     my ($self, $client, $auth, $circ_ids) = @_;
3904
3905     my $e = new_editor(authtoken => $auth);
3906     return $e->event unless $e->checkauth;
3907
3908     $circ_ids = [$circ_ids] unless ref $circ_ids eq 'ARRAY';
3909
3910     for my $circ_id (@$circ_ids) {
3911         my $resp = get_itemsout_notices_impl($e, $circ_id);
3912
3913         if ($U->is_event($resp)) {
3914             $client->respond($resp);
3915             return;
3916         }
3917
3918         $client->respond({circ_id => $circ_id, %$resp});
3919     }
3920
3921     return undef;
3922 }
3923
3924
3925
3926 sub get_itemsout_notices_impl {
3927     my ($e, $circId) = @_;
3928
3929     my $requestorId = $e->requestor->id;
3930
3931     my $circ = $e->retrieve_action_circulation($circId) or return $e->event;
3932
3933     my $patronId = $circ->usr;
3934
3935     if( $patronId ne $requestorId ){
3936         my $user = $e->retrieve_actor_user($requestorId) or return $e->event;
3937         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
3938     }
3939
3940     #my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3941     #my $req = $ses->request('open-ils.trigger.events_by_target',
3942     #   'circ', {target => [$circId], event=> {state=>'complete'}});
3943     # ^ Above removed in favor of faster json_query.
3944     #
3945     # SQL:
3946     # select complete_time
3947     # from action_trigger.event atev
3948     #     JOIN action_trigger.event_definition def ON (def.id = atev.event_def)
3949     #     JOIN action_trigger.hook athook ON (athook.key = def.hook)
3950     # where hook = 'checkout.due' AND state = 'complete' and target = <circId>;
3951     #
3952
3953     my $ctx_loc = $e->requestor->ws_ou;
3954     my $exclude_courtesy_notices = $U->ou_ancestor_setting_value(
3955         $ctx_loc, 'webstaff.circ.itemsout_notice_count_excludes_courtesies');
3956
3957     my $query = {
3958             select => { atev => ["complete_time"] },
3959             from => {
3960                     atev => {
3961                             atevdef => { field => "id",fkey => "event_def"}
3962                     }
3963             },
3964             where => {
3965             "+atevdef" => { active => 't', hook => 'checkout.due' },
3966             "+atev" => { target => $circId, state => 'complete' }
3967         }
3968     };
3969
3970     if ($exclude_courtesy_notices){
3971         $query->{"where"}->{"+atevdef"}->{validator} = { "<>" => "CircIsOpen"};
3972     }
3973
3974     my %resblob = ( numNotices => 0, lastDt => undef );
3975
3976     my $res = $e->json_query($query);
3977     for my $ndate (@$res) {
3978         $resblob{numNotices}++;
3979         if( !defined $resblob{lastDt}){
3980             $resblob{lastDt} = $$ndate{complete_time};
3981         }
3982
3983         if ($resblob{lastDt} lt $$ndate{complete_time}){
3984            $resblob{lastDt} = $$ndate{complete_time};
3985         }
3986    }
3987
3988     return \%resblob;
3989 }
3990
3991 __PACKAGE__->register_method (
3992     method      => 'update_events',
3993     api_name    => 'open-ils.actor.user.event.cancel.batch',
3994     stream      => 1,
3995 );
3996 __PACKAGE__->register_method (
3997     method      => 'update_events',
3998     api_name    => 'open-ils.actor.user.event.reset.batch',
3999     stream      => 1,
4000 );
4001
4002 sub update_events {
4003     my($self, $conn, $auth, $event_ids) = @_;
4004     my $e = new_editor(xact => 1, authtoken => $auth);
4005     return $e->die_event unless $e->checkauth;
4006
4007     my $x = 1;
4008     for my $id (@$event_ids) {
4009
4010         # do a little dance to determine what user we are ultimately affecting
4011         my $event = $e->retrieve_action_trigger_event([
4012             $id,
4013             {   flesh => 2,
4014                 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
4015             }
4016         ]) or return $e->die_event;
4017
4018         my $user_id;
4019         if($event->event_def->hook->core_type eq 'circ') {
4020             $user_id = $e->retrieve_action_circulation($event->target)->usr;
4021         } elsif($event->event_def->hook->core_type eq 'ahr') {
4022             $user_id = $e->retrieve_action_hold_request($event->target)->usr;
4023         } else {
4024             return 0;
4025         }
4026
4027         my $user = $e->retrieve_actor_user($user_id);
4028         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
4029
4030         if($self->api_name =~ /cancel/) {
4031             $event->state('invalid');
4032         } elsif($self->api_name =~ /reset/) {
4033             $event->clear_start_time;
4034             $event->clear_update_time;
4035             $event->state('pending');
4036         }
4037
4038         $e->update_action_trigger_event($event) or return $e->die_event;
4039         $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
4040     }
4041
4042     $e->commit;
4043     return {complete => 1};
4044 }
4045
4046
4047 __PACKAGE__->register_method (
4048     method      => 'really_delete_user',
4049     api_name    => 'open-ils.actor.user.delete.override',
4050     signature   => q/@see open-ils.actor.user.delete/
4051 );
4052
4053 __PACKAGE__->register_method (
4054     method      => 'really_delete_user',
4055     api_name    => 'open-ils.actor.user.delete',
4056     signature   => q/
4057         It anonymizes all personally identifiable information in actor.usr. By calling actor.usr_purge_data()
4058         it also purges related data from other tables, sometimes by transferring it to a designated destination user.
4059         The usrname field (along with first_given_name and family_name) is updated to id '-PURGED-' now().
4060         dest_usr_id is only required when deleting a user that performs staff functions.
4061     /
4062 );
4063
4064 sub really_delete_user {
4065     my($self, $conn, $auth, $user_id, $dest_user_id, $oargs) = @_;
4066     my $e = new_editor(authtoken => $auth, xact => 1);
4067     return $e->die_event unless $e->checkauth;
4068     $oargs = { all => 1 } unless defined $oargs;
4069
4070     # Find all unclosed billings for for user $user_id, thereby, also checking for open circs
4071     my $open_bills = $e->json_query({
4072         select => { mbts => ['id'] },
4073         from => 'mbts',
4074         where => {
4075             xact_finish => { '=' => undef },
4076             usr => { '=' => $user_id },
4077         }
4078     }) or return $e->die_event;
4079
4080     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
4081
4082     # No deleting patrons with open billings or checked out copies, unless perm-enabled override
4083     if (@$open_bills) {
4084         return $e->die_event(OpenILS::Event->new('ACTOR_USER_DELETE_OPEN_XACTS'))
4085         unless $self->api_name =~ /override/o && ($oargs->{all} || grep { $_ eq 'ACTOR_USER_DELETE_OPEN_XACTS' } @{$oargs->{events}})
4086         && $e->allowed('ACTOR_USER_DELETE_OPEN_XACTS.override', $user->home_ou);
4087     }
4088     # No deleting yourself - UI is supposed to stop you first, though.
4089     return $e->die_event unless $e->requestor->id != $user->id;
4090     return $e->die_event unless $e->allowed('DELETE_USER', $user->home_ou);
4091     # Check if you are allowed to mess with this patron permission group at all
4092     my $evt = group_perm_failed($e, $e->requestor, $user);
4093     return $e->die_event($evt) if $evt;
4094     my $stat = $e->json_query(
4095         {from => ['actor.usr_delete', $user_id, $dest_user_id]})->[0]
4096         or return $e->die_event;
4097     $e->commit;
4098     return 1;
4099 }
4100
4101
4102 __PACKAGE__->register_method (
4103     method      => 'user_payments',
4104     api_name    => 'open-ils.actor.user.payments.retrieve',
4105     stream => 1,
4106     signature   => q/
4107         Returns all payments for a given user.  Default order is newest payments first.
4108         @param auth Authentication token
4109         @param user_id The user ID
4110         @param filters An optional hash of filters, including limit, offset, and order_by definitions
4111     /
4112 );
4113
4114 sub user_payments {
4115     my($self, $conn, $auth, $user_id, $filters) = @_;
4116     $filters ||= {};
4117
4118     my $e = new_editor(authtoken => $auth);
4119     return $e->die_event unless $e->checkauth;
4120
4121     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4122     return $e->event unless
4123         $e->requestor->id == $user_id or
4124         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
4125
4126     # Find all payments for all transactions for user $user_id
4127     my $query = {
4128         select => {mp => ['id']},
4129         from => 'mp',
4130         where => {
4131             xact => {
4132                 in => {
4133                     select => {mbt => ['id']},
4134                     from => 'mbt',
4135                     where => {usr => $user_id}
4136                 }
4137             }
4138         },
4139         order_by => [
4140             { # by default, order newest payments first
4141                 class => 'mp',
4142                 field => 'payment_ts',
4143                 direction => 'desc'
4144             }, {
4145                 # secondary sort in ID as a tie-breaker, since payments created
4146                 # within the same transaction will have identical payment_ts's
4147                 class => 'mp',
4148                 field => 'id'
4149             }
4150         ]
4151     };
4152
4153     for (qw/order_by limit offset/) {
4154         $query->{$_} = $filters->{$_} if defined $filters->{$_};
4155     }
4156
4157     if(defined $filters->{where}) {
4158         foreach (keys %{$filters->{where}}) {
4159             # don't allow the caller to expand the result set to other users
4160             $query->{where}->{$_} = $filters->{where}->{$_} unless $_ eq 'xact';
4161         }
4162     }
4163
4164     my $payment_ids = $e->json_query($query);
4165     for my $pid (@$payment_ids) {
4166         my $pay = $e->retrieve_money_payment([
4167             $pid->{id},
4168             {   flesh => 6,
4169                 flesh_fields => {
4170                     mp => ['xact'],
4171                     mbt => ['summary', 'circulation', 'grocery'],
4172                     circ => ['target_copy'],
4173                     acp => ['call_number'],
4174                     acn => ['record']
4175                 }
4176             }
4177         ]);
4178
4179         my $resp = {
4180             mp => $pay,
4181             xact_type => $pay->xact->summary->xact_type,
4182             last_billing_type => $pay->xact->summary->last_billing_type,
4183         };
4184
4185         if($pay->xact->summary->xact_type eq 'circulation') {
4186             $resp->{barcode} = $pay->xact->circulation->target_copy->barcode;
4187             $resp->{title} = $U->record_to_mvr($pay->xact->circulation->target_copy->call_number->record)->title;
4188         }
4189
4190         $pay->xact($pay->xact->id); # de-flesh
4191         $conn->respond($resp);
4192     }
4193
4194     return undef;
4195 }
4196
4197
4198
4199 __PACKAGE__->register_method (
4200     method      => 'negative_balance_users',
4201     api_name    => 'open-ils.actor.users.negative_balance',
4202     stream => 1,
4203     signature   => q/
4204         Returns all users that have an overall negative balance
4205         @param auth Authentication token
4206         @param org_id The context org unit as an ID or list of IDs.  This will be the home
4207         library of the user.  If no org_unit is specified, no org unit filter is applied
4208     /
4209 );
4210
4211 sub negative_balance_users {
4212     my($self, $conn, $auth, $org_id, $options) = @_;
4213
4214     $options ||= {};
4215     $options->{limit} = 1000 unless $options->{limit};
4216     $options->{offset} = 0 unless $options->{offset};
4217
4218     my $e = new_editor(authtoken => $auth);
4219     return $e->die_event unless $e->checkauth;
4220     return $e->die_event unless $e->allowed('VIEW_USER', $org_id);
4221
4222     my $query = {
4223         select => {
4224             mous => ['usr', 'balance_owed'],
4225             au => ['home_ou'],
4226             mbts => [
4227                 {column => 'last_billing_ts', transform => 'max', aggregate => 1},
4228                 {column => 'last_payment_ts', transform => 'max', aggregate => 1},
4229             ]
4230         },
4231         from => {
4232             mous => {
4233                 au => {
4234                     fkey => 'usr',
4235                     field => 'id',
4236                     join => {
4237                         mbts => {
4238                             key => 'id',
4239                             field => 'usr'
4240                         }
4241                     }
4242                 }
4243             }
4244         },
4245         where => {'+mous' => {balance_owed => {'<' => 0}}},
4246         offset => $options->{offset},
4247         limit => $options->{limit},
4248         order_by => [{class => 'mous', field => 'usr'}]
4249     };
4250
4251     $org_id = $U->get_org_descendants($org_id) if $options->{org_descendants};
4252
4253     $query->{from}->{mous}->{au}->{filter}->{home_ou} = $org_id if $org_id;
4254
4255     my $list = $e->json_query($query, {timeout => 600});
4256
4257     for my $data (@$list) {
4258         $conn->respond({
4259             usr => $e->retrieve_actor_user([$data->{usr}, {flesh => 1, flesh_fields => {au => ['card']}}]),
4260             balance_owed => $data->{balance_owed},
4261             last_billing_activity => max($data->{last_billing_ts}, $data->{last_payment_ts})
4262         });
4263     }
4264
4265     return undef;
4266 }
4267
4268 __PACKAGE__->register_method(
4269     method  => "request_password_reset",
4270     api_name    => "open-ils.actor.patron.password_reset.request",
4271     signature   => {
4272         desc => "Generates a UUID token usable with the open-ils.actor.patron.password_reset.commit " .
4273                 "method for changing a user's password.  The UUID token is distributed via A/T "      .
4274                 "templates (i.e. email to the user).",
4275         params => [
4276             { desc => 'user_id_type', type => 'string' },
4277             { desc => 'user_id', type => 'string' },
4278             { desc => 'optional (based on library setting) matching email address for authorizing request', type => 'string' },
4279         ],
4280         return => {desc => '1 on success, Event on error'}
4281     }
4282 );
4283 sub request_password_reset {
4284     my($self, $conn, $user_id_type, $user_id, $email) = @_;
4285
4286     # Check to see if password reset requests are already being throttled:
4287     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
4288
4289     my $e = new_editor(xact => 1);
4290     my $user;
4291
4292     # Get the user, if any, depending on the input value
4293     if ($user_id_type eq 'username') {
4294         $user = $e->search_actor_user({usrname => $user_id})->[0];
4295         if (!$user) {
4296             $e->die_event;
4297             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
4298         }
4299     } elsif ($user_id_type eq 'barcode') {
4300         my $card = $e->search_actor_card([
4301             {barcode => $user_id},
4302             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
4303         if (!$card) {
4304             $e->die_event;
4305             return OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
4306         }
4307         $user = $card->usr;
4308     }
4309
4310     # If the user doesn't have an email address, we can't help them
4311     if (!$user->email) {
4312         $e->die_event;
4313         return OpenILS::Event->new('PATRON_NO_EMAIL_ADDRESS');
4314     }
4315
4316     my $email_must_match = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_requires_matching_email');
4317     if ($email_must_match) {
4318         if (lc($user->email) ne lc($email)) {
4319             return OpenILS::Event->new('EMAIL_VERIFICATION_FAILED');
4320         }
4321     }
4322
4323     _reset_password_request($conn, $e, $user);
4324 }
4325
4326 # Once we have the user, we can issue the password reset request
4327 # XXX Add a wrapper method that accepts barcode + email input
4328 sub _reset_password_request {
4329     my ($conn, $e, $user) = @_;
4330
4331     # 1. Get throttle threshold and time-to-live from OU_settings
4332     my $aupr_throttle = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_throttle') || 1000;
4333     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4334
4335     my $threshold_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl)->iso8601();
4336
4337     # 2. Get time of last request and number of active requests (num_active)
4338     my $active_requests = $e->json_query({
4339         from => 'aupr',
4340         select => {
4341             aupr => [
4342                 {
4343                     column => 'uuid',
4344                     transform => 'COUNT'
4345                 },
4346                 {
4347                     column => 'request_time',
4348                     transform => 'MAX'
4349                 }
4350             ]
4351         },
4352         where => {
4353             has_been_reset => { '=' => 'f' },
4354             request_time => { '>' => $threshold_time }
4355         }
4356     });
4357
4358     # Guard against no active requests
4359     if ($active_requests->[0]->{'request_time'}) {
4360         my $last_request = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($active_requests->[0]->{'request_time'}));
4361         my $now = DateTime::Format::ISO8601->new();
4362
4363         # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute)
4364         if (($active_requests->[0]->{'usr'} > $aupr_throttle) &&
4365             ($last_request->add_duration('1 minute') > $now)) {
4366             $cache->put_cache('open-ils.actor.password.throttle', DateTime::Format::ISO8601->new(), 60);
4367             $e->die_event;
4368             return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4369         }
4370     }
4371
4372     # TODO Check to see if the user is in a password-reset-restricted group
4373
4374     # Otherwise, go ahead and try to get the user.
4375
4376     # Check the number of active requests for this user
4377     $active_requests = $e->json_query({
4378         from => 'aupr',
4379         select => {
4380             aupr => [
4381                 {
4382                     column => 'usr',
4383                     transform => 'COUNT'
4384                 }
4385             ]
4386         },
4387         where => {
4388             usr => { '=' => $user->id },
4389             has_been_reset => { '=' => 'f' },
4390             request_time => { '>' => $threshold_time }
4391         }
4392     });
4393
4394     $logger->info("User " . $user->id . " has " . $active_requests->[0]->{'usr'} . " active password reset requests.");
4395
4396     # if less than or equal to per-user threshold, proceed; otherwise, return event
4397     my $aupr_per_user_limit = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_per_user_limit') || 3;
4398     if ($active_requests->[0]->{'usr'} > $aupr_per_user_limit) {
4399         $e->die_event;
4400         return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4401     }
4402
4403     # Create the aupr object and insert into the database
4404     my $reset_request = Fieldmapper::actor::usr_password_reset->new;
4405     my $uuid = create_uuid_as_string(UUID_V4);
4406     $reset_request->uuid($uuid);
4407     $reset_request->usr($user->id);
4408
4409     my $aupr = $e->create_actor_usr_password_reset($reset_request) or return $e->die_event;
4410     $e->commit;
4411
4412     # Create an event to notify user of the URL to reset their password
4413
4414     # Can we stuff this in the user_data param for trigger autocreate?
4415     my $hostname = $U->ou_ancestor_setting_value($user->home_ou, 'lib.hostname') || 'localhost';
4416
4417     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
4418     $ses->request('open-ils.trigger.event.autocreate', 'password.reset_request', $aupr, $user->home_ou);
4419
4420     # Trunk only
4421     # $U->create_trigger_event('password.reset_request', $aupr, $user->home_ou);
4422
4423     return 1;
4424 }
4425
4426 __PACKAGE__->register_method(
4427     method  => "commit_password_reset",
4428     api_name    => "open-ils.actor.patron.password_reset.commit",
4429     signature   => {
4430         desc => "Checks a UUID token generated by the open-ils.actor.patron.password_reset.request method for " .
4431                 "validity, and if valid, uses it as authorization for changing the associated user's password " .
4432                 "with the supplied password.",
4433         params => [
4434             { desc => 'uuid', type => 'string' },
4435             { desc => 'password', type => 'string' },
4436         ],
4437         return => {desc => '1 on success, Event on error'}
4438     }
4439 );
4440 sub commit_password_reset {
4441     my($self, $conn, $uuid, $password) = @_;
4442
4443     # Check to see if password reset requests are already being throttled:
4444     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
4445     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
4446     my $throttle = $cache->get_cache('open-ils.actor.password.throttle') || undef;
4447     if ($throttle) {
4448         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4449     }
4450
4451     my $e = new_editor(xact => 1);
4452
4453     my $aupr = $e->search_actor_usr_password_reset({
4454         uuid => $uuid,
4455         has_been_reset => 0
4456     });
4457
4458     if (!$aupr->[0]) {
4459         $e->die_event;
4460         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4461     }
4462     my $user_id = $aupr->[0]->usr;
4463     my $user = $e->retrieve_actor_user($user_id);
4464
4465     # Ensure we're still within the TTL for the request
4466     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4467     my $threshold = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($aupr->[0]->request_time))->add(seconds => $aupr_ttl);
4468     if ($threshold < DateTime->now(time_zone => 'local')) {
4469         $e->die_event;
4470         $logger->info("Password reset request needed to be submitted before $threshold");
4471         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4472     }
4473
4474     # Check complexity of password against OU-defined regex
4475     my $pw_regex = $U->ou_ancestor_setting_value($user->home_ou, 'global.password_regex');
4476
4477     my $is_strong = 0;
4478     if ($pw_regex) {
4479         # Calling JSON2perl on the $pw_regex causes failure, even before the fancy Unicode regex
4480         # ($pw_regex = OpenSRF::Utils::JSON->JSON2perl($pw_regex)) =~ s/\\u([0-9a-fA-F]{4})/\\x{$1}/gs;
4481         $is_strong = check_password_strength_custom($password, $pw_regex);
4482     } else {
4483         $is_strong = check_password_strength_default($password);
4484     }
4485
4486     if (!$is_strong) {
4487         $e->die_event;
4488         return OpenILS::Event->new('PATRON_PASSWORD_WAS_NOT_STRONG');
4489     }
4490
4491     # All is well; update the password
4492     modify_migrated_user_password($e, $user->id, $password);
4493
4494     # And flag that this password reset request has been honoured
4495     $aupr->[0]->has_been_reset('t');
4496     $e->update_actor_usr_password_reset($aupr->[0]);
4497     $e->commit;
4498
4499     return 1;
4500 }
4501
4502 sub check_password_strength_default {
4503     my $password = shift;
4504     # Use the default set of checks
4505     if ( (length($password) < 7) or
4506             ($password !~ m/.*\d+.*/) or
4507             ($password !~ m/.*[A-Za-z]+.*/)
4508     ) {
4509         return 0;
4510     }
4511     return 1;
4512 }
4513
4514 sub check_password_strength_custom {
4515     my ($password, $pw_regex) = @_;
4516
4517     $pw_regex = qr/$pw_regex/;
4518     if ($password !~  /$pw_regex/) {
4519         return 0;
4520     }
4521     return 1;
4522 }
4523
4524 __PACKAGE__->register_method(
4525     method    => "fire_test_notification",
4526     api_name  => "open-ils.actor.event.test_notification"
4527 );
4528
4529 sub fire_test_notification {
4530     my($self, $conn, $auth, $args) = @_;
4531     my $e = new_editor(authtoken => $auth);
4532     return $e->event unless $e->checkauth;
4533     if ($e->requestor->id != $$args{target}) {
4534         my $home_ou = $e->retrieve_actor_user($$args{target})->home_ou;
4535         return $e->die_event unless $home_ou && $e->allowed('VIEW_USER', $home_ou);
4536     }
4537
4538     my $event_hook = $$args{hook} or return $e->event;
4539     return $e->event unless ($event_hook eq 'au.email.test' or $event_hook eq 'au.sms_text.test');
4540
4541     my $usr = $e->retrieve_actor_user($$args{target});
4542     return $e->event unless $usr;
4543
4544     return $U->fire_object_event(undef, $event_hook, $usr, $e->requestor->ws_ou);
4545 }
4546
4547
4548 __PACKAGE__->register_method(
4549     method    => "event_def_opt_in_settings",
4550     api_name  => "open-ils.actor.event_def.opt_in.settings",
4551     stream => 1,
4552     signature => {
4553         desc   => 'Streams the set of "cust" objects that are used as opt-in settings for event definitions',
4554         params => [
4555             { desc => 'Authentication token',  type => 'string'},
4556             {
4557                 desc => 'Org Unit ID.  (optional).  If no org ID is present, the home_ou of the requesting user is used',
4558                 type => 'number'
4559             },
4560         ],
4561         return => {
4562             desc => q/set of "cust" objects that are used as opt-in settings for event definitions at the specified org unit/,
4563             type => 'object',
4564             class => 'cust'
4565         }
4566     }
4567 );
4568
4569 sub event_def_opt_in_settings {
4570     my($self, $conn, $auth, $org_id) = @_;
4571     my $e = new_editor(authtoken => $auth);
4572     return $e->event unless $e->checkauth;
4573
4574     if(defined $org_id and $org_id != $e->requestor->home_ou) {
4575         return $e->event unless
4576             $e->allowed(['VIEW_USER_SETTING_TYPE', 'ADMIN_USER_SETTING_TYPE'], $org_id);
4577     } else {
4578         $org_id = $e->requestor->home_ou;
4579     }
4580
4581     # find all config.user_setting_type's related to event_defs for the requested org unit
4582     my $types = $e->json_query({
4583         select => {cust => ['name']},
4584         from => {atevdef => 'cust'},
4585         where => {
4586             '+atevdef' => {
4587                 owner => $U->get_org_ancestors($org_id), # context org plus parents
4588                 active => 't'
4589             }
4590         }
4591     });
4592
4593     if(@$types) {
4594         $conn->respond($_) for
4595             @{$e->search_config_usr_setting_type({name => [map {$_->{name}} @$types]})};
4596     }
4597
4598     return undef;
4599 }
4600
4601
4602 __PACKAGE__->register_method(
4603     method    => "user_circ_history",
4604     api_name  => "open-ils.actor.history.circ",
4605     stream => 1,
4606     authoritative => 1,
4607     signature => {
4608         desc   => 'Returns user circ history objects for the calling user',
4609         params => [
4610             { desc => 'Authentication token',  type => 'string'},
4611             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4612         ],
4613         return => {
4614             desc => q/Stream of 'auch' circ history objects/,
4615             type => 'object',
4616         }
4617     }
4618 );
4619
4620 __PACKAGE__->register_method(
4621     method    => "user_circ_history",
4622     api_name  => "open-ils.actor.history.circ.clear",
4623     stream => 1,
4624     signature => {
4625         desc   => 'Delete all user circ history entries for the calling user',
4626         params => [
4627             { desc => 'Authentication token',  type => 'string'},
4628             { desc => "Options hash. 'circ_ids' is an arrayref of circulation IDs to delete", type => 'object' },
4629         ],
4630         return => {
4631             desc => q/1 on success, event on error/,
4632             type => 'object',
4633         }
4634     }
4635 );
4636
4637 __PACKAGE__->register_method(
4638     method    => "user_circ_history",
4639     api_name  => "open-ils.actor.history.circ.print",
4640     stream => 1,
4641     signature => {
4642         desc   => q/Returns printable output for the caller's circ history objects/,
4643         params => [
4644             { desc => 'Authentication token',  type => 'string'},
4645             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4646         ],
4647         return => {
4648             desc => q/An action_trigger.event object or error event./,
4649             type => 'object',
4650         }
4651     }
4652 );
4653
4654 __PACKAGE__->register_method(
4655     method    => "user_circ_history",
4656     api_name  => "open-ils.actor.history.circ.email",
4657     stream => 1,
4658     signature => {
4659         desc   => q/Emails the caller's circ history/,
4660         params => [
4661             { desc => 'Authentication token',  type => 'string'},
4662             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4663             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4664         ],
4665         return => {
4666             desc => q/undef, or event on error/
4667         }
4668     }
4669 );
4670
4671 sub user_circ_history {
4672     my ($self, $conn, $auth, $options) = @_;
4673     $options ||= {};
4674
4675     my $for_print = ($self->api_name =~ /print/);
4676     my $for_email = ($self->api_name =~ /email/);
4677     my $for_clear = ($self->api_name =~ /clear/);
4678
4679     # No perm check is performed.  Caller may only access his/her own
4680     # circ history entries.
4681     my $e = new_editor(authtoken => $auth);
4682     return $e->event unless $e->checkauth;
4683
4684     my %limits = ();
4685     if (!$for_clear) { # clear deletes all
4686         $limits{offset} = $options->{offset} if defined $options->{offset};
4687         $limits{limit} = $options->{limit} if defined $options->{limit};
4688     }
4689
4690     my %circ_id_filter = $options->{circ_ids} ?
4691         (id => $options->{circ_ids}) : ();
4692
4693     my $circs = $e->search_action_user_circ_history([
4694         {   usr => $e->requestor->id,
4695             %circ_id_filter
4696         },
4697         {   # order newest to oldest by default
4698             order_by => {auch => 'xact_start DESC'},
4699             %limits
4700         },
4701         {substream => 1} # could be a large list
4702     ]);
4703
4704     if ($for_print) {
4705         return $U->fire_object_event(undef,
4706             'circ.format.history.print', $circs, $e->requestor->home_ou);
4707     }
4708
4709     $e->xact_begin if $for_clear;
4710     $conn->respond_complete(1) if $for_email;  # no sense in waiting
4711
4712     for my $circ (@$circs) {
4713
4714         if ($for_email) {
4715             # events will be fired from action_trigger_runner
4716             $U->create_events_for_hook('circ.format.history.email',
4717                 $circ, $e->editor->home_ou, undef, undef, 1);
4718
4719         } elsif ($for_clear) {
4720
4721             $e->delete_action_user_circ_history($circ)
4722                 or return $e->die_event;
4723
4724         } else {
4725             $conn->respond($circ);
4726         }
4727     }
4728
4729     if ($for_clear) {
4730         $e->commit;
4731         return 1;
4732     }
4733
4734     return undef;
4735 }
4736
4737
4738 __PACKAGE__->register_method(
4739     method    => "user_visible_holds",
4740     api_name  => "open-ils.actor.history.hold.visible",
4741     stream => 1,
4742     signature => {
4743         desc   => 'Returns the set of opt-in visible holds',
4744         params => [
4745             { desc => 'Authentication token',  type => 'string'},
4746             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4747             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4748         ],
4749         return => {
4750             desc => q/An object with 1 field: "hold"/,
4751             type => 'object',
4752         }
4753     }
4754 );
4755
4756 __PACKAGE__->register_method(
4757     method    => "user_visible_holds",
4758     api_name  => "open-ils.actor.history.hold.visible.print",
4759     stream => 1,
4760     signature => {
4761         desc   => 'Returns printable output for the set of opt-in visible holds',
4762         params => [
4763             { desc => 'Authentication token',  type => 'string'},
4764             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4765             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4766         ],
4767         return => {
4768             desc => q/An action_trigger.event object or error event./,
4769             type => 'object',
4770         }
4771     }
4772 );
4773
4774 __PACKAGE__->register_method(
4775     method    => "user_visible_holds",
4776     api_name  => "open-ils.actor.history.hold.visible.email",
4777     stream => 1,
4778     signature => {
4779         desc   => 'Emails the set of opt-in visible holds to the requestor',
4780         params => [
4781             { desc => 'Authentication token',  type => 'string'},
4782             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4783             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4784         ],
4785         return => {
4786             desc => q/undef, or event on error/
4787         }
4788     }
4789 );
4790
4791 sub user_visible_holds {
4792     my($self, $conn, $auth, $user_id, $options) = @_;
4793
4794     my $is_hold = 1;
4795     my $for_print = ($self->api_name =~ /print/);
4796     my $for_email = ($self->api_name =~ /email/);
4797     my $e = new_editor(authtoken => $auth);
4798     return $e->event unless $e->checkauth;
4799
4800     $user_id ||= $e->requestor->id;
4801     $options ||= {};
4802     $options->{limit} ||= 50;
4803     $options->{offset} ||= 0;
4804
4805     if($user_id != $e->requestor->id) {
4806         my $perm = ($is_hold) ? 'VIEW_HOLD' : 'VIEW_CIRCULATIONS';
4807         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4808         return $e->event unless $e->allowed($perm, $user->home_ou);
4809     }
4810
4811     my $db_func = ($is_hold) ? 'action.usr_visible_holds' : 'action.usr_visible_circs';
4812
4813     my $data = $e->json_query({
4814         from => [$db_func, $user_id],
4815         limit => $$options{limit},
4816         offset => $$options{offset}
4817
4818         # TODO: I only want IDs. code below didn't get me there
4819         # {"select":{"au":[{"column":"id", "result_field":"id",
4820         # "transform":"action.usr_visible_circs"}]}, "where":{"id":10}, "from":"au"}
4821     },{
4822         substream => 1
4823     });
4824
4825     return undef unless @$data;
4826
4827     if ($for_print) {
4828
4829         # collect the batch of objects
4830
4831         if($is_hold) {
4832
4833             my $hold_list = $e->search_action_hold_request({id => [map { $_->{id} } @$data]});
4834             return $U->fire_object_event(undef, 'ahr.format.history.print', $hold_list, $$hold_list[0]->request_lib);
4835
4836         } else {
4837
4838             my $circ_list = $e->search_action_circulation({id => [map { $_->{id} } @$data]});
4839             return $U->fire_object_event(undef, 'circ.format.history.print', $circ_list, $$circ_list[0]->circ_lib);
4840         }
4841
4842     } elsif ($for_email) {
4843
4844         $conn->respond_complete(1) if $for_email;  # no sense in waiting
4845
4846         foreach (@$data) {
4847
4848             my $id = $_->{id};
4849
4850             if($is_hold) {
4851
4852                 my $hold = $e->retrieve_action_hold_request($id);
4853                 $U->create_events_for_hook('ahr.format.history.email', $hold, $hold->request_lib, undef, undef, 1);
4854                 # events will be fired from action_trigger_runner
4855
4856             } else {
4857
4858                 my $circ = $e->retrieve_action_circulation($id);
4859                 $U->create_events_for_hook('circ.format.history.email', $circ, $circ->circ_lib, undef, undef, 1);
4860                 # events will be fired from action_trigger_runner
4861             }
4862         }
4863
4864     } else { # just give me the data please
4865
4866         foreach (@$data) {
4867
4868             my $id = $_->{id};
4869
4870             if($is_hold) {
4871
4872                 my $hold = $e->retrieve_action_hold_request($id);
4873                 $conn->respond({hold => $hold});
4874
4875             } else {
4876
4877                 my $circ = $e->retrieve_action_circulation($id);
4878                 $conn->respond({
4879                     circ => $circ,
4880                     summary => $U->create_circ_chain_summary($e, $id)
4881                 });
4882             }
4883         }
4884     }
4885
4886     return undef;
4887 }
4888
4889 __PACKAGE__->register_method(
4890     method     => "user_saved_search_cud",
4891     api_name   => "open-ils.actor.user.saved_search.cud",
4892     stream     => 1,
4893     signature  => {
4894         desc   => 'Create/Update/Delete Access to user saved searches',
4895         params => [
4896             { desc => 'Authentication token', type => 'string' },
4897             { desc => 'Saved Search Object', type => 'object', class => 'auss' }
4898         ],
4899         return => {
4900             desc   => q/The retrieved or updated saved search object, or id of a deleted object; Event on error/,
4901             class  => 'auss'
4902         }
4903     }
4904 );
4905
4906 __PACKAGE__->register_method(
4907     method     => "user_saved_search_cud",
4908     api_name   => "open-ils.actor.user.saved_search.retrieve",
4909     stream     => 1,
4910     signature  => {
4911         desc   => 'Retrieve a saved search object',
4912         params => [
4913             { desc => 'Authentication token', type => 'string' },
4914             { desc => 'Saved Search ID', type => 'number' }
4915         ],
4916         return => {
4917             desc   => q/The saved search object, Event on error/,
4918             class  => 'auss'
4919         }
4920     }
4921 );
4922
4923 sub user_saved_search_cud {
4924     my( $self, $client, $auth, $search ) = @_;
4925     my $e = new_editor( authtoken=>$auth );
4926     return $e->die_event unless $e->checkauth;
4927
4928     my $o_search;      # prior version of the object, if any
4929     my $res;           # to be returned
4930
4931     # branch on the operation type
4932
4933     if( $self->api_name =~ /retrieve/ ) {                    # Retrieve
4934
4935         # Get the old version, to check ownership
4936         $o_search = $e->retrieve_actor_usr_saved_search( $search )
4937             or return $e->die_event;
4938
4939         # You can't read somebody else's search
4940         return OpenILS::Event->new('BAD_PARAMS')
4941             unless $o_search->owner == $e->requestor->id;
4942
4943         $res = $o_search;
4944
4945     } else {
4946
4947         $e->xact_begin;               # start an editor transaction
4948
4949         if( $search->isnew ) {                               # Create
4950
4951             # You can't create a search for somebody else
4952             return OpenILS::Event->new('BAD_PARAMS')
4953                 unless $search->owner == $e->requestor->id;
4954
4955             $e->create_actor_usr_saved_search( $search )
4956                 or return $e->die_event;
4957
4958             $res = $search->id;
4959
4960         } elsif( $search->ischanged ) {                      # Update
4961
4962             # You can't change ownership of a search
4963             return OpenILS::Event->new('BAD_PARAMS')
4964                 unless $search->owner == $e->requestor->id;
4965
4966             # Get the old version, to check ownership
4967             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4968                 or return $e->die_event;
4969
4970             # You can't update somebody else's search
4971             return OpenILS::Event->new('BAD_PARAMS')
4972                 unless $o_search->owner == $e->requestor->id;
4973
4974             # Do the update
4975             $e->update_actor_usr_saved_search( $search )
4976                 or return $e->die_event;
4977
4978             $res = $search;
4979
4980         } elsif( $search->isdeleted ) {                      # Delete
4981
4982             # Get the old version, to check ownership
4983             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4984                 or return $e->die_event;
4985
4986             # You can't delete somebody else's search
4987             return OpenILS::Event->new('BAD_PARAMS')
4988                 unless $o_search->owner == $e->requestor->id;
4989
4990             # Do the delete
4991             $e->delete_actor_usr_saved_search( $o_search )
4992                 or return $e->die_event;
4993
4994             $res = $search->id;
4995         }
4996
4997         $e->commit;
4998     }
4999
5000     return $res;
5001 }
5002
5003 __PACKAGE__->register_method(
5004     method   => "get_barcodes",
5005     api_name => "open-ils.actor.get_barcodes"
5006 );
5007
5008 sub get_barcodes {
5009     my( $self, $client, $auth, $org_id, $context, $barcode ) = @_;
5010     my $e = new_editor(authtoken => $auth);
5011     return $e->event unless $e->checkauth;
5012     return $e->event unless $e->allowed('STAFF_LOGIN', $org_id);
5013
5014     my $db_result = $e->json_query(
5015         {   from => [
5016                 'evergreen.get_barcodes',
5017                 $org_id, $context, $barcode,
5018             ]
5019         }
5020     );
5021     if($context =~ /actor/) {
5022         my $filter_result = ();
5023         my $patron;
5024         foreach my $result (@$db_result) {
5025             if($result->{type} eq 'actor') {
5026                 if($e->requestor->id != $result->{id}) {
5027                     $patron = $e->retrieve_actor_user($result->{id});
5028                     if(!$patron) {
5029                         push(@$filter_result, $e->event);
5030                         next;
5031                     }
5032                     if($e->allowed('VIEW_USER', $patron->home_ou)) {
5033                         push(@$filter_result, $result);
5034                     }
5035                     else {
5036                         push(@$filter_result, $e->event);
5037                     }
5038                 }
5039                 else {
5040                     push(@$filter_result, $result);
5041                 }
5042             }
5043             else {
5044                 push(@$filter_result, $result);
5045             }
5046         }
5047         return $filter_result;
5048     }
5049     else {
5050         return $db_result;
5051     }
5052 }
5053 __PACKAGE__->register_method(
5054     method   => 'address_alert_test',
5055     api_name => 'open-ils.actor.address_alert.test',
5056     signature => {
5057         desc => "Tests a set of address fields to determine if they match with an address_alert",
5058         params => [
5059             {desc => 'Authentication token', type => 'string'},
5060             {desc => 'Org Unit',             type => 'number'},
5061             {desc => 'Fields',               type => 'hash'},
5062         ],
5063         return => {desc => 'List of matching address_alerts'}
5064     }
5065 );
5066
5067 sub address_alert_test {
5068     my ($self, $client, $auth, $org_unit, $fields) = @_;
5069     return [] unless $fields and grep {$_} values %$fields;
5070
5071     my $e = new_editor(authtoken => $auth);
5072     return $e->event unless $e->checkauth;
5073     return $e->event unless $e->allowed('CREATE_USER', $org_unit);
5074     $org_unit ||= $e->requestor->ws_ou;
5075
5076     my $alerts = $e->json_query({
5077         from => [
5078             'actor.address_alert_matches',
5079             $org_unit,
5080             $$fields{street1},
5081             $$fields{street2},
5082             $$fields{city},
5083             $$fields{county},
5084             $$fields{state},
5085             $$fields{country},
5086             $$fields{post_code},
5087             $$fields{mailing_address},
5088             $$fields{billing_address}
5089         ]
5090     });
5091
5092     # map the json_query hashes to real objects
5093     return [
5094         map {$e->retrieve_actor_address_alert($_)}
5095             (map {$_->{id}} @$alerts)
5096     ];
5097 }
5098
5099 __PACKAGE__->register_method(
5100     method   => "mark_users_contact_invalid",
5101     api_name => "open-ils.actor.invalidate.email",
5102     signature => {
5103         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",
5104         params => [
5105             {desc => "Authentication token", type => "string"},
5106             {desc => "Patron ID (optional if Email address specified)", type => "number"},
5107             {desc => "Additional note text (optional)", type => "string"},
5108             {desc => "penalty org unit ID (optional)", type => "number"},
5109             {desc => "Email address (optional)", type => "string"}
5110         ],
5111         return => {desc => "Event describing success or failure", type => "object"}
5112     }
5113 );
5114
5115 __PACKAGE__->register_method(
5116     method   => "mark_users_contact_invalid",
5117     api_name => "open-ils.actor.invalidate.day_phone",
5118     signature => {
5119         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",
5120         params => [
5121             {desc => "Authentication token", type => "string"},
5122             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
5123             {desc => "Additional note text (optional)", type => "string"},
5124             {desc => "penalty org unit ID (optional)", type => "number"},
5125             {desc => "Phone Number (optional)", type => "string"}
5126         ],
5127         return => {desc => "Event describing success or failure", type => "object"}
5128     }
5129 );
5130
5131 __PACKAGE__->register_method(
5132     method   => "mark_users_contact_invalid",
5133     api_name => "open-ils.actor.invalidate.evening_phone",
5134     signature => {
5135         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",
5136         params => [
5137             {desc => "Authentication token", type => "string"},
5138             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
5139             {desc => "Additional note text (optional)", type => "string"},
5140             {desc => "penalty org unit ID (optional)", type => "number"},
5141             {desc => "Phone Number (optional)", type => "string"}
5142         ],
5143         return => {desc => "Event describing success or failure", type => "object"}
5144     }
5145 );
5146
5147 __PACKAGE__->register_method(
5148     method   => "mark_users_contact_invalid",
5149     api_name => "open-ils.actor.invalidate.other_phone",
5150     signature => {
5151         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",
5152         params => [
5153             {desc => "Authentication token", type => "string"},
5154             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
5155             {desc => "Additional note text (optional)", type => "string"},
5156             {desc => "penalty org unit ID (optional, default to top of org tree)",
5157                 type => "number"},
5158             {desc => "Phone Number (optional)", type => "string"}
5159         ],
5160         return => {desc => "Event describing success or failure", type => "object"}
5161     }
5162 );
5163
5164 sub mark_users_contact_invalid {
5165     my ($self, $conn, $auth, $patron_id, $addl_note, $penalty_ou, $contact) = @_;
5166
5167     # This method invalidates an email address or a phone_number which
5168     # removes the bad email address or phone number, copying its contents
5169     # to a patron note, and institutes a standing penalty for "bad email"
5170     # or "bad phone number" which is cleared when the user is saved or
5171     # optionally only when the user is saved with an email address or
5172     # phone number (or staff manually delete the penalty).
5173
5174     my $contact_type = ($self->api_name =~ /invalidate.(\w+)(\.|$)/)[0];
5175
5176     my $e = new_editor(authtoken => $auth, xact => 1);
5177     return $e->die_event unless $e->checkauth;
5178     
5179     my $howfind = {};
5180     if (defined $patron_id && $patron_id ne "") {
5181         $howfind = {usr => $patron_id};
5182     } elsif (defined $contact && $contact ne "") {
5183         $howfind = {$contact_type => $contact};
5184     } else {
5185         # Error out if no patron id set or no contact is set.
5186         return OpenILS::Event->new('BAD_PARAMS');
5187     }
5188  
5189     return OpenILS::Utils::BadContact->mark_users_contact_invalid(
5190         $e, $contact_type, $howfind,
5191         $addl_note, $penalty_ou, $e->requestor->id
5192     );
5193 }
5194
5195 # Putting the following method in open-ils.actor is a bad fit, except in that
5196 # it serves an interface that lives under 'actor' in the templates directory,
5197 # and in that there's nowhere else obvious to put it (open-ils.trigger is
5198 # private).
5199 __PACKAGE__->register_method(
5200     api_name => "open-ils.actor.action_trigger.reactors.all_in_use",
5201     method   => "get_all_at_reactors_in_use",
5202     api_level=> 1,
5203     argc     => 1,
5204     signature=> {
5205         params => [
5206             { name => 'authtoken', type => 'string' }
5207         ],
5208         return => {
5209             desc => 'list of reactor names', type => 'array'
5210         }
5211     }
5212 );
5213
5214 sub get_all_at_reactors_in_use {
5215     my ($self, $conn, $auth) = @_;
5216
5217     my $e = new_editor(authtoken => $auth);
5218     $e->checkauth or return $e->die_event;
5219     return $e->die_event unless $e->allowed('VIEW_TRIGGER_EVENT_DEF');
5220
5221     my $reactors = $e->json_query({
5222         select => {
5223             atevdef => [{column => "reactor", transform => "distinct"}]
5224         },
5225         from => {atevdef => {}}
5226     });
5227
5228     return $e->die_event unless ref $reactors eq "ARRAY";
5229     $e->disconnect;
5230
5231     return [ map { $_->{reactor} } @$reactors ];
5232 }
5233
5234 __PACKAGE__->register_method(
5235     method   => "filter_group_entry_crud",
5236     api_name => "open-ils.actor.filter_group_entry.crud",
5237     signature => {
5238         desc => q/
5239             Provides CRUD access to filter group entry objects.  These are not full accessible
5240             via PCRUD, since they requre "asq" objects for storing the query, and "asq" objects
5241             are not accessible via PCRUD (because they have no fields against which to link perms)
5242             /,
5243         params => [
5244             {desc => "Authentication token", type => "string"},
5245             {desc => "Entry ID / Entry Object", type => "number"},
5246             {desc => "Additional note text (optional)", type => "string"},
5247             {desc => "penalty org unit ID (optional, default to top of org tree)",
5248                 type => "number"}
5249         ],
5250         return => {
5251             desc => "Entry fleshed with query on Create, Retrieve, and Uupdate.  1 on Delete",
5252             type => "object"
5253         }
5254     }
5255 );
5256
5257 sub filter_group_entry_crud {
5258     my ($self, $conn, $auth, $arg) = @_;
5259
5260     return OpenILS::Event->new('BAD_PARAMS') unless $arg;
5261     my $e = new_editor(authtoken => $auth, xact => 1);
5262     return $e->die_event unless $e->checkauth;
5263
5264     if (ref $arg) {
5265
5266         if ($arg->isnew) {
5267
5268             my $grp = $e->retrieve_actor_search_filter_group($arg->grp)
5269                 or return $e->die_event;
5270
5271             return $e->die_event unless $e->allowed(
5272                 'ADMIN_SEARCH_FILTER_GROUP', $grp->owner);
5273
5274             my $query = $arg->query;
5275             $query = $e->create_actor_search_query($query) or return $e->die_event;
5276             $arg->query($query->id);
5277             my $entry = $e->create_actor_search_filter_group_entry($arg) or return $e->die_event;
5278             $entry->query($query);
5279
5280             $e->commit;
5281             return $entry;
5282
5283         } elsif ($arg->ischanged) {
5284
5285             my $entry = $e->retrieve_actor_search_filter_group_entry([
5286                 $arg->id, {
5287                     flesh => 1,
5288                     flesh_fields => {asfge => ['grp']}
5289                 }
5290             ]) or return $e->die_event;
5291
5292             return $e->die_event unless $e->allowed(
5293                 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
5294
5295             my $query = $e->update_actor_search_query($arg->query) or return $e->die_event;
5296             $arg->query($arg->query->id);
5297             $e->update_actor_search_filter_group_entry($arg) or return $e->die_event;
5298             $arg->query($query);
5299
5300             $e->commit;
5301             return $arg;
5302
5303         } elsif ($arg->isdeleted) {
5304
5305             my $entry = $e->retrieve_actor_search_filter_group_entry([
5306                 $arg->id, {
5307                     flesh => 1,
5308                     flesh_fields => {asfge => ['grp', 'query']}
5309                 }
5310             ]) or return $e->die_event;
5311
5312             return $e->die_event unless $e->allowed(
5313                 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
5314
5315             $e->delete_actor_search_filter_group_entry($entry) or return $e->die_event;
5316             $e->delete_actor_search_query($entry->query) or return $e->die_event;
5317
5318             $e->commit;
5319             return 1;
5320
5321         } else {
5322
5323             $e->rollback;
5324             return undef;
5325         }
5326
5327     } else {
5328
5329         my $entry = $e->retrieve_actor_search_filter_group_entry([
5330             $arg, {
5331                 flesh => 1,
5332                 flesh_fields => {asfge => ['grp', 'query']}
5333             }
5334         ]) or return $e->die_event;
5335
5336         return $e->die_event unless $e->allowed(
5337             ['ADMIN_SEARCH_FILTER_GROUP', 'VIEW_SEARCH_FILTER_GROUP'],
5338             $entry->grp->owner);
5339
5340         $e->rollback;
5341         $entry->grp($entry->grp->id); # for consistency
5342         return $entry;
5343     }
5344 }
5345
5346
5347 __PACKAGE__->register_method(
5348     method        => 'user_billing_xacts',
5349     api_name      => 'open-ils.actor.user.transactions.for_billing',
5350     signature     => {
5351         desc   => q/Returns a stream of user billing data appropriate for
5352             display in the user bills UI.  API is natively "authoritative"./,
5353         params => [
5354             {desc => 'Authentication token', type => 'string'},
5355             {desc => 'User ID', type => 'number'}
5356         ],
5357         return => {
5358             desc => q/First response is the user money summary, following
5359                 responses are fleshed billable transactions/
5360         }
5361     }
5362 );
5363
5364 sub user_billing_xacts {
5365     my ($self, $client, $auth, $user_id) = @_;
5366
5367     my $e = new_editor(authtoken => $auth, xact => 1);
5368     return $e->die_event unless $e->checkauth;
5369
5370     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
5371
5372     return $e->die_event unless 
5373         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
5374
5375     # Start with the user summary.
5376     $client->respond($e->retrieve_money_user_summary($user_id));
5377
5378     my $xact_ids = $e->json_query({
5379         select => {mbts => ['id']},
5380         from => 'mbts',
5381         where => {
5382             usr => $user_id,
5383             balance_owed => {'<>' => 0}
5384         },
5385         order_by => {mbts => {xact_start => 'asc'}}
5386     });
5387
5388     for my $xact_id (map { $_->{id} } @$xact_ids) {
5389
5390         my $xact = $e->retrieve_money_billable_transaction([
5391             $xact_id, {
5392                 flesh => 5,
5393                 flesh_fields => {
5394                                     mbt => [qw/summary circulation grocery/],
5395                                     circ => [qw/
5396                         target_copy 
5397                         workstation 
5398                         checkin_workstation 
5399                     /],
5400                                     acp =>  [qw/
5401                                             call_number
5402                                             holds_count
5403                                             status
5404                                             circ_lib
5405                                             location
5406                                             floating
5407                                             age_protect
5408                                             parts
5409                                     /],
5410                                     acpm => [qw/part/],
5411                                     acn =>  [qw/record owning_lib prefix suffix/],
5412                                     bre =>  [qw/wide_display_entry/]
5413                 },
5414                 # Avoid adding the MARXML
5415                 # Fleshed fields are implicitly included.
5416                 select => {bre => ['id']} 
5417             }
5418         ]);
5419
5420         $client->respond($xact);
5421     }
5422
5423     $e->rollback;
5424
5425     return undef;
5426 }
5427
5428 1;