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