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