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