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