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