]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
LP#1450519: remove unauthorized access to library setting history
[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   => "patron_adv_search",
1512     api_name => "open-ils.actor.patron.search.advanced"
1513 );
1514
1515 __PACKAGE__->register_method(
1516     method   => "patron_adv_search",
1517     api_name => "open-ils.actor.patron.search.advanced.fleshed",
1518     stream => 1,
1519     # Flush the response stream at most 5 patrons in for UI responsiveness.
1520     max_bundle_count => 5,
1521     signature => {
1522         desc => q/Returns a stream of fleshed user objects instead of
1523             a pile of identifiers/
1524     }
1525 );
1526
1527 sub patron_adv_search {
1528     my( $self, $client, $auth, $search_hash, $search_limit,
1529         $search_sort, $include_inactive, $search_ou, $flesh_fields, $offset) = @_;
1530
1531     # API params sanity checks.
1532     # Exit early with empty result if no filter exists.
1533     # .fleshed call is streaming.  Non-fleshed is effectively atomic.
1534     my $fleshed = ($self->api_name =~ /fleshed/);
1535     return ($fleshed ? undef : []) unless (ref $search_hash ||'') eq 'HASH';
1536     my $search_ok = 0;
1537     for my $key (keys %$search_hash) {
1538         next if $search_hash->{$key}{value} =~ /^\s*$/; # empty filter
1539         $search_ok = 1;
1540         last;
1541     }
1542     return ($fleshed ? undef : []) unless $search_ok;
1543
1544     my $e = new_editor(authtoken=>$auth);
1545     return $e->event unless $e->checkauth;
1546     return $e->event unless $e->allowed('VIEW_USER');
1547
1548     # depth boundary outside of which patrons must opt-in, default to 0
1549     my $opt_boundary = 0;
1550     $opt_boundary = $U->ou_ancestor_setting_value($e->requestor->ws_ou,'org.patron_opt_boundary') if user_opt_in_enabled($self);
1551
1552     if (not defined $search_ou) {
1553         my $depth = $U->ou_ancestor_setting_value(
1554             $e->requestor->ws_ou,
1555             'circ.patron_edit.duplicate_patron_check_depth'
1556         );
1557
1558         if (defined $depth) {
1559             $search_ou = $U->org_unit_ancestor_at_depth(
1560                 $e->requestor->ws_ou, $depth
1561             );
1562         }
1563     }
1564
1565     my $ids = $U->storagereq(
1566         "open-ils.storage.actor.user.crazy_search", $search_hash,
1567         $search_limit, $search_sort, $include_inactive,
1568         $e->requestor->ws_ou, $search_ou, $opt_boundary, $offset);
1569
1570     return $ids unless $self->api_name =~ /fleshed/;
1571
1572     $client->respond(new_flesh_user($_, $flesh_fields, $e)) for @$ids;
1573
1574     return;
1575 }
1576
1577
1578 # A migrated (main) password has the form:
1579 # CRYPT( MD5( pw_salt || MD5(real_password) ), pw_salt )
1580 sub modify_migrated_user_password {
1581     my ($e, $user_id, $passwd) = @_;
1582
1583     # new password gets a new salt
1584     my $new_salt = $e->json_query({
1585         from => ['actor.create_salt', 'main']})->[0];
1586     $new_salt = $new_salt->{'actor.create_salt'};
1587
1588     $e->json_query({
1589         from => [
1590             'actor.set_passwd',
1591             $user_id,
1592             'main',
1593             md5_hex($new_salt . md5_hex($passwd)),
1594             $new_salt
1595         ]
1596     });
1597 }
1598
1599
1600
1601 __PACKAGE__->register_method(
1602     method    => "update_passwd",
1603     api_name  => "open-ils.actor.user.password.update",
1604     signature => {
1605         desc   => "Update the operator's password",
1606         params => [
1607             { desc => 'Authentication token', type => 'string' },
1608             { desc => 'New password',         type => 'string' },
1609             { desc => 'Current password',     type => 'string' }
1610         ],
1611         return => {desc => '1 on success, Event on error or incorrect current password'}
1612     }
1613 );
1614
1615 __PACKAGE__->register_method(
1616     method    => "update_passwd",
1617     api_name  => "open-ils.actor.user.username.update",
1618     signature => {
1619         desc   => "Update the operator's username",
1620         params => [
1621             { desc => 'Authentication token', type => 'string' },
1622             { desc => 'New username',         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.email.update",
1632     signature => {
1633         desc   => "Update the operator's email address",
1634         params => [
1635             { desc => 'Authentication token', type => 'string' },
1636             { desc => 'New email address',    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 sub update_passwd {
1644     my( $self, $conn, $auth, $new_val, $orig_pw ) = @_;
1645     my $e = new_editor(xact=>1, authtoken=>$auth);
1646     return $e->die_event unless $e->checkauth;
1647
1648     my $db_user = $e->retrieve_actor_user($e->requestor->id)
1649         or return $e->die_event;
1650     my $api = $self->api_name;
1651
1652     if (!$U->verify_migrated_user_password($e, $db_user->id, $orig_pw)) {
1653         $e->rollback;
1654         return new OpenILS::Event('INCORRECT_PASSWORD');
1655     }
1656
1657     my $at_event = 0;
1658     if( $api =~ /password/o ) {
1659         # NOTE: with access to the plain text password we could crypt
1660         # the password without the extra MD5 pre-hashing.  Other changes
1661         # would be required.  Noting here for future reference.
1662         modify_migrated_user_password($e, $db_user->id, $new_val);
1663         $db_user->passwd('');
1664
1665     } else {
1666
1667         # if we don't clear the password, the user will be updated with
1668         # a hashed version of the hashed version of their password
1669         $db_user->clear_passwd;
1670
1671         if( $api =~ /username/o ) {
1672
1673             # make sure no one else has this username
1674             my $exist = $e->search_actor_user({usrname=>$new_val},{idlist=>1});
1675             if (@$exist) {
1676                 $e->rollback;
1677                 return new OpenILS::Event('USERNAME_EXISTS');
1678             }
1679             $db_user->usrname($new_val);
1680             $at_event++;
1681
1682         } elsif( $api =~ /email/o ) {
1683             $db_user->email($new_val);
1684             $at_event++;
1685         }
1686     }
1687
1688     $e->update_actor_user($db_user) or return $e->die_event;
1689     $e->commit;
1690
1691     $U->create_events_for_hook('au.updated', $db_user, $e->requestor->ws_ou)
1692         if $at_event;
1693
1694     # update the cached user to pick up these changes
1695     $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1);
1696     return 1;
1697 }
1698
1699
1700
1701 __PACKAGE__->register_method(
1702     method   => "check_user_perms",
1703     api_name => "open-ils.actor.user.perm.check",
1704     notes    => <<"    NOTES");
1705     Takes a login session, user id, an org id, and an array of perm type strings.  For each
1706     perm type, if the user does *not* have the given permission it is added
1707     to a list which is returned from the method.  If all permissions
1708     are allowed, an empty list is returned
1709     if the logged in user does not match 'user_id', then the logged in user must
1710     have VIEW_PERMISSION priveleges.
1711     NOTES
1712
1713 sub check_user_perms {
1714     my( $self, $client, $login_session, $user_id, $org_id, $perm_types ) = @_;
1715
1716     my( $staff, $evt ) = $apputils->checkses($login_session);
1717     return $evt if $evt;
1718
1719     if($staff->id ne $user_id) {
1720         if( $evt = $apputils->check_perms(
1721             $staff->id, $org_id, 'VIEW_PERMISSION') ) {
1722             return $evt;
1723         }
1724     }
1725
1726     my @not_allowed;
1727     for my $perm (@$perm_types) {
1728         if($apputils->check_perms($user_id, $org_id, $perm)) {
1729             push @not_allowed, $perm;
1730         }
1731     }
1732
1733     return \@not_allowed
1734 }
1735
1736 __PACKAGE__->register_method(
1737     method  => "check_user_perms2",
1738     api_name    => "open-ils.actor.user.perm.check.multi_org",
1739     notes       => q/
1740         Checks the permissions on a list of perms and orgs for a user
1741         @param authtoken The login session key
1742         @param user_id The id of the user to check
1743         @param orgs The array of org ids
1744         @param perms The array of permission names
1745         @return An array of  [ orgId, permissionName ] arrays that FAILED the check
1746         if the logged in user does not match 'user_id', then the logged in user must
1747         have VIEW_PERMISSION priveleges.
1748     /);
1749
1750 sub check_user_perms2 {
1751     my( $self, $client, $authtoken, $user_id, $orgs, $perms ) = @_;
1752
1753     my( $staff, $target, $evt ) = $apputils->checkses_requestor(
1754         $authtoken, $user_id, 'VIEW_PERMISSION' );
1755     return $evt if $evt;
1756
1757     my @not_allowed;
1758     for my $org (@$orgs) {
1759         for my $perm (@$perms) {
1760             if($apputils->check_perms($user_id, $org, $perm)) {
1761                 push @not_allowed, [ $org, $perm ];
1762             }
1763         }
1764     }
1765
1766     return \@not_allowed
1767 }
1768
1769
1770 __PACKAGE__->register_method(
1771     method => 'check_user_perms3',
1772     api_name    => 'open-ils.actor.user.perm.highest_org',
1773     notes       => q/
1774         Returns the highest org unit id at which a user has a given permission
1775         If the requestor does not match the target user, the requestor must have
1776         'VIEW_PERMISSION' rights at the home org unit of the target user
1777         @param authtoken The login session key
1778         @param userid The id of the user in question
1779         @param perm The permission to check
1780         @return The org unit highest in the org tree within which the user has
1781         the requested permission
1782     /);
1783
1784 sub check_user_perms3 {
1785     my($self, $client, $authtoken, $user_id, $perm) = @_;
1786     my $e = new_editor(authtoken=>$authtoken);
1787     return $e->event unless $e->checkauth;
1788
1789     my $tree = $U->get_org_tree();
1790
1791     unless($e->requestor->id == $user_id) {
1792         my $user = $e->retrieve_actor_user($user_id)
1793             or return $e->event;
1794         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1795         return $U->find_highest_perm_org($perm, $user_id, $user->home_ou, $tree );
1796     }
1797
1798     return $U->find_highest_perm_org($perm, $user_id, $e->requestor->ws_ou, $tree);
1799 }
1800
1801 __PACKAGE__->register_method(
1802     method => 'user_has_work_perm_at',
1803     api_name    => 'open-ils.actor.user.has_work_perm_at',
1804     authoritative => 1,
1805     signature => {
1806         desc => q/
1807             Returns a set of org unit IDs which represent the highest orgs in
1808             the org tree where the user has the requested permission.  The
1809             purpose of this method is to return the smallest set of org units
1810             which represent the full expanse of the user's ability to perform
1811             the requested action.  The user whose perms this method should
1812             check is implied by the authtoken. /,
1813         params => [
1814             {desc => 'authtoken', type => 'string'},
1815             {desc => 'permission name', type => 'string'},
1816             {desc => q/user id, optional.  If present, check perms for
1817                 this user instead of the logged in user/, type => 'number'},
1818         ],
1819         return => {desc => 'An array of org IDs'}
1820     }
1821 );
1822
1823 sub user_has_work_perm_at {
1824     my($self, $conn, $auth, $perm, $user_id) = @_;
1825     my $e = new_editor(authtoken=>$auth);
1826     return $e->event unless $e->checkauth;
1827     if(defined $user_id) {
1828         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1829         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1830     }
1831     return $U->user_has_work_perm_at($e, $perm, undef, $user_id);
1832 }
1833
1834 __PACKAGE__->register_method(
1835     method => 'user_has_work_perm_at_batch',
1836     api_name    => 'open-ils.actor.user.has_work_perm_at.batch',
1837     authoritative => 1,
1838 );
1839
1840 sub user_has_work_perm_at_batch {
1841     my($self, $conn, $auth, $perms, $user_id) = @_;
1842     my $e = new_editor(authtoken=>$auth);
1843     return $e->event unless $e->checkauth;
1844     if(defined $user_id) {
1845         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1846         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1847     }
1848     my $map = {};
1849     $map->{$_} = $U->user_has_work_perm_at($e, $_) for @$perms;
1850     return $map;
1851 }
1852
1853
1854
1855 __PACKAGE__->register_method(
1856     method => 'check_user_perms4',
1857     api_name    => 'open-ils.actor.user.perm.highest_org.batch',
1858     notes       => q/
1859         Returns the highest org unit id at which a user has a given permission
1860         If the requestor does not match the target user, the requestor must have
1861         'VIEW_PERMISSION' rights at the home org unit of the target user
1862         @param authtoken The login session key
1863         @param userid The id of the user in question
1864         @param perms An array of perm names to check
1865         @return An array of orgId's  representing the org unit
1866         highest in the org tree within which the user has the requested permission
1867         The arrah of orgId's has matches the order of the perms array
1868     /);
1869
1870 sub check_user_perms4 {
1871     my( $self, $client, $authtoken, $userid, $perms ) = @_;
1872
1873     my( $staff, $target, $org, $evt );
1874
1875     ( $staff, $target, $evt ) = $apputils->checkses_requestor(
1876         $authtoken, $userid, 'VIEW_PERMISSION' );
1877     return $evt if $evt;
1878
1879     my @arr;
1880     return [] unless ref($perms);
1881     my $tree = $U->get_org_tree();
1882
1883     for my $p (@$perms) {
1884         push( @arr, $U->find_highest_perm_org( $p, $userid, $target->home_ou, $tree ) );
1885     }
1886     return \@arr;
1887 }
1888
1889
1890 __PACKAGE__->register_method(
1891     method        => "user_fines_summary",
1892     api_name      => "open-ils.actor.user.fines.summary",
1893     authoritative => 1,
1894     signature     => {
1895         desc   => 'Returns a short summary of the users total open fines, '  .
1896                 'excluding voided fines Params are login_session, user_id' ,
1897         params => [
1898             {desc => 'Authentication token', type => 'string'},
1899             {desc => 'User ID',              type => 'string'}  # number?
1900         ],
1901         return => {
1902             desc => "a 'mous' object, event on error",
1903         }
1904     }
1905 );
1906
1907 sub user_fines_summary {
1908     my( $self, $client, $auth, $user_id ) = @_;
1909
1910     my $e = new_editor(authtoken=>$auth);
1911     return $e->event unless $e->checkauth;
1912
1913     if( $user_id ne $e->requestor->id ) {
1914         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1915         return $e->event unless
1916             $e->allowed('VIEW_USER_FINES_SUMMARY', $user->home_ou);
1917     }
1918
1919     return $e->search_money_open_user_summary({usr => $user_id})->[0];
1920 }
1921
1922
1923 __PACKAGE__->register_method(
1924     method        => "user_opac_vitals",
1925     api_name      => "open-ils.actor.user.opac.vital_stats",
1926     argc          => 1,
1927     authoritative => 1,
1928     signature     => {
1929         desc   => 'Returns a short summary of the users vital stats, including '  .
1930                 'identification information, accumulated balance, number of holds, ' .
1931                 'and current open circulation stats' ,
1932         params => [
1933             {desc => 'Authentication token',                          type => 'string'},
1934             {desc => 'Optional User ID, for use in the staff client', type => 'number'}  # number?
1935         ],
1936         return => {
1937             desc => "An object with four properties: user, fines, checkouts and holds."
1938         }
1939     }
1940 );
1941
1942 sub user_opac_vitals {
1943     my( $self, $client, $auth, $user_id ) = @_;
1944
1945     my $e = new_editor(authtoken=>$auth);
1946     return $e->event unless $e->checkauth;
1947
1948     $user_id ||= $e->requestor->id;
1949
1950     my $user = $e->retrieve_actor_user( $user_id );
1951
1952     my ($fines) = $self
1953         ->method_lookup('open-ils.actor.user.fines.summary')
1954         ->run($auth => $user_id);
1955     return $fines if (defined($U->event_code($fines)));
1956
1957     if (!$fines) {
1958         $fines = new Fieldmapper::money::open_user_summary ();
1959         $fines->balance_owed(0.00);
1960         $fines->total_owed(0.00);
1961         $fines->total_paid(0.00);
1962         $fines->usr($user_id);
1963     }
1964
1965     my ($holds) = $self
1966         ->method_lookup('open-ils.actor.user.hold_requests.count')
1967         ->run($auth => $user_id);
1968     return $holds if (defined($U->event_code($holds)));
1969
1970     my ($out) = $self
1971         ->method_lookup('open-ils.actor.user.checked_out.count')
1972         ->run($auth => $user_id);
1973     return $out if (defined($U->event_code($out)));
1974
1975     $out->{"total_out"} = reduce { $a + $out->{$b} } 0, qw/out overdue/;
1976
1977     my $unread_msgs = $e->search_actor_usr_message([
1978         {usr => $user_id, read_date => undef, deleted => 'f'},
1979         {idlist => 1}
1980     ]);
1981
1982     return {
1983         user => {
1984             first_given_name  => $user->first_given_name,
1985             second_given_name => $user->second_given_name,
1986             family_name       => $user->family_name,
1987             alias             => $user->alias,
1988             usrname           => $user->usrname
1989         },
1990         fines => $fines->to_bare_hash,
1991         checkouts => $out,
1992         holds => $holds,
1993         messages => { unread => scalar(@$unread_msgs) }
1994     };
1995 }
1996
1997
1998 ##### a small consolidation of related method registrations
1999 my $common_params = [
2000     { desc => 'Authentication token', type => 'string' },
2001     { desc => 'User ID',              type => 'string' },
2002     { desc => 'Transactions type (optional, defaults to all)', type => 'string' },
2003     { desc => 'Options hash.  May contain limit and offset for paged results.', type => 'object' },
2004 ];
2005 my %methods = (
2006     'open-ils.actor.user.transactions'                      => '',
2007     'open-ils.actor.user.transactions.fleshed'              => '',
2008     'open-ils.actor.user.transactions.have_charge'          => ' that have an initial charge',
2009     'open-ils.actor.user.transactions.have_charge.fleshed'  => ' that have an initial charge',
2010     'open-ils.actor.user.transactions.have_balance'         => ' that have an outstanding balance',
2011     'open-ils.actor.user.transactions.have_balance.fleshed' => ' that have an outstanding balance',
2012 );
2013
2014 foreach (keys %methods) {
2015     my %args = (
2016         method    => "user_transactions",
2017         api_name  => $_,
2018         signature => {
2019             desc   => 'For a given user, retrieve a list of '
2020                     . (/\.fleshed/ ? 'fleshed ' : '')
2021                     . 'transactions' . $methods{$_}
2022                     . ' optionally limited to transactions of a given type.',
2023             params => $common_params,
2024             return => {
2025                 desc => "List of objects, or event on error.  Each object is a hash containing: transaction, circ, record. "
2026                     . 'These represent the relevant (mbts) transaction, attached circulation and title pointed to in the circ, respectively.',
2027             }
2028         }
2029     );
2030     $args{authoritative} = 1;
2031     __PACKAGE__->register_method(%args);
2032 }
2033
2034 # Now for the counts
2035 %methods = (
2036     'open-ils.actor.user.transactions.count'              => '',
2037     'open-ils.actor.user.transactions.have_charge.count'  => ' that have an initial charge',
2038     'open-ils.actor.user.transactions.have_balance.count' => ' that have an outstanding balance',
2039 );
2040
2041 foreach (keys %methods) {
2042     my %args = (
2043         method    => "user_transactions",
2044         api_name  => $_,
2045         signature => {
2046             desc   => 'For a given user, retrieve a count of open '
2047                     . 'transactions' . $methods{$_}
2048                     . ' optionally limited to transactions of a given type.',
2049             params => $common_params,
2050             return => { desc => "Integer count of transactions, or event on error" }
2051         }
2052     );
2053     /\.have_balance/ and $args{authoritative} = 1;     # FIXME: I don't know why have_charge isn't authoritative
2054     __PACKAGE__->register_method(%args);
2055 }
2056
2057 __PACKAGE__->register_method(
2058     method        => "user_transactions",
2059     api_name      => "open-ils.actor.user.transactions.have_balance.total",
2060     authoritative => 1,
2061     signature     => {
2062         desc   => 'For a given user, retrieve the total balance owed for open transactions,'
2063                 . ' optionally limited to transactions of a given type.',
2064         params => $common_params,
2065         return => { desc => "Decimal balance value, or event on error" }
2066     }
2067 );
2068
2069
2070 sub user_transactions {
2071     my( $self, $client, $auth, $user_id, $type, $options ) = @_;
2072     $options ||= {};
2073
2074     my $e = new_editor(authtoken => $auth);
2075     return $e->event unless $e->checkauth;
2076
2077     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
2078
2079     return $e->event unless
2080         $e->requestor->id == $user_id or
2081         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
2082
2083     my $api = $self->api_name();
2084
2085     my $filter = ($api =~ /have_balance/o) ?
2086         { 'balance_owed' => { '<>' => 0 } }:
2087         { 'total_owed' => { '>' => 0 } };
2088
2089     my $method = 'open-ils.actor.user.transactions.history.still_open';
2090     $method = "$method.authoritative" if $api =~ /authoritative/;
2091     my ($trans) = $self->method_lookup($method)->run($auth, $user_id, $type, $filter, $options);
2092
2093     if($api =~ /total/o) {
2094         my $total = 0.0;
2095         $total += $_->balance_owed for @$trans;
2096         return $total;
2097     }
2098
2099     ($api =~ /count/o  ) and return scalar @$trans;
2100     ($api !~ /fleshed/o) and return $trans;
2101
2102     my @resp;
2103     for my $t (@$trans) {
2104
2105         if( $t->xact_type ne 'circulation' ) {
2106             push @resp, {transaction => $t};
2107             next;
2108         }
2109
2110         my $circ_data = flesh_circ($e, $t->id);
2111         push @resp, {transaction => $t, %$circ_data};
2112     }
2113
2114     return \@resp;
2115 }
2116
2117
2118 __PACKAGE__->register_method(
2119     method   => "user_transaction_retrieve",
2120     api_name => "open-ils.actor.user.transaction.fleshed.retrieve",
2121     argc     => 1,
2122     authoritative => 1,
2123     notes    => "Returns a fleshed transaction record"
2124 );
2125
2126 __PACKAGE__->register_method(
2127     method   => "user_transaction_retrieve",
2128     api_name => "open-ils.actor.user.transaction.retrieve",
2129     argc     => 1,
2130     authoritative => 1,
2131     notes    => "Returns a transaction record"
2132 );
2133
2134 sub user_transaction_retrieve {
2135     my($self, $client, $auth, $bill_id) = @_;
2136
2137     my $e = new_editor(authtoken => $auth);
2138     return $e->event unless $e->checkauth;
2139
2140     my $trans = $e->retrieve_money_billable_transaction_summary(
2141         [$bill_id, {flesh => 1, flesh_fields => {mbts => ['usr']}}]) or return $e->event;
2142
2143     return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $trans->usr->home_ou);
2144
2145     $trans->usr($trans->usr->id); # de-flesh for backwards compat
2146
2147     return $trans unless $self->api_name =~ /flesh/;
2148     return {transaction => $trans} if $trans->xact_type ne 'circulation';
2149
2150     my $circ_data = flesh_circ($e, $trans->id, 1);
2151
2152     return {transaction => $trans, %$circ_data};
2153 }
2154
2155 sub flesh_circ {
2156     my $e = shift;
2157     my $circ_id = shift;
2158     my $flesh_copy = shift;
2159
2160     my $circ = $e->retrieve_action_circulation([
2161         $circ_id, {
2162             flesh => 3,
2163             flesh_fields => {
2164                 circ => ['target_copy'],
2165                 acp => ['call_number'],
2166                 acn => ['record']
2167             }
2168         }
2169     ]);
2170
2171     my $mods;
2172     my $copy = $circ->target_copy;
2173
2174     if($circ->target_copy->call_number->id == OILS_PRECAT_CALL_NUMBER) {
2175         $mods = new Fieldmapper::metabib::virtual_record;
2176         $mods->doc_id(OILS_PRECAT_RECORD);
2177         $mods->title($copy->dummy_title);
2178         $mods->author($copy->dummy_author);
2179
2180     } else {
2181         $mods = $U->record_to_mvr($circ->target_copy->call_number->record);
2182     }
2183
2184     # more de-fleshiing
2185     $circ->target_copy($circ->target_copy->id);
2186     $copy->call_number($copy->call_number->id);
2187
2188     return {circ => $circ, record => $mods, copy => ($flesh_copy) ? $copy : undef };
2189 }
2190
2191
2192 __PACKAGE__->register_method(
2193     method        => "hold_request_count",
2194     api_name      => "open-ils.actor.user.hold_requests.count",
2195     authoritative => 1,
2196     argc          => 1,
2197     notes         => q/
2198         Returns hold ready vs. total counts.
2199         If a context org unit is provided, a third value
2200         is returned with key 'behind_desk', which reports
2201         how many holds are ready at the pickup library
2202         with the behind_desk flag set to true.
2203     /
2204 );
2205
2206 sub hold_request_count {
2207     my( $self, $client, $authtoken, $user_id, $ctx_org ) = @_;
2208     my $e = new_editor(authtoken => $authtoken);
2209     return $e->event unless $e->checkauth;
2210
2211     $user_id = $e->requestor->id unless defined $user_id;
2212
2213     if($e->requestor->id ne $user_id) {
2214         my $user = $e->retrieve_actor_user($user_id);
2215         return $e->event unless $e->allowed('VIEW_HOLD', $user->home_ou);
2216     }
2217
2218     my $holds = $e->json_query({
2219         select => {ahr => ['pickup_lib', 'current_shelf_lib', 'behind_desk']},
2220         from => 'ahr',
2221         where => {
2222             usr => $user_id,
2223             fulfillment_time => {"=" => undef },
2224             cancel_time => undef,
2225         }
2226     });
2227
2228     my @ready = grep {
2229         $_->{current_shelf_lib} and # avoid undef warnings
2230         $_->{pickup_lib} eq $_->{current_shelf_lib}
2231     } @$holds;
2232
2233     my $resp = {
2234         total => scalar(@$holds),
2235         ready => scalar(@ready)
2236     };
2237
2238     if ($ctx_org) {
2239         # count of holds ready at pickup lib with behind_desk true.
2240         $resp->{behind_desk} = scalar(
2241             grep {
2242                 $_->{pickup_lib} == $ctx_org and
2243                 $U->is_true($_->{behind_desk})
2244             } @ready
2245         );
2246     }
2247
2248     return $resp;
2249 }
2250
2251 __PACKAGE__->register_method(
2252     method        => "checked_out",
2253     api_name      => "open-ils.actor.user.checked_out",
2254     authoritative => 1,
2255     argc          => 2,
2256     signature     => {
2257         desc => "For a given user, returns a structure of circulations objects sorted by out, overdue, lost, claims_returned, long_overdue. "
2258             . "A list of IDs are returned of each type.  Circs marked lost, long_overdue, and claims_returned will not be 'finished' "
2259             . "(i.e., outstanding balance or some other pending action on the circ). "
2260             . "The .count method also includes a 'total' field which sums all open circs.",
2261         params => [
2262             { desc => 'Authentication Token', type => 'string'},
2263             { desc => 'User ID',              type => 'string'},
2264         ],
2265         return => {
2266             desc => 'Returns event on error, or an object with ID lists, like: '
2267                 . '{"out":[12552,451232], "claims_returned":[], "long_overdue":[23421] "overdue":[], "lost":[]}'
2268         },
2269     }
2270 );
2271
2272 __PACKAGE__->register_method(
2273     method        => "checked_out",
2274     api_name      => "open-ils.actor.user.checked_out.count",
2275     authoritative => 1,
2276     argc          => 2,
2277     signature     => q/@see open-ils.actor.user.checked_out/
2278 );
2279
2280 sub checked_out {
2281     my( $self, $conn, $auth, $userid ) = @_;
2282
2283     my $e = new_editor(authtoken=>$auth);
2284     return $e->event unless $e->checkauth;
2285
2286     if( $userid ne $e->requestor->id ) {
2287         my $user = $e->retrieve_actor_user($userid) or return $e->event;
2288         unless($e->allowed('VIEW_CIRCULATIONS', $user->home_ou)) {
2289
2290             # see if there is a friend link allowing circ.view perms
2291             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
2292                 $e, $userid, $e->requestor->id, 'circ.view');
2293             return $e->event unless $allowed;
2294         }
2295     }
2296
2297     my $count = $self->api_name =~ /count/;
2298     return _checked_out( $count, $e, $userid );
2299 }
2300
2301 sub _checked_out {
2302     my( $iscount, $e, $userid ) = @_;
2303
2304     my %result = (
2305         out => [],
2306         overdue => [],
2307         lost => [],
2308         claims_returned => [],
2309         long_overdue => []
2310     );
2311     my $meth = 'retrieve_action_open_circ_';
2312
2313     if ($iscount) {
2314         $meth .= 'count';
2315         %result = (
2316             out => 0,
2317             overdue => 0,
2318             lost => 0,
2319             claims_returned => 0,
2320             long_overdue => 0
2321         );
2322     } else {
2323         $meth .= 'list';
2324     }
2325
2326     my $data = $e->$meth($userid);
2327
2328     if ($data) {
2329         if ($iscount) {
2330             $result{$_} += $data->$_() for (keys %result);
2331             $result{total} += $data->$_() for (keys %result);
2332         } else {
2333             for my $k (keys %result) {
2334                 $result{$k} = [ grep { $_ > 0 } split( ',', $data->$k()) ];
2335             }
2336         }
2337     }
2338
2339     return \%result;
2340 }
2341
2342
2343
2344 __PACKAGE__->register_method(
2345     method        => "checked_in_with_fines",
2346     api_name      => "open-ils.actor.user.checked_in_with_fines",
2347     authoritative => 1,
2348     argc          => 2,
2349     signature     => q/@see open-ils.actor.user.checked_out/
2350 );
2351
2352 sub checked_in_with_fines {
2353     my( $self, $conn, $auth, $userid ) = @_;
2354
2355     my $e = new_editor(authtoken=>$auth);
2356     return $e->event unless $e->checkauth;
2357
2358     if( $userid ne $e->requestor->id ) {
2359         return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2360     }
2361
2362     # money is owed on these items and they are checked in
2363     my $open = $e->search_action_circulation(
2364         {
2365             usr             => $userid,
2366             xact_finish     => undef,
2367             checkin_time    => { "!=" => undef },
2368         }
2369     );
2370
2371
2372     my( @lost, @cr, @lo );
2373     for my $c (@$open) {
2374         push( @lost, $c->id ) if ($c->stop_fines eq 'LOST');
2375         push( @cr, $c->id ) if $c->stop_fines eq 'CLAIMSRETURNED';
2376         push( @lo, $c->id ) if $c->stop_fines eq 'LONGOVERDUE';
2377     }
2378
2379     return {
2380         lost        => \@lost,
2381         claims_returned => \@cr,
2382         long_overdue        => \@lo
2383     };
2384 }
2385
2386
2387 sub _sigmaker {
2388     my ($api, $desc, $auth) = @_;
2389     $desc = $desc ? (" " . $desc) : '';
2390     my $ids = ($api =~ /ids$/) ? 1 : 0;
2391     my @sig = (
2392         argc      => 1,
2393         method    => "user_transaction_history",
2394         api_name  => "open-ils.actor.user.transactions.$api",
2395         signature => {
2396             desc   => "For a given User ID, returns a list of billable transaction" .
2397                     ($ids ? " id" : '') .
2398                     "s$desc, optionally filtered by type and/or fields in money.billable_xact_summary.  " .
2399                     "The VIEW_USER_TRANSACTIONS permission is required to view another user's transactions",
2400             params => [
2401                 {desc => 'Authentication token',        type => 'string'},
2402                 {desc => 'User ID',                     type => 'number'},
2403                 {desc => 'Transaction type (optional)', type => 'number'},
2404                 {desc => 'Hash of Billable Transaction Summary filters (optional)', type => 'object'}
2405             ],
2406             return => {
2407                 desc => 'List of transaction' . ($ids ? " id" : '') . 's, Event on error'
2408             },
2409         }
2410     );
2411     $auth and push @sig, (authoritative => 1);
2412     return @sig;
2413 }
2414
2415 my %auth_hist_methods = (
2416     'history'             => '',
2417     'history.have_charge' => 'that have an initial charge',
2418     'history.still_open'  => 'that are not finished',
2419     'history.have_balance'         => 'that have a balance',
2420     'history.have_bill'            => 'that have billings',
2421     'history.have_bill_or_payment' => 'that have non-zero-sum billings or at least 1 payment',
2422     'history.have_payment' => 'that have at least 1 payment',
2423 );
2424
2425 foreach (keys %auth_hist_methods) {
2426     __PACKAGE__->register_method(_sigmaker($_,       $auth_hist_methods{$_}, 1));
2427     __PACKAGE__->register_method(_sigmaker("$_.ids", $auth_hist_methods{$_}, 1));
2428     __PACKAGE__->register_method(_sigmaker("$_.fleshed", $auth_hist_methods{$_}, 1));
2429 }
2430
2431 sub user_transaction_history {
2432     my( $self, $conn, $auth, $userid, $type, $filter, $options ) = @_;
2433     $filter ||= {};
2434     $options ||= {};
2435
2436     my $e = new_editor(authtoken=>$auth);
2437     return $e->die_event unless $e->checkauth;
2438
2439     if ($e->requestor->id ne $userid) {
2440         return $e->die_event unless $e->allowed('VIEW_USER_TRANSACTIONS');
2441     }
2442
2443     my $api = $self->api_name;
2444     my @xact_finish  = (xact_finish => undef ) if ($api =~ /history\.still_open$/);     # What about history.still_open.ids?
2445
2446     if(defined($type)) {
2447         $filter->{'xact_type'} = $type;
2448     }
2449
2450     if($api =~ /have_bill_or_payment/o) {
2451
2452         # transactions that have a non-zero sum across all billings or at least 1 payment
2453         $filter->{'-or'} = {
2454             'balance_owed' => { '<>' => 0 },
2455             'last_payment_ts' => { '<>' => undef }
2456         };
2457
2458     } elsif($api =~ /have_payment/) {
2459
2460         $filter->{last_payment_ts} ||= {'<>' => undef};
2461
2462     } elsif( $api =~ /have_balance/o) {
2463
2464         # transactions that have a non-zero overall balance
2465         $filter->{'balance_owed'} = { '<>' => 0 };
2466
2467     } elsif( $api =~ /have_charge/o) {
2468
2469         # transactions that have at least 1 billing, regardless of whether it was voided
2470         $filter->{'last_billing_ts'} = { '<>' => undef };
2471
2472     } elsif( $api =~ /have_bill/o) {    # needs to be an elsif, or we double-match have_bill_or_payment!
2473
2474         # transactions that have non-zero sum across all billings.  This will exclude
2475         # xacts where all billings have been voided
2476         $filter->{'total_owed'} = { '<>' => 0 };
2477     }
2478
2479     my $options_clause = { order_by => { mbt => 'xact_start DESC' } };
2480     $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'};
2481     $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'};
2482
2483     my $mbts = $e->search_money_billable_transaction_summary(
2484         [   { usr => $userid, @xact_finish, %$filter },
2485             $options_clause
2486         ]
2487     );
2488
2489     return [map {$_->id} @$mbts] if $api =~ /\.ids/;
2490     return $mbts unless $api =~ /fleshed/;
2491
2492     my @resp;
2493     for my $t (@$mbts) {
2494
2495         if( $t->xact_type ne 'circulation' ) {
2496             push @resp, {transaction => $t};
2497             next;
2498         }
2499
2500         my $circ_data = flesh_circ($e, $t->id);
2501         push @resp, {transaction => $t, %$circ_data};
2502     }
2503
2504     return \@resp;
2505 }
2506
2507
2508
2509 __PACKAGE__->register_method(
2510     method   => "user_perms",
2511     api_name => "open-ils.actor.permissions.user_perms.retrieve",
2512     argc     => 1,
2513     notes    => "Returns a list of permissions"
2514 );
2515
2516 sub user_perms {
2517     my( $self, $client, $authtoken, $user ) = @_;
2518
2519     my( $staff, $evt ) = $apputils->checkses($authtoken);
2520     return $evt if $evt;
2521
2522     $user ||= $staff->id;
2523
2524     if( $user != $staff->id and $evt = $apputils->check_perms( $staff->id, $staff->home_ou, 'VIEW_PERMISSION') ) {
2525         return $evt;
2526     }
2527
2528     return $apputils->simple_scalar_request(
2529         "open-ils.storage",
2530         "open-ils.storage.permission.user_perms.atomic",
2531         $user);
2532 }
2533
2534 __PACKAGE__->register_method(
2535     method   => "retrieve_perms",
2536     api_name => "open-ils.actor.permissions.retrieve",
2537     notes    => "Returns a list of permissions"
2538 );
2539 sub retrieve_perms {
2540     my( $self, $client ) = @_;
2541     return $apputils->simple_scalar_request(
2542         "open-ils.cstore",
2543         "open-ils.cstore.direct.permission.perm_list.search.atomic",
2544         { id => { '!=' => undef } }
2545     );
2546 }
2547
2548 __PACKAGE__->register_method(
2549     method   => "retrieve_groups",
2550     api_name => "open-ils.actor.groups.retrieve",
2551     notes    => "Returns a list of user groups"
2552 );
2553 sub retrieve_groups {
2554     my( $self, $client ) = @_;
2555     return new_editor()->retrieve_all_permission_grp_tree();
2556 }
2557
2558 __PACKAGE__->register_method(
2559     method  => "retrieve_org_address",
2560     api_name    => "open-ils.actor.org_unit.address.retrieve",
2561     notes        => <<'    NOTES');
2562     Returns an org_unit address by ID
2563     @param An org_address ID
2564     NOTES
2565 sub retrieve_org_address {
2566     my( $self, $client, $id ) = @_;
2567     return $apputils->simple_scalar_request(
2568         "open-ils.cstore",
2569         "open-ils.cstore.direct.actor.org_address.retrieve",
2570         $id
2571     );
2572 }
2573
2574 __PACKAGE__->register_method(
2575     method   => "retrieve_groups_tree",
2576     api_name => "open-ils.actor.groups.tree.retrieve",
2577     notes    => "Returns a list of user groups"
2578 );
2579
2580 sub retrieve_groups_tree {
2581     my( $self, $client ) = @_;
2582     return new_editor()->search_permission_grp_tree(
2583         [
2584             { parent => undef},
2585             {
2586                 flesh               => -1,
2587                 flesh_fields    => { pgt => ["children"] },
2588                 order_by            => { pgt => 'name'}
2589             }
2590         ]
2591     )->[0];
2592 }
2593
2594
2595 __PACKAGE__->register_method(
2596     method   => "add_user_to_groups",
2597     api_name => "open-ils.actor.user.set_groups",
2598     notes    => "Adds a user to one or more permission groups"
2599 );
2600
2601 sub add_user_to_groups {
2602     my( $self, $client, $authtoken, $userid, $groups ) = @_;
2603
2604     my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2605         $authtoken, $userid, 'CREATE_USER_GROUP_LINK' );
2606     return $evt if $evt;
2607
2608     ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2609         $authtoken, $userid, 'REMOVE_USER_GROUP_LINK' );
2610     return $evt if $evt;
2611
2612     $apputils->simplereq(
2613         'open-ils.storage',
2614         'open-ils.storage.direct.permission.usr_grp_map.mass_delete', { usr => $userid } );
2615
2616     for my $group (@$groups) {
2617         my $link = Fieldmapper::permission::usr_grp_map->new;
2618         $link->grp($group);
2619         $link->usr($userid);
2620
2621         my $id = $apputils->simplereq(
2622             'open-ils.storage',
2623             'open-ils.storage.direct.permission.usr_grp_map.create', $link );
2624     }
2625
2626     return 1;
2627 }
2628
2629 __PACKAGE__->register_method(
2630     method   => "get_user_perm_groups",
2631     api_name => "open-ils.actor.user.get_groups",
2632     notes    => "Retrieve a user's permission groups."
2633 );
2634
2635
2636 sub get_user_perm_groups {
2637     my( $self, $client, $authtoken, $userid ) = @_;
2638
2639     my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2640         $authtoken, $userid, 'VIEW_PERM_GROUPS' );
2641     return $evt if $evt;
2642
2643     return $apputils->simplereq(
2644         'open-ils.cstore',
2645         'open-ils.cstore.direct.permission.usr_grp_map.search.atomic', { usr => $userid } );
2646 }
2647
2648
2649 __PACKAGE__->register_method(
2650     method   => "get_user_work_ous",
2651     api_name => "open-ils.actor.user.get_work_ous",
2652     notes    => "Retrieve a user's work org units."
2653 );
2654
2655 __PACKAGE__->register_method(
2656     method   => "get_user_work_ous",
2657     api_name => "open-ils.actor.user.get_work_ous.ids",
2658     notes    => "Retrieve a user's work org units."
2659 );
2660
2661 sub get_user_work_ous {
2662     my( $self, $client, $auth, $userid ) = @_;
2663     my $e = new_editor(authtoken=>$auth);
2664     return $e->event unless $e->checkauth;
2665     $userid ||= $e->requestor->id;
2666
2667     if($e->requestor->id != $userid) {
2668         my $user = $e->retrieve_actor_user($userid)
2669             or return $e->event;
2670         return $e->event unless $e->allowed('ASSIGN_WORK_ORG_UNIT', $user->home_ou);
2671     }
2672
2673     return $e->search_permission_usr_work_ou_map({usr => $userid})
2674         unless $self->api_name =~ /.ids$/;
2675
2676     # client just wants a list of org IDs
2677     return $U->get_user_work_ou_ids($e, $userid);
2678 }
2679
2680
2681
2682 __PACKAGE__->register_method(
2683     method    => 'register_workstation',
2684     api_name  => 'open-ils.actor.workstation.register.override',
2685     signature => q/@see open-ils.actor.workstation.register/
2686 );
2687
2688 __PACKAGE__->register_method(
2689     method    => 'register_workstation',
2690     api_name  => 'open-ils.actor.workstation.register',
2691     signature => q/
2692         Registers a new workstion in the system
2693         @param authtoken The login session key
2694         @param name The name of the workstation id
2695         @param owner The org unit that owns this workstation
2696         @return The workstation id on success, WORKSTATION_NAME_EXISTS
2697         if the name is already in use.
2698     /
2699 );
2700
2701 sub register_workstation {
2702     my( $self, $conn, $authtoken, $name, $owner, $oargs ) = @_;
2703
2704     my $e = new_editor(authtoken=>$authtoken, xact=>1);
2705     return $e->die_event unless $e->checkauth;
2706     return $e->die_event unless $e->allowed('REGISTER_WORKSTATION', $owner);
2707     my $existing = $e->search_actor_workstation({name => $name})->[0];
2708     $oargs = { all => 1 } unless defined $oargs;
2709
2710     if( $existing ) {
2711
2712         if( $self->api_name =~ /override/o && ($oargs->{all} || grep { $_ eq 'WORKSTATION_NAME_EXISTS' } @{$oargs->{events}}) ) {
2713             # workstation with the given name exists.
2714
2715             if($owner ne $existing->owning_lib) {
2716                 # if necessary, update the owning_lib of the workstation
2717
2718                 $logger->info("changing owning lib of workstation ".$existing->id.
2719                     " from ".$existing->owning_lib." to $owner");
2720                 return $e->die_event unless
2721                     $e->allowed('UPDATE_WORKSTATION', $existing->owning_lib);
2722
2723                 return $e->die_event unless $e->allowed('UPDATE_WORKSTATION', $owner);
2724
2725                 $existing->owning_lib($owner);
2726                 return $e->die_event unless $e->update_actor_workstation($existing);
2727
2728                 $e->commit;
2729
2730             } else {
2731                 $logger->info(
2732                     "attempt to register an existing workstation.  returning existing ID");
2733             }
2734
2735             return $existing->id;
2736
2737         } else {
2738             return OpenILS::Event->new('WORKSTATION_NAME_EXISTS')
2739         }
2740     }
2741
2742     my $ws = Fieldmapper::actor::workstation->new;
2743     $ws->owning_lib($owner);
2744     $ws->name($name);
2745     $e->create_actor_workstation($ws) or return $e->die_event;
2746     $e->commit;
2747     return $ws->id; # note: editor sets the id on the new object for us
2748 }
2749
2750 __PACKAGE__->register_method(
2751     method    => 'workstation_list',
2752     api_name  => 'open-ils.actor.workstation.list',
2753     signature => q/
2754         Returns a list of workstations registered at the given location
2755         @param authtoken The login session key
2756         @param ids A list of org_unit.id's for the workstation owners
2757     /
2758 );
2759
2760 sub workstation_list {
2761     my( $self, $conn, $authtoken, @orgs ) = @_;
2762
2763     my $e = new_editor(authtoken=>$authtoken);
2764     return $e->event unless $e->checkauth;
2765     my %results;
2766
2767     for my $o (@orgs) {
2768         return $e->event
2769             unless $e->allowed('REGISTER_WORKSTATION', $o);
2770         $results{$o} = $e->search_actor_workstation({owning_lib=>$o});
2771     }
2772     return \%results;
2773 }
2774
2775
2776 __PACKAGE__->register_method(
2777     method        => 'fetch_patron_note',
2778     api_name      => 'open-ils.actor.note.retrieve.all',
2779     authoritative => 1,
2780     signature     => q/
2781         Returns a list of notes for a given user
2782         Requestor must have VIEW_USER permission if pub==false and
2783         @param authtoken The login session key
2784         @param args Hash of params including
2785             patronid : the patron's id
2786             pub : true if retrieving only public notes
2787     /
2788 );
2789
2790 sub fetch_patron_note {
2791     my( $self, $conn, $authtoken, $args ) = @_;
2792     my $patronid = $$args{patronid};
2793
2794     my($reqr, $evt) = $U->checkses($authtoken);
2795     return $evt if $evt;
2796
2797     my $patron;
2798     ($patron, $evt) = $U->fetch_user($patronid);
2799     return $evt if $evt;
2800
2801     if($$args{pub}) {
2802         if( $patronid ne $reqr->id ) {
2803             $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
2804             return $evt if $evt;
2805         }
2806         return $U->cstorereq(
2807             'open-ils.cstore.direct.actor.usr_note.search.atomic',
2808             { usr => $patronid, pub => 't' } );
2809     }
2810
2811     $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
2812     return $evt if $evt;
2813
2814     return $U->cstorereq(
2815         'open-ils.cstore.direct.actor.usr_note.search.atomic', { usr => $patronid } );
2816 }
2817
2818 __PACKAGE__->register_method(
2819     method    => 'create_user_note',
2820     api_name  => 'open-ils.actor.note.create',
2821     signature => q/
2822         Creates a new note for the given user
2823         @param authtoken The login session key
2824         @param note The note object
2825     /
2826 );
2827 sub create_user_note {
2828     my( $self, $conn, $authtoken, $note ) = @_;
2829     my $e = new_editor(xact=>1, authtoken=>$authtoken);
2830     return $e->die_event unless $e->checkauth;
2831
2832     my $user = $e->retrieve_actor_user($note->usr)
2833         or return $e->die_event;
2834
2835     return $e->die_event unless
2836         $e->allowed('UPDATE_USER',$user->home_ou);
2837
2838     $note->creator($e->requestor->id);
2839     $e->create_actor_usr_note($note) or return $e->die_event;
2840     $e->commit;
2841     return $note->id;
2842 }
2843
2844
2845 __PACKAGE__->register_method(
2846     method    => 'delete_user_note',
2847     api_name  => 'open-ils.actor.note.delete',
2848     signature => q/
2849         Deletes a note for the given user
2850         @param authtoken The login session key
2851         @param noteid The note id
2852     /
2853 );
2854 sub delete_user_note {
2855     my( $self, $conn, $authtoken, $noteid ) = @_;
2856
2857     my $e = new_editor(xact=>1, authtoken=>$authtoken);
2858     return $e->die_event unless $e->checkauth;
2859     my $note = $e->retrieve_actor_usr_note($noteid)
2860         or return $e->die_event;
2861     my $user = $e->retrieve_actor_user($note->usr)
2862         or return $e->die_event;
2863     return $e->die_event unless
2864         $e->allowed('UPDATE_USER', $user->home_ou);
2865
2866     $e->delete_actor_usr_note($note) or return $e->die_event;
2867     $e->commit;
2868     return 1;
2869 }
2870
2871
2872 __PACKAGE__->register_method(
2873     method    => 'update_user_note',
2874     api_name  => 'open-ils.actor.note.update',
2875     signature => q/
2876         @param authtoken The login session key
2877         @param note The note
2878     /
2879 );
2880
2881 sub update_user_note {
2882     my( $self, $conn, $auth, $note ) = @_;
2883     my $e = new_editor(authtoken=>$auth, xact=>1);
2884     return $e->die_event unless $e->checkauth;
2885     my $patron = $e->retrieve_actor_user($note->usr)
2886         or return $e->die_event;
2887     return $e->die_event unless
2888         $e->allowed('UPDATE_USER', $patron->home_ou);
2889     $e->update_actor_user_note($note)
2890         or return $e->die_event;
2891     $e->commit;
2892     return 1;
2893 }
2894
2895 __PACKAGE__->register_method(
2896     method        => 'fetch_patron_messages',
2897     api_name      => 'open-ils.actor.message.retrieve',
2898     authoritative => 1,
2899     signature     => q/
2900         Returns a list of notes for a given user, not
2901         including ones marked deleted
2902         @param authtoken The login session key
2903         @param patronid patron ID
2904         @param options hash containing optional limit and offset
2905     /
2906 );
2907
2908 sub fetch_patron_messages {
2909     my( $self, $conn, $auth, $patronid, $options ) = @_;
2910
2911     $options ||= {};
2912
2913     my $e = new_editor(authtoken => $auth);
2914     return $e->die_event unless $e->checkauth;
2915
2916     if ($e->requestor->id ne $patronid) {
2917         return $e->die_event unless $e->allowed('VIEW_USER');
2918     }
2919
2920     my $select_clause = { usr => $patronid };
2921     my $options_clause = { order_by => { aum => 'create_date DESC' } };
2922     $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'};
2923     $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'};
2924
2925     my $aum = $e->search_actor_usr_message([ $select_clause, $options_clause ]);
2926     return $aum;
2927 }
2928
2929
2930 __PACKAGE__->register_method(
2931     method    => 'usrname_exists',
2932     api_name  => 'open-ils.actor.username.exists',
2933     signature => {
2934         desc  => 'Check if a username is already taken (by an undeleted patron)',
2935         param => [
2936             {desc => 'Authentication token', type => 'string'},
2937             {desc => 'Username',             type => 'string'}
2938         ],
2939         return => {
2940             desc => 'id of existing user if username exists, undef otherwise.  Event on error'
2941         },
2942     }
2943 );
2944
2945 sub usrname_exists {
2946     my( $self, $conn, $auth, $usrname ) = @_;
2947     my $e = new_editor(authtoken=>$auth);
2948     return $e->event unless $e->checkauth;
2949     my $a = $e->search_actor_user({usrname => $usrname}, {idlist=>1});
2950     return $$a[0] if $a and @$a;
2951     return undef;
2952 }
2953
2954 __PACKAGE__->register_method(
2955     method        => 'barcode_exists',
2956     api_name      => 'open-ils.actor.barcode.exists',
2957     authoritative => 1,
2958     signature     => 'Returns 1 if the requested barcode exists, returns 0 otherwise'
2959 );
2960
2961 sub barcode_exists {
2962     my( $self, $conn, $auth, $barcode ) = @_;
2963     my $e = new_editor(authtoken=>$auth);
2964     return $e->event unless $e->checkauth;
2965     my $card = $e->search_actor_card({barcode => $barcode});
2966     if (@$card) {
2967         return 1;
2968     } else {
2969         return 0;
2970     }
2971     #return undef unless @$card;
2972     #return $card->[0]->usr;
2973 }
2974
2975
2976 __PACKAGE__->register_method(
2977     method   => 'retrieve_net_levels',
2978     api_name => 'open-ils.actor.net_access_level.retrieve.all',
2979 );
2980
2981 sub retrieve_net_levels {
2982     my( $self, $conn, $auth ) = @_;
2983     my $e = new_editor(authtoken=>$auth);
2984     return $e->event unless $e->checkauth;
2985     return $e->retrieve_all_config_net_access_level();
2986 }
2987
2988 # Retain the old typo API name just in case
2989 __PACKAGE__->register_method(
2990     method   => 'fetch_org_by_shortname',
2991     api_name => 'open-ils.actor.org_unit.retrieve_by_shorname',
2992 );
2993 __PACKAGE__->register_method(
2994     method   => 'fetch_org_by_shortname',
2995     api_name => 'open-ils.actor.org_unit.retrieve_by_shortname',
2996 );
2997 sub fetch_org_by_shortname {
2998     my( $self, $conn, $sname ) = @_;
2999     my $e = new_editor();
3000     my $org = $e->search_actor_org_unit({ shortname => uc($sname)})->[0];
3001     return $e->event unless $org;
3002     return $org;
3003 }
3004
3005
3006 __PACKAGE__->register_method(
3007     method   => 'session_home_lib',
3008     api_name => 'open-ils.actor.session.home_lib',
3009 );
3010
3011 sub session_home_lib {
3012     my( $self, $conn, $auth ) = @_;
3013     my $e = new_editor(authtoken=>$auth);
3014     return undef unless $e->checkauth;
3015     my $org = $e->retrieve_actor_org_unit($e->requestor->home_ou);
3016     return $org->shortname;
3017 }
3018
3019 __PACKAGE__->register_method(
3020     method    => 'session_safe_token',
3021     api_name  => 'open-ils.actor.session.safe_token',
3022     signature => q/
3023         Returns a hashed session ID that is safe for export to the world.
3024         This safe token will expire after 1 hour of non-use.
3025         @param auth Active authentication token
3026     /
3027 );
3028
3029 sub session_safe_token {
3030     my( $self, $conn, $auth ) = @_;
3031     my $e = new_editor(authtoken=>$auth);
3032     return undef unless $e->checkauth;
3033
3034     my $safe_token = md5_hex($auth);
3035
3036     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
3037
3038     # add more user fields as needed
3039     $cache->put_cache(
3040         "safe-token-user-$safe_token", {
3041             id => $e->requestor->id,
3042             home_ou_shortname => $e->retrieve_actor_org_unit(
3043                 $e->requestor->home_ou)->shortname,
3044         },
3045         60 * 60
3046     );
3047
3048     return $safe_token;
3049 }
3050
3051
3052 __PACKAGE__->register_method(
3053     method    => 'safe_token_home_lib',
3054     api_name  => 'open-ils.actor.safe_token.home_lib.shortname',
3055     signature => q/
3056         Returns the home library shortname from the session
3057         asscociated with a safe token from generated by
3058         open-ils.actor.session.safe_token.
3059         @param safe_token Active safe token
3060         @param who Optional user activity "ewho" value
3061     /
3062 );
3063
3064 sub safe_token_home_lib {
3065     my( $self, $conn, $safe_token, $who ) = @_;
3066     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
3067
3068     my $blob = $cache->get_cache("safe-token-user-$safe_token");
3069     return unless $blob;
3070
3071     $U->log_user_activity($blob->{id}, $who, 'verify');
3072     return $blob->{home_ou_shortname};
3073 }
3074
3075
3076 __PACKAGE__->register_method(
3077     method   => "update_penalties",
3078     api_name => "open-ils.actor.user.penalties.update"
3079 );
3080
3081 sub update_penalties {
3082     my($self, $conn, $auth, $user_id) = @_;
3083     my $e = new_editor(authtoken=>$auth, xact => 1);
3084     return $e->die_event unless $e->checkauth;
3085     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3086     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3087     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $e->requestor->ws_ou);
3088     return $evt if $evt;
3089     $e->commit;
3090     return 1;
3091 }
3092
3093
3094 __PACKAGE__->register_method(
3095     method   => "apply_penalty",
3096     api_name => "open-ils.actor.user.penalty.apply"
3097 );
3098
3099 sub apply_penalty {
3100     my($self, $conn, $auth, $penalty) = @_;
3101
3102     my $e = new_editor(authtoken=>$auth, xact => 1);
3103     return $e->die_event unless $e->checkauth;
3104
3105     my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
3106     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3107
3108     my $ptype = $e->retrieve_config_standing_penalty($penalty->standing_penalty) or return $e->die_event;
3109
3110     my $ctx_org =
3111         (defined $ptype->org_depth) ?
3112         $U->org_unit_ancestor_at_depth($penalty->org_unit, $ptype->org_depth) :
3113         $penalty->org_unit;
3114
3115     $penalty->org_unit($ctx_org);
3116     $penalty->staff($e->requestor->id);
3117     $e->create_actor_user_standing_penalty($penalty) or return $e->die_event;
3118
3119     $e->commit;
3120     return $penalty->id;
3121 }
3122
3123 __PACKAGE__->register_method(
3124     method   => "remove_penalty",
3125     api_name => "open-ils.actor.user.penalty.remove"
3126 );
3127
3128 sub remove_penalty {
3129     my($self, $conn, $auth, $penalty) = @_;
3130     my $e = new_editor(authtoken=>$auth, xact => 1);
3131     return $e->die_event unless $e->checkauth;
3132     my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
3133     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3134
3135     $e->delete_actor_user_standing_penalty($penalty) or return $e->die_event;
3136     $e->commit;
3137     return 1;
3138 }
3139
3140 __PACKAGE__->register_method(
3141     method   => "update_penalty_note",
3142     api_name => "open-ils.actor.user.penalty.note.update"
3143 );
3144
3145 sub update_penalty_note {
3146     my($self, $conn, $auth, $penalty_ids, $note) = @_;
3147     my $e = new_editor(authtoken=>$auth, xact => 1);
3148     return $e->die_event unless $e->checkauth;
3149     for my $penalty_id (@$penalty_ids) {
3150         my $penalty = $e->search_actor_user_standing_penalty( { id => $penalty_id } )->[0];
3151         if (! $penalty ) { return $e->die_event; }
3152         my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
3153         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3154
3155         $penalty->note( $note ); $penalty->ischanged( 1 );
3156
3157         $e->update_actor_user_standing_penalty($penalty) or return $e->die_event;
3158     }
3159     $e->commit;
3160     return 1;
3161 }
3162
3163 __PACKAGE__->register_method(
3164     method   => "ranged_penalty_thresholds",
3165     api_name => "open-ils.actor.grp_penalty_threshold.ranged.retrieve",
3166     stream   => 1
3167 );
3168
3169 sub ranged_penalty_thresholds {
3170     my($self, $conn, $auth, $context_org) = @_;
3171     my $e = new_editor(authtoken=>$auth);
3172     return $e->event unless $e->checkauth;
3173     return $e->event unless $e->allowed('VIEW_GROUP_PENALTY_THRESHOLD', $context_org);
3174     my $list = $e->search_permission_grp_penalty_threshold([
3175         {org_unit => $U->get_org_ancestors($context_org)},
3176         {order_by => {pgpt => 'id'}}
3177     ]);
3178     $conn->respond($_) for @$list;
3179     return undef;
3180 }
3181
3182
3183
3184 __PACKAGE__->register_method(
3185     method        => "user_retrieve_fleshed_by_id",
3186     authoritative => 1,
3187     api_name      => "open-ils.actor.user.fleshed.retrieve",
3188 );
3189
3190 sub user_retrieve_fleshed_by_id {
3191     my( $self, $client, $auth, $user_id, $fields ) = @_;
3192     my $e = new_editor(authtoken => $auth);
3193     return $e->event unless $e->checkauth;
3194
3195     if( $e->requestor->id != $user_id ) {
3196         return $e->event unless $e->allowed('VIEW_USER');
3197     }
3198
3199     $fields ||= [
3200         "cards",
3201         "card",
3202         "groups",
3203         "standing_penalties",
3204         "settings",
3205         "addresses",
3206         "billing_address",
3207         "mailing_address",
3208         "stat_cat_entries",
3209         "waiver_entries",
3210         "usr_activity" ];
3211     return new_flesh_user($user_id, $fields, $e);
3212 }
3213
3214
3215 sub new_flesh_user {
3216
3217     my $id = shift;
3218     my $fields = shift || [];
3219     my $e = shift;
3220
3221     my $fetch_penalties = 0;
3222     if(grep {$_ eq 'standing_penalties'} @$fields) {
3223         $fields = [grep {$_ ne 'standing_penalties'} @$fields];
3224         $fetch_penalties = 1;
3225     }
3226
3227     my $fetch_usr_act = 0;
3228     if(grep {$_ eq 'usr_activity'} @$fields) {
3229         $fields = [grep {$_ ne 'usr_activity'} @$fields];
3230         $fetch_usr_act = 1;
3231     }
3232
3233     my $user = $e->retrieve_actor_user(
3234     [
3235         $id,
3236         {
3237             "flesh"             => 1,
3238             "flesh_fields" =>  { "au" => $fields }
3239         }
3240     ]
3241     ) or return $e->die_event;
3242
3243
3244     if( grep { $_ eq 'addresses' } @$fields ) {
3245
3246         $user->addresses([]) unless @{$user->addresses};
3247         # don't expose "replaced" addresses by default
3248         $user->addresses([grep {$_->id >= 0} @{$user->addresses}]);
3249
3250         if( ref $user->billing_address ) {
3251             unless( grep { $user->billing_address->id == $_->id } @{$user->addresses} ) {
3252                 push( @{$user->addresses}, $user->billing_address );
3253             }
3254         }
3255
3256         if( ref $user->mailing_address ) {
3257             unless( grep { $user->mailing_address->id == $_->id } @{$user->addresses} ) {
3258                 push( @{$user->addresses}, $user->mailing_address );
3259             }
3260         }
3261     }
3262
3263     if($fetch_penalties) {
3264         # grab the user penalties ranged for this location
3265         $user->standing_penalties(
3266             $e->search_actor_user_standing_penalty([
3267                 {   usr => $id,
3268                     '-or' => [
3269                         {stop_date => undef},
3270                         {stop_date => {'>' => 'now'}}
3271                     ],
3272                     org_unit => $U->get_org_full_path($e->requestor->ws_ou)
3273                 },
3274                 {   flesh => 1,
3275                     flesh_fields => {ausp => ['standing_penalty']}
3276                 }
3277             ])
3278         );
3279     }
3280
3281     # retrieve the most recent usr_activity entry
3282     if ($fetch_usr_act) {
3283
3284         # max number to return for simple patron fleshing
3285         my $limit = $U->ou_ancestor_setting_value(
3286             $e->requestor->ws_ou,
3287             'circ.patron.usr_activity_retrieve.max');
3288
3289         my $opts = {
3290             flesh => 1,
3291             flesh_fields => {auact => ['etype']},
3292             order_by => {auact => 'event_time DESC'},
3293         };
3294
3295         # 0 == none, <0 == return all
3296         $limit = 1 unless defined $limit;
3297         $opts->{limit} = $limit if $limit > 0;
3298
3299         $user->usr_activity(
3300             ($limit == 0) ?
3301                 [] : # skip the DB call
3302                 $e->search_actor_usr_activity([{usr => $user->id}, $opts])
3303         );
3304     }
3305
3306     $e->rollback;
3307     $user->clear_passwd();
3308     return $user;
3309 }
3310
3311
3312
3313
3314 __PACKAGE__->register_method(
3315     method   => "user_retrieve_parts",
3316     api_name => "open-ils.actor.user.retrieve.parts",
3317 );
3318
3319 sub user_retrieve_parts {
3320     my( $self, $client, $auth, $user_id, $fields ) = @_;
3321     my $e = new_editor(authtoken => $auth);
3322     return $e->event unless $e->checkauth;
3323     $user_id ||= $e->requestor->id;
3324     if( $e->requestor->id != $user_id ) {
3325         return $e->event unless $e->allowed('VIEW_USER');
3326     }
3327     my @resp;
3328     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3329     push(@resp, $user->$_()) for(@$fields);
3330     return \@resp;
3331 }
3332
3333
3334
3335 __PACKAGE__->register_method(
3336     method    => 'user_opt_in_enabled',
3337     api_name  => 'open-ils.actor.user.org_unit_opt_in.enabled',
3338     signature => '@return 1 if user opt-in is globally enabled, 0 otherwise.'
3339 );
3340
3341 sub user_opt_in_enabled {
3342     my($self, $conn) = @_;
3343     my $sc = OpenSRF::Utils::SettingsClient->new;
3344     return 1 if lc($sc->config_value(share => user => 'opt_in')) eq 'true';
3345     return 0;
3346 }
3347
3348
3349 __PACKAGE__->register_method(
3350     method    => 'user_opt_in_at_org',
3351     api_name  => 'open-ils.actor.user.org_unit_opt_in.check',
3352     signature => q/
3353         @param $auth The auth token
3354         @param user_id The ID of the user to test
3355         @return 1 if the user has opted in at the specified org,
3356             2 if opt-in is disallowed for the user's home org,
3357             event on error, and 0 otherwise. /
3358 );
3359 sub user_opt_in_at_org {
3360     my($self, $conn, $auth, $user_id) = @_;
3361
3362     # see if we even need to enforce the opt-in value
3363     return 1 unless user_opt_in_enabled($self);
3364
3365     my $e = new_editor(authtoken => $auth);
3366     return $e->event unless $e->checkauth;
3367
3368     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3369     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3370
3371     my $ws_org = $e->requestor->ws_ou;
3372     # user is automatically opted-in if they are from the local org
3373     return 1 if $user->home_ou eq $ws_org;
3374
3375     # get the boundary setting
3376     my $opt_boundary = $U->ou_ancestor_setting_value($e->requestor->ws_ou,'org.patron_opt_boundary');
3377
3378     # auto opt in if user falls within the opt boundary
3379     my $opt_orgs = $U->get_org_descendants($ws_org, $opt_boundary);
3380
3381     return 1 if grep $_ eq $user->home_ou, @$opt_orgs;
3382
3383     # check whether opt-in is restricted at the user's home library
3384     my $opt_restrict_depth = $U->ou_ancestor_setting_value($user->home_ou, 'org.restrict_opt_to_depth');
3385     if ($opt_restrict_depth) {
3386         my $restrict_ancestor = $U->org_unit_ancestor_at_depth($user->home_ou, $opt_restrict_depth);
3387         my $unrestricted_orgs = $U->get_org_descendants($restrict_ancestor);
3388
3389         # opt-in is disallowed unless the workstation org is within the home
3390         # library's opt-in scope
3391         return 2 unless grep $_ eq $e->requestor->ws_ou, @$unrestricted_orgs;
3392     }
3393
3394     my $vals = $e->search_actor_usr_org_unit_opt_in(
3395         {org_unit=>$opt_orgs, usr=>$user_id},{idlist=>1});
3396
3397     return 1 if @$vals;
3398     return 0;
3399 }
3400
3401 __PACKAGE__->register_method(
3402     method    => 'create_user_opt_in_at_org',
3403     api_name  => 'open-ils.actor.user.org_unit_opt_in.create',
3404     signature => q/
3405         @param $auth The auth token
3406         @param user_id The ID of the user to test
3407         @return The ID of the newly created object, event on error./
3408 );
3409
3410 sub create_user_opt_in_at_org {
3411     my($self, $conn, $auth, $user_id, $org_id) = @_;
3412
3413     my $e = new_editor(authtoken => $auth, xact=>1);
3414     return $e->die_event unless $e->checkauth;
3415
3416     # if a specific org unit wasn't passed in, get one based on the defaults;
3417     if(!$org_id){
3418         my $wsou = $e->requestor->ws_ou;
3419         # get the default opt depth
3420         my $opt_depth = $U->ou_ancestor_setting_value($wsou,'org.patron_opt_default');
3421         # get the org unit at that depth
3422         my $org = $e->json_query({
3423             from => [ 'actor.org_unit_ancestor_at_depth', $wsou, $opt_depth ]})->[0];
3424         $org_id = $org->{id};
3425     }
3426     if (!$org_id) {
3427         # fall back to the workstation OU, the pre-opt-in-boundary way
3428         $org_id = $e->requestor->ws_ou;
3429     }
3430
3431     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3432     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3433
3434     my $opt_in = Fieldmapper::actor::usr_org_unit_opt_in->new;
3435
3436     $opt_in->org_unit($org_id);
3437     $opt_in->usr($user_id);
3438     $opt_in->staff($e->requestor->id);
3439     $opt_in->opt_in_ts('now');
3440     $opt_in->opt_in_ws($e->requestor->wsid);
3441
3442     $opt_in = $e->create_actor_usr_org_unit_opt_in($opt_in)
3443         or return $e->die_event;
3444
3445     $e->commit;
3446
3447     return $opt_in->id;
3448 }
3449
3450
3451 __PACKAGE__->register_method (
3452     method      => 'retrieve_org_hours',
3453     api_name    => 'open-ils.actor.org_unit.hours_of_operation.retrieve',
3454     signature   => q/
3455         Returns the hours of operation for a specified org unit
3456         @param authtoken The login session key
3457         @param org_id The org_unit ID
3458     /
3459 );
3460
3461 sub retrieve_org_hours {
3462     my($self, $conn, $auth, $org_id) = @_;
3463     my $e = new_editor(authtoken => $auth);
3464     return $e->die_event unless $e->checkauth;
3465     $org_id ||= $e->requestor->ws_ou;
3466     return $e->retrieve_actor_org_unit_hours_of_operation($org_id);
3467 }
3468
3469
3470 __PACKAGE__->register_method (
3471     method      => 'verify_user_password',
3472     api_name    => 'open-ils.actor.verify_user_password',
3473     signature   => q/
3474         Given a barcode or username and the MD5 encoded password,
3475         returns 1 if the password is correct.  Returns 0 otherwise.
3476     /
3477 );
3478
3479 sub verify_user_password {
3480     my($self, $conn, $auth, $barcode, $username, $password) = @_;
3481     my $e = new_editor(authtoken => $auth);
3482     return $e->die_event unless $e->checkauth;
3483     my $user;
3484     my $user_by_barcode;
3485     my $user_by_username;
3486     if($barcode) {
3487         my $card = $e->search_actor_card([
3488             {barcode => $barcode},
3489             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0] or return 0;
3490         $user_by_barcode = $card->usr;
3491         $user = $user_by_barcode;
3492     }
3493     if ($username) {
3494         $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return 0;
3495         $user = $user_by_username;
3496     }
3497     return 0 if (!$user || $U->is_true($user->deleted));
3498     return 0 if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3499     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3500     return $U->verify_migrated_user_password($e, $user->id, $password, 1);
3501 }
3502
3503 __PACKAGE__->register_method (
3504     method      => 'retrieve_usr_id_via_barcode_or_usrname',
3505     api_name    => "open-ils.actor.user.retrieve_id_by_barcode_or_username",
3506     signature   => q/
3507         Given a barcode or username returns the id for the user or
3508         a failure event.
3509     /
3510 );
3511
3512 sub retrieve_usr_id_via_barcode_or_usrname {
3513     my($self, $conn, $auth, $barcode, $username) = @_;
3514     my $e = new_editor(authtoken => $auth);
3515     return $e->die_event unless $e->checkauth;
3516     my $id_as_barcode= OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.actor' => app_settings => 'id_as_barcode');
3517     my $user;
3518     my $user_by_barcode;
3519     my $user_by_username;
3520     $logger->info("$id_as_barcode is the ID as BARCODE");
3521     if($barcode) {
3522         my $card = $e->search_actor_card([
3523             {barcode => $barcode},
3524             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
3525         if ($id_as_barcode =~ /^t/i) {
3526             if (!$card) {
3527                 $user = $e->retrieve_actor_user($barcode);
3528                 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$user);
3529             }else {
3530                 $user_by_barcode = $card->usr;
3531                 $user = $user_by_barcode;
3532             }
3533         }else {
3534             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$card);
3535             $user_by_barcode = $card->usr;
3536             $user = $user_by_barcode;
3537         }
3538     }
3539
3540     if ($username) {
3541         $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return OpenILS::Event->new( 'ACTOR_USR_NOT_FOUND' );
3542
3543         $user = $user_by_username;
3544     }
3545     return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if (!$user);
3546     return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3547     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3548     return $user->id;
3549 }
3550
3551
3552 __PACKAGE__->register_method (
3553     method      => 'merge_users',
3554     api_name    => 'open-ils.actor.user.merge',
3555     signature   => {
3556         desc => q/
3557             Given a list of source users and destination user, transfer all data from the source
3558             to the dest user and delete the source user.  All user related data is
3559             transferred, including circulations, holds, bookbags, etc.
3560         /
3561     }
3562 );
3563
3564 sub merge_users {
3565     my($self, $conn, $auth, $master_id, $user_ids, $options) = @_;
3566     my $e = new_editor(xact => 1, authtoken => $auth);
3567     return $e->die_event unless $e->checkauth;
3568
3569     # disallow the merge if any subordinate accounts are in collections
3570     my $colls = $e->search_money_collections_tracker({usr => $user_ids}, {idlist => 1});
3571     return OpenILS::Event->new('MERGED_USER_IN_COLLECTIONS', payload => $user_ids) if @$colls;
3572
3573     return OpenILS::Event->new('MERGE_SELF_NOT_ALLOWED')
3574         if $master_id == $e->requestor->id;
3575
3576     my $master_user = $e->retrieve_actor_user($master_id) or return $e->die_event;
3577     my $evt = group_perm_failed($e, $e->requestor, $master_user);
3578     return $evt if $evt;
3579
3580     my $del_addrs = ($U->ou_ancestor_setting_value(
3581         $master_user->home_ou, 'circ.user_merge.delete_addresses', $e)) ? 't' : 'f';
3582     my $del_cards = ($U->ou_ancestor_setting_value(
3583         $master_user->home_ou, 'circ.user_merge.delete_cards', $e)) ? 't' : 'f';
3584     my $deactivate_cards = ($U->ou_ancestor_setting_value(
3585         $master_user->home_ou, 'circ.user_merge.deactivate_cards', $e)) ? 't' : 'f';
3586
3587     for my $src_id (@$user_ids) {
3588
3589         my $src_user = $e->retrieve_actor_user($src_id) or return $e->die_event;
3590         my $evt = group_perm_failed($e, $e->requestor, $src_user);
3591         return $evt if $evt;
3592
3593         return OpenILS::Event->new('MERGE_SELF_NOT_ALLOWED')
3594             if $src_id == $e->requestor->id;
3595
3596         return $e->die_event unless $e->allowed('MERGE_USERS', $src_user->home_ou);
3597         if($src_user->home_ou ne $master_user->home_ou) {
3598             return $e->die_event unless $e->allowed('MERGE_USERS', $master_user->home_ou);
3599         }
3600
3601         return $e->die_event unless
3602             $e->json_query({from => [
3603                 'actor.usr_merge',
3604                 $src_id,
3605                 $master_id,
3606                 $del_addrs,
3607                 $del_cards,
3608                 $deactivate_cards
3609             ]});
3610     }
3611
3612     $e->commit;
3613     return 1;
3614 }
3615
3616
3617 __PACKAGE__->register_method (
3618     method      => 'approve_user_address',
3619     api_name    => 'open-ils.actor.user.pending_address.approve',
3620     signature   => {
3621         desc => q/
3622         /
3623     }
3624 );
3625
3626 sub approve_user_address {
3627     my($self, $conn, $auth, $addr) = @_;
3628     my $e = new_editor(xact => 1, authtoken => $auth);
3629     return $e->die_event unless $e->checkauth;
3630     if(ref $addr) {
3631         # if the caller passes an address object, assume they want to
3632         # update it first before approving it
3633         $e->update_actor_user_address($addr) or return $e->die_event;
3634     } else {
3635         $addr = $e->retrieve_actor_user_address($addr) or return $e->die_event;
3636     }
3637     my $user = $e->retrieve_actor_user($addr->usr);
3638     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3639     my $result = $e->json_query({from => ['actor.approve_pending_address', $addr->id]})->[0]
3640         or return $e->die_event;
3641     $e->commit;
3642     return [values %$result]->[0];
3643 }
3644
3645
3646 __PACKAGE__->register_method (
3647     method      => 'retrieve_friends',
3648     api_name    => 'open-ils.actor.friends.retrieve',
3649     signature   => {
3650         desc => q/
3651             returns { confirmed: [], pending_out: [], pending_in: []}
3652             pending_out are users I'm requesting friendship with
3653             pending_in are users requesting friendship with me
3654         /
3655     }
3656 );
3657
3658 sub retrieve_friends {
3659     my($self, $conn, $auth, $user_id, $options) = @_;
3660     my $e = new_editor(authtoken => $auth);
3661     return $e->event unless $e->checkauth;
3662     $user_id ||= $e->requestor->id;
3663
3664     if($user_id != $e->requestor->id) {
3665         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3666         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3667     }
3668
3669     return OpenILS::Application::Actor::Friends->retrieve_friends(
3670         $e, $user_id, $options);
3671 }
3672
3673
3674
3675 __PACKAGE__->register_method (
3676     method      => 'apply_friend_perms',
3677     api_name    => 'open-ils.actor.friends.perms.apply',
3678     signature   => {
3679         desc => q/
3680         /
3681     }
3682 );
3683 sub apply_friend_perms {
3684     my($self, $conn, $auth, $user_id, $delegate_id, @perms) = @_;
3685     my $e = new_editor(authtoken => $auth, xact => 1);
3686     return $e->die_event unless $e->checkauth;
3687
3688     if($user_id != $e->requestor->id) {
3689         my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3690         return $e->die_event unless $e->allowed('VIEW_USER', $user->home_ou);
3691     }
3692
3693     for my $perm (@perms) {
3694         my $evt =
3695             OpenILS::Application::Actor::Friends->apply_friend_perm(
3696                 $e, $user_id, $delegate_id, $perm);
3697         return $evt if $evt;
3698     }
3699
3700     $e->commit;
3701     return 1;
3702 }
3703
3704
3705 __PACKAGE__->register_method (
3706     method      => 'update_user_pending_address',
3707     api_name    => 'open-ils.actor.user.address.pending.cud'
3708 );
3709
3710 sub update_user_pending_address {
3711     my($self, $conn, $auth, $addr) = @_;
3712     my $e = new_editor(authtoken => $auth, xact => 1);
3713     return $e->die_event unless $e->checkauth;
3714
3715     my $user = $e->retrieve_actor_user($addr->usr) or return $e->die_event;
3716     if($addr->usr != $e->requestor->id) {
3717         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3718     }
3719
3720     if($addr->isnew) {
3721         $e->create_actor_user_address($addr) or return $e->die_event;
3722     } elsif($addr->isdeleted) {
3723         $e->delete_actor_user_address($addr) or return $e->die_event;
3724     } else {
3725         $e->update_actor_user_address($addr) or return $e->die_event;
3726     }
3727
3728     $e->commit;
3729     $U->create_events_for_hook('au.updated', $user, $e->requestor->ws_ou);
3730
3731     return $addr->id;
3732 }
3733
3734
3735 __PACKAGE__->register_method (
3736     method      => 'user_events',
3737     api_name    => 'open-ils.actor.user.events.circ',
3738     stream      => 1,
3739 );
3740 __PACKAGE__->register_method (
3741     method      => 'user_events',
3742     api_name    => 'open-ils.actor.user.events.ahr',
3743     stream      => 1,
3744 );
3745
3746 sub user_events {
3747     my($self, $conn, $auth, $user_id, $filters) = @_;
3748     my $e = new_editor(authtoken => $auth);
3749     return $e->event unless $e->checkauth;
3750
3751     (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3752     my $user_field = 'usr';
3753
3754     $filters ||= {};
3755     $filters->{target} = {
3756         select => { $obj_type => ['id'] },
3757         from => $obj_type,
3758         where => {usr => $user_id}
3759     };
3760
3761     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3762     if($e->requestor->id != $user_id) {
3763         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3764     }
3765
3766     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3767     my $req = $ses->request('open-ils.trigger.events_by_target',
3768         $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3769
3770     while(my $resp = $req->recv) {
3771         my $val = $resp->content;
3772         my $tgt = $val->target;
3773
3774         if($obj_type eq 'circ') {
3775             $tgt->target_copy($e->retrieve_asset_copy($tgt->target_copy));
3776
3777         } elsif($obj_type eq 'ahr') {
3778             $tgt->current_copy($e->retrieve_asset_copy($tgt->current_copy))
3779                 if $tgt->current_copy;
3780         }
3781
3782         $conn->respond($val) if $val;
3783     }
3784
3785     return undef;
3786 }
3787
3788 __PACKAGE__->register_method (
3789     method      => 'copy_events',
3790     api_name    => 'open-ils.actor.copy.events.circ',
3791     stream      => 1,
3792 );
3793 __PACKAGE__->register_method (
3794     method      => 'copy_events',
3795     api_name    => 'open-ils.actor.copy.events.ahr',
3796     stream      => 1,
3797 );
3798
3799 sub copy_events {
3800     my($self, $conn, $auth, $copy_id, $filters) = @_;
3801     my $e = new_editor(authtoken => $auth);
3802     return $e->event unless $e->checkauth;
3803
3804     (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3805
3806     my $copy = $e->retrieve_asset_copy($copy_id) or return $e->event;
3807
3808     my $copy_field = 'target_copy';
3809     $copy_field = 'current_copy' if $obj_type eq 'ahr';
3810
3811     $filters ||= {};
3812     $filters->{target} = {
3813         select => { $obj_type => ['id'] },
3814         from => $obj_type,
3815         where => {$copy_field => $copy_id}
3816     };
3817
3818
3819     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3820     my $req = $ses->request('open-ils.trigger.events_by_target',
3821         $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3822
3823     while(my $resp = $req->recv) {
3824         my $val = $resp->content;
3825         my $tgt = $val->target;
3826
3827         my $user = $e->retrieve_actor_user($tgt->usr);
3828         if($e->requestor->id != $user->id) {
3829             return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3830         }
3831
3832         $tgt->$copy_field($copy);
3833
3834         $tgt->usr($user);
3835         $conn->respond($val) if $val;
3836     }
3837
3838     return undef;
3839 }
3840
3841
3842 __PACKAGE__->register_method (
3843     method      => 'get_itemsout_notices',
3844     api_name    => 'open-ils.actor.user.itemsout.notices',
3845     stream      => 1,
3846     argc        => 2,
3847     signature   => {
3848         desc => q/Summary counts of circulat notices/,
3849         params => [
3850             {desc => 'authtoken', type => 'string'},
3851             {desc => 'circulation identifiers', type => 'array of numbers'}
3852         ],
3853         return => q/Stream of summary objects/
3854     }
3855 );
3856
3857 sub get_itemsout_notices {
3858     my ($self, $client, $auth, $circ_ids) = @_;
3859
3860     my $e = new_editor(authtoken => $auth);
3861     return $e->event unless $e->checkauth;
3862
3863     $circ_ids = [$circ_ids] unless ref $circ_ids eq 'ARRAY';
3864
3865     for my $circ_id (@$circ_ids) {
3866         my $resp = get_itemsout_notices_impl($e, $circ_id);
3867
3868         if ($U->is_event($resp)) {
3869             $client->respond($resp);
3870             return;
3871         }
3872
3873         $client->respond({circ_id => $circ_id, %$resp});
3874     }
3875
3876     return undef;
3877 }
3878
3879
3880
3881 sub get_itemsout_notices_impl {
3882     my ($e, $circId) = @_;
3883
3884     my $requestorId = $e->requestor->id;
3885
3886     my $circ = $e->retrieve_action_circulation($circId) or return $e->event;
3887
3888     my $patronId = $circ->usr;
3889
3890     if( $patronId ne $requestorId ){
3891         my $user = $e->retrieve_actor_user($requestorId) or return $e->event;
3892         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
3893     }
3894
3895     #my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3896     #my $req = $ses->request('open-ils.trigger.events_by_target',
3897     #   'circ', {target => [$circId], event=> {state=>'complete'}});
3898     # ^ Above removed in favor of faster json_query.
3899     #
3900     # SQL:
3901     # select complete_time
3902     # from action_trigger.event atev
3903     #     JOIN action_trigger.event_definition def ON (def.id = atev.event_def)
3904     #     JOIN action_trigger.hook athook ON (athook.key = def.hook)
3905     # where hook = 'checkout.due' AND state = 'complete' and target = <circId>;
3906     #
3907
3908     my $ctx_loc = $e->requestor->ws_ou;
3909     my $exclude_courtesy_notices = $U->ou_ancestor_setting_value(
3910         $ctx_loc, 'webstaff.circ.itemsout_notice_count_excludes_courtesies');
3911
3912     my $query = {
3913             select => { atev => ["complete_time"] },
3914             from => {
3915                     atev => {
3916                             atevdef => { field => "id",fkey => "event_def"}
3917                     }
3918             },
3919             where => {
3920             "+atevdef" => { active => 't', hook => 'checkout.due' },
3921             "+atev" => { target => $circId, state => 'complete' }
3922         }
3923     };
3924
3925     if ($exclude_courtesy_notices){
3926         $query->{"where"}->{"+atevdef"}->{validator} = { "<>" => "CircIsOpen"};
3927     }
3928
3929     my %resblob = ( numNotices => 0, lastDt => undef );
3930
3931     my $res = $e->json_query($query);
3932     for my $ndate (@$res) {
3933         $resblob{numNotices}++;
3934         if( !defined $resblob{lastDt}){
3935             $resblob{lastDt} = $$ndate{complete_time};
3936         }
3937
3938         if ($resblob{lastDt} lt $$ndate{complete_time}){
3939            $resblob{lastDt} = $$ndate{complete_time};
3940         }
3941    }
3942
3943     return \%resblob;
3944 }
3945
3946 __PACKAGE__->register_method (
3947     method      => 'update_events',
3948     api_name    => 'open-ils.actor.user.event.cancel.batch',
3949     stream      => 1,
3950 );
3951 __PACKAGE__->register_method (
3952     method      => 'update_events',
3953     api_name    => 'open-ils.actor.user.event.reset.batch',
3954     stream      => 1,
3955 );
3956
3957 sub update_events {
3958     my($self, $conn, $auth, $event_ids) = @_;
3959     my $e = new_editor(xact => 1, authtoken => $auth);
3960     return $e->die_event unless $e->checkauth;
3961
3962     my $x = 1;
3963     for my $id (@$event_ids) {
3964
3965         # do a little dance to determine what user we are ultimately affecting
3966         my $event = $e->retrieve_action_trigger_event([
3967             $id,
3968             {   flesh => 2,
3969                 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
3970             }
3971         ]) or return $e->die_event;
3972
3973         my $user_id;
3974         if($event->event_def->hook->core_type eq 'circ') {
3975             $user_id = $e->retrieve_action_circulation($event->target)->usr;
3976         } elsif($event->event_def->hook->core_type eq 'ahr') {
3977             $user_id = $e->retrieve_action_hold_request($event->target)->usr;
3978         } else {
3979             return 0;
3980         }
3981
3982         my $user = $e->retrieve_actor_user($user_id);
3983         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3984
3985         if($self->api_name =~ /cancel/) {
3986             $event->state('invalid');
3987         } elsif($self->api_name =~ /reset/) {
3988             $event->clear_start_time;
3989             $event->clear_update_time;
3990             $event->state('pending');
3991         }
3992
3993         $e->update_action_trigger_event($event) or return $e->die_event;
3994         $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
3995     }
3996
3997     $e->commit;
3998     return {complete => 1};
3999 }
4000
4001
4002 __PACKAGE__->register_method (
4003     method      => 'really_delete_user',
4004     api_name    => 'open-ils.actor.user.delete.override',
4005     signature   => q/@see open-ils.actor.user.delete/
4006 );
4007
4008 __PACKAGE__->register_method (
4009     method      => 'really_delete_user',
4010     api_name    => 'open-ils.actor.user.delete',
4011     signature   => q/
4012         It anonymizes all personally identifiable information in actor.usr. By calling actor.usr_purge_data()
4013         it also purges related data from other tables, sometimes by transferring it to a designated destination user.
4014         The usrname field (along with first_given_name and family_name) is updated to id '-PURGED-' now().
4015         dest_usr_id is only required when deleting a user that performs staff functions.
4016     /
4017 );
4018
4019 sub really_delete_user {
4020     my($self, $conn, $auth, $user_id, $dest_user_id, $oargs) = @_;
4021     my $e = new_editor(authtoken => $auth, xact => 1);
4022     return $e->die_event unless $e->checkauth;
4023     $oargs = { all => 1 } unless defined $oargs;
4024
4025     # Find all unclosed billings for for user $user_id, thereby, also checking for open circs
4026     my $open_bills = $e->json_query({
4027         select => { mbts => ['id'] },
4028         from => 'mbts',
4029         where => {
4030             xact_finish => { '=' => undef },
4031             usr => { '=' => $user_id },
4032         }
4033     }) or return $e->die_event;
4034
4035     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
4036
4037     # No deleting patrons with open billings or checked out copies, unless perm-enabled override
4038     if (@$open_bills) {
4039         return $e->die_event(OpenILS::Event->new('ACTOR_USER_DELETE_OPEN_XACTS'))
4040         unless $self->api_name =~ /override/o && ($oargs->{all} || grep { $_ eq 'ACTOR_USER_DELETE_OPEN_XACTS' } @{$oargs->{events}})
4041         && $e->allowed('ACTOR_USER_DELETE_OPEN_XACTS.override', $user->home_ou);
4042     }
4043     # No deleting yourself - UI is supposed to stop you first, though.
4044     return $e->die_event unless $e->requestor->id != $user->id;
4045     return $e->die_event unless $e->allowed('DELETE_USER', $user->home_ou);
4046     # Check if you are allowed to mess with this patron permission group at all
4047     my $evt = group_perm_failed($e, $e->requestor, $user);
4048     return $e->die_event($evt) if $evt;
4049     my $stat = $e->json_query(
4050         {from => ['actor.usr_delete', $user_id, $dest_user_id]})->[0]
4051         or return $e->die_event;
4052     $e->commit;
4053     return 1;
4054 }
4055
4056
4057 __PACKAGE__->register_method (
4058     method      => 'user_payments',
4059     api_name    => 'open-ils.actor.user.payments.retrieve',
4060     stream => 1,
4061     signature   => q/
4062         Returns all payments for a given user.  Default order is newest payments first.
4063         @param auth Authentication token
4064         @param user_id The user ID
4065         @param filters An optional hash of filters, including limit, offset, and order_by definitions
4066     /
4067 );
4068
4069 sub user_payments {
4070     my($self, $conn, $auth, $user_id, $filters) = @_;
4071     $filters ||= {};
4072
4073     my $e = new_editor(authtoken => $auth);
4074     return $e->die_event unless $e->checkauth;
4075
4076     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4077     return $e->event unless
4078         $e->requestor->id == $user_id or
4079         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
4080
4081     # Find all payments for all transactions for user $user_id
4082     my $query = {
4083         select => {mp => ['id']},
4084         from => 'mp',
4085         where => {
4086             xact => {
4087                 in => {
4088                     select => {mbt => ['id']},
4089                     from => 'mbt',
4090                     where => {usr => $user_id}
4091                 }
4092             }
4093         },
4094         order_by => [
4095             { # by default, order newest payments first
4096                 class => 'mp',
4097                 field => 'payment_ts',
4098                 direction => 'desc'
4099             }, {
4100                 # secondary sort in ID as a tie-breaker, since payments created
4101                 # within the same transaction will have identical payment_ts's
4102                 class => 'mp',
4103                 field => 'id'
4104             }
4105         ]
4106     };
4107
4108     for (qw/order_by limit offset/) {
4109         $query->{$_} = $filters->{$_} if defined $filters->{$_};
4110     }
4111
4112     if(defined $filters->{where}) {
4113         foreach (keys %{$filters->{where}}) {
4114             # don't allow the caller to expand the result set to other users
4115             $query->{where}->{$_} = $filters->{where}->{$_} unless $_ eq 'xact';
4116         }
4117     }
4118
4119     my $payment_ids = $e->json_query($query);
4120     for my $pid (@$payment_ids) {
4121         my $pay = $e->retrieve_money_payment([
4122             $pid->{id},
4123             {   flesh => 6,
4124                 flesh_fields => {
4125                     mp => ['xact'],
4126                     mbt => ['summary', 'circulation', 'grocery'],
4127                     circ => ['target_copy'],
4128                     acp => ['call_number'],
4129                     acn => ['record']
4130                 }
4131             }
4132         ]);
4133
4134         my $resp = {
4135             mp => $pay,
4136             xact_type => $pay->xact->summary->xact_type,
4137             last_billing_type => $pay->xact->summary->last_billing_type,
4138         };
4139
4140         if($pay->xact->summary->xact_type eq 'circulation') {
4141             $resp->{barcode} = $pay->xact->circulation->target_copy->barcode;
4142             $resp->{title} = $U->record_to_mvr($pay->xact->circulation->target_copy->call_number->record)->title;
4143         }
4144
4145         $pay->xact($pay->xact->id); # de-flesh
4146         $conn->respond($resp);
4147     }
4148
4149     return undef;
4150 }
4151
4152
4153
4154 __PACKAGE__->register_method (
4155     method      => 'negative_balance_users',
4156     api_name    => 'open-ils.actor.users.negative_balance',
4157     stream => 1,
4158     signature   => q/
4159         Returns all users that have an overall negative balance
4160         @param auth Authentication token
4161         @param org_id The context org unit as an ID or list of IDs.  This will be the home
4162         library of the user.  If no org_unit is specified, no org unit filter is applied
4163     /
4164 );
4165
4166 sub negative_balance_users {
4167     my($self, $conn, $auth, $org_id) = @_;
4168
4169     my $e = new_editor(authtoken => $auth);
4170     return $e->die_event unless $e->checkauth;
4171     return $e->die_event unless $e->allowed('VIEW_USER', $org_id);
4172
4173     my $query = {
4174         select => {
4175             mous => ['usr', 'balance_owed'],
4176             au => ['home_ou'],
4177             mbts => [
4178                 {column => 'last_billing_ts', transform => 'max', aggregate => 1},
4179                 {column => 'last_payment_ts', transform => 'max', aggregate => 1},
4180             ]
4181         },
4182         from => {
4183             mous => {
4184                 au => {
4185                     fkey => 'usr',
4186                     field => 'id',
4187                     join => {
4188                         mbts => {
4189                             key => 'id',
4190                             field => 'usr'
4191                         }
4192                     }
4193                 }
4194             }
4195         },
4196         where => {'+mous' => {balance_owed => {'<' => 0}}}
4197     };
4198
4199     $query->{from}->{mous}->{au}->{filter}->{home_ou} = $org_id if $org_id;
4200
4201     my $list = $e->json_query($query, {timeout => 600});
4202
4203     for my $data (@$list) {
4204         $conn->respond({
4205             usr => $e->retrieve_actor_user([$data->{usr}, {flesh => 1, flesh_fields => {au => ['card']}}]),
4206             balance_owed => $data->{balance_owed},
4207             last_billing_activity => max($data->{last_billing_ts}, $data->{last_payment_ts})
4208         });
4209     }
4210
4211     return undef;
4212 }
4213
4214 __PACKAGE__->register_method(
4215     method  => "request_password_reset",
4216     api_name    => "open-ils.actor.patron.password_reset.request",
4217     signature   => {
4218         desc => "Generates a UUID token usable with the open-ils.actor.patron.password_reset.commit " .
4219                 "method for changing a user's password.  The UUID token is distributed via A/T "      .
4220                 "templates (i.e. email to the user).",
4221         params => [
4222             { desc => 'user_id_type', type => 'string' },
4223             { desc => 'user_id', type => 'string' },
4224             { desc => 'optional (based on library setting) matching email address for authorizing request', type => 'string' },
4225         ],
4226         return => {desc => '1 on success, Event on error'}
4227     }
4228 );
4229 sub request_password_reset {
4230     my($self, $conn, $user_id_type, $user_id, $email) = @_;
4231
4232     # Check to see if password reset requests are already being throttled:
4233     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
4234
4235     my $e = new_editor(xact => 1);
4236     my $user;
4237
4238     # Get the user, if any, depending on the input value
4239     if ($user_id_type eq 'username') {
4240         $user = $e->search_actor_user({usrname => $user_id})->[0];
4241         if (!$user) {
4242             $e->die_event;
4243             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
4244         }
4245     } elsif ($user_id_type eq 'barcode') {
4246         my $card = $e->search_actor_card([
4247             {barcode => $user_id},
4248             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
4249         if (!$card) {
4250             $e->die_event;
4251             return OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
4252         }
4253         $user = $card->usr;
4254     }
4255
4256     # If the user doesn't have an email address, we can't help them
4257     if (!$user->email) {
4258         $e->die_event;
4259         return OpenILS::Event->new('PATRON_NO_EMAIL_ADDRESS');
4260     }
4261
4262     my $email_must_match = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_requires_matching_email');
4263     if ($email_must_match) {
4264         if (lc($user->email) ne lc($email)) {
4265             return OpenILS::Event->new('EMAIL_VERIFICATION_FAILED');
4266         }
4267     }
4268
4269     _reset_password_request($conn, $e, $user);
4270 }
4271
4272 # Once we have the user, we can issue the password reset request
4273 # XXX Add a wrapper method that accepts barcode + email input
4274 sub _reset_password_request {
4275     my ($conn, $e, $user) = @_;
4276
4277     # 1. Get throttle threshold and time-to-live from OU_settings
4278     my $aupr_throttle = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_throttle') || 1000;
4279     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4280
4281     my $threshold_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl)->iso8601();
4282
4283     # 2. Get time of last request and number of active requests (num_active)
4284     my $active_requests = $e->json_query({
4285         from => 'aupr',
4286         select => {
4287             aupr => [
4288                 {
4289                     column => 'uuid',
4290                     transform => 'COUNT'
4291                 },
4292                 {
4293                     column => 'request_time',
4294                     transform => 'MAX'
4295                 }
4296             ]
4297         },
4298         where => {
4299             has_been_reset => { '=' => 'f' },
4300             request_time => { '>' => $threshold_time }
4301         }
4302     });
4303
4304     # Guard against no active requests
4305     if ($active_requests->[0]->{'request_time'}) {
4306         my $last_request = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($active_requests->[0]->{'request_time'}));
4307         my $now = DateTime::Format::ISO8601->new();
4308
4309         # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute)
4310         if (($active_requests->[0]->{'usr'} > $aupr_throttle) &&
4311             ($last_request->add_duration('1 minute') > $now)) {
4312             $cache->put_cache('open-ils.actor.password.throttle', DateTime::Format::ISO8601->new(), 60);
4313             $e->die_event;
4314             return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4315         }
4316     }
4317
4318     # TODO Check to see if the user is in a password-reset-restricted group
4319
4320     # Otherwise, go ahead and try to get the user.
4321
4322     # Check the number of active requests for this user
4323     $active_requests = $e->json_query({
4324         from => 'aupr',
4325         select => {
4326             aupr => [
4327                 {
4328                     column => 'usr',
4329                     transform => 'COUNT'
4330                 }
4331             ]
4332         },
4333         where => {
4334             usr => { '=' => $user->id },
4335             has_been_reset => { '=' => 'f' },
4336             request_time => { '>' => $threshold_time }
4337         }
4338     });
4339
4340     $logger->info("User " . $user->id . " has " . $active_requests->[0]->{'usr'} . " active password reset requests.");
4341
4342     # if less than or equal to per-user threshold, proceed; otherwise, return event
4343     my $aupr_per_user_limit = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_per_user_limit') || 3;
4344     if ($active_requests->[0]->{'usr'} > $aupr_per_user_limit) {
4345         $e->die_event;
4346         return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4347     }
4348
4349     # Create the aupr object and insert into the database
4350     my $reset_request = Fieldmapper::actor::usr_password_reset->new;
4351     my $uuid = create_uuid_as_string(UUID_V4);
4352     $reset_request->uuid($uuid);
4353     $reset_request->usr($user->id);
4354
4355     my $aupr = $e->create_actor_usr_password_reset($reset_request) or return $e->die_event;
4356     $e->commit;
4357
4358     # Create an event to notify user of the URL to reset their password
4359
4360     # Can we stuff this in the user_data param for trigger autocreate?
4361     my $hostname = $U->ou_ancestor_setting_value($user->home_ou, 'lib.hostname') || 'localhost';
4362
4363     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
4364     $ses->request('open-ils.trigger.event.autocreate', 'password.reset_request', $aupr, $user->home_ou);
4365
4366     # Trunk only
4367     # $U->create_trigger_event('password.reset_request', $aupr, $user->home_ou);
4368
4369     return 1;
4370 }
4371
4372 __PACKAGE__->register_method(
4373     method  => "commit_password_reset",
4374     api_name    => "open-ils.actor.patron.password_reset.commit",
4375     signature   => {
4376         desc => "Checks a UUID token generated by the open-ils.actor.patron.password_reset.request method for " .
4377                 "validity, and if valid, uses it as authorization for changing the associated user's password " .
4378                 "with the supplied password.",
4379         params => [
4380             { desc => 'uuid', type => 'string' },
4381             { desc => 'password', type => 'string' },
4382         ],
4383         return => {desc => '1 on success, Event on error'}
4384     }
4385 );
4386 sub commit_password_reset {
4387     my($self, $conn, $uuid, $password) = @_;
4388
4389     # Check to see if password reset requests are already being throttled:
4390     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
4391     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
4392     my $throttle = $cache->get_cache('open-ils.actor.password.throttle') || undef;
4393     if ($throttle) {
4394         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4395     }
4396
4397     my $e = new_editor(xact => 1);
4398
4399     my $aupr = $e->search_actor_usr_password_reset({
4400         uuid => $uuid,
4401         has_been_reset => 0
4402     });
4403
4404     if (!$aupr->[0]) {
4405         $e->die_event;
4406         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4407     }
4408     my $user_id = $aupr->[0]->usr;
4409     my $user = $e->retrieve_actor_user($user_id);
4410
4411     # Ensure we're still within the TTL for the request
4412     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4413     my $threshold = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($aupr->[0]->request_time))->add(seconds => $aupr_ttl);
4414     if ($threshold < DateTime->now(time_zone => 'local')) {
4415         $e->die_event;
4416         $logger->info("Password reset request needed to be submitted before $threshold");
4417         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4418     }
4419
4420     # Check complexity of password against OU-defined regex
4421     my $pw_regex = $U->ou_ancestor_setting_value($user->home_ou, 'global.password_regex');
4422
4423     my $is_strong = 0;
4424     if ($pw_regex) {
4425         # Calling JSON2perl on the $pw_regex causes failure, even before the fancy Unicode regex
4426         # ($pw_regex = OpenSRF::Utils::JSON->JSON2perl($pw_regex)) =~ s/\\u([0-9a-fA-F]{4})/\\x{$1}/gs;
4427         $is_strong = check_password_strength_custom($password, $pw_regex);
4428     } else {
4429         $is_strong = check_password_strength_default($password);
4430     }
4431
4432     if (!$is_strong) {
4433         $e->die_event;
4434         return OpenILS::Event->new('PATRON_PASSWORD_WAS_NOT_STRONG');
4435     }
4436
4437     # All is well; update the password
4438     modify_migrated_user_password($e, $user->id, $password);
4439
4440     # And flag that this password reset request has been honoured
4441     $aupr->[0]->has_been_reset('t');
4442     $e->update_actor_usr_password_reset($aupr->[0]);
4443     $e->commit;
4444
4445     return 1;
4446 }
4447
4448 sub check_password_strength_default {
4449     my $password = shift;
4450     # Use the default set of checks
4451     if ( (length($password) < 7) or
4452             ($password !~ m/.*\d+.*/) or
4453             ($password !~ m/.*[A-Za-z]+.*/)
4454     ) {
4455         return 0;
4456     }
4457     return 1;
4458 }
4459
4460 sub check_password_strength_custom {
4461     my ($password, $pw_regex) = @_;
4462
4463     $pw_regex = qr/$pw_regex/;
4464     if ($password !~  /$pw_regex/) {
4465         return 0;
4466     }
4467     return 1;
4468 }
4469
4470 __PACKAGE__->register_method(
4471     method    => "fire_test_notification",
4472     api_name  => "open-ils.actor.event.test_notification"
4473 );
4474
4475 sub fire_test_notification {
4476     my($self, $conn, $auth, $args) = @_;
4477     my $e = new_editor(authtoken => $auth);
4478     return $e->event unless $e->checkauth;
4479     if ($e->requestor->id != $$args{target}) {
4480         my $home_ou = $e->retrieve_actor_user($$args{target})->home_ou;
4481         return $e->die_event unless $home_ou && $e->allowed('VIEW_USER', $home_ou);
4482     }
4483
4484     my $event_hook = $$args{hook} or return $e->event;
4485     return $e->event unless ($event_hook eq 'au.email.test' or $event_hook eq 'au.sms_text.test');
4486
4487     my $usr = $e->retrieve_actor_user($$args{target});
4488     return $e->event unless $usr;
4489
4490     return $U->fire_object_event(undef, $event_hook, $usr, $e->requestor->ws_ou);
4491 }
4492
4493
4494 __PACKAGE__->register_method(
4495     method    => "event_def_opt_in_settings",
4496     api_name  => "open-ils.actor.event_def.opt_in.settings",
4497     stream => 1,
4498     signature => {
4499         desc   => 'Streams the set of "cust" objects that are used as opt-in settings for event definitions',
4500         params => [
4501             { desc => 'Authentication token',  type => 'string'},
4502             {
4503                 desc => 'Org Unit ID.  (optional).  If no org ID is present, the home_ou of the requesting user is used',
4504                 type => 'number'
4505             },
4506         ],
4507         return => {
4508             desc => q/set of "cust" objects that are used as opt-in settings for event definitions at the specified org unit/,
4509             type => 'object',
4510             class => 'cust'
4511         }
4512     }
4513 );
4514
4515 sub event_def_opt_in_settings {
4516     my($self, $conn, $auth, $org_id) = @_;
4517     my $e = new_editor(authtoken => $auth);
4518     return $e->event unless $e->checkauth;
4519
4520     if(defined $org_id and $org_id != $e->requestor->home_ou) {
4521         return $e->event unless
4522             $e->allowed(['VIEW_USER_SETTING_TYPE', 'ADMIN_USER_SETTING_TYPE'], $org_id);
4523     } else {
4524         $org_id = $e->requestor->home_ou;
4525     }
4526
4527     # find all config.user_setting_type's related to event_defs for the requested org unit
4528     my $types = $e->json_query({
4529         select => {cust => ['name']},
4530         from => {atevdef => 'cust'},
4531         where => {
4532             '+atevdef' => {
4533                 owner => $U->get_org_ancestors($org_id), # context org plus parents
4534                 active => 't'
4535             }
4536         }
4537     });
4538
4539     if(@$types) {
4540         $conn->respond($_) for
4541             @{$e->search_config_usr_setting_type({name => [map {$_->{name}} @$types]})};
4542     }
4543
4544     return undef;
4545 }
4546
4547
4548 __PACKAGE__->register_method(
4549     method    => "user_circ_history",
4550     api_name  => "open-ils.actor.history.circ",
4551     stream => 1,
4552     authoritative => 1,
4553     signature => {
4554         desc   => 'Returns user circ history objects for the calling user',
4555         params => [
4556             { desc => 'Authentication token',  type => 'string'},
4557             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4558         ],
4559         return => {
4560             desc => q/Stream of 'auch' circ history objects/,
4561             type => 'object',
4562         }
4563     }
4564 );
4565
4566 __PACKAGE__->register_method(
4567     method    => "user_circ_history",
4568     api_name  => "open-ils.actor.history.circ.clear",
4569     stream => 1,
4570     signature => {
4571         desc   => 'Delete all user circ history entries for the calling user',
4572         params => [
4573             { desc => 'Authentication token',  type => 'string'},
4574             { desc => "Options hash. 'circ_ids' is an arrayref of circulation IDs to delete", type => 'object' },
4575         ],
4576         return => {
4577             desc => q/1 on success, event on error/,
4578             type => 'object',
4579         }
4580     }
4581 );
4582
4583 __PACKAGE__->register_method(
4584     method    => "user_circ_history",
4585     api_name  => "open-ils.actor.history.circ.print",
4586     stream => 1,
4587     signature => {
4588         desc   => q/Returns printable output for the caller's circ history objects/,
4589         params => [
4590             { desc => 'Authentication token',  type => 'string'},
4591             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4592         ],
4593         return => {
4594             desc => q/An action_trigger.event object or error event./,
4595             type => 'object',
4596         }
4597     }
4598 );
4599
4600 __PACKAGE__->register_method(
4601     method    => "user_circ_history",
4602     api_name  => "open-ils.actor.history.circ.email",
4603     stream => 1,
4604     signature => {
4605         desc   => q/Emails the caller's circ history/,
4606         params => [
4607             { desc => 'Authentication token',  type => 'string'},
4608             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4609             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4610         ],
4611         return => {
4612             desc => q/undef, or event on error/
4613         }
4614     }
4615 );
4616
4617 sub user_circ_history {
4618     my ($self, $conn, $auth, $options) = @_;
4619     $options ||= {};
4620
4621     my $for_print = ($self->api_name =~ /print/);
4622     my $for_email = ($self->api_name =~ /email/);
4623     my $for_clear = ($self->api_name =~ /clear/);
4624
4625     # No perm check is performed.  Caller may only access his/her own
4626     # circ history entries.
4627     my $e = new_editor(authtoken => $auth);
4628     return $e->event unless $e->checkauth;
4629
4630     my %limits = ();
4631     if (!$for_clear) { # clear deletes all
4632         $limits{offset} = $options->{offset} if defined $options->{offset};
4633         $limits{limit} = $options->{limit} if defined $options->{limit};
4634     }
4635
4636     my %circ_id_filter = $options->{circ_ids} ?
4637         (id => $options->{circ_ids}) : ();
4638
4639     my $circs = $e->search_action_user_circ_history([
4640         {   usr => $e->requestor->id,
4641             %circ_id_filter
4642         },
4643         {   # order newest to oldest by default
4644             order_by => {auch => 'xact_start DESC'},
4645             %limits
4646         },
4647         {substream => 1} # could be a large list
4648     ]);
4649
4650     if ($for_print) {
4651         return $U->fire_object_event(undef,
4652             'circ.format.history.print', $circs, $e->requestor->home_ou);
4653     }
4654
4655     $e->xact_begin if $for_clear;
4656     $conn->respond_complete(1) if $for_email;  # no sense in waiting
4657
4658     for my $circ (@$circs) {
4659
4660         if ($for_email) {
4661             # events will be fired from action_trigger_runner
4662             $U->create_events_for_hook('circ.format.history.email',
4663                 $circ, $e->editor->home_ou, undef, undef, 1);
4664
4665         } elsif ($for_clear) {
4666
4667             $e->delete_action_user_circ_history($circ)
4668                 or return $e->die_event;
4669
4670         } else {
4671             $conn->respond($circ);
4672         }
4673     }
4674
4675     if ($for_clear) {
4676         $e->commit;
4677         return 1;
4678     }
4679
4680     return undef;
4681 }
4682
4683
4684 __PACKAGE__->register_method(
4685     method    => "user_visible_holds",
4686     api_name  => "open-ils.actor.history.hold.visible",
4687     stream => 1,
4688     signature => {
4689         desc   => 'Returns the set of opt-in visible holds',
4690         params => [
4691             { desc => 'Authentication token',  type => 'string'},
4692             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4693             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4694         ],
4695         return => {
4696             desc => q/An object with 1 field: "hold"/,
4697             type => 'object',
4698         }
4699     }
4700 );
4701
4702 __PACKAGE__->register_method(
4703     method    => "user_visible_holds",
4704     api_name  => "open-ils.actor.history.hold.visible.print",
4705     stream => 1,
4706     signature => {
4707         desc   => 'Returns printable output for 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 action_trigger.event object or error event./,
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.email",
4723     stream => 1,
4724     signature => {
4725         desc   => 'Emails the set of opt-in visible holds to the requestor',
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/undef, or event on error/
4733         }
4734     }
4735 );
4736
4737 sub user_visible_holds {
4738     my($self, $conn, $auth, $user_id, $options) = @_;
4739
4740     my $is_hold = 1;
4741     my $for_print = ($self->api_name =~ /print/);
4742     my $for_email = ($self->api_name =~ /email/);
4743     my $e = new_editor(authtoken => $auth);
4744     return $e->event unless $e->checkauth;
4745
4746     $user_id ||= $e->requestor->id;
4747     $options ||= {};
4748     $options->{limit} ||= 50;
4749     $options->{offset} ||= 0;
4750
4751     if($user_id != $e->requestor->id) {
4752         my $perm = ($is_hold) ? 'VIEW_HOLD' : 'VIEW_CIRCULATIONS';
4753         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4754         return $e->event unless $e->allowed($perm, $user->home_ou);
4755     }
4756
4757     my $db_func = ($is_hold) ? 'action.usr_visible_holds' : 'action.usr_visible_circs';
4758
4759     my $data = $e->json_query({
4760         from => [$db_func, $user_id],
4761         limit => $$options{limit},
4762         offset => $$options{offset}
4763
4764         # TODO: I only want IDs. code below didn't get me there
4765         # {"select":{"au":[{"column":"id", "result_field":"id",
4766         # "transform":"action.usr_visible_circs"}]}, "where":{"id":10}, "from":"au"}
4767     },{
4768         substream => 1
4769     });
4770
4771     return undef unless @$data;
4772
4773     if ($for_print) {
4774
4775         # collect the batch of objects
4776
4777         if($is_hold) {
4778
4779             my $hold_list = $e->search_action_hold_request({id => [map { $_->{id} } @$data]});
4780             return $U->fire_object_event(undef, 'ahr.format.history.print', $hold_list, $$hold_list[0]->request_lib);
4781
4782         } else {
4783
4784             my $circ_list = $e->search_action_circulation({id => [map { $_->{id} } @$data]});
4785             return $U->fire_object_event(undef, 'circ.format.history.print', $circ_list, $$circ_list[0]->circ_lib);
4786         }
4787
4788     } elsif ($for_email) {
4789
4790         $conn->respond_complete(1) if $for_email;  # no sense in waiting
4791
4792         foreach (@$data) {
4793
4794             my $id = $_->{id};
4795
4796             if($is_hold) {
4797
4798                 my $hold = $e->retrieve_action_hold_request($id);
4799                 $U->create_events_for_hook('ahr.format.history.email', $hold, $hold->request_lib, undef, undef, 1);
4800                 # events will be fired from action_trigger_runner
4801
4802             } else {
4803
4804                 my $circ = $e->retrieve_action_circulation($id);
4805                 $U->create_events_for_hook('circ.format.history.email', $circ, $circ->circ_lib, undef, undef, 1);
4806                 # events will be fired from action_trigger_runner
4807             }
4808         }
4809
4810     } else { # just give me the data please
4811
4812         foreach (@$data) {
4813
4814             my $id = $_->{id};
4815
4816             if($is_hold) {
4817
4818                 my $hold = $e->retrieve_action_hold_request($id);
4819                 $conn->respond({hold => $hold});
4820
4821             } else {
4822
4823                 my $circ = $e->retrieve_action_circulation($id);
4824                 $conn->respond({
4825                     circ => $circ,
4826                     summary => $U->create_circ_chain_summary($e, $id)
4827                 });
4828             }
4829         }
4830     }
4831
4832     return undef;
4833 }
4834
4835 __PACKAGE__->register_method(
4836     method     => "user_saved_search_cud",
4837     api_name   => "open-ils.actor.user.saved_search.cud",
4838     stream     => 1,
4839     signature  => {
4840         desc   => 'Create/Update/Delete Access to user saved searches',
4841         params => [
4842             { desc => 'Authentication token', type => 'string' },
4843             { desc => 'Saved Search Object', type => 'object', class => 'auss' }
4844         ],
4845         return => {
4846             desc   => q/The retrieved or updated saved search object, or id of a deleted object; Event on error/,
4847             class  => 'auss'
4848         }
4849     }
4850 );
4851
4852 __PACKAGE__->register_method(
4853     method     => "user_saved_search_cud",
4854     api_name   => "open-ils.actor.user.saved_search.retrieve",
4855     stream     => 1,
4856     signature  => {
4857         desc   => 'Retrieve a saved search object',
4858         params => [
4859             { desc => 'Authentication token', type => 'string' },
4860             { desc => 'Saved Search ID', type => 'number' }
4861         ],
4862         return => {
4863             desc   => q/The saved search object, Event on error/,
4864             class  => 'auss'
4865         }
4866     }
4867 );
4868
4869 sub user_saved_search_cud {
4870     my( $self, $client, $auth, $search ) = @_;
4871     my $e = new_editor( authtoken=>$auth );
4872     return $e->die_event unless $e->checkauth;
4873
4874     my $o_search;      # prior version of the object, if any
4875     my $res;           # to be returned
4876
4877     # branch on the operation type
4878
4879     if( $self->api_name =~ /retrieve/ ) {                    # Retrieve
4880
4881         # Get the old version, to check ownership
4882         $o_search = $e->retrieve_actor_usr_saved_search( $search )
4883             or return $e->die_event;
4884
4885         # You can't read somebody else's search
4886         return OpenILS::Event->new('BAD_PARAMS')
4887             unless $o_search->owner == $e->requestor->id;
4888
4889         $res = $o_search;
4890
4891     } else {
4892
4893         $e->xact_begin;               # start an editor transaction
4894
4895         if( $search->isnew ) {                               # Create
4896
4897             # You can't create a search for somebody else
4898             return OpenILS::Event->new('BAD_PARAMS')
4899                 unless $search->owner == $e->requestor->id;
4900
4901             $e->create_actor_usr_saved_search( $search )
4902                 or return $e->die_event;
4903
4904             $res = $search->id;
4905
4906         } elsif( $search->ischanged ) {                      # Update
4907
4908             # You can't change ownership of a search
4909             return OpenILS::Event->new('BAD_PARAMS')
4910                 unless $search->owner == $e->requestor->id;
4911
4912             # Get the old version, to check ownership
4913             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4914                 or return $e->die_event;
4915
4916             # You can't update somebody else's search
4917             return OpenILS::Event->new('BAD_PARAMS')
4918                 unless $o_search->owner == $e->requestor->id;
4919
4920             # Do the update
4921             $e->update_actor_usr_saved_search( $search )
4922                 or return $e->die_event;
4923
4924             $res = $search;
4925
4926         } elsif( $search->isdeleted ) {                      # Delete
4927
4928             # Get the old version, to check ownership
4929             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4930                 or return $e->die_event;
4931
4932             # You can't delete somebody else's search
4933             return OpenILS::Event->new('BAD_PARAMS')
4934                 unless $o_search->owner == $e->requestor->id;
4935
4936             # Do the delete
4937             $e->delete_actor_usr_saved_search( $o_search )
4938                 or return $e->die_event;
4939
4940             $res = $search->id;
4941         }
4942
4943         $e->commit;
4944     }
4945
4946     return $res;
4947 }
4948
4949 __PACKAGE__->register_method(
4950     method   => "get_barcodes",
4951     api_name => "open-ils.actor.get_barcodes"
4952 );
4953
4954 sub get_barcodes {
4955     my( $self, $client, $auth, $org_id, $context, $barcode ) = @_;
4956     my $e = new_editor(authtoken => $auth);
4957     return $e->event unless $e->checkauth;
4958     return $e->event unless $e->allowed('STAFF_LOGIN', $org_id);
4959
4960     my $db_result = $e->json_query(
4961         {   from => [
4962                 'evergreen.get_barcodes',
4963                 $org_id, $context, $barcode,
4964             ]
4965         }
4966     );
4967     if($context =~ /actor/) {
4968         my $filter_result = ();
4969         my $patron;
4970         foreach my $result (@$db_result) {
4971             if($result->{type} eq 'actor') {
4972                 if($e->requestor->id != $result->{id}) {
4973                     $patron = $e->retrieve_actor_user($result->{id});
4974                     if(!$patron) {
4975                         push(@$filter_result, $e->event);
4976                         next;
4977                     }
4978                     if($e->allowed('VIEW_USER', $patron->home_ou)) {
4979                         push(@$filter_result, $result);
4980                     }
4981                     else {
4982                         push(@$filter_result, $e->event);
4983                     }
4984                 }
4985                 else {
4986                     push(@$filter_result, $result);
4987                 }
4988             }
4989             else {
4990                 push(@$filter_result, $result);
4991             }
4992         }
4993         return $filter_result;
4994     }
4995     else {
4996         return $db_result;
4997     }
4998 }
4999 __PACKAGE__->register_method(
5000     method   => 'address_alert_test',
5001     api_name => 'open-ils.actor.address_alert.test',
5002     signature => {
5003         desc => "Tests a set of address fields to determine if they match with an address_alert",
5004         params => [
5005             {desc => 'Authentication token', type => 'string'},
5006             {desc => 'Org Unit',             type => 'number'},
5007             {desc => 'Fields',               type => 'hash'},
5008         ],
5009         return => {desc => 'List of matching address_alerts'}
5010     }
5011 );
5012
5013 sub address_alert_test {
5014     my ($self, $client, $auth, $org_unit, $fields) = @_;
5015     return [] unless $fields and grep {$_} values %$fields;
5016
5017     my $e = new_editor(authtoken => $auth);
5018     return $e->event unless $e->checkauth;
5019     return $e->event unless $e->allowed('CREATE_USER', $org_unit);
5020     $org_unit ||= $e->requestor->ws_ou;
5021
5022     my $alerts = $e->json_query({
5023         from => [
5024             'actor.address_alert_matches',
5025             $org_unit,
5026             $$fields{street1},
5027             $$fields{street2},
5028             $$fields{city},
5029             $$fields{county},
5030             $$fields{state},
5031             $$fields{country},
5032             $$fields{post_code},
5033             $$fields{mailing_address},
5034             $$fields{billing_address}
5035         ]
5036     });
5037
5038     # map the json_query hashes to real objects
5039     return [
5040         map {$e->retrieve_actor_address_alert($_)}
5041             (map {$_->{id}} @$alerts)
5042     ];
5043 }
5044
5045 __PACKAGE__->register_method(
5046     method   => "mark_users_contact_invalid",
5047     api_name => "open-ils.actor.invalidate.email",
5048     signature => {
5049         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",
5050         params => [
5051             {desc => "Authentication token", type => "string"},
5052             {desc => "Patron ID (optional if Email address specified)", type => "number"},
5053             {desc => "Additional note text (optional)", type => "string"},
5054             {desc => "penalty org unit ID (optional)", type => "number"},
5055             {desc => "Email address (optional)", type => "string"}
5056         ],
5057         return => {desc => "Event describing success or failure", type => "object"}
5058     }
5059 );
5060
5061 __PACKAGE__->register_method(
5062     method   => "mark_users_contact_invalid",
5063     api_name => "open-ils.actor.invalidate.day_phone",
5064     signature => {
5065         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",
5066         params => [
5067             {desc => "Authentication token", type => "string"},
5068             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
5069             {desc => "Additional note text (optional)", type => "string"},
5070             {desc => "penalty org unit ID (optional)", type => "number"},
5071             {desc => "Phone Number (optional)", type => "string"}
5072         ],
5073         return => {desc => "Event describing success or failure", type => "object"}
5074     }
5075 );
5076
5077 __PACKAGE__->register_method(
5078     method   => "mark_users_contact_invalid",
5079     api_name => "open-ils.actor.invalidate.evening_phone",
5080     signature => {
5081         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",
5082         params => [
5083             {desc => "Authentication token", type => "string"},
5084             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
5085             {desc => "Additional note text (optional)", type => "string"},
5086             {desc => "penalty org unit ID (optional)", type => "number"},
5087             {desc => "Phone Number (optional)", type => "string"}
5088         ],
5089         return => {desc => "Event describing success or failure", type => "object"}
5090     }
5091 );
5092
5093 __PACKAGE__->register_method(
5094     method   => "mark_users_contact_invalid",
5095     api_name => "open-ils.actor.invalidate.other_phone",
5096     signature => {
5097         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",
5098         params => [
5099             {desc => "Authentication token", type => "string"},
5100             {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
5101             {desc => "Additional note text (optional)", type => "string"},
5102             {desc => "penalty org unit ID (optional, default to top of org tree)",
5103                 type => "number"},
5104             {desc => "Phone Number (optional)", type => "string"}
5105         ],
5106         return => {desc => "Event describing success or failure", type => "object"}
5107     }
5108 );
5109
5110 sub mark_users_contact_invalid {
5111     my ($self, $conn, $auth, $patron_id, $addl_note, $penalty_ou, $contact) = @_;
5112
5113     # This method invalidates an email address or a phone_number which
5114     # removes the bad email address or phone number, copying its contents
5115     # to a patron note, and institutes a standing penalty for "bad email"
5116     # or "bad phone number" which is cleared when the user is saved or
5117     # optionally only when the user is saved with an email address or
5118     # phone number (or staff manually delete the penalty).
5119
5120     my $contact_type = ($self->api_name =~ /invalidate.(\w+)(\.|$)/)[0];
5121
5122     my $e = new_editor(authtoken => $auth, xact => 1);
5123     return $e->die_event unless $e->checkauth;
5124     
5125     my $howfind = {};
5126     if (defined $patron_id && $patron_id ne "") {
5127         $howfind = {usr => $patron_id};
5128     } elsif (defined $contact && $contact ne "") {
5129         $howfind = {$contact_type => $contact};
5130     } else {
5131         # Error out if no patron id set or no contact is set.
5132         return OpenILS::Event->new('BAD_PARAMS');
5133     }
5134  
5135     return OpenILS::Utils::BadContact->mark_users_contact_invalid(
5136         $e, $contact_type, $howfind,
5137         $addl_note, $penalty_ou, $e->requestor->id
5138     );
5139 }
5140
5141 # Putting the following method in open-ils.actor is a bad fit, except in that
5142 # it serves an interface that lives under 'actor' in the templates directory,
5143 # and in that there's nowhere else obvious to put it (open-ils.trigger is
5144 # private).
5145 __PACKAGE__->register_method(
5146     api_name => "open-ils.actor.action_trigger.reactors.all_in_use",
5147     method   => "get_all_at_reactors_in_use",
5148     api_level=> 1,
5149     argc     => 1,
5150     signature=> {
5151         params => [
5152             { name => 'authtoken', type => 'string' }
5153         ],
5154         return => {
5155             desc => 'list of reactor names', type => 'array'
5156         }
5157     }
5158 );
5159
5160 sub get_all_at_reactors_in_use {
5161     my ($self, $conn, $auth) = @_;
5162
5163     my $e = new_editor(authtoken => $auth);
5164     $e->checkauth or return $e->die_event;
5165     return $e->die_event unless $e->allowed('VIEW_TRIGGER_EVENT_DEF');
5166
5167     my $reactors = $e->json_query({
5168         select => {
5169             atevdef => [{column => "reactor", transform => "distinct"}]
5170         },
5171         from => {atevdef => {}}
5172     });
5173
5174     return $e->die_event unless ref $reactors eq "ARRAY";
5175     $e->disconnect;
5176
5177     return [ map { $_->{reactor} } @$reactors ];
5178 }
5179
5180 __PACKAGE__->register_method(
5181     method   => "filter_group_entry_crud",
5182     api_name => "open-ils.actor.filter_group_entry.crud",
5183     signature => {
5184         desc => q/
5185             Provides CRUD access to filter group entry objects.  These are not full accessible
5186             via PCRUD, since they requre "asq" objects for storing the query, and "asq" objects
5187             are not accessible via PCRUD (because they have no fields against which to link perms)
5188             /,
5189         params => [
5190             {desc => "Authentication token", type => "string"},
5191             {desc => "Entry ID / Entry Object", type => "number"},
5192             {desc => "Additional note text (optional)", type => "string"},
5193             {desc => "penalty org unit ID (optional, default to top of org tree)",
5194                 type => "number"}
5195         ],
5196         return => {
5197             desc => "Entry fleshed with query on Create, Retrieve, and Uupdate.  1 on Delete",
5198             type => "object"
5199         }
5200     }
5201 );
5202
5203 sub filter_group_entry_crud {
5204     my ($self, $conn, $auth, $arg) = @_;
5205
5206     return OpenILS::Event->new('BAD_PARAMS') unless $arg;
5207     my $e = new_editor(authtoken => $auth, xact => 1);
5208     return $e->die_event unless $e->checkauth;
5209
5210     if (ref $arg) {
5211
5212         if ($arg->isnew) {
5213
5214             my $grp = $e->retrieve_actor_search_filter_group($arg->grp)
5215                 or return $e->die_event;
5216
5217             return $e->die_event unless $e->allowed(
5218                 'ADMIN_SEARCH_FILTER_GROUP', $grp->owner);
5219
5220             my $query = $arg->query;
5221             $query = $e->create_actor_search_query($query) or return $e->die_event;
5222             $arg->query($query->id);
5223             my $entry = $e->create_actor_search_filter_group_entry($arg) or return $e->die_event;
5224             $entry->query($query);
5225
5226             $e->commit;
5227             return $entry;
5228
5229         } elsif ($arg->ischanged) {
5230
5231             my $entry = $e->retrieve_actor_search_filter_group_entry([
5232                 $arg->id, {
5233                     flesh => 1,
5234                     flesh_fields => {asfge => ['grp']}
5235                 }
5236             ]) or return $e->die_event;
5237
5238             return $e->die_event unless $e->allowed(
5239                 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
5240
5241             my $query = $e->update_actor_search_query($arg->query) or return $e->die_event;
5242             $arg->query($arg->query->id);
5243             $e->update_actor_search_filter_group_entry($arg) or return $e->die_event;
5244             $arg->query($query);
5245
5246             $e->commit;
5247             return $arg;
5248
5249         } elsif ($arg->isdeleted) {
5250
5251             my $entry = $e->retrieve_actor_search_filter_group_entry([
5252                 $arg->id, {
5253                     flesh => 1,
5254                     flesh_fields => {asfge => ['grp', 'query']}
5255                 }
5256             ]) or return $e->die_event;
5257
5258             return $e->die_event unless $e->allowed(
5259                 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
5260
5261             $e->delete_actor_search_filter_group_entry($entry) or return $e->die_event;
5262             $e->delete_actor_search_query($entry->query) or return $e->die_event;
5263
5264             $e->commit;
5265             return 1;
5266
5267         } else {
5268
5269             $e->rollback;
5270             return undef;
5271         }
5272
5273     } else {
5274
5275         my $entry = $e->retrieve_actor_search_filter_group_entry([
5276             $arg, {
5277                 flesh => 1,
5278                 flesh_fields => {asfge => ['grp', 'query']}
5279             }
5280         ]) or return $e->die_event;
5281
5282         return $e->die_event unless $e->allowed(
5283             ['ADMIN_SEARCH_FILTER_GROUP', 'VIEW_SEARCH_FILTER_GROUP'],
5284             $entry->grp->owner);
5285
5286         $e->rollback;
5287         $entry->grp($entry->grp->id); # for consistency
5288         return $entry;
5289     }
5290 }
5291
5292 1;