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