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