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