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