]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
lp1777677 Security tweaks
[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        => 3
3770 );
3771
3772 sub get_itemsout_notices{
3773     my( $self, $conn, $auth, $circId, $patronId) = @_;
3774
3775     my $e = new_editor(authtoken => $auth);
3776     return $e->event unless $e->checkauth;
3777
3778     my $requestorId = $e->requestor->id;
3779
3780     if( $patronId ne $requestorId ){
3781         my $user = $e->retrieve_actor_user($requestorId) or return $e->event;
3782         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
3783     }
3784
3785     #my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3786     #my $req = $ses->request('open-ils.trigger.events_by_target',
3787     #   'circ', {target => [$circId], event=> {state=>'complete'}});
3788     # ^ Above removed in favor of faster json_query.
3789     #
3790     # SQL:
3791     # select complete_time
3792     # from action_trigger.event atev
3793     #     JOIN action_trigger.event_definition def ON (def.id = atev.event_def)
3794     #     JOIN action_trigger.hook athook ON (athook.key = def.hook)
3795     # where hook = 'checkout.due' AND state = 'complete' and target = <circId>;
3796     #
3797
3798     my $ctx_loc = $e->requestor->ws_ou;
3799     my $exclude_courtesy_notices = $U->ou_ancestor_setting_value($ctx_loc, 'webstaff.circ.itemsout_notice_count_excludes_courtesies');
3800     my $query = {
3801             select => { atev => ["complete_time"] },
3802             from => {
3803                     atev => {
3804                             atevdef => { field => "id",fkey => "event_def", join => { ath => { field => "key", fkey => "hook" }} }
3805                     }
3806             },
3807             where => {"+ath" => { key => "checkout.due" },"+atevdef" => { active => 't' },"+atev" => { target => $circId, state => 'complete' }}
3808     };
3809
3810     if ($exclude_courtesy_notices){
3811         $query->{"where"}->{"+atevdef"}->{validator} = { "<>" => "CircIsOpen"};
3812     }
3813
3814     my %resblob = ( numNotices => 0, lastDt => undef );
3815
3816     my $res = $e->json_query($query);
3817     for my $ndate (@$res) {
3818         $resblob{numNotices}++;
3819         if( !defined $resblob{lastDt}){
3820             $resblob{lastDt} = $$ndate{complete_time};
3821         }
3822
3823         if ($resblob{lastDt} lt $$ndate{complete_time}){
3824            $resblob{lastDt} = $$ndate{complete_time};
3825         }
3826    }
3827
3828     $conn->respond(\%resblob);
3829     return undef;
3830 }
3831
3832 __PACKAGE__->register_method (
3833     method      => 'update_events',
3834     api_name    => 'open-ils.actor.user.event.cancel.batch',
3835     stream      => 1,
3836 );
3837 __PACKAGE__->register_method (
3838     method      => 'update_events',
3839     api_name    => 'open-ils.actor.user.event.reset.batch',
3840     stream      => 1,
3841 );
3842
3843 sub update_events {
3844     my($self, $conn, $auth, $event_ids) = @_;
3845     my $e = new_editor(xact => 1, authtoken => $auth);
3846     return $e->die_event unless $e->checkauth;
3847
3848     my $x = 1;
3849     for my $id (@$event_ids) {
3850
3851         # do a little dance to determine what user we are ultimately affecting
3852         my $event = $e->retrieve_action_trigger_event([
3853             $id,
3854             {   flesh => 2,
3855                 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
3856             }
3857         ]) or return $e->die_event;
3858
3859         my $user_id;
3860         if($event->event_def->hook->core_type eq 'circ') {
3861             $user_id = $e->retrieve_action_circulation($event->target)->usr;
3862         } elsif($event->event_def->hook->core_type eq 'ahr') {
3863             $user_id = $e->retrieve_action_hold_request($event->target)->usr;
3864         } else {
3865             return 0;
3866         }
3867
3868         my $user = $e->retrieve_actor_user($user_id);
3869         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3870
3871         if($self->api_name =~ /cancel/) {
3872             $event->state('invalid');
3873         } elsif($self->api_name =~ /reset/) {
3874             $event->clear_start_time;
3875             $event->clear_update_time;
3876             $event->state('pending');
3877         }
3878
3879         $e->update_action_trigger_event($event) or return $e->die_event;
3880         $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
3881     }
3882
3883     $e->commit;
3884     return {complete => 1};
3885 }
3886
3887
3888 __PACKAGE__->register_method (
3889     method      => 'really_delete_user',
3890     api_name    => 'open-ils.actor.user.delete.override',
3891     signature   => q/@see open-ils.actor.user.delete/
3892 );
3893
3894 __PACKAGE__->register_method (
3895     method      => 'really_delete_user',
3896     api_name    => 'open-ils.actor.user.delete',
3897     signature   => q/
3898         It anonymizes all personally identifiable information in actor.usr. By calling actor.usr_purge_data()
3899         it also purges related data from other tables, sometimes by transferring it to a designated destination user.
3900         The usrname field (along with first_given_name and family_name) is updated to id '-PURGED-' now().
3901         dest_usr_id is only required when deleting a user that performs staff functions.
3902     /
3903 );
3904
3905 sub really_delete_user {
3906     my($self, $conn, $auth, $user_id, $dest_user_id, $oargs) = @_;
3907     my $e = new_editor(authtoken => $auth, xact => 1);
3908     return $e->die_event unless $e->checkauth;
3909     $oargs = { all => 1 } unless defined $oargs;
3910
3911     # Find all unclosed billings for for user $user_id, thereby, also checking for open circs
3912     my $open_bills = $e->json_query({
3913         select => { mbts => ['id'] },
3914         from => 'mbts',
3915         where => {
3916             xact_finish => { '=' => undef },
3917             usr => { '=' => $user_id },
3918         }
3919     }) or return $e->die_event;
3920
3921     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3922
3923     # No deleting patrons with open billings or checked out copies, unless perm-enabled override
3924     if (@$open_bills) {
3925         return $e->die_event(OpenILS::Event->new('ACTOR_USER_DELETE_OPEN_XACTS'))
3926         unless $self->api_name =~ /override/o && ($oargs->{all} || grep { $_ eq 'ACTOR_USER_DELETE_OPEN_XACTS' } @{$oargs->{events}})
3927         && $e->allowed('ACTOR_USER_DELETE_OPEN_XACTS.override', $user->home_ou);
3928     }
3929     # No deleting yourself - UI is supposed to stop you first, though.
3930     return $e->die_event unless $e->requestor->id != $user->id;
3931     return $e->die_event unless $e->allowed('DELETE_USER', $user->home_ou);
3932     # Check if you are allowed to mess with this patron permission group at all
3933     my $evt = group_perm_failed($e, $e->requestor, $user);
3934     return $e->die_event($evt) if $evt;
3935     my $stat = $e->json_query(
3936         {from => ['actor.usr_delete', $user_id, $dest_user_id]})->[0]
3937         or return $e->die_event;
3938     $e->commit;
3939     return 1;
3940 }
3941
3942
3943 __PACKAGE__->register_method (
3944     method      => 'user_payments',
3945     api_name    => 'open-ils.actor.user.payments.retrieve',
3946     stream => 1,
3947     signature   => q/
3948         Returns all payments for a given user.  Default order is newest payments first.
3949         @param auth Authentication token
3950         @param user_id The user ID
3951         @param filters An optional hash of filters, including limit, offset, and order_by definitions
3952     /
3953 );
3954
3955 sub user_payments {
3956     my($self, $conn, $auth, $user_id, $filters) = @_;
3957     $filters ||= {};
3958
3959     my $e = new_editor(authtoken => $auth);
3960     return $e->die_event unless $e->checkauth;
3961
3962     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3963     return $e->event unless
3964         $e->requestor->id == $user_id or
3965         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
3966
3967     # Find all payments for all transactions for user $user_id
3968     my $query = {
3969         select => {mp => ['id']},
3970         from => 'mp',
3971         where => {
3972             xact => {
3973                 in => {
3974                     select => {mbt => ['id']},
3975                     from => 'mbt',
3976                     where => {usr => $user_id}
3977                 }
3978             }
3979         },
3980         order_by => [
3981             { # by default, order newest payments first
3982                 class => 'mp',
3983                 field => 'payment_ts',
3984                 direction => 'desc'
3985             }, {
3986                 # secondary sort in ID as a tie-breaker, since payments created
3987                 # within the same transaction will have identical payment_ts's
3988                 class => 'mp',
3989                 field => 'id'
3990             }
3991         ]
3992     };
3993
3994     for (qw/order_by limit offset/) {
3995         $query->{$_} = $filters->{$_} if defined $filters->{$_};
3996     }
3997
3998     if(defined $filters->{where}) {
3999         foreach (keys %{$filters->{where}}) {
4000             # don't allow the caller to expand the result set to other users
4001             $query->{where}->{$_} = $filters->{where}->{$_} unless $_ eq 'xact';
4002         }
4003     }
4004
4005     my $payment_ids = $e->json_query($query);
4006     for my $pid (@$payment_ids) {
4007         my $pay = $e->retrieve_money_payment([
4008             $pid->{id},
4009             {   flesh => 6,
4010                 flesh_fields => {
4011                     mp => ['xact'],
4012                     mbt => ['summary', 'circulation', 'grocery'],
4013                     circ => ['target_copy'],
4014                     acp => ['call_number'],
4015                     acn => ['record']
4016                 }
4017             }
4018         ]);
4019
4020         my $resp = {
4021             mp => $pay,
4022             xact_type => $pay->xact->summary->xact_type,
4023             last_billing_type => $pay->xact->summary->last_billing_type,
4024         };
4025
4026         if($pay->xact->summary->xact_type eq 'circulation') {
4027             $resp->{barcode} = $pay->xact->circulation->target_copy->barcode;
4028             $resp->{title} = $U->record_to_mvr($pay->xact->circulation->target_copy->call_number->record)->title;
4029         }
4030
4031         $pay->xact($pay->xact->id); # de-flesh
4032         $conn->respond($resp);
4033     }
4034
4035     return undef;
4036 }
4037
4038
4039
4040 __PACKAGE__->register_method (
4041     method      => 'negative_balance_users',
4042     api_name    => 'open-ils.actor.users.negative_balance',
4043     stream => 1,
4044     signature   => q/
4045         Returns all users that have an overall negative balance
4046         @param auth Authentication token
4047         @param org_id The context org unit as an ID or list of IDs.  This will be the home
4048         library of the user.  If no org_unit is specified, no org unit filter is applied
4049     /
4050 );
4051
4052 sub negative_balance_users {
4053     my($self, $conn, $auth, $org_id) = @_;
4054
4055     my $e = new_editor(authtoken => $auth);
4056     return $e->die_event unless $e->checkauth;
4057     return $e->die_event unless $e->allowed('VIEW_USER', $org_id);
4058
4059     my $query = {
4060         select => {
4061             mous => ['usr', 'balance_owed'],
4062             au => ['home_ou'],
4063             mbts => [
4064                 {column => 'last_billing_ts', transform => 'max', aggregate => 1},
4065                 {column => 'last_payment_ts', transform => 'max', aggregate => 1},
4066             ]
4067         },
4068         from => {
4069             mous => {
4070                 au => {
4071                     fkey => 'usr',
4072                     field => 'id',
4073                     join => {
4074                         mbts => {
4075                             key => 'id',
4076                             field => 'usr'
4077                         }
4078                     }
4079                 }
4080             }
4081         },
4082         where => {'+mous' => {balance_owed => {'<' => 0}}}
4083     };
4084
4085     $query->{from}->{mous}->{au}->{filter}->{home_ou} = $org_id if $org_id;
4086
4087     my $list = $e->json_query($query, {timeout => 600});
4088
4089     for my $data (@$list) {
4090         $conn->respond({
4091             usr => $e->retrieve_actor_user([$data->{usr}, {flesh => 1, flesh_fields => {au => ['card']}}]),
4092             balance_owed => $data->{balance_owed},
4093             last_billing_activity => max($data->{last_billing_ts}, $data->{last_payment_ts})
4094         });
4095     }
4096
4097     return undef;
4098 }
4099
4100 __PACKAGE__->register_method(
4101     method  => "request_password_reset",
4102     api_name    => "open-ils.actor.patron.password_reset.request",
4103     signature   => {
4104         desc => "Generates a UUID token usable with the open-ils.actor.patron.password_reset.commit " .
4105                 "method for changing a user's password.  The UUID token is distributed via A/T "      .
4106                 "templates (i.e. email to the user).",
4107         params => [
4108             { desc => 'user_id_type', type => 'string' },
4109             { desc => 'user_id', type => 'string' },
4110             { desc => 'optional (based on library setting) matching email address for authorizing request', type => 'string' },
4111         ],
4112         return => {desc => '1 on success, Event on error'}
4113     }
4114 );
4115 sub request_password_reset {
4116     my($self, $conn, $user_id_type, $user_id, $email) = @_;
4117
4118     # Check to see if password reset requests are already being throttled:
4119     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
4120
4121     my $e = new_editor(xact => 1);
4122     my $user;
4123
4124     # Get the user, if any, depending on the input value
4125     if ($user_id_type eq 'username') {
4126         $user = $e->search_actor_user({usrname => $user_id})->[0];
4127         if (!$user) {
4128             $e->die_event;
4129             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
4130         }
4131     } elsif ($user_id_type eq 'barcode') {
4132         my $card = $e->search_actor_card([
4133             {barcode => $user_id},
4134             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
4135         if (!$card) {
4136             $e->die_event;
4137             return OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
4138         }
4139         $user = $card->usr;
4140     }
4141
4142     # If the user doesn't have an email address, we can't help them
4143     if (!$user->email) {
4144         $e->die_event;
4145         return OpenILS::Event->new('PATRON_NO_EMAIL_ADDRESS');
4146     }
4147
4148     my $email_must_match = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_requires_matching_email');
4149     if ($email_must_match) {
4150         if (lc($user->email) ne lc($email)) {
4151             return OpenILS::Event->new('EMAIL_VERIFICATION_FAILED');
4152         }
4153     }
4154
4155     _reset_password_request($conn, $e, $user);
4156 }
4157
4158 # Once we have the user, we can issue the password reset request
4159 # XXX Add a wrapper method that accepts barcode + email input
4160 sub _reset_password_request {
4161     my ($conn, $e, $user) = @_;
4162
4163     # 1. Get throttle threshold and time-to-live from OU_settings
4164     my $aupr_throttle = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_throttle') || 1000;
4165     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4166
4167     my $threshold_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl)->iso8601();
4168
4169     # 2. Get time of last request and number of active requests (num_active)
4170     my $active_requests = $e->json_query({
4171         from => 'aupr',
4172         select => {
4173             aupr => [
4174                 {
4175                     column => 'uuid',
4176                     transform => 'COUNT'
4177                 },
4178                 {
4179                     column => 'request_time',
4180                     transform => 'MAX'
4181                 }
4182             ]
4183         },
4184         where => {
4185             has_been_reset => { '=' => 'f' },
4186             request_time => { '>' => $threshold_time }
4187         }
4188     });
4189
4190     # Guard against no active requests
4191     if ($active_requests->[0]->{'request_time'}) {
4192         my $last_request = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($active_requests->[0]->{'request_time'}));
4193         my $now = DateTime::Format::ISO8601->new();
4194
4195         # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute)
4196         if (($active_requests->[0]->{'usr'} > $aupr_throttle) &&
4197             ($last_request->add_duration('1 minute') > $now)) {
4198             $cache->put_cache('open-ils.actor.password.throttle', DateTime::Format::ISO8601->new(), 60);
4199             $e->die_event;
4200             return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4201         }
4202     }
4203
4204     # TODO Check to see if the user is in a password-reset-restricted group
4205
4206     # Otherwise, go ahead and try to get the user.
4207
4208     # Check the number of active requests for this user
4209     $active_requests = $e->json_query({
4210         from => 'aupr',
4211         select => {
4212             aupr => [
4213                 {
4214                     column => 'usr',
4215                     transform => 'COUNT'
4216                 }
4217             ]
4218         },
4219         where => {
4220             usr => { '=' => $user->id },
4221             has_been_reset => { '=' => 'f' },
4222             request_time => { '>' => $threshold_time }
4223         }
4224     });
4225
4226     $logger->info("User " . $user->id . " has " . $active_requests->[0]->{'usr'} . " active password reset requests.");
4227
4228     # if less than or equal to per-user threshold, proceed; otherwise, return event
4229     my $aupr_per_user_limit = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_per_user_limit') || 3;
4230     if ($active_requests->[0]->{'usr'} > $aupr_per_user_limit) {
4231         $e->die_event;
4232         return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4233     }
4234
4235     # Create the aupr object and insert into the database
4236     my $reset_request = Fieldmapper::actor::usr_password_reset->new;
4237     my $uuid = create_uuid_as_string(UUID_V4);
4238     $reset_request->uuid($uuid);
4239     $reset_request->usr($user->id);
4240
4241     my $aupr = $e->create_actor_usr_password_reset($reset_request) or return $e->die_event;
4242     $e->commit;
4243
4244     # Create an event to notify user of the URL to reset their password
4245
4246     # Can we stuff this in the user_data param for trigger autocreate?
4247     my $hostname = $U->ou_ancestor_setting_value($user->home_ou, 'lib.hostname') || 'localhost';
4248
4249     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
4250     $ses->request('open-ils.trigger.event.autocreate', 'password.reset_request', $aupr, $user->home_ou);
4251
4252     # Trunk only
4253     # $U->create_trigger_event('password.reset_request', $aupr, $user->home_ou);
4254
4255     return 1;
4256 }
4257
4258 __PACKAGE__->register_method(
4259     method  => "commit_password_reset",
4260     api_name    => "open-ils.actor.patron.password_reset.commit",
4261     signature   => {
4262         desc => "Checks a UUID token generated by the open-ils.actor.patron.password_reset.request method for " .
4263                 "validity, and if valid, uses it as authorization for changing the associated user's password " .
4264                 "with the supplied password.",
4265         params => [
4266             { desc => 'uuid', type => 'string' },
4267             { desc => 'password', type => 'string' },
4268         ],
4269         return => {desc => '1 on success, Event on error'}
4270     }
4271 );
4272 sub commit_password_reset {
4273     my($self, $conn, $uuid, $password) = @_;
4274
4275     # Check to see if password reset requests are already being throttled:
4276     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
4277     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
4278     my $throttle = $cache->get_cache('open-ils.actor.password.throttle') || undef;
4279     if ($throttle) {
4280         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4281     }
4282
4283     my $e = new_editor(xact => 1);
4284
4285     my $aupr = $e->search_actor_usr_password_reset({
4286         uuid => $uuid,
4287         has_been_reset => 0
4288     });
4289
4290     if (!$aupr->[0]) {
4291         $e->die_event;
4292         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4293     }
4294     my $user_id = $aupr->[0]->usr;
4295     my $user = $e->retrieve_actor_user($user_id);
4296
4297     # Ensure we're still within the TTL for the request
4298     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4299     my $threshold = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($aupr->[0]->request_time))->add(seconds => $aupr_ttl);
4300     if ($threshold < DateTime->now(time_zone => 'local')) {
4301         $e->die_event;
4302         $logger->info("Password reset request needed to be submitted before $threshold");
4303         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4304     }
4305
4306     # Check complexity of password against OU-defined regex
4307     my $pw_regex = $U->ou_ancestor_setting_value($user->home_ou, 'global.password_regex');
4308
4309     my $is_strong = 0;
4310     if ($pw_regex) {
4311         # Calling JSON2perl on the $pw_regex causes failure, even before the fancy Unicode regex
4312         # ($pw_regex = OpenSRF::Utils::JSON->JSON2perl($pw_regex)) =~ s/\\u([0-9a-fA-F]{4})/\\x{$1}/gs;
4313         $is_strong = check_password_strength_custom($password, $pw_regex);
4314     } else {
4315         $is_strong = check_password_strength_default($password);
4316     }
4317
4318     if (!$is_strong) {
4319         $e->die_event;
4320         return OpenILS::Event->new('PATRON_PASSWORD_WAS_NOT_STRONG');
4321     }
4322
4323     # All is well; update the password
4324     modify_migrated_user_password($e, $user->id, $password);
4325
4326     # And flag that this password reset request has been honoured
4327     $aupr->[0]->has_been_reset('t');
4328     $e->update_actor_usr_password_reset($aupr->[0]);
4329     $e->commit;
4330
4331     return 1;
4332 }
4333
4334 sub check_password_strength_default {
4335     my $password = shift;
4336     # Use the default set of checks
4337     if ( (length($password) < 7) or
4338             ($password !~ m/.*\d+.*/) or
4339             ($password !~ m/.*[A-Za-z]+.*/)
4340     ) {
4341         return 0;
4342     }
4343     return 1;
4344 }
4345
4346 sub check_password_strength_custom {
4347     my ($password, $pw_regex) = @_;
4348
4349     $pw_regex = qr/$pw_regex/;
4350     if ($password !~  /$pw_regex/) {
4351         return 0;
4352     }
4353     return 1;
4354 }
4355
4356 __PACKAGE__->register_method(
4357     method    => "fire_test_notification",
4358     api_name  => "open-ils.actor.event.test_notification"
4359 );
4360
4361 sub fire_test_notification {
4362     my($self, $conn, $auth, $args) = @_;
4363     my $e = new_editor(authtoken => $auth);
4364     return $e->event unless $e->checkauth;
4365     if ($e->requestor->id != $$args{target}) {
4366         my $home_ou = $e->retrieve_actor_user($$args{target})->home_ou;
4367         return $e->die_event unless $home_ou && $e->allowed('USER_VIEW', $home_ou);
4368     }
4369
4370     my $event_hook = $$args{hook} or return $e->event;
4371     my $usr = $e->retrieve_actor_user($$args{target});
4372
4373     return $e->event unless $usr;
4374
4375     return $U->fire_object_event(undef, $event_hook, $usr, $e->requestor->ws_ou);
4376 }
4377
4378
4379 __PACKAGE__->register_method(
4380     method    => "event_def_opt_in_settings",
4381     api_name  => "open-ils.actor.event_def.opt_in.settings",
4382     stream => 1,
4383     signature => {
4384         desc   => 'Streams the set of "cust" objects that are used as opt-in settings for event definitions',
4385         params => [
4386             { desc => 'Authentication token',  type => 'string'},
4387             {
4388                 desc => 'Org Unit ID.  (optional).  If no org ID is present, the home_ou of the requesting user is used',
4389                 type => 'number'
4390             },
4391         ],
4392         return => {
4393             desc => q/set of "cust" objects that are used as opt-in settings for event definitions at the specified org unit/,
4394             type => 'object',
4395             class => 'cust'
4396         }
4397     }
4398 );
4399
4400 sub event_def_opt_in_settings {
4401     my($self, $conn, $auth, $org_id) = @_;
4402     my $e = new_editor(authtoken => $auth);
4403     return $e->event unless $e->checkauth;
4404
4405     if(defined $org_id and $org_id != $e->requestor->home_ou) {
4406         return $e->event unless
4407             $e->allowed(['VIEW_USER_SETTING_TYPE', 'ADMIN_USER_SETTING_TYPE'], $org_id);
4408     } else {
4409         $org_id = $e->requestor->home_ou;
4410     }
4411
4412     # find all config.user_setting_type's related to event_defs for the requested org unit
4413     my $types = $e->json_query({
4414         select => {cust => ['name']},
4415         from => {atevdef => 'cust'},
4416         where => {
4417             '+atevdef' => {
4418                 owner => $U->get_org_ancestors($org_id), # context org plus parents
4419                 active => 't'
4420             }
4421         }
4422     });
4423
4424     if(@$types) {
4425         $conn->respond($_) for
4426             @{$e->search_config_usr_setting_type({name => [map {$_->{name}} @$types]})};
4427     }
4428
4429     return undef;
4430 }
4431
4432
4433 __PACKAGE__->register_method(
4434     method    => "user_circ_history",
4435     api_name  => "open-ils.actor.history.circ",
4436     stream => 1,
4437     authoritative => 1,
4438     signature => {
4439         desc   => 'Returns user circ history objects for the calling user',
4440         params => [
4441             { desc => 'Authentication token',  type => 'string'},
4442             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4443         ],
4444         return => {
4445             desc => q/Stream of 'auch' circ history objects/,
4446             type => 'object',
4447         }
4448     }
4449 );
4450
4451 __PACKAGE__->register_method(
4452     method    => "user_circ_history",
4453     api_name  => "open-ils.actor.history.circ.clear",
4454     stream => 1,
4455     signature => {
4456         desc   => 'Delete all user circ history entries for the calling user',
4457         params => [
4458             { desc => 'Authentication token',  type => 'string'},
4459             { desc => "Options hash. 'circ_ids' is an arrayref of circulation IDs to delete", type => 'object' },
4460         ],
4461         return => {
4462             desc => q/1 on success, event on error/,
4463             type => 'object',
4464         }
4465     }
4466 );
4467
4468 __PACKAGE__->register_method(
4469     method    => "user_circ_history",
4470     api_name  => "open-ils.actor.history.circ.print",
4471     stream => 1,
4472     signature => {
4473         desc   => q/Returns printable output for the caller's circ history objects/,
4474         params => [
4475             { desc => 'Authentication token',  type => 'string'},
4476             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4477         ],
4478         return => {
4479             desc => q/An action_trigger.event object or error event./,
4480             type => 'object',
4481         }
4482     }
4483 );
4484
4485 __PACKAGE__->register_method(
4486     method    => "user_circ_history",
4487     api_name  => "open-ils.actor.history.circ.email",
4488     stream => 1,
4489     signature => {
4490         desc   => q/Emails the caller's circ history/,
4491         params => [
4492             { desc => 'Authentication token',  type => 'string'},
4493             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4494             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4495         ],
4496         return => {
4497             desc => q/undef, or event on error/
4498         }
4499     }
4500 );
4501
4502 sub user_circ_history {
4503     my ($self, $conn, $auth, $options) = @_;
4504     $options ||= {};
4505
4506     my $for_print = ($self->api_name =~ /print/);
4507     my $for_email = ($self->api_name =~ /email/);
4508     my $for_clear = ($self->api_name =~ /clear/);
4509
4510     # No perm check is performed.  Caller may only access his/her own
4511     # circ history entries.
4512     my $e = new_editor(authtoken => $auth);
4513     return $e->event unless $e->checkauth;
4514
4515     my %limits = ();
4516     if (!$for_clear) { # clear deletes all
4517         $limits{offset} = $options->{offset} if defined $options->{offset};
4518         $limits{limit} = $options->{limit} if defined $options->{limit};
4519     }
4520
4521     my %circ_id_filter = $options->{circ_ids} ?
4522         (id => $options->{circ_ids}) : ();
4523
4524     my $circs = $e->search_action_user_circ_history([
4525         {   usr => $e->requestor->id,
4526             %circ_id_filter
4527         },
4528         {   # order newest to oldest by default
4529             order_by => {auch => 'xact_start DESC'},
4530             %limits
4531         },
4532         {substream => 1} # could be a large list
4533     ]);
4534
4535     if ($for_print) {
4536         return $U->fire_object_event(undef,
4537             'circ.format.history.print', $circs, $e->requestor->home_ou);
4538     }
4539
4540     $e->xact_begin if $for_clear;
4541     $conn->respond_complete(1) if $for_email;  # no sense in waiting
4542
4543     for my $circ (@$circs) {
4544
4545         if ($for_email) {
4546             # events will be fired from action_trigger_runner
4547             $U->create_events_for_hook('circ.format.history.email',
4548                 $circ, $e->editor->home_ou, undef, undef, 1);
4549
4550         } elsif ($for_clear) {
4551
4552             $e->delete_action_user_circ_history($circ)
4553                 or return $e->die_event;
4554
4555         } else {
4556             $conn->respond($circ);
4557         }
4558     }
4559
4560     if ($for_clear) {
4561         $e->commit;
4562         return 1;
4563     }
4564
4565     return undef;
4566 }
4567
4568
4569 __PACKAGE__->register_method(
4570     method    => "user_visible_holds",
4571     api_name  => "open-ils.actor.history.hold.visible",
4572     stream => 1,
4573     signature => {
4574         desc   => 'Returns the set of opt-in visible holds',
4575         params => [
4576             { desc => 'Authentication token',  type => 'string'},
4577             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4578             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4579         ],
4580         return => {
4581             desc => q/An object with 1 field: "hold"/,
4582             type => 'object',
4583         }
4584     }
4585 );
4586
4587 __PACKAGE__->register_method(
4588     method    => "user_visible_holds",
4589     api_name  => "open-ils.actor.history.hold.visible.print",
4590     stream => 1,
4591     signature => {
4592         desc   => 'Returns printable output for the set of opt-in visible holds',
4593         params => [
4594             { desc => 'Authentication token',  type => 'string'},
4595             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4596             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4597         ],
4598         return => {
4599             desc => q/An action_trigger.event object or error event./,
4600             type => 'object',
4601         }
4602     }
4603 );
4604
4605 __PACKAGE__->register_method(
4606     method    => "user_visible_holds",
4607     api_name  => "open-ils.actor.history.hold.visible.email",
4608     stream => 1,
4609     signature => {
4610         desc   => 'Emails the set of opt-in visible holds to the requestor',
4611         params => [
4612             { desc => 'Authentication token',  type => 'string'},
4613             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4614             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4615         ],
4616         return => {
4617             desc => q/undef, or event on error/
4618         }
4619     }
4620 );
4621
4622 sub user_visible_holds {
4623     my($self, $conn, $auth, $user_id, $options) = @_;
4624
4625     my $is_hold = 1;
4626     my $for_print = ($self->api_name =~ /print/);
4627     my $for_email = ($self->api_name =~ /email/);
4628     my $e = new_editor(authtoken => $auth);
4629     return $e->event unless $e->checkauth;
4630
4631     $user_id ||= $e->requestor->id;
4632     $options ||= {};
4633     $options->{limit} ||= 50;
4634     $options->{offset} ||= 0;
4635
4636     if($user_id != $e->requestor->id) {
4637         my $perm = ($is_hold) ? 'VIEW_HOLD' : 'VIEW_CIRCULATIONS';
4638         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4639         return $e->event unless $e->allowed($perm, $user->home_ou);
4640     }
4641
4642     my $db_func = ($is_hold) ? 'action.usr_visible_holds' : 'action.usr_visible_circs';
4643
4644     my $data = $e->json_query({
4645         from => [$db_func, $user_id],
4646         limit => $$options{limit},
4647         offset => $$options{offset}
4648
4649         # TODO: I only want IDs. code below didn't get me there
4650         # {"select":{"au":[{"column":"id", "result_field":"id",
4651         # "transform":"action.usr_visible_circs"}]}, "where":{"id":10}, "from":"au"}
4652     },{
4653         substream => 1
4654     });
4655
4656     return undef unless @$data;
4657
4658     if ($for_print) {
4659
4660         # collect the batch of objects
4661
4662         if($is_hold) {
4663
4664             my $hold_list = $e->search_action_hold_request({id => [map { $_->{id} } @$data]});
4665             return $U->fire_object_event(undef, 'ahr.format.history.print', $hold_list, $$hold_list[0]->request_lib);
4666
4667         } else {
4668
4669             my $circ_list = $e->search_action_circulation({id => [map { $_->{id} } @$data]});
4670             return $U->fire_object_event(undef, 'circ.format.history.print', $circ_list, $$circ_list[0]->circ_lib);
4671         }
4672
4673     } elsif ($for_email) {
4674
4675         $conn->respond_complete(1) if $for_email;  # no sense in waiting
4676
4677         foreach (@$data) {
4678
4679             my $id = $_->{id};
4680
4681             if($is_hold) {
4682
4683                 my $hold = $e->retrieve_action_hold_request($id);
4684                 $U->create_events_for_hook('ahr.format.history.email', $hold, $hold->request_lib, undef, undef, 1);
4685                 # events will be fired from action_trigger_runner
4686
4687             } else {
4688
4689                 my $circ = $e->retrieve_action_circulation($id);
4690                 $U->create_events_for_hook('circ.format.history.email', $circ, $circ->circ_lib, undef, undef, 1);
4691                 # events will be fired from action_trigger_runner
4692             }
4693         }
4694
4695     } else { # just give me the data please
4696
4697         foreach (@$data) {
4698
4699             my $id = $_->{id};
4700
4701             if($is_hold) {
4702
4703                 my $hold = $e->retrieve_action_hold_request($id);
4704                 $conn->respond({hold => $hold});
4705
4706             } else {
4707
4708                 my $circ = $e->retrieve_action_circulation($id);
4709                 $conn->respond({
4710                     circ => $circ,
4711                     summary => $U->create_circ_chain_summary($e, $id)
4712                 });
4713             }
4714         }
4715     }
4716
4717     return undef;
4718 }
4719
4720 __PACKAGE__->register_method(
4721     method     => "user_saved_search_cud",
4722     api_name   => "open-ils.actor.user.saved_search.cud",
4723     stream     => 1,
4724     signature  => {
4725         desc   => 'Create/Update/Delete Access to user saved searches',
4726         params => [
4727             { desc => 'Authentication token', type => 'string' },
4728             { desc => 'Saved Search Object', type => 'object', class => 'auss' }
4729         ],
4730         return => {
4731             desc   => q/The retrieved or updated saved search object, or id of a deleted object; Event on error/,
4732             class  => 'auss'
4733         }
4734     }
4735 );
4736
4737 __PACKAGE__->register_method(
4738     method     => "user_saved_search_cud",
4739     api_name   => "open-ils.actor.user.saved_search.retrieve",
4740     stream     => 1,
4741     signature  => {
4742         desc   => 'Retrieve a saved search object',
4743         params => [
4744             { desc => 'Authentication token', type => 'string' },
4745             { desc => 'Saved Search ID', type => 'number' }
4746         ],
4747         return => {
4748             desc   => q/The saved search object, Event on error/,
4749             class  => 'auss'
4750         }
4751     }
4752 );
4753
4754 sub user_saved_search_cud {
4755     my( $self, $client, $auth, $search ) = @_;
4756     my $e = new_editor( authtoken=>$auth );
4757     return $e->die_event unless $e->checkauth;
4758
4759     my $o_search;      # prior version of the object, if any
4760     my $res;           # to be returned
4761
4762     # branch on the operation type
4763
4764     if( $self->api_name =~ /retrieve/ ) {                    # Retrieve
4765
4766         # Get the old version, to check ownership
4767         $o_search = $e->retrieve_actor_usr_saved_search( $search )
4768             or return $e->die_event;
4769
4770         # You can't read somebody else's search
4771         return OpenILS::Event->new('BAD_PARAMS')
4772             unless $o_search->owner == $e->requestor->id;
4773
4774         $res = $o_search;
4775
4776     } else {
4777
4778         $e->xact_begin;               # start an editor transaction
4779
4780         if( $search->isnew ) {                               # Create
4781
4782             # You can't create a search for somebody else
4783             return OpenILS::Event->new('BAD_PARAMS')
4784                 unless $search->owner == $e->requestor->id;
4785
4786             $e->create_actor_usr_saved_search( $search )
4787                 or return $e->die_event;
4788
4789             $res = $search->id;
4790
4791         } elsif( $search->ischanged ) {                      # Update
4792
4793             # You can't change ownership of a search
4794             return OpenILS::Event->new('BAD_PARAMS')
4795                 unless $search->owner == $e->requestor->id;
4796
4797             # Get the old version, to check ownership
4798             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4799                 or return $e->die_event;
4800
4801             # You can't update somebody else's search
4802             return OpenILS::Event->new('BAD_PARAMS')
4803                 unless $o_search->owner == $e->requestor->id;
4804
4805             # Do the update
4806             $e->update_actor_usr_saved_search( $search )
4807                 or return $e->die_event;
4808
4809             $res = $search;
4810
4811         } elsif( $search->isdeleted ) {                      # Delete
4812
4813             # Get the old version, to check ownership
4814             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4815                 or return $e->die_event;
4816
4817             # You can't delete somebody else's search
4818             return OpenILS::Event->new('BAD_PARAMS')
4819                 unless $o_search->owner == $e->requestor->id;
4820
4821             # Do the delete
4822             $e->delete_actor_usr_saved_search( $o_search )
4823                 or return $e->die_event;
4824
4825             $res = $search->id;
4826         }
4827
4828         $e->commit;
4829     }
4830
4831     return $res;
4832 }
4833
4834 __PACKAGE__->register_method(
4835     method   => "get_barcodes",
4836     api_name => "open-ils.actor.get_barcodes"
4837 );
4838
4839 sub get_barcodes {
4840     my( $self, $client, $auth, $org_id, $context, $barcode ) = @_;
4841     my $e = new_editor(authtoken => $auth);
4842     return $e->event unless $e->checkauth;
4843     return $e->event unless $e->allowed('STAFF_LOGIN', $org_id);
4844
4845     my $db_result = $e->json_query(
4846         {   from => [
4847                 'evergreen.get_barcodes',
4848                 $org_id, $context, $barcode,
4849             ]
4850         }
4851     );
4852     if($context =~ /actor/) {
4853         my $filter_result = ();
4854         my $patron;
4855         foreach my $result (@$db_result) {
4856             if($result->{type} eq 'actor') {
4857                 if($e->requestor->id != $result->{id}) {
4858                     $patron = $e->retrieve_actor_user($result->{id});
4859                     if(!$patron) {
4860                         push(@$filter_result, $e->event);
4861                         next;
4862                     }
4863                     if($e->allowed('VIEW_USER', $patron->home_ou)) {
4864                         push(@$filter_result, $result);
4865                     }
4866                     else {
4867                         push(@$filter_result, $e->event);
4868                     }
4869                 }
4870                 else {
4871                     push(@$filter_result, $result);
4872                 }
4873             }
4874             else {
4875                 push(@$filter_result, $result);
4876             }
4877         }
4878         return $filter_result;
4879     }
4880     else {
4881         return $db_result;
4882     }
4883 }
4884 __PACKAGE__->register_method(
4885     method   => 'address_alert_test',
4886     api_name => 'open-ils.actor.address_alert.test',
4887     signature => {
4888         desc => "Tests a set of address fields to determine if they match with an address_alert",
4889         params => [
4890             {desc => 'Authentication token', type => 'string'},
4891             {desc => 'Org Unit',             type => 'number'},
4892             {desc => 'Fields',               type => 'hash'},
4893         ],
4894         return => {desc => 'List of matching address_alerts'}
4895     }
4896 );
4897
4898 sub address_alert_test {
4899     my ($self, $client, $auth, $org_unit, $fields) = @_;
4900     return [] unless $fields and grep {$_} values %$fields;
4901
4902     my $e = new_editor(authtoken => $auth);
4903     return $e->event unless $e->checkauth;
4904     return $e->event unless $e->allowed('CREATE_USER', $org_unit);
4905     $org_unit ||= $e->requestor->ws_ou;
4906
4907     my $alerts = $e->json_query({
4908         from => [
4909             'actor.address_alert_matches',
4910             $org_unit,
4911             $$fields{street1},
4912             $$fields{street2},
4913             $$fields{city},
4914             $$fields{county},
4915             $$fields{state},
4916             $$fields{country},
4917             $$fields{post_code},
4918             $$fields{mailing_address},
4919             $$fields{billing_address}
4920         ]
4921     });
4922
4923     # map the json_query hashes to real objects
4924     return [
4925         map {$e->retrieve_actor_address_alert($_)}
4926             (map {$_->{id}} @$alerts)
4927     ];
4928 }
4929
4930 __PACKAGE__->register_method(
4931     method   => "mark_users_contact_invalid",
4932     api_name => "open-ils.actor.invalidate.email",
4933     signature => {
4934         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",
4935         params => [
4936             {desc => "Authentication token", type => "string"},
4937             {desc => "Patron ID (optional if Email address specified)", type => "number"},
4938             {desc => "Additional note text (optional)", type => "string"},
4939             {desc => "penalty org unit ID (optional)", type => "number"},
4940             {desc => "Email address (optional)", type => "string"}
4941         ],
4942         return => {desc => "Event describing success or failure", type => "object"}
4943     }
4944 );
4945
4946 __PACKAGE__->register_method(
4947     method   => "mark_users_contact_invalid",
4948     api_name => "open-ils.actor.invalidate.day_phone",
4949     signature => {
4950         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",
4951         params => [
4952             {desc => "Authentication token", type => "string"},
4953             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
4954             {desc => "Additional note text (optional)", type => "string"},
4955             {desc => "penalty org unit ID (optional)", type => "number"},
4956             {desc => "Phone Number (optional)", type => "string"}
4957         ],
4958         return => {desc => "Event describing success or failure", type => "object"}
4959     }
4960 );
4961
4962 __PACKAGE__->register_method(
4963     method   => "mark_users_contact_invalid",
4964     api_name => "open-ils.actor.invalidate.evening_phone",
4965     signature => {
4966         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",
4967         params => [
4968             {desc => "Authentication token", type => "string"},
4969             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
4970             {desc => "Additional note text (optional)", type => "string"},
4971             {desc => "penalty org unit ID (optional)", type => "number"},
4972             {desc => "Phone Number (optional)", type => "string"}
4973         ],
4974         return => {desc => "Event describing success or failure", type => "object"}
4975     }
4976 );
4977
4978 __PACKAGE__->register_method(
4979     method   => "mark_users_contact_invalid",
4980     api_name => "open-ils.actor.invalidate.other_phone",
4981     signature => {
4982         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",
4983         params => [
4984             {desc => "Authentication token", type => "string"},
4985             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
4986             {desc => "Additional note text (optional)", type => "string"},
4987             {desc => "penalty org unit ID (optional, default to top of org tree)",
4988                 type => "number"},
4989             {desc => "Phone Number (optional)", type => "string"}
4990         ],
4991         return => {desc => "Event describing success or failure", type => "object"}
4992     }
4993 );
4994
4995 sub mark_users_contact_invalid {
4996     my ($self, $conn, $auth, $patron_id, $addl_note, $penalty_ou, $contact) = @_;
4997
4998     # This method invalidates an email address or a phone_number which
4999     # removes the bad email address or phone number, copying its contents
5000     # to a patron note, and institutes a standing penalty for "bad email"
5001     # or "bad phone number" which is cleared when the user is saved or
5002     # optionally only when the user is saved with an email address or
5003     # phone number (or staff manually delete the penalty).
5004
5005     my $contact_type = ($self->api_name =~ /invalidate.(\w+)(\.|$)/)[0];
5006
5007     my $e = new_editor(authtoken => $auth, xact => 1);
5008     return $e->die_event unless $e->checkauth;
5009     
5010     my $howfind = {};
5011     if (defined $patron_id && $patron_id ne "") {
5012         $howfind = {usr => $patron_id};
5013     } elsif (defined $contact && $contact ne "") {
5014         $howfind = {$contact_type => $contact};
5015     } else {
5016         # Error out if no patron id set or no contact is set.
5017         return OpenILS::Event->new('BAD_PARAMS');
5018     }
5019  
5020     return OpenILS::Utils::BadContact->mark_users_contact_invalid(
5021         $e, $contact_type, $howfind,
5022         $addl_note, $penalty_ou, $e->requestor->id
5023     );
5024 }
5025
5026 # Putting the following method in open-ils.actor is a bad fit, except in that
5027 # it serves an interface that lives under 'actor' in the templates directory,
5028 # and in that there's nowhere else obvious to put it (open-ils.trigger is
5029 # private).
5030 __PACKAGE__->register_method(
5031     api_name => "open-ils.actor.action_trigger.reactors.all_in_use",
5032     method   => "get_all_at_reactors_in_use",
5033     api_level=> 1,
5034     argc     => 1,
5035     signature=> {
5036         params => [
5037             { name => 'authtoken', type => 'string' }
5038         ],
5039         return => {
5040             desc => 'list of reactor names', type => 'array'
5041         }
5042     }
5043 );
5044
5045 sub get_all_at_reactors_in_use {
5046     my ($self, $conn, $auth) = @_;
5047
5048     my $e = new_editor(authtoken => $auth);
5049     $e->checkauth or return $e->die_event;
5050     return $e->die_event unless $e->allowed('VIEW_TRIGGER_EVENT_DEF');
5051
5052     my $reactors = $e->json_query({
5053         select => {
5054             atevdef => [{column => "reactor", transform => "distinct"}]
5055         },
5056         from => {atevdef => {}}
5057     });
5058
5059     return $e->die_event unless ref $reactors eq "ARRAY";
5060     $e->disconnect;
5061
5062     return [ map { $_->{reactor} } @$reactors ];
5063 }
5064
5065 __PACKAGE__->register_method(
5066     method   => "filter_group_entry_crud",
5067     api_name => "open-ils.actor.filter_group_entry.crud",
5068     signature => {
5069         desc => q/
5070             Provides CRUD access to filter group entry objects.  These are not full accessible
5071             via PCRUD, since they requre "asq" objects for storing the query, and "asq" objects
5072             are not accessible via PCRUD (because they have no fields against which to link perms)
5073             /,
5074         params => [
5075             {desc => "Authentication token", type => "string"},
5076             {desc => "Entry ID / Entry Object", type => "number"},
5077             {desc => "Additional note text (optional)", type => "string"},
5078             {desc => "penalty org unit ID (optional, default to top of org tree)",
5079                 type => "number"}
5080         ],
5081         return => {
5082             desc => "Entry fleshed with query on Create, Retrieve, and Uupdate.  1 on Delete",
5083             type => "object"
5084         }
5085     }
5086 );
5087
5088 sub filter_group_entry_crud {
5089     my ($self, $conn, $auth, $arg) = @_;
5090
5091     return OpenILS::Event->new('BAD_PARAMS') unless $arg;
5092     my $e = new_editor(authtoken => $auth, xact => 1);
5093     return $e->die_event unless $e->checkauth;
5094
5095     if (ref $arg) {
5096
5097         if ($arg->isnew) {
5098
5099             my $grp = $e->retrieve_actor_search_filter_group($arg->grp)
5100                 or return $e->die_event;
5101
5102             return $e->die_event unless $e->allowed(
5103                 'ADMIN_SEARCH_FILTER_GROUP', $grp->owner);
5104
5105             my $query = $arg->query;
5106             $query = $e->create_actor_search_query($query) or return $e->die_event;
5107             $arg->query($query->id);
5108             my $entry = $e->create_actor_search_filter_group_entry($arg) or return $e->die_event;
5109             $entry->query($query);
5110
5111             $e->commit;
5112             return $entry;
5113
5114         } elsif ($arg->ischanged) {
5115
5116             my $entry = $e->retrieve_actor_search_filter_group_entry([
5117                 $arg->id, {
5118                     flesh => 1,
5119                     flesh_fields => {asfge => ['grp']}
5120                 }
5121             ]) or return $e->die_event;
5122
5123             return $e->die_event unless $e->allowed(
5124                 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
5125
5126             my $query = $e->update_actor_search_query($arg->query) or return $e->die_event;
5127             $arg->query($arg->query->id);
5128             $e->update_actor_search_filter_group_entry($arg) or return $e->die_event;
5129             $arg->query($query);
5130
5131             $e->commit;
5132             return $arg;
5133
5134         } elsif ($arg->isdeleted) {
5135
5136             my $entry = $e->retrieve_actor_search_filter_group_entry([
5137                 $arg->id, {
5138                     flesh => 1,
5139                     flesh_fields => {asfge => ['grp', 'query']}
5140                 }
5141             ]) or return $e->die_event;
5142
5143             return $e->die_event unless $e->allowed(
5144                 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
5145
5146             $e->delete_actor_search_filter_group_entry($entry) or return $e->die_event;
5147             $e->delete_actor_search_query($entry->query) or return $e->die_event;
5148
5149             $e->commit;
5150             return 1;
5151
5152         } else {
5153
5154             $e->rollback;
5155             return undef;
5156         }
5157
5158     } else {
5159
5160         my $entry = $e->retrieve_actor_search_filter_group_entry([
5161             $arg, {
5162                 flesh => 1,
5163                 flesh_fields => {asfge => ['grp', 'query']}
5164             }
5165         ]) or return $e->die_event;
5166
5167         return $e->die_event unless $e->allowed(
5168             ['ADMIN_SEARCH_FILTER_GROUP', 'VIEW_SEARCH_FILTER_GROUP'],
5169             $entry->grp->owner);
5170
5171         $e->rollback;
5172         $entry->grp($entry->grp->id); # for consistency
5173         return $entry;
5174     }
5175 }
5176
5177 1;