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