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