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