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