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