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