]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Actor.pm
< is not >; get our time-to-live threshold comparison correct
[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, $user_session, $barcode) = @_;
1027
1028         $logger->debug("Searching for user with barcode $barcode");
1029         my ($user_obj, $evt) = $apputils->checkses($user_session);
1030         return $evt if $evt;
1031
1032         my $card = OpenILS::Application::AppUtils->simple_scalar_request(
1033                         "open-ils.cstore", 
1034                         "open-ils.cstore.direct.actor.card.search.atomic",
1035                         { barcode => $barcode }
1036         );
1037
1038         if(!$card || !$card->[0]) {
1039                 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
1040         }
1041
1042         $card = $card->[0];
1043         my $user = flesh_user($card->usr(), new_editor(requestor => $user_obj));
1044
1045         $evt = $U->check_perms($user_obj->id, $user->home_ou, 'VIEW_USER');
1046         return $evt if $evt;
1047
1048         if(!$user) { return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ); }
1049         return $user;
1050
1051 }
1052
1053
1054
1055 __PACKAGE__->register_method(
1056     method        => "get_user_by_id",
1057     authoritative => 1,
1058     api_name      => "open-ils.actor.user.retrieve",
1059 );
1060
1061 sub get_user_by_id {
1062         my ($self, $client, $auth, $id) = @_;
1063         my $e = new_editor(authtoken=>$auth);
1064         return $e->event unless $e->checkauth;
1065         my $user = $e->retrieve_actor_user($id)
1066                 or return $e->event;
1067         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);       
1068         return $user;
1069 }
1070
1071
1072 __PACKAGE__->register_method(
1073     method   => "get_org_types",
1074     api_name => "open-ils.actor.org_types.retrieve",
1075 );
1076 sub get_org_types {
1077     return $U->get_org_types();
1078 }
1079
1080
1081 __PACKAGE__->register_method(
1082     method   => "get_user_ident_types",
1083     api_name => "open-ils.actor.user.ident_types.retrieve",
1084 );
1085 my $ident_types;
1086 sub get_user_ident_types {
1087         return $ident_types if $ident_types;
1088         return $ident_types = 
1089                 new_editor()->retrieve_all_config_identification_type();
1090 }
1091
1092
1093 __PACKAGE__->register_method(
1094     method   => "get_org_unit",
1095     api_name => "open-ils.actor.org_unit.retrieve",
1096 );
1097
1098 sub get_org_unit {
1099         my( $self, $client, $user_session, $org_id ) = @_;
1100         my $e = new_editor(authtoken => $user_session);
1101         if(!$org_id) {
1102                 return $e->event unless $e->checkauth;
1103                 $org_id = $e->requestor->ws_ou;
1104         }
1105         my $o = $e->retrieve_actor_org_unit($org_id)
1106                 or return $e->event;
1107         return $o;
1108 }
1109
1110 __PACKAGE__->register_method(
1111     method   => "search_org_unit",
1112     api_name => "open-ils.actor.org_unit_list.search",
1113 );
1114
1115 sub search_org_unit {
1116
1117         my( $self, $client, $field, $value ) = @_;
1118
1119         my $list = OpenILS::Application::AppUtils->simple_scalar_request(
1120                 "open-ils.cstore",
1121                 "open-ils.cstore.direct.actor.org_unit.search.atomic", 
1122                 { $field => $value } );
1123
1124         return $list;
1125 }
1126
1127
1128 # build the org tree
1129
1130 __PACKAGE__->register_method(
1131         method  => "get_org_tree",
1132         api_name        => "open-ils.actor.org_tree.retrieve",
1133         argc            => 0, 
1134         note            => "Returns the entire org tree structure",
1135 );
1136
1137 sub get_org_tree {
1138         my $self = shift;
1139         my $client = shift;
1140         return $U->get_org_tree($client->session->session_locale);
1141 }
1142
1143
1144 __PACKAGE__->register_method(
1145         method  => "get_org_descendants",
1146         api_name        => "open-ils.actor.org_tree.descendants.retrieve"
1147 );
1148
1149 # depth is optional.  org_unit is the id
1150 sub get_org_descendants {
1151         my( $self, $client, $org_unit, $depth ) = @_;
1152
1153     if(ref $org_unit eq 'ARRAY') {
1154         $depth ||= [];
1155         my @trees;
1156         for my $i (0..scalar(@$org_unit)-1) {
1157             my $list = $U->simple_scalar_request(
1158                             "open-ils.storage", 
1159                             "open-ils.storage.actor.org_unit.descendants.atomic",
1160                             $org_unit->[$i], $depth->[$i] );
1161             push(@trees, $U->build_org_tree($list));
1162         }
1163         return \@trees;
1164
1165     } else {
1166             my $orglist = $apputils->simple_scalar_request(
1167                             "open-ils.storage", 
1168                             "open-ils.storage.actor.org_unit.descendants.atomic",
1169                             $org_unit, $depth );
1170             return $U->build_org_tree($orglist);
1171     }
1172 }
1173
1174
1175 __PACKAGE__->register_method(
1176         method  => "get_org_ancestors",
1177         api_name        => "open-ils.actor.org_tree.ancestors.retrieve"
1178 );
1179
1180 # depth is optional.  org_unit is the id
1181 sub get_org_ancestors {
1182         my( $self, $client, $org_unit, $depth ) = @_;
1183         my $orglist = $apputils->simple_scalar_request(
1184                         "open-ils.storage", 
1185                         "open-ils.storage.actor.org_unit.ancestors.atomic",
1186                         $org_unit, $depth );
1187         return $U->build_org_tree($orglist);
1188 }
1189
1190
1191 __PACKAGE__->register_method(
1192         method  => "get_standings",
1193         api_name        => "open-ils.actor.standings.retrieve"
1194 );
1195
1196 my $user_standings;
1197 sub get_standings {
1198         return $user_standings if $user_standings;
1199         return $user_standings = 
1200                 $apputils->simple_scalar_request(
1201                         "open-ils.cstore",
1202                         "open-ils.cstore.direct.config.standing.search.atomic",
1203                         { id => { "!=" => undef } }
1204                 );
1205 }
1206
1207
1208 __PACKAGE__->register_method(
1209     method   => "get_my_org_path",
1210     api_name => "open-ils.actor.org_unit.full_path.retrieve"
1211 );
1212
1213 sub get_my_org_path {
1214         my( $self, $client, $auth, $org_id ) = @_;
1215         my $e = new_editor(authtoken=>$auth);
1216         return $e->event unless $e->checkauth;
1217         $org_id = $e->requestor->ws_ou unless defined $org_id;
1218
1219         return $apputils->simple_scalar_request(
1220                 "open-ils.storage",
1221                 "open-ils.storage.actor.org_unit.full_path.atomic",
1222                 $org_id );
1223 }
1224
1225
1226 __PACKAGE__->register_method(
1227     method   => "patron_adv_search",
1228     api_name => "open-ils.actor.patron.search.advanced"
1229 );
1230 sub patron_adv_search {
1231         my( $self, $client, $auth, $search_hash, 
1232         $search_limit, $search_sort, $include_inactive, $search_depth ) = @_;
1233
1234         my $e = new_editor(authtoken=>$auth);
1235         return $e->event unless $e->checkauth;
1236         return $e->event unless $e->allowed('VIEW_USER');
1237         return $U->storagereq(
1238                 "open-ils.storage.actor.user.crazy_search", $search_hash, 
1239             $search_limit, $search_sort, $include_inactive, $e->requestor->ws_ou, $search_depth);
1240 }
1241
1242
1243 __PACKAGE__->register_method(
1244     method    => "update_passwd",
1245     api_name  => "open-ils.actor.user.password.update",
1246     signature => {
1247         desc   => "Update the operator's password", 
1248         params => [
1249             { desc => 'Authentication token', type => 'string' },
1250             { desc => 'New password',         type => 'string' },
1251             { desc => 'Current password',     type => 'string' }
1252         ],
1253         return => {desc => '1 on success, Event on error or incorrect current password'}
1254     }
1255 );
1256
1257 __PACKAGE__->register_method(
1258     method    => "update_passwd",
1259     api_name  => "open-ils.actor.user.username.update",
1260     signature => {
1261         desc   => "Update the operator's username", 
1262         params => [
1263             { desc => 'Authentication token', type => 'string' },
1264             { desc => 'New username',         type => 'string' }
1265         ],
1266         return => {desc => '1 on success, Event on error'}
1267     }
1268 );
1269
1270 __PACKAGE__->register_method(
1271     method    => "update_passwd",
1272     api_name  => "open-ils.actor.user.email.update",
1273     signature => {
1274         desc   => "Update the operator's email address", 
1275         params => [
1276             { desc => 'Authentication token', type => 'string' },
1277             { desc => 'New email address',    type => 'string' }
1278         ],
1279         return => {desc => '1 on success, Event on error'}
1280     }
1281 );
1282
1283 sub update_passwd {
1284     my( $self, $conn, $auth, $new_val, $orig_pw ) = @_;
1285     my $e = new_editor(xact=>1, authtoken=>$auth);
1286     return $e->die_event unless $e->checkauth;
1287
1288     my $db_user = $e->retrieve_actor_user($e->requestor->id)
1289         or return $e->die_event;
1290     my $api = $self->api_name;
1291
1292     if( $api =~ /password/o ) {
1293
1294         # make sure the original password matches the in-database password
1295         return OpenILS::Event->new('INCORRECT_PASSWORD')
1296             if md5_hex($orig_pw) ne $db_user->passwd;
1297         $db_user->passwd($new_val);
1298
1299     } else {
1300
1301         # if we don't clear the password, the user will be updated with
1302         # a hashed version of the hashed version of their password
1303         $db_user->clear_passwd;
1304
1305         if( $api =~ /username/o ) {
1306
1307             # make sure no one else has this username
1308             my $exist = $e->search_actor_user({usrname=>$new_val},{idlist=>1}); 
1309                         return OpenILS::Event->new('USERNAME_EXISTS') if @$exist;
1310             $db_user->usrname($new_val);
1311
1312         } elsif( $api =~ /email/o ) {
1313             $db_user->email($new_val);
1314         }
1315     }
1316
1317     $e->update_actor_user($db_user) or return $e->die_event;
1318     $e->commit;
1319     return 1;
1320 }
1321
1322
1323
1324 __PACKAGE__->register_method(
1325     method   => "check_user_perms",
1326     api_name => "open-ils.actor.user.perm.check",
1327     notes    => <<"     NOTES");
1328         Takes a login session, user id, an org id, and an array of perm type strings.  For each
1329         perm type, if the user does *not* have the given permission it is added
1330         to a list which is returned from the method.  If all permissions
1331         are allowed, an empty list is returned
1332         if the logged in user does not match 'user_id', then the logged in user must
1333         have VIEW_PERMISSION priveleges.
1334         NOTES
1335
1336 sub check_user_perms {
1337         my( $self, $client, $login_session, $user_id, $org_id, $perm_types ) = @_;
1338
1339         my( $staff, $evt ) = $apputils->checkses($login_session);
1340         return $evt if $evt;
1341
1342         if($staff->id ne $user_id) {
1343                 if( $evt = $apputils->check_perms(
1344                         $staff->id, $org_id, 'VIEW_PERMISSION') ) {
1345                         return $evt;
1346                 }
1347         }
1348
1349         my @not_allowed;
1350         for my $perm (@$perm_types) {
1351                 if($apputils->check_perms($user_id, $org_id, $perm)) {
1352                         push @not_allowed, $perm;
1353                 }
1354         }
1355
1356         return \@not_allowed
1357 }
1358
1359 __PACKAGE__->register_method(
1360         method  => "check_user_perms2",
1361         api_name        => "open-ils.actor.user.perm.check.multi_org",
1362         notes           => q/
1363                 Checks the permissions on a list of perms and orgs for a user
1364                 @param authtoken The login session key
1365                 @param user_id The id of the user to check
1366                 @param orgs The array of org ids
1367                 @param perms The array of permission names
1368                 @return An array of  [ orgId, permissionName ] arrays that FAILED the check
1369                 if the logged in user does not match 'user_id', then the logged in user must
1370                 have VIEW_PERMISSION priveleges.
1371         /);
1372
1373 sub check_user_perms2 {
1374         my( $self, $client, $authtoken, $user_id, $orgs, $perms ) = @_;
1375
1376         my( $staff, $target, $evt ) = $apputils->checkses_requestor(
1377                 $authtoken, $user_id, 'VIEW_PERMISSION' );
1378         return $evt if $evt;
1379
1380         my @not_allowed;
1381         for my $org (@$orgs) {
1382                 for my $perm (@$perms) {
1383                         if($apputils->check_perms($user_id, $org, $perm)) {
1384                                 push @not_allowed, [ $org, $perm ];
1385                         }
1386                 }
1387         }
1388
1389         return \@not_allowed
1390 }
1391
1392
1393 __PACKAGE__->register_method(
1394         method => 'check_user_perms3',
1395         api_name        => 'open-ils.actor.user.perm.highest_org',
1396         notes           => q/
1397                 Returns the highest org unit id at which a user has a given permission
1398                 If the requestor does not match the target user, the requestor must have
1399                 'VIEW_PERMISSION' rights at the home org unit of the target user
1400                 @param authtoken The login session key
1401                 @param userid The id of the user in question
1402                 @param perm The permission to check
1403                 @return The org unit highest in the org tree within which the user has
1404                 the requested permission
1405         /);
1406
1407 sub check_user_perms3 {
1408         my($self, $client, $authtoken, $user_id, $perm) = @_;
1409         my $e = new_editor(authtoken=>$authtoken);
1410         return $e->event unless $e->checkauth;
1411
1412         my $tree = $U->get_org_tree();
1413
1414     unless($e->requestor->id == $user_id) {
1415         my $user = $e->retrieve_actor_user($user_id)
1416             or return $e->event;
1417         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1418             return $U->find_highest_perm_org($perm, $user_id, $user->home_ou, $tree );
1419     }
1420
1421     return $U->find_highest_perm_org($perm, $user_id, $e->requestor->ws_ou, $tree);
1422 }
1423
1424 __PACKAGE__->register_method(
1425         method => 'user_has_work_perm_at',
1426         api_name        => 'open-ils.actor.user.has_work_perm_at',
1427     authoritative => 1,
1428     signature => {
1429         desc => q/
1430             Returns a set of org unit IDs which represent the highest orgs in 
1431             the org tree where the user has the requested permission.  The
1432             purpose of this method is to return the smallest set of org units
1433             which represent the full expanse of the user's ability to perform
1434             the requested action.  The user whose perms this method should
1435             check is implied by the authtoken. /,
1436         params => [
1437                     {desc => 'authtoken', type => 'string'},
1438             {desc => 'permission name', type => 'string'},
1439             {desc => q/user id, optional.  If present, check perms for 
1440                 this user instead of the logged in user/, type => 'number'},
1441         ],
1442         return => {desc => 'An array of org IDs'}
1443     }
1444 );
1445
1446 sub user_has_work_perm_at {
1447     my($self, $conn, $auth, $perm, $user_id) = @_;
1448     my $e = new_editor(authtoken=>$auth);
1449     return $e->event unless $e->checkauth;
1450     if(defined $user_id) {
1451         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1452         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1453     }
1454     return $U->user_has_work_perm_at($e, $perm, undef, $user_id);
1455 }
1456
1457 __PACKAGE__->register_method(
1458         method => 'user_has_work_perm_at_batch',
1459         api_name        => 'open-ils.actor.user.has_work_perm_at.batch',
1460     authoritative => 1,
1461 );
1462
1463 sub user_has_work_perm_at_batch {
1464     my($self, $conn, $auth, $perms, $user_id) = @_;
1465     my $e = new_editor(authtoken=>$auth);
1466     return $e->event unless $e->checkauth;
1467     if(defined $user_id) {
1468         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1469         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1470     }
1471     my $map = {};
1472     $map->{$_} = $U->user_has_work_perm_at($e, $_) for @$perms;
1473     return $map;
1474 }
1475
1476
1477
1478 __PACKAGE__->register_method(
1479         method => 'check_user_perms4',
1480         api_name        => 'open-ils.actor.user.perm.highest_org.batch',
1481         notes           => q/
1482                 Returns the highest org unit id at which a user has a given permission
1483                 If the requestor does not match the target user, the requestor must have
1484                 'VIEW_PERMISSION' rights at the home org unit of the target user
1485                 @param authtoken The login session key
1486                 @param userid The id of the user in question
1487                 @param perms An array of perm names to check 
1488                 @return An array of orgId's  representing the org unit 
1489                 highest in the org tree within which the user has the requested permission
1490                 The arrah of orgId's has matches the order of the perms array
1491         /);
1492
1493 sub check_user_perms4 {
1494         my( $self, $client, $authtoken, $userid, $perms ) = @_;
1495         
1496         my( $staff, $target, $org, $evt );
1497
1498         ( $staff, $target, $evt ) = $apputils->checkses_requestor(
1499                 $authtoken, $userid, 'VIEW_PERMISSION' );
1500         return $evt if $evt;
1501
1502         my @arr;
1503         return [] unless ref($perms);
1504         my $tree = $U->get_org_tree();
1505
1506         for my $p (@$perms) {
1507                 push( @arr, $U->find_highest_perm_org( $p, $userid, $target->home_ou, $tree ) );
1508         }
1509         return \@arr;
1510 }
1511
1512
1513 __PACKAGE__->register_method(
1514     method        => "user_fines_summary",
1515     api_name      => "open-ils.actor.user.fines.summary",
1516     authoritative => 1,
1517     signature     => {
1518         desc   => 'Returns a short summary of the users total open fines, '  .
1519                   'excluding voided fines Params are login_session, user_id' ,
1520         params => [
1521             {desc => 'Authentication token', type => 'string'},
1522             {desc => 'User ID',              type => 'string'}  # number?
1523         ],
1524         return => {
1525             desc => "a 'mous' object, event on error",
1526         }
1527     }
1528 );
1529
1530 sub user_fines_summary {
1531         my( $self, $client, $auth, $user_id ) = @_;
1532
1533         my $e = new_editor(authtoken=>$auth);
1534         return $e->event unless $e->checkauth;
1535
1536         if( $user_id ne $e->requestor->id ) {
1537             my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1538                 return $e->event unless 
1539                         $e->allowed('VIEW_USER_FINES_SUMMARY', $user->home_ou);
1540         }
1541
1542     return $e->search_money_open_user_summary({usr => $user_id})->[0];
1543 }
1544
1545
1546 ##### a small consolidation of related method registrations
1547 my $common_params = [
1548     { desc => 'Authentication token', type => 'string' },
1549     { desc => 'User ID',              type => 'string' },
1550     { desc => 'Transactions type (optional, defaults to all)', type => 'string' },
1551     { desc => 'Options hash.  May contain limit and offset for paged results.', type => 'object' },
1552 ];
1553 my %methods = (
1554     'open-ils.actor.user.transactions'                      => '',
1555     'open-ils.actor.user.transactions.fleshed'              => '',
1556     'open-ils.actor.user.transactions.have_charge'          => ' that have an initial charge',
1557     'open-ils.actor.user.transactions.have_charge.fleshed'  => ' that have an initial charge',
1558     'open-ils.actor.user.transactions.have_balance'         => ' that have an outstanding balance',
1559     'open-ils.actor.user.transactions.have_balance.fleshed' => ' that have an outstanding balance',
1560 );
1561
1562 foreach (keys %methods) {
1563     my %args = (
1564         method    => "user_transactions",
1565         api_name  => $_,
1566         signature => {
1567             desc   => 'For a given user, retrieve a list of '
1568                     . (/\.fleshed/ ? 'fleshed ' : '')
1569                     . 'transactions' . $methods{$_}
1570                     . ' optionally limited to transactions of a given type.',
1571             params => $common_params,
1572             return => {
1573                 desc => "List of objects, or event on error.  Each object is a hash containing: transaction, circ, record. "
1574                       . 'These represent the relevant (mbts) transaction, attached circulation and title pointed to in the circ, respectively.',
1575             }
1576         }
1577     );
1578     /\.have_balance/ and $args{authoritative} = 1;     # FIXME: I don't know why have_charge isn't authoritative
1579     __PACKAGE__->register_method(%args);
1580 }
1581
1582 # Now for the counts
1583 %methods = (
1584     'open-ils.actor.user.transactions.count'              => '',
1585     'open-ils.actor.user.transactions.have_charge.count'  => ' that have an initial charge',
1586     'open-ils.actor.user.transactions.have_balance.count' => ' that have an outstanding balance',
1587 );
1588
1589 foreach (keys %methods) {
1590     my %args = (
1591         method    => "user_transactions",
1592         api_name  => $_,
1593         signature => {
1594             desc   => 'For a given user, retrieve a count of open '
1595                     . 'transactions' . $methods{$_}
1596                     . ' optionally limited to transactions of a given type.',
1597             params => $common_params,
1598             return => { desc => "Integer count of transactions, or event on error" }
1599         }
1600     );
1601     /\.have_balance/ and $args{authoritative} = 1;     # FIXME: I don't know why have_charge isn't authoritative
1602     __PACKAGE__->register_method(%args);
1603 }
1604
1605 __PACKAGE__->register_method(
1606     method        => "user_transactions",
1607     api_name      => "open-ils.actor.user.transactions.have_balance.total",
1608     authoritative => 1,
1609     signature     => {
1610         desc   => 'For a given user, retrieve the total balance owed for open transactions,'
1611                 . ' optionally limited to transactions of a given type.',
1612         params => $common_params,
1613         return => { desc => "Decimal balance value, or event on error" }
1614     }
1615 );
1616
1617
1618 sub user_transactions {
1619         my( $self, $client, $login_session, $user_id, $type, $options ) = @_;
1620     $options ||= {};
1621
1622         my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
1623                 $login_session, $user_id, 'VIEW_USER_TRANSACTIONS' );
1624         return $evt if $evt;
1625
1626     my $api = $self->api_name();
1627
1628     my $filter = ($api =~ /have_balance/o) ?
1629         { 'balance_owed' => { '<>' => 0 } }:
1630         { 'total_owed' => { '>' => 0 } };
1631
1632     my ($trans) = $self->method_lookup(
1633         'open-ils.actor.user.transactions.history.still_open')
1634       ->run( $login_session, $user_id, $type, $filter, $options );
1635
1636         if($api =~ /total/o) { 
1637                 my $total = 0.0;
1638                 for my $t (@$trans) {
1639                         $total += $t->balance_owed;
1640                 }
1641
1642                 $logger->debug("Total balance owed by user $user_id: $total");
1643                 return $total;
1644         }
1645
1646     ($api =~ /count/o  ) and return scalar @$trans;
1647     ($api !~ /fleshed/o) and return $trans;
1648
1649         my @resp;
1650         for my $t (@$trans) {
1651                         
1652                 if( $t->xact_type ne 'circulation' ) {
1653                         push @resp, {transaction => $t};
1654                         next;
1655                 }
1656
1657                 my $circ = $apputils->simple_scalar_request(
1658                                 "open-ils.cstore",
1659                                 "open-ils.cstore.direct.action.circulation.retrieve",
1660                                 $t->id );
1661
1662                 next unless $circ;
1663
1664                 my $title = $apputils->simple_scalar_request(
1665                         "open-ils.storage", 
1666                         "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
1667                         $circ->target_copy );
1668
1669                 next unless $title;
1670
1671                 my $u = OpenILS::Utils::ModsParser->new();
1672                 $u->start_mods_batch($title->marc());
1673                 my $mods = $u->finish_mods_batch();
1674                 $mods->doc_id($title->id) if $mods;
1675
1676                 push @resp, {transaction => $t, circ => $circ, record => $mods };
1677
1678         }
1679
1680         return \@resp; 
1681
1682
1683
1684 __PACKAGE__->register_method(
1685     method   => "user_transaction_retrieve",
1686     api_name => "open-ils.actor.user.transaction.fleshed.retrieve",
1687     argc     => 1,
1688     notes    => "Returns a fleshed transaction record"
1689 );
1690
1691 __PACKAGE__->register_method(
1692     method   => "user_transaction_retrieve",
1693     api_name => "open-ils.actor.user.transaction.retrieve",
1694     argc     => 1,
1695     notes    => "Returns a transaction record"
1696 );
1697
1698 sub user_transaction_retrieve {
1699         my( $self, $client, $login_session, $bill_id ) = @_;
1700
1701         # I think I'm deprecated... make sure.   phasefx says, "No, I'll use you :)
1702
1703         my $trans = $apputils->simple_scalar_request( 
1704                 "open-ils.cstore",
1705                 "open-ils.cstore.direct.money.billable_transaction_summary.retrieve",
1706                 $bill_id
1707         );
1708
1709         my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
1710                 $login_session, $trans->usr, 'VIEW_USER_TRANSACTIONS' );
1711         return $evt if $evt;
1712         
1713         my $api = $self->api_name();
1714         if($api !~ /fleshed/o) { return $trans; }
1715
1716         if( $trans->xact_type ne 'circulation' ) {
1717                 $logger->debug("Returning non-circ transaction");
1718                 return {transaction => $trans};
1719         }
1720
1721         my $circ = $apputils->simple_scalar_request(
1722                         "open-ils.cstore",
1723                         "open-ils.cstore.direct.action.circulation.retrieve",
1724                         $trans->id );
1725
1726         return {transaction => $trans} unless $circ;
1727         $logger->debug("Found the circ transaction");
1728
1729         my $title = $apputils->simple_scalar_request(
1730                 "open-ils.storage", 
1731                 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
1732                 $circ->target_copy );
1733
1734         return {transaction => $trans, circ => $circ } unless $title;
1735         $logger->debug("Found the circ title");
1736
1737         my $mods;
1738     my $copy = $apputils->simple_scalar_request(
1739         "open-ils.cstore",
1740         "open-ils.cstore.direct.asset.copy.retrieve",
1741         $circ->target_copy );
1742
1743         try {
1744                 my $u = OpenILS::Utils::ModsParser->new();
1745                 $u->start_mods_batch($title->marc());
1746                 $mods = $u->finish_mods_batch();
1747         } otherwise {
1748                 if ($title->id == OILS_PRECAT_RECORD) {
1749                         $mods = new Fieldmapper::metabib::virtual_record;
1750                         $mods->doc_id(OILS_PRECAT_RECORD);
1751                         $mods->title($copy->dummy_title);
1752                         $mods->author($copy->dummy_author);
1753                 }
1754         };
1755
1756         $logger->debug("MODSized the circ title");
1757
1758         return {transaction => $trans, circ => $circ, record => $mods, copy => $copy };
1759 }
1760
1761
1762 __PACKAGE__->register_method(
1763     method        => "hold_request_count",
1764     api_name      => "open-ils.actor.user.hold_requests.count",
1765     authoritative => 1,
1766     argc          => 1,
1767     notes         => 'Returns hold ready/total counts'
1768 );
1769         
1770 sub hold_request_count {
1771         my( $self, $client, $login_session, $userid ) = @_;
1772
1773         my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
1774                 $login_session, $userid, 'VIEW_HOLD' );
1775         return $evt if $evt;
1776         
1777
1778         my $holds = $apputils->simple_scalar_request(
1779                         "open-ils.cstore",
1780                         "open-ils.cstore.direct.action.hold_request.search.atomic",
1781                         { 
1782                                 usr => $userid,
1783                                 fulfillment_time => {"=" => undef },
1784                                 cancel_time => undef,
1785                         }
1786         );
1787
1788         my @ready;
1789         for my $h (@$holds) {
1790                 next unless $h->capture_time and $h->current_copy;
1791
1792                 my $copy = $apputils->simple_scalar_request(
1793                         "open-ils.cstore",
1794                         "open-ils.cstore.direct.asset.copy.retrieve",
1795                         $h->current_copy
1796                 );
1797
1798                 if ($copy and $copy->status == 8) {
1799                         push @ready, $h;
1800                 }
1801         }
1802
1803         return { total => scalar(@$holds), ready => scalar(@ready) };
1804 }
1805
1806
1807 __PACKAGE__->register_method(
1808     method    => "checkedout_count",
1809     api_name  => "open-ils.actor.user.checked_out.count__",
1810     argc      => 1,
1811     signature => {
1812         desc => "[DEPRECATED] For a given user, returns the total number of items checked out, "
1813               . ' and the total number of items overdue',
1814         params => [
1815             { desc => 'Authentication Token', type => 'string'},
1816             { desc => 'User ID',              type => 'string'},
1817         ],
1818         return => {
1819             desc => 'Counts of total checked out and overdue, like { total => $i, overdue => $j }',
1820         },
1821     }
1822 );
1823         
1824
1825 # XXX Deprecate Me
1826 sub checkedout_count {
1827         my( $self, $client, $login_session, $userid ) = @_;
1828
1829         my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
1830                 $login_session, $userid, 'VIEW_CIRCULATIONS' );
1831         return $evt if $evt;
1832         
1833         my $circs = $apputils->simple_scalar_request(
1834                         "open-ils.cstore",
1835                         "open-ils.cstore.direct.action.circulation.search.atomic",
1836                         { usr => $userid, stop_fines => undef }
1837                         #{ usr => $userid, checkin_time => {"=" => undef } }
1838         );
1839
1840         my $parser = DateTime::Format::ISO8601->new;
1841
1842         my $overdue = 0;
1843         for my $c (@$circs) {
1844                 my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $c->due_date ) );
1845                 if ($due_dt->epoch < DateTime->today->epoch) {
1846                         $overdue++;
1847                 }
1848         }
1849
1850         return { total => scalar(@$circs), overdue => $overdue };
1851 }
1852
1853
1854 __PACKAGE__->register_method(
1855     method        => "checked_out",
1856     api_name      => "open-ils.actor.user.checked_out",
1857     authoritative => 1,
1858     argc          => 2,
1859         signature     => {
1860         desc => "For a given user, returns a structure of circulations objects sorted by out, overdue, lost, claims_returned, long_overdue. "
1861               . "A list of IDs are returned of each type.  Circs marked lost, long_overdue, and claims_returned will not be 'finished' "
1862               . "(i.e., outstanding balance or some other pending action on the circ). "
1863               . "The .count method also includes a 'total' field which sums all open circs.",
1864         params => [
1865             { desc => 'Authentication Token', type => 'string'},
1866             { desc => 'User ID',              type => 'string'},
1867         ],
1868         return => {
1869             desc => 'Returns event on error, or an object with ID lists, like: '
1870                   . '{"out":[12552,451232], "claims_returned":[], "long_overdue":[23421] "overdue":[], "lost":[]}'
1871         },
1872     }
1873 );
1874
1875 __PACKAGE__->register_method(
1876     method        => "checked_out",
1877     api_name      => "open-ils.actor.user.checked_out.count",
1878     authoritative => 1,
1879     argc          => 2,
1880     signature     => q/@see open-ils.actor.user.checked_out/
1881 );
1882
1883 sub checked_out {
1884         my( $self, $conn, $auth, $userid ) = @_;
1885
1886         my $e = new_editor(authtoken=>$auth);
1887         return $e->event unless $e->checkauth;
1888
1889         if( $userid ne $e->requestor->id ) {
1890         my $user = $e->retrieve_actor_user($userid) or return $e->event;
1891                 unless($e->allowed('VIEW_CIRCULATIONS', $user->home_ou)) {
1892
1893             # see if there is a friend link allowing circ.view perms
1894             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
1895                 $e, $userid, $e->requestor->id, 'circ.view');
1896             return $e->event unless $allowed;
1897         }
1898         }
1899
1900         my $count = $self->api_name =~ /count/;
1901         return _checked_out( $count, $e, $userid );
1902 }
1903
1904 sub _checked_out {
1905         my( $iscount, $e, $userid ) = @_;
1906         my $meth = 'open-ils.storage.actor.user.checked_out';
1907         $meth = "$meth.count" if $iscount;
1908         return $U->storagereq($meth, $userid);
1909 }
1910
1911
1912 sub _checked_out_WHAT {
1913         my( $iscount, $e, $userid ) = @_;
1914
1915         my $circs = $e->search_action_circulation( 
1916                 { usr => $userid, stop_fines => undef });
1917
1918         my $mcircs = $e->search_action_circulation( 
1919                 { 
1920                         usr => $userid, 
1921                         checkin_time => undef, 
1922                         xact_finish => undef, 
1923                 });
1924
1925         
1926         push( @$circs, @$mcircs );
1927
1928         my $parser = DateTime::Format::ISO8601->new;
1929
1930         # split the circs up into overdue and not-overdue circs
1931         my (@out,@overdue);
1932         for my $c (@$circs) {
1933                 if( $c->due_date ) {
1934                         my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $c->due_date ) );
1935                         my $due = $due_dt->epoch;
1936                         if ($due < DateTime->today->epoch) {
1937                                 push @overdue, $c->id;
1938                         } else {
1939                                 push @out, $c->id;
1940                         }
1941                 } else {
1942                         push @out, $c->id;
1943                 }
1944         }
1945
1946         # grab all of the lost, claims-returned, and longoverdue circs
1947         #my $open = $e->search_action_circulation(
1948         #       {usr => $userid, stop_fines => { '!=' => undef }, xact_finish => undef });
1949
1950
1951         # these items have stop_fines, but no xact_finish, so money
1952         # is owed on them and they have not been checked in
1953         my $open = $e->search_action_circulation(
1954                 {
1955                         usr                             => $userid, 
1956                         stop_fines              => { in => [ qw/LOST CLAIMSRETURNED LONGOVERDUE/ ] }, 
1957                         xact_finish             => undef,
1958                         checkin_time    => undef,
1959                 }
1960         );
1961
1962
1963         my( @lost, @cr, @lo );
1964         for my $c (@$open) {
1965                 push( @lost, $c->id ) if $c->stop_fines eq 'LOST';
1966                 push( @cr, $c->id ) if $c->stop_fines eq 'CLAIMSRETURNED';
1967                 push( @lo, $c->id ) if $c->stop_fines eq 'LONGOVERDUE';
1968         }
1969
1970
1971         if( $iscount ) {
1972                 return {
1973                         total           => @$circs + @lost + @cr + @lo,
1974                         out             => scalar(@out),
1975                         overdue => scalar(@overdue),
1976                         lost            => scalar(@lost),
1977                         claims_returned => scalar(@cr),
1978                         long_overdue            => scalar(@lo)
1979                 };
1980         }
1981
1982         return {
1983                 out             => \@out,
1984                 overdue => \@overdue,
1985                 lost            => \@lost,
1986                 claims_returned => \@cr,
1987                 long_overdue            => \@lo
1988         };
1989 }
1990
1991
1992
1993 __PACKAGE__->register_method(
1994     method        => "checked_in_with_fines",
1995     api_name      => "open-ils.actor.user.checked_in_with_fines",
1996     authoritative => 1,
1997     argc          => 2,
1998     signature     => q/@see open-ils.actor.user.checked_out/
1999 );
2000
2001 sub checked_in_with_fines {
2002         my( $self, $conn, $auth, $userid ) = @_;
2003
2004         my $e = new_editor(authtoken=>$auth);
2005         return $e->event unless $e->checkauth;
2006
2007         if( $userid ne $e->requestor->id ) {
2008                 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2009         }
2010
2011         # money is owed on these items and they are checked in
2012         my $open = $e->search_action_circulation(
2013                 {
2014                         usr                             => $userid, 
2015                         xact_finish             => undef,
2016                         checkin_time    => { "!=" => undef },
2017                 }
2018         );
2019
2020
2021         my( @lost, @cr, @lo );
2022         for my $c (@$open) {
2023                 push( @lost, $c->id ) if $c->stop_fines eq 'LOST';
2024                 push( @cr, $c->id ) if $c->stop_fines eq 'CLAIMSRETURNED';
2025                 push( @lo, $c->id ) if $c->stop_fines eq 'LONGOVERDUE';
2026         }
2027
2028         return {
2029                 lost            => \@lost,
2030                 claims_returned => \@cr,
2031                 long_overdue            => \@lo
2032         };
2033 }
2034
2035
2036 sub _sigmaker {
2037     my ($api, $desc, $auth) = @_;
2038     $desc = $desc ? (" " . $desc) : '';
2039     my $ids = ($api =~ /ids$/) ? 1 : 0;
2040     my @sig = (
2041         argc      => 1,
2042         method    => "user_transaction_history",
2043         api_name  => "open-ils.actor.user.transactions.$api",
2044         signature => {
2045             desc   => "For a given User ID, returns a list of billable transaction" .
2046                       ($ids ? " id" : '') .
2047                       "s$desc, optionally filtered by type and/or fields in money.billable_xact_summary.  " .
2048                       "The VIEW_USER_TRANSACTIONS permission is required to view another user's transactions",
2049             params => [
2050                 {desc => 'Authentication token',        type => 'string'},
2051                 {desc => 'User ID',                     type => 'number'},
2052                 {desc => 'Transaction type (optional)', type => 'number'},
2053                 {desc => 'Hash of Billable Transaction Summary filters (optional)', type => 'object'}
2054             ],
2055             return => {
2056                 desc => 'List of transaction' . ($ids ? " id" : '') . 's, Event on error'
2057             },
2058         }
2059     );
2060     $auth and push @sig, (authoritative => 1);
2061     return @sig;
2062 }
2063
2064 my %hist_methods = (
2065     'history'             => '',
2066     'history.have_charge' => 'that have an initial charge',
2067     'history.still_open'  => 'that are not finished',
2068 );
2069 my %auth_hist_methods = (
2070     'history.have_balance'         => 'that have a balance',
2071     'history.have_bill'            => 'that have billings',
2072     'history.have_bill_or_payment' => 'that have non-zero-sum billings or at least 1 payment',
2073 );
2074 foreach (keys %hist_methods) {
2075     __PACKAGE__->register_method(_sigmaker($_,       $hist_methods{$_}));
2076     __PACKAGE__->register_method(_sigmaker("$_.ids", $hist_methods{$_}));
2077 }
2078 foreach (keys %auth_hist_methods) {
2079     __PACKAGE__->register_method(_sigmaker($_,       $auth_hist_methods{$_}, 1));
2080     __PACKAGE__->register_method(_sigmaker("$_.ids", $auth_hist_methods{$_}, 1));
2081 }
2082
2083 sub user_transaction_history {
2084         my( $self, $conn, $auth, $userid, $type, $filter, $options ) = @_;
2085     $filter ||= {};
2086     $options ||= {};
2087
2088         # run inside of a transaction to prevent replication delays
2089         my $e = new_editor(authtoken=>$auth);
2090         return $e->die_event unless $e->checkauth;
2091
2092         if ($e->requestor->id ne $userid) {
2093         return $e->die_event unless $e->allowed('VIEW_USER_TRANSACTIONS');
2094         }
2095
2096         my $api = $self->api_name;
2097         my @xact_finish  = (xact_finish => undef ) if ($api =~ /history\.still_open$/);     # What about history.still_open.ids?
2098
2099         if(defined($type)) {
2100                 $filter->{'xact_type'} = $type;
2101         }
2102
2103         if($api =~ /have_bill_or_payment/o) {
2104
2105         # transactions that have a non-zero sum across all billings or at least 1 payment
2106         $filter->{'-or'} = {
2107             'balance_owed' => { '<>' => 0 },
2108             'last_payment_ts' => { '<>' => undef }
2109         };
2110
2111     } elsif( $api =~ /have_balance/o) {
2112
2113         # transactions that have a non-zero overall balance
2114         $filter->{'balance_owed'} = { '<>' => 0 };
2115
2116         } elsif( $api =~ /have_charge/o) {
2117
2118         # transactions that have at least 1 billing, regardless of whether it was voided
2119         $filter->{'last_billing_ts'} = { '<>' => undef };
2120
2121         } elsif( $api =~ /have_bill/o) {    # needs to be an elsif, or we double-match have_bill_or_payment!
2122
2123         # transactions that have non-zero sum across all billings.  This will exclude
2124         # xacts where all billings have been voided
2125         $filter->{'total_owed'} = { '<>' => 0 };
2126         }
2127
2128     my $options_clause = { order_by => { mbt => 'xact_start DESC' } };
2129     $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'}; 
2130     $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'}; 
2131
2132     my $mbts = $e->search_money_billable_transaction_summary(
2133         [ 
2134             { usr => $userid, @xact_finish, %$filter },
2135             $options_clause
2136         ]
2137     );
2138
2139     if ($api =~ /\.ids/) {
2140         return [map {$_->id} @$mbts];
2141     } else {
2142         return $mbts;
2143     }
2144 }
2145
2146
2147
2148 __PACKAGE__->register_method(
2149     method   => "user_perms",
2150     api_name => "open-ils.actor.permissions.user_perms.retrieve",
2151     argc     => 1,
2152     notes    => "Returns a list of permissions"
2153 );
2154         
2155 sub user_perms {
2156         my( $self, $client, $authtoken, $user ) = @_;
2157
2158         my( $staff, $evt ) = $apputils->checkses($authtoken);
2159         return $evt if $evt;
2160
2161         $user ||= $staff->id;
2162
2163         if( $user != $staff->id and $evt = $apputils->check_perms( $staff->id, $staff->home_ou, 'VIEW_PERMISSION') ) {
2164                 return $evt;
2165         }
2166
2167         return $apputils->simple_scalar_request(
2168                 "open-ils.storage",
2169                 "open-ils.storage.permission.user_perms.atomic",
2170                 $user);
2171 }
2172
2173 __PACKAGE__->register_method(
2174     method   => "retrieve_perms",
2175     api_name => "open-ils.actor.permissions.retrieve",
2176     notes    => "Returns a list of permissions"
2177 );
2178 sub retrieve_perms {
2179         my( $self, $client ) = @_;
2180         return $apputils->simple_scalar_request(
2181                 "open-ils.cstore",
2182                 "open-ils.cstore.direct.permission.perm_list.search.atomic",
2183                 { id => { '!=' => undef } }
2184         );
2185 }
2186
2187 __PACKAGE__->register_method(
2188     method   => "retrieve_groups",
2189     api_name => "open-ils.actor.groups.retrieve",
2190     notes    => "Returns a list of user groups"
2191 );
2192 sub retrieve_groups {
2193         my( $self, $client ) = @_;
2194         return new_editor()->retrieve_all_permission_grp_tree();
2195 }
2196
2197 __PACKAGE__->register_method(
2198         method  => "retrieve_org_address",
2199         api_name        => "open-ils.actor.org_unit.address.retrieve",
2200         notes           => <<'  NOTES');
2201         Returns an org_unit address by ID
2202         @param An org_address ID
2203         NOTES
2204 sub retrieve_org_address {
2205         my( $self, $client, $id ) = @_;
2206         return $apputils->simple_scalar_request(
2207                 "open-ils.cstore",
2208                 "open-ils.cstore.direct.actor.org_address.retrieve",
2209                 $id
2210         );
2211 }
2212
2213 __PACKAGE__->register_method(
2214     method   => "retrieve_groups_tree",
2215     api_name => "open-ils.actor.groups.tree.retrieve",
2216     notes    => "Returns a list of user groups"
2217 );
2218         
2219 sub retrieve_groups_tree {
2220         my( $self, $client ) = @_;
2221         return new_editor()->search_permission_grp_tree(
2222                 [
2223                         { parent => undef},
2224                         {       
2225                                 flesh                           => -1,
2226                                 flesh_fields    => { pgt => ["children"] }, 
2227                                 order_by                        => { pgt => 'name'}
2228                         }
2229                 ]
2230         )->[0];
2231 }
2232
2233
2234 __PACKAGE__->register_method(
2235     method   => "add_user_to_groups",
2236     api_name => "open-ils.actor.user.set_groups",
2237     notes    => "Adds a user to one or more permission groups"
2238 );
2239         
2240 sub add_user_to_groups {
2241         my( $self, $client, $authtoken, $userid, $groups ) = @_;
2242
2243         my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2244                 $authtoken, $userid, 'CREATE_USER_GROUP_LINK' );
2245         return $evt if $evt;
2246
2247         ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2248                 $authtoken, $userid, 'REMOVE_USER_GROUP_LINK' );
2249         return $evt if $evt;
2250
2251         $apputils->simplereq(
2252                 'open-ils.storage',
2253                 'open-ils.storage.direct.permission.usr_grp_map.mass_delete', { usr => $userid } );
2254                 
2255         for my $group (@$groups) {
2256                 my $link = Fieldmapper::permission::usr_grp_map->new;
2257                 $link->grp($group);
2258                 $link->usr($userid);
2259
2260                 my $id = $apputils->simplereq(
2261                         'open-ils.storage',
2262                         'open-ils.storage.direct.permission.usr_grp_map.create', $link );
2263         }
2264
2265         return 1;
2266 }
2267
2268 __PACKAGE__->register_method(
2269     method   => "get_user_perm_groups",
2270     api_name => "open-ils.actor.user.get_groups",
2271     notes    => "Retrieve a user's permission groups."
2272 );
2273
2274
2275 sub get_user_perm_groups {
2276         my( $self, $client, $authtoken, $userid ) = @_;
2277
2278         my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2279                 $authtoken, $userid, 'VIEW_PERM_GROUPS' );
2280         return $evt if $evt;
2281
2282         return $apputils->simplereq(
2283                 'open-ils.cstore',
2284                 'open-ils.cstore.direct.permission.usr_grp_map.search.atomic', { usr => $userid } );
2285 }       
2286
2287
2288 __PACKAGE__->register_method(
2289     method   => "get_user_work_ous",
2290     api_name => "open-ils.actor.user.get_work_ous",
2291     notes    => "Retrieve a user's work org units."
2292 );
2293
2294 __PACKAGE__->register_method(
2295     method   => "get_user_work_ous",
2296     api_name => "open-ils.actor.user.get_work_ous.ids",
2297     notes    => "Retrieve a user's work org units."
2298 );
2299
2300 sub get_user_work_ous {
2301         my( $self, $client, $auth, $userid ) = @_;
2302     my $e = new_editor(authtoken=>$auth);
2303     return $e->event unless $e->checkauth;
2304     $userid ||= $e->requestor->id;
2305
2306     if($e->requestor->id != $userid) {
2307         my $user = $e->retrieve_actor_user($userid)
2308             or return $e->event;
2309         return $e->event unless $e->allowed('ASSIGN_WORK_ORG_UNIT', $user->home_ou);
2310     }
2311
2312     return $e->search_permission_usr_work_ou_map({usr => $userid})
2313         unless $self->api_name =~ /.ids$/;
2314
2315     # client just wants a list of org IDs
2316     return $U->get_user_work_ou_ids($e, $userid);
2317 }       
2318
2319
2320
2321 __PACKAGE__->register_method(
2322     method    => 'register_workstation',
2323     api_name  => 'open-ils.actor.workstation.register.override',
2324     signature => q/@see open-ils.actor.workstation.register/
2325 );
2326
2327 __PACKAGE__->register_method(
2328     method    => 'register_workstation',
2329     api_name  => 'open-ils.actor.workstation.register',
2330     signature => q/
2331                 Registers a new workstion in the system
2332                 @param authtoken The login session key
2333                 @param name The name of the workstation id
2334                 @param owner The org unit that owns this workstation
2335                 @return The workstation id on success, WORKSTATION_NAME_EXISTS
2336                 if the name is already in use.
2337         /
2338 );
2339
2340 sub register_workstation {
2341         my( $self, $conn, $authtoken, $name, $owner ) = @_;
2342
2343         my $e = new_editor(authtoken=>$authtoken, xact=>1);
2344         return $e->die_event unless $e->checkauth;
2345         return $e->die_event unless $e->allowed('REGISTER_WORKSTATION', $owner);
2346         my $existing = $e->search_actor_workstation({name => $name})->[0];
2347
2348         if( $existing ) {
2349
2350                 if( $self->api_name =~ /override/o ) {
2351             # workstation with the given name exists.  
2352
2353             if($owner ne $existing->owning_lib) {
2354                 # if necessary, update the owning_lib of the workstation
2355
2356                 $logger->info("changing owning lib of workstation ".$existing->id.
2357                     " from ".$existing->owning_lib." to $owner");
2358                             return $e->die_event unless 
2359                     $e->allowed('UPDATE_WORKSTATION', $existing->owning_lib); 
2360
2361                             return $e->die_event unless $e->allowed('UPDATE_WORKSTATION', $owner); 
2362
2363                 $existing->owning_lib($owner);
2364                             return $e->die_event unless $e->update_actor_workstation($existing);
2365
2366                 $e->commit;
2367
2368             } else {
2369                 $logger->info(  
2370                     "attempt to register an existing workstation.  returning existing ID");
2371             }
2372
2373             return $existing->id;
2374
2375                 } else {
2376                         return OpenILS::Event->new('WORKSTATION_NAME_EXISTS')
2377                 }
2378         }
2379
2380         my $ws = Fieldmapper::actor::workstation->new;
2381         $ws->owning_lib($owner);
2382         $ws->name($name);
2383         $e->create_actor_workstation($ws) or return $e->die_event;
2384         $e->commit;
2385         return $ws->id; # note: editor sets the id on the new object for us
2386 }
2387
2388 __PACKAGE__->register_method(
2389     method    => 'workstation_list',
2390     api_name  => 'open-ils.actor.workstation.list',
2391     signature => q/
2392                 Returns a list of workstations registered at the given location
2393                 @param authtoken The login session key
2394                 @param ids A list of org_unit.id's for the workstation owners
2395         /
2396 );
2397
2398 sub workstation_list {
2399         my( $self, $conn, $authtoken, @orgs ) = @_;
2400
2401         my $e = new_editor(authtoken=>$authtoken);
2402         return $e->event unless $e->checkauth;
2403     my %results;
2404
2405     for my $o (@orgs) {
2406             return $e->event 
2407             unless $e->allowed('REGISTER_WORKSTATION', $o);
2408         $results{$o} = $e->search_actor_workstation({owning_lib=>$o});
2409     }
2410     return \%results;
2411 }
2412
2413
2414 __PACKAGE__->register_method(
2415     method        => 'fetch_patron_note',
2416     api_name      => 'open-ils.actor.note.retrieve.all',
2417     authoritative => 1,
2418     signature     => q/
2419                 Returns a list of notes for a given user
2420                 Requestor must have VIEW_USER permission if pub==false and
2421                 @param authtoken The login session key
2422                 @param args Hash of params including
2423                         patronid : the patron's id
2424                         pub : true if retrieving only public notes
2425         /
2426 );
2427
2428 sub fetch_patron_note {
2429         my( $self, $conn, $authtoken, $args ) = @_;
2430         my $patronid = $$args{patronid};
2431
2432         my($reqr, $evt) = $U->checkses($authtoken);
2433         return $evt if $evt;
2434
2435         my $patron;
2436         ($patron, $evt) = $U->fetch_user($patronid);
2437         return $evt if $evt;
2438
2439         if($$args{pub}) {
2440                 if( $patronid ne $reqr->id ) {
2441                         $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
2442                         return $evt if $evt;
2443                 }
2444                 return $U->cstorereq(
2445                         'open-ils.cstore.direct.actor.usr_note.search.atomic', 
2446                         { usr => $patronid, pub => 't' } );
2447         }
2448
2449         $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
2450         return $evt if $evt;
2451
2452         return $U->cstorereq(
2453                 'open-ils.cstore.direct.actor.usr_note.search.atomic', { usr => $patronid } );
2454 }
2455
2456 __PACKAGE__->register_method(
2457     method    => 'create_user_note',
2458     api_name  => 'open-ils.actor.note.create',
2459     signature => q/
2460                 Creates a new note for the given user
2461                 @param authtoken The login session key
2462                 @param note The note object
2463         /
2464 );
2465 sub create_user_note {
2466         my( $self, $conn, $authtoken, $note ) = @_;
2467         my $e = new_editor(xact=>1, authtoken=>$authtoken);
2468         return $e->die_event unless $e->checkauth;
2469
2470         my $user = $e->retrieve_actor_user($note->usr)
2471                 or return $e->die_event;
2472
2473         return $e->die_event unless 
2474                 $e->allowed('UPDATE_USER',$user->home_ou);
2475
2476         $note->creator($e->requestor->id);
2477         $e->create_actor_usr_note($note) or return $e->die_event;
2478         $e->commit;
2479         return $note->id;
2480 }
2481
2482
2483 __PACKAGE__->register_method(
2484     method    => 'delete_user_note',
2485     api_name  => 'open-ils.actor.note.delete',
2486     signature => q/
2487                 Deletes a note for the given user
2488                 @param authtoken The login session key
2489                 @param noteid The note id
2490         /
2491 );
2492 sub delete_user_note {
2493         my( $self, $conn, $authtoken, $noteid ) = @_;
2494
2495         my $e = new_editor(xact=>1, authtoken=>$authtoken);
2496         return $e->die_event unless $e->checkauth;
2497         my $note = $e->retrieve_actor_usr_note($noteid)
2498                 or return $e->die_event;
2499         my $user = $e->retrieve_actor_user($note->usr)
2500                 or return $e->die_event;
2501         return $e->die_event unless 
2502                 $e->allowed('UPDATE_USER', $user->home_ou);
2503         
2504         $e->delete_actor_usr_note($note) or return $e->die_event;
2505         $e->commit;
2506         return 1;
2507 }
2508
2509
2510 __PACKAGE__->register_method(
2511     method    => 'update_user_note',
2512     api_name  => 'open-ils.actor.note.update',
2513     signature => q/
2514                 @param authtoken The login session key
2515                 @param note The note
2516         /
2517 );
2518
2519 sub update_user_note {
2520         my( $self, $conn, $auth, $note ) = @_;
2521         my $e = new_editor(authtoken=>$auth, xact=>1);
2522         return $e->event unless $e->checkauth;
2523         my $patron = $e->retrieve_actor_user($note->usr)
2524                 or return $e->event;
2525         return $e->event unless 
2526                 $e->allowed('UPDATE_USER', $patron->home_ou);
2527         $e->update_actor_user_note($note)
2528                 or return $e->event;
2529         $e->commit;
2530         return 1;
2531 }
2532
2533
2534
2535 __PACKAGE__->register_method(
2536     method    => 'create_closed_date',
2537     api_name  => 'open-ils.actor.org_unit.closed_date.create',
2538     signature => q/
2539                 Creates a new closing entry for the given org_unit
2540                 @param authtoken The login session key
2541                 @param note The closed_date object
2542         /
2543 );
2544 sub create_closed_date {
2545         my( $self, $conn, $authtoken, $cd ) = @_;
2546
2547         my( $user, $evt ) = $U->checkses($authtoken);
2548         return $evt if $evt;
2549
2550         $evt = $U->check_perms($user->id, $cd->org_unit, 'CREATE_CLOSEING');
2551         return $evt if $evt;
2552
2553         $logger->activity("user ".$user->id." creating library closing for ".$cd->org_unit);
2554
2555         my $id = $U->storagereq(
2556                 'open-ils.storage.direct.actor.org_unit.closed_date.create', $cd );
2557         return $U->DB_UPDATE_FAILED($cd) unless $id;
2558         return $id;
2559 }
2560
2561
2562 __PACKAGE__->register_method(
2563     method    => 'delete_closed_date',
2564     api_name  => 'open-ils.actor.org_unit.closed_date.delete',
2565     signature => q/
2566                 Deletes a closing entry for the given org_unit
2567                 @param authtoken The login session key
2568                 @param noteid The close_date id
2569         /
2570 );
2571 sub delete_closed_date {
2572         my( $self, $conn, $authtoken, $cd ) = @_;
2573
2574         my( $user, $evt ) = $U->checkses($authtoken);
2575         return $evt if $evt;
2576
2577         my $cd_obj;
2578         ($cd_obj, $evt) = fetch_closed_date($cd);
2579         return $evt if $evt;
2580
2581         $evt = $U->check_perms($user->id, $cd->org_unit, 'DELETE_CLOSEING');
2582         return $evt if $evt;
2583
2584         $logger->activity("user ".$user->id." deleting library closing for ".$cd->org_unit);
2585
2586         my $stat = $U->storagereq(
2587                 'open-ils.storage.direct.actor.org_unit.closed_date.delete', $cd );
2588         return $U->DB_UPDATE_FAILED($cd) unless $stat;
2589         return $stat;
2590 }
2591
2592
2593 __PACKAGE__->register_method(
2594     method    => 'usrname_exists',
2595     api_name  => 'open-ils.actor.username.exists',
2596     signature => {
2597         desc  => 'Check if a username is already taken (by an undeleted patron)',
2598         param => [
2599             {desc => 'Authentication token', type => 'string'},
2600             {desc => 'Username',             type => 'string'}
2601         ],
2602         return => {
2603             desc => 'id of existing user if username exists, undef otherwise.  Event on error'
2604         },
2605     }
2606 );
2607
2608 sub usrname_exists {
2609         my( $self, $conn, $auth, $usrname ) = @_;
2610         my $e = new_editor(authtoken=>$auth);
2611         return $e->event unless $e->checkauth;
2612         my $a = $e->search_actor_user({usrname => $usrname, deleted=>'f'}, {idlist=>1});
2613         return $$a[0] if $a and @$a;
2614         return undef;
2615 }
2616
2617 __PACKAGE__->register_method(
2618     method        => 'barcode_exists',
2619     api_name      => 'open-ils.actor.barcode.exists',
2620     authoritative => 1,
2621     signature     => 'Returns 1 if the requested barcode exists, returns 0 otherwise'
2622 );
2623
2624 sub barcode_exists {
2625         my( $self, $conn, $auth, $barcode ) = @_;
2626         my $e = new_editor(authtoken=>$auth);
2627         return $e->event unless $e->checkauth;
2628         my $card = $e->search_actor_card({barcode => $barcode});
2629         if (@$card) {
2630                 return 1;
2631         } else {
2632                 return 0;
2633         }
2634         #return undef unless @$card;
2635         #return $card->[0]->usr;
2636 }
2637
2638
2639 __PACKAGE__->register_method(
2640     method   => 'retrieve_net_levels',
2641     api_name => 'open-ils.actor.net_access_level.retrieve.all',
2642 );
2643
2644 sub retrieve_net_levels {
2645         my( $self, $conn, $auth ) = @_;
2646         my $e = new_editor(authtoken=>$auth);
2647         return $e->event unless $e->checkauth;
2648         return $e->retrieve_all_config_net_access_level();
2649 }
2650
2651 # Retain the old typo API name just in case
2652 __PACKAGE__->register_method(
2653     method   => 'fetch_org_by_shortname',
2654     api_name => 'open-ils.actor.org_unit.retrieve_by_shorname',
2655 );
2656 __PACKAGE__->register_method(
2657     method   => 'fetch_org_by_shortname',
2658     api_name => 'open-ils.actor.org_unit.retrieve_by_shortname',
2659 );
2660 sub fetch_org_by_shortname {
2661         my( $self, $conn, $sname ) = @_;
2662         my $e = new_editor();
2663         my $org = $e->search_actor_org_unit({ shortname => uc($sname)})->[0];
2664         return $e->event unless $org;
2665         return $org;
2666 }
2667
2668
2669 __PACKAGE__->register_method(
2670     method   => 'session_home_lib',
2671     api_name => 'open-ils.actor.session.home_lib',
2672 );
2673
2674 sub session_home_lib {
2675         my( $self, $conn, $auth ) = @_;
2676         my $e = new_editor(authtoken=>$auth);
2677         return undef unless $e->checkauth;
2678         my $org = $e->retrieve_actor_org_unit($e->requestor->home_ou);
2679         return $org->shortname;
2680 }
2681
2682 __PACKAGE__->register_method(
2683     method    => 'session_safe_token',
2684     api_name  => 'open-ils.actor.session.safe_token',
2685     signature => q/
2686                 Returns a hashed session ID that is safe for export to the world.
2687                 This safe token will expire after 1 hour of non-use.
2688                 @param auth Active authentication token
2689         /
2690 );
2691
2692 sub session_safe_token {
2693         my( $self, $conn, $auth ) = @_;
2694         my $e = new_editor(authtoken=>$auth);
2695         return undef unless $e->checkauth;
2696
2697         my $safe_token = md5_hex($auth);
2698
2699         $cache ||= OpenSRF::Utils::Cache->new("global", 0);
2700
2701         # Add more like the following if needed...
2702         $cache->put_cache(
2703                 "safe-token-home_lib-shortname-$safe_token",
2704                 $e->retrieve_actor_org_unit(
2705                         $e->requestor->home_ou
2706                 )->shortname,
2707                 60 * 60
2708         );
2709
2710         return $safe_token;
2711 }
2712
2713
2714 __PACKAGE__->register_method(
2715     method    => 'safe_token_home_lib',
2716     api_name  => 'open-ils.actor.safe_token.home_lib.shortname',
2717     signature => q/
2718                 Returns the home library shortname from the session
2719                 asscociated with a safe token from generated by
2720                 open-ils.actor.session.safe_token.
2721                 @param safe_token Active safe token
2722         /
2723 );
2724
2725 sub safe_token_home_lib {
2726         my( $self, $conn, $safe_token ) = @_;
2727
2728         $cache ||= OpenSRF::Utils::Cache->new("global", 0);
2729         return $cache->get_cache( 'safe-token-home_lib-shortname-'. $safe_token );
2730 }
2731
2732
2733
2734 __PACKAGE__->register_method(
2735     method   => 'slim_tree',
2736     api_name => "open-ils.actor.org_tree.slim_hash.retrieve",
2737 );
2738 sub slim_tree {
2739         my $tree = new_editor()->search_actor_org_unit( 
2740                 [
2741                         {"parent_ou" => undef },
2742                         {
2743                                 flesh                           => -1,
2744                                 flesh_fields    => { aou =>  ['children'] },
2745                                 order_by                        => { aou => 'name'},
2746                                 select                  => { aou => ["id","shortname", "name"]},
2747                         }
2748                 ]
2749         )->[0];
2750
2751         return trim_tree($tree);
2752 }
2753
2754
2755 sub trim_tree {
2756         my $tree = shift;
2757         return undef unless $tree;
2758         my $htree = {
2759                 code => $tree->shortname,
2760                 name => $tree->name,
2761         };
2762         if( $tree->children and @{$tree->children} ) {
2763                 $htree->{children} = [];
2764                 for my $c (@{$tree->children}) {
2765                         push( @{$htree->{children}}, trim_tree($c) );
2766                 }
2767         }
2768
2769         return $htree;
2770 }
2771
2772
2773 __PACKAGE__->register_method(
2774     method   => "update_penalties",
2775     api_name => "open-ils.actor.user.penalties.update"
2776 );
2777
2778 sub update_penalties {
2779         my($self, $conn, $auth, $user_id) = @_;
2780         my $e = new_editor(authtoken=>$auth, xact => 1);
2781         return $e->die_event unless $e->checkauth;
2782     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
2783     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2784     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $e->requestor->ws_ou);
2785     return $evt if $evt;
2786     $e->commit;
2787     return 1;
2788 }
2789
2790
2791 __PACKAGE__->register_method(
2792     method   => "apply_penalty",
2793     api_name => "open-ils.actor.user.penalty.apply"
2794 );
2795
2796 sub apply_penalty {
2797         my($self, $conn, $auth, $penalty) = @_;
2798
2799         my $e = new_editor(authtoken=>$auth, xact => 1);
2800         return $e->die_event unless $e->checkauth;
2801
2802     my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
2803     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2804
2805     my $ptype = $e->retrieve_config_standing_penalty($penalty->standing_penalty) or return $e->die_event;
2806     
2807     my $ctx_org = 
2808         (defined $ptype->org_depth) ?
2809         $U->org_unit_ancestor_at_depth($penalty->org_unit, $ptype->org_depth) :
2810         $penalty->org_unit;
2811
2812     $penalty->org_unit($ctx_org);
2813     $penalty->staff($e->requestor->id);
2814     $e->create_actor_user_standing_penalty($penalty) or return $e->die_event;
2815
2816     $e->commit;
2817     return $penalty->id;
2818 }
2819
2820 __PACKAGE__->register_method(
2821     method   => "remove_penalty",
2822     api_name => "open-ils.actor.user.penalty.remove"
2823 );
2824
2825 sub remove_penalty {
2826         my($self, $conn, $auth, $penalty) = @_;
2827         my $e = new_editor(authtoken=>$auth, xact => 1);
2828         return $e->die_event unless $e->checkauth;
2829     my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
2830     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2831
2832     $e->delete_actor_user_standing_penalty($penalty) or return $e->die_event;
2833     $e->commit;
2834     return 1;
2835 }
2836
2837 __PACKAGE__->register_method(
2838     method   => "update_penalty_note",
2839     api_name => "open-ils.actor.user.penalty.note.update"
2840 );
2841
2842 sub update_penalty_note {
2843         my($self, $conn, $auth, $penalty_ids, $note) = @_;
2844         my $e = new_editor(authtoken=>$auth, xact => 1);
2845         return $e->die_event unless $e->checkauth;
2846     for my $penalty_id (@$penalty_ids) {
2847         my $penalty = $e->search_actor_user_standing_penalty( { id => $penalty_id } )->[0];
2848         if (! $penalty ) { return $e->die_event; }
2849         my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
2850         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2851
2852         $penalty->note( $note ); $penalty->ischanged( 1 );
2853
2854         $e->update_actor_user_standing_penalty($penalty) or return $e->die_event;
2855     }
2856     $e->commit;
2857     return 1;
2858 }
2859
2860 __PACKAGE__->register_method(
2861     method   => "ranged_penalty_thresholds",
2862     api_name => "open-ils.actor.grp_penalty_threshold.ranged.retrieve",
2863     stream   => 1
2864 );
2865
2866 sub ranged_penalty_thresholds {
2867         my($self, $conn, $auth, $context_org) = @_;
2868         my $e = new_editor(authtoken=>$auth);
2869         return $e->event unless $e->checkauth;
2870     return $e->event unless $e->allowed('VIEW_GROUP_PENALTY_THRESHOLD', $context_org);
2871     my $list = $e->search_permission_grp_penalty_threshold([
2872         {org_unit => $U->get_org_ancestors($context_org)},
2873         {order_by => {pgpt => 'id'}}
2874     ]);
2875     $conn->respond($_) for @$list;
2876     return undef;
2877 }
2878
2879
2880
2881 __PACKAGE__->register_method(
2882     method        => "user_retrieve_fleshed_by_id",
2883     authoritative => 1,
2884     api_name      => "open-ils.actor.user.fleshed.retrieve",
2885 );
2886
2887 sub user_retrieve_fleshed_by_id {
2888         my( $self, $client, $auth, $user_id, $fields ) = @_;
2889         my $e = new_editor(authtoken => $auth);
2890         return $e->event unless $e->checkauth;
2891
2892         if( $e->requestor->id != $user_id ) {
2893                 return $e->event unless $e->allowed('VIEW_USER');
2894         }
2895
2896         $fields ||= [
2897                 "cards",
2898                 "card",
2899                 "standing_penalties",
2900                 "addresses",
2901                 "billing_address",
2902                 "mailing_address",
2903                 "stat_cat_entries" ];
2904         return new_flesh_user($user_id, $fields, $e);
2905 }
2906
2907
2908 sub new_flesh_user {
2909
2910         my $id = shift;
2911         my $fields = shift || [];
2912         my $e = shift;
2913
2914     my $fetch_penalties = 0;
2915     if(grep {$_ eq 'standing_penalties'} @$fields) {
2916         $fields = [grep {$_ ne 'standing_penalties'} @$fields];
2917         $fetch_penalties = 1;
2918     }
2919
2920         my $user = $e->retrieve_actor_user(
2921         [
2922         $id,
2923         {
2924                 "flesh"                         => 1,
2925                 "flesh_fields" =>  { "au" => $fields }
2926         }
2927         ]
2928         ) or return $e->event;
2929
2930
2931         if( grep { $_ eq 'addresses' } @$fields ) {
2932
2933                 $user->addresses([]) unless @{$user->addresses};
2934         # don't expose "replaced" addresses by default
2935         $user->addresses([grep {$_->id >= 0} @{$user->addresses}]);
2936         
2937                 if( ref $user->billing_address ) {
2938                         unless( grep { $user->billing_address->id == $_->id } @{$user->addresses} ) {
2939                                 push( @{$user->addresses}, $user->billing_address );
2940                         }
2941                 }
2942         
2943                 if( ref $user->mailing_address ) {
2944                         unless( grep { $user->mailing_address->id == $_->id } @{$user->addresses} ) {
2945                                 push( @{$user->addresses}, $user->mailing_address );
2946                         }
2947                 }
2948         }
2949
2950     if($fetch_penalties) {
2951         # grab the user penalties ranged for this location
2952         $user->standing_penalties(
2953             $e->search_actor_user_standing_penalty([
2954                 {   usr => $id, 
2955                     '-or' => [
2956                         {stop_date => undef},
2957                         {stop_date => {'>' => 'now'}}
2958                     ],
2959                     org_unit => $U->get_org_ancestors($e->requestor->ws_ou)
2960                 },
2961                 {   flesh => 1,
2962                     flesh_fields => {ausp => ['standing_penalty']}
2963                 }
2964             ])
2965         );
2966     }
2967
2968         $e->rollback;
2969         $user->clear_passwd();
2970         return $user;
2971 }
2972
2973
2974
2975
2976 __PACKAGE__->register_method(
2977     method   => "user_retrieve_parts",
2978     api_name => "open-ils.actor.user.retrieve.parts",
2979 );
2980
2981 sub user_retrieve_parts {
2982         my( $self, $client, $auth, $user_id, $fields ) = @_;
2983         my $e = new_editor(authtoken => $auth);
2984         return $e->event unless $e->checkauth;
2985     $user_id ||= $e->requestor->id;
2986         if( $e->requestor->id != $user_id ) {
2987                 return $e->event unless $e->allowed('VIEW_USER');
2988         }
2989         my @resp;
2990         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
2991         push(@resp, $user->$_()) for(@$fields);
2992         return \@resp;
2993 }
2994
2995
2996
2997 __PACKAGE__->register_method(
2998     method    => 'user_opt_in_enabled',
2999     api_name  => 'open-ils.actor.user.org_unit_opt_in.enabled',
3000     signature => '@return 1 if user opt-in is globally enabled, 0 otherwise.'
3001 );
3002
3003 sub user_opt_in_enabled {
3004     my($self, $conn) = @_;
3005     my $sc = OpenSRF::Utils::SettingsClient->new;
3006     return 1 if lc($sc->config_value(share => user => 'opt_in')) eq 'true'; 
3007     return 0;
3008 }
3009     
3010
3011 __PACKAGE__->register_method(
3012     method    => 'user_opt_in_at_org',
3013     api_name  => 'open-ils.actor.user.org_unit_opt_in.check',
3014     signature => q/
3015         @param $auth The auth token
3016         @param user_id The ID of the user to test
3017         @return 1 if the user has opted in at the specified org,
3018             event on error, and 0 otherwise. /
3019 );
3020 sub user_opt_in_at_org {
3021     my($self, $conn, $auth, $user_id) = @_;
3022
3023     # see if we even need to enforce the opt-in value
3024     return 1 unless user_opt_in_enabled($self);
3025
3026         my $e = new_editor(authtoken => $auth);
3027         return $e->event unless $e->checkauth;
3028     my $org_id = $e->requestor->ws_ou;
3029
3030     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3031         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3032
3033     # user is automatically opted-in at the home org
3034     return 1 if $user->home_ou eq $org_id;
3035
3036     my $vals = $e->search_actor_usr_org_unit_opt_in(
3037         {org_unit=>$org_id, usr=>$user_id},{idlist=>1});
3038
3039     return 1 if @$vals;
3040     return 0;
3041 }
3042
3043 __PACKAGE__->register_method(
3044     method    => 'create_user_opt_in_at_org',
3045     api_name  => 'open-ils.actor.user.org_unit_opt_in.create',
3046     signature => q/
3047         @param $auth The auth token
3048         @param user_id The ID of the user to test
3049         @return The ID of the newly created object, event on error./
3050 );
3051
3052 sub create_user_opt_in_at_org {
3053     my($self, $conn, $auth, $user_id) = @_;
3054
3055         my $e = new_editor(authtoken => $auth, xact=>1);
3056         return $e->die_event unless $e->checkauth;
3057     my $org_id = $e->requestor->ws_ou;
3058
3059     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3060         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3061
3062     my $opt_in = Fieldmapper::actor::usr_org_unit_opt_in->new;
3063
3064     $opt_in->org_unit($org_id);
3065     $opt_in->usr($user_id);
3066     $opt_in->staff($e->requestor->id);
3067     $opt_in->opt_in_ts('now');
3068     $opt_in->opt_in_ws($e->requestor->wsid);
3069
3070     $opt_in = $e->create_actor_usr_org_unit_opt_in($opt_in)
3071         or return $e->die_event;
3072
3073     $e->commit;
3074
3075     return $opt_in->id;
3076 }
3077
3078
3079 __PACKAGE__->register_method (
3080         method          => 'retrieve_org_hours',
3081         api_name        => 'open-ils.actor.org_unit.hours_of_operation.retrieve',
3082         signature       => q/
3083         Returns the hours of operation for a specified org unit
3084                 @param authtoken The login session key
3085                 @param org_id The org_unit ID
3086         /
3087 );
3088
3089 sub retrieve_org_hours {
3090     my($self, $conn, $auth, $org_id) = @_;
3091     my $e = new_editor(authtoken => $auth);
3092         return $e->die_event unless $e->checkauth;
3093     $org_id ||= $e->requestor->ws_ou;
3094     return $e->retrieve_actor_org_unit_hours_of_operation($org_id);
3095 }
3096
3097
3098 __PACKAGE__->register_method (
3099         method          => 'verify_user_password',
3100         api_name        => 'open-ils.actor.verify_user_password',
3101         signature       => q/
3102         Given a barcode or username and the MD5 encoded password, 
3103         returns 1 if the password is correct.  Returns 0 otherwise.
3104         /
3105 );
3106
3107 sub verify_user_password {
3108     my($self, $conn, $auth, $barcode, $username, $password) = @_;
3109     my $e = new_editor(authtoken => $auth);
3110         return $e->die_event unless $e->checkauth;
3111     my $user;
3112     my $user_by_barcode;
3113     my $user_by_username;
3114     if($barcode) {
3115         my $card = $e->search_actor_card([
3116             {barcode => $barcode},
3117             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0] or return 0;
3118         $user_by_barcode = $card->usr;
3119         $user = $user_by_barcode;
3120     }
3121     if ($username) {
3122         $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return 0;
3123         $user = $user_by_username;
3124     }
3125     return 0 if (!$user);
3126     return 0 if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id); 
3127     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3128     return 1 if $user->passwd eq $password;
3129     return 0;
3130 }
3131
3132 __PACKAGE__->register_method (
3133         method          => 'retrieve_usr_id_via_barcode_or_usrname',
3134         api_name        => "open-ils.actor.user.retrieve_id_by_barcode_or_username",
3135         signature       => q/
3136         Given a barcode or username returns the id for the user or
3137         a failure event.
3138         /
3139 );
3140
3141 sub retrieve_usr_id_via_barcode_or_usrname {
3142     my($self, $conn, $auth, $barcode, $username) = @_;
3143     my $e = new_editor(authtoken => $auth);
3144         return $e->die_event unless $e->checkauth;
3145     my $id_as_barcode= OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.actor' => app_settings => 'id_as_barcode');
3146     my $user;
3147     my $user_by_barcode;
3148     my $user_by_username;
3149     $logger->info("$id_as_barcode is the ID as BARCODE");
3150     if($barcode) {
3151         my $card = $e->search_actor_card([
3152             {barcode => $barcode},
3153             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
3154         if ($id_as_barcode =~ /^t/i) {
3155             if (!$card) {
3156                 $user = $e->retrieve_actor_user($barcode);
3157                 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$user);
3158             }else {
3159                 $user_by_barcode = $card->usr;
3160                 $user = $user_by_barcode;
3161             }
3162         }else {
3163             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$card);
3164             $user_by_barcode = $card->usr;
3165             $user = $user_by_barcode;
3166         }
3167     }
3168
3169     if ($username) {
3170         $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return OpenILS::Event->new( 'ACTOR_USR_NOT_FOUND' );
3171
3172         $user = $user_by_username;
3173     }
3174         return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if (!$user);
3175         return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id); 
3176     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3177     return $user->id;
3178 }
3179
3180
3181 __PACKAGE__->register_method (
3182         method          => 'merge_users',
3183         api_name        => 'open-ils.actor.user.merge',
3184         signature       => {
3185         desc => q/
3186             Given a list of source users and destination user, transfer all data from the source
3187             to the dest user and delete the source user.  All user related data is 
3188             transferred, including circulations, holds, bookbags, etc.
3189         /
3190     }
3191 );
3192
3193 sub merge_users {
3194     my($self, $conn, $auth, $master_id, $user_ids, $options) = @_;
3195     my $e = new_editor(xact => 1, authtoken => $auth);
3196         return $e->die_event unless $e->checkauth;
3197
3198     # disallow the merge if any subordinate accounts are in collections
3199     my $colls = $e->search_money_collections_tracker({usr => $user_ids}, {idlist => 1});
3200     return OpenILS::Event->new('MERGED_USER_IN_COLLECTIONS', payload => $user_ids) if @$colls;
3201
3202     my $master_user = $e->retrieve_actor_user($master_id) or return $e->die_event;
3203     my $del_addrs = ($U->ou_ancestor_setting_value(
3204         $master_user->home_ou, 'circ.user_merge.delete_addresses', $e)) ? 't' : 'f';
3205     my $del_cards = ($U->ou_ancestor_setting_value(
3206         $master_user->home_ou, 'circ.user_merge.delete_cards', $e)) ? 't' : 'f';
3207     my $deactivate_cards = ($U->ou_ancestor_setting_value(
3208         $master_user->home_ou, 'circ.user_merge.deactivate_cards', $e)) ? 't' : 'f';
3209
3210     for my $src_id (@$user_ids) {
3211         my $src_user = $e->retrieve_actor_user($src_id) or return $e->die_event;
3212
3213         return $e->die_event unless $e->allowed('MERGE_USERS', $src_user->home_ou);
3214         if($src_user->home_ou ne $master_user->home_ou) {
3215             return $e->die_event unless $e->allowed('MERGE_USERS', $master_user->home_ou);
3216         }
3217
3218         return $e->die_event unless 
3219             $e->json_query({from => [
3220                 'actor.usr_merge', 
3221                 $src_id, 
3222                 $master_id,
3223                 $del_addrs,
3224                 $del_cards,
3225                 $deactivate_cards
3226             ]});
3227     }
3228
3229     $e->commit;
3230     return 1;
3231 }
3232
3233
3234 __PACKAGE__->register_method (
3235         method          => 'approve_user_address',
3236         api_name        => 'open-ils.actor.user.pending_address.approve',
3237         signature       => {
3238         desc => q/
3239         /
3240     }
3241 );
3242
3243 sub approve_user_address {
3244     my($self, $conn, $auth, $addr) = @_;
3245     my $e = new_editor(xact => 1, authtoken => $auth);
3246         return $e->die_event unless $e->checkauth;
3247     if(ref $addr) {
3248         # if the caller passes an address object, assume they want to 
3249         # update it first before approving it
3250         $e->update_actor_user_address($addr) or return $e->die_event;
3251     } else {
3252         $addr = $e->retrieve_actor_user_address($addr) or return $e->die_event;
3253     }
3254     my $user = $e->retrieve_actor_user($addr->usr);
3255     return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3256     my $result = $e->json_query({from => ['actor.approve_pending_address', $addr->id]})->[0]
3257         or return $e->die_event;
3258     $e->commit;
3259     return [values %$result]->[0]; 
3260 }
3261
3262
3263 __PACKAGE__->register_method (
3264         method          => 'retrieve_friends',
3265         api_name        => 'open-ils.actor.friends.retrieve',
3266         signature       => {
3267         desc => q/
3268             returns { confirmed: [], pending_out: [], pending_in: []}
3269             pending_out are users I'm requesting friendship with
3270             pending_in are users requesting friendship with me
3271         /
3272     }
3273 );
3274
3275 sub retrieve_friends {
3276     my($self, $conn, $auth, $user_id, $options) = @_;
3277     my $e = new_editor(authtoken => $auth);
3278     return $e->event unless $e->checkauth;
3279     $user_id ||= $e->requestor->id;
3280
3281     if($user_id != $e->requestor->id) {
3282         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3283         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3284     }
3285
3286     return OpenILS::Application::Actor::Friends->retrieve_friends(  
3287         $e, $user_id, $options);
3288 }
3289
3290
3291
3292 __PACKAGE__->register_method (
3293         method          => 'apply_friend_perms',
3294         api_name        => 'open-ils.actor.friends.perms.apply',
3295         signature       => {
3296         desc => q/
3297         /
3298     }
3299 );
3300 sub apply_friend_perms {
3301     my($self, $conn, $auth, $user_id, $delegate_id, @perms) = @_;
3302     my $e = new_editor(authtoken => $auth, xact => 1);
3303     return $e->event unless $e->checkauth;
3304
3305     if($user_id != $e->requestor->id) {
3306         my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3307         return $e->die_event unless $e->allowed('VIEW_USER', $user->home_ou);
3308     }
3309
3310     for my $perm (@perms) {
3311         my $evt = 
3312             OpenILS::Application::Actor::Friends->apply_friend_perm(
3313                 $e, $user_id, $delegate_id, $perm);
3314         return $evt if $evt;
3315     }
3316
3317     $e->commit;
3318     return 1;
3319 }
3320
3321
3322 __PACKAGE__->register_method (
3323         method          => 'update_user_pending_address',
3324         api_name        => 'open-ils.actor.user.address.pending.cud'
3325 );
3326
3327 sub update_user_pending_address {
3328     my($self, $conn, $auth, $addr) = @_;
3329     my $e = new_editor(authtoken => $auth, xact => 1);
3330     return $e->event unless $e->checkauth;
3331
3332     if($addr->usr != $e->requestor->id) {
3333         my $user = $e->retrieve_actor_user($addr->usr) or return $e->die_event;
3334         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3335     }
3336
3337     if($addr->isnew) {
3338         $e->create_actor_user_address($addr) or return $e->die_event;
3339     } elsif($addr->isdeleted) {
3340         $e->delete_actor_user_address($addr) or return $e->die_event;
3341     } else {
3342         $e->update_actor_user_address($addr) or return $e->die_event;
3343     }
3344
3345     $e->commit;
3346     return $addr->id;
3347 }
3348
3349
3350 __PACKAGE__->register_method (
3351         method          => 'user_events',
3352         api_name    => 'open-ils.actor.user.events.circ',
3353     stream      => 1,
3354 );
3355 __PACKAGE__->register_method (
3356         method          => 'user_events',
3357         api_name    => 'open-ils.actor.user.events.ahr',
3358     stream      => 1,
3359 );
3360
3361 sub user_events {
3362     my($self, $conn, $auth, $user_id, $filters) = @_;
3363     my $e = new_editor(authtoken => $auth);
3364     return $e->event unless $e->checkauth;
3365
3366     (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3367     my $user_field = 'usr';
3368
3369     $filters ||= {};
3370     $filters->{target} = { 
3371         select => { $obj_type => ['id'] },
3372         from => $obj_type,
3373         where => {usr => $user_id}
3374     };
3375
3376     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3377     if($e->requestor->id != $user_id) {
3378         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3379     }
3380
3381     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3382     my $req = $ses->request('open-ils.trigger.events_by_target', 
3383         $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3384
3385     while(my $resp = $req->recv) {
3386         my $val = $resp->content;
3387         my $tgt = $val->target;
3388
3389         if($obj_type eq 'circ') {
3390             $tgt->target_copy($e->retrieve_asset_copy($tgt->target_copy));
3391
3392         } elsif($obj_type eq 'ahr') {
3393             $tgt->current_copy($e->retrieve_asset_copy($tgt->current_copy))
3394                 if $tgt->current_copy;
3395         }
3396
3397         $conn->respond($val) if $val;
3398     }
3399
3400     return undef;
3401 }
3402
3403 __PACKAGE__->register_method (
3404         method          => 'copy_events',
3405         api_name    => 'open-ils.actor.copy.events.circ',
3406     stream      => 1,
3407 );
3408 __PACKAGE__->register_method (
3409         method          => 'copy_events',
3410         api_name    => 'open-ils.actor.copy.events.ahr',
3411     stream      => 1,
3412 );
3413
3414 sub copy_events {
3415     my($self, $conn, $auth, $copy_id, $filters) = @_;
3416     my $e = new_editor(authtoken => $auth);
3417     return $e->event unless $e->checkauth;
3418
3419     (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3420
3421     my $copy = $e->retrieve_asset_copy($copy_id) or return $e->event;
3422
3423     my $copy_field = 'target_copy';
3424     $copy_field = 'current_copy' if $obj_type eq 'ahr';
3425
3426     $filters ||= {};
3427     $filters->{target} = { 
3428         select => { $obj_type => ['id'] },
3429         from => $obj_type,
3430         where => {$copy_field => $copy_id}
3431     };
3432
3433
3434     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3435     my $req = $ses->request('open-ils.trigger.events_by_target', 
3436         $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3437
3438     while(my $resp = $req->recv) {
3439         my $val = $resp->content;
3440         my $tgt = $val->target;
3441         
3442         my $user = $e->retrieve_actor_user($tgt->usr);
3443         if($e->requestor->id != $user->id) {
3444             return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3445         }
3446
3447         $tgt->$copy_field($copy);
3448
3449         $tgt->usr($user);
3450         $conn->respond($val) if $val;
3451     }
3452
3453     return undef;
3454 }
3455
3456
3457
3458
3459 __PACKAGE__->register_method (
3460         method          => 'update_events',
3461         api_name    => 'open-ils.actor.user.event.cancel.batch',
3462     stream      => 1,
3463 );
3464 __PACKAGE__->register_method (
3465         method          => 'update_events',
3466         api_name    => 'open-ils.actor.user.event.reset.batch',
3467     stream      => 1,
3468 );
3469
3470 sub update_events {
3471     my($self, $conn, $auth, $event_ids) = @_;
3472     my $e = new_editor(xact => 1, authtoken => $auth);
3473     return $e->die_event unless $e->checkauth;
3474
3475     my $x = 1;
3476     for my $id (@$event_ids) {
3477
3478         # do a little dance to determine what user we are ultimately affecting
3479         my $event = $e->retrieve_action_trigger_event([
3480             $id,
3481             {   flesh => 2,
3482                 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
3483             }
3484         ]) or return $e->die_event;
3485
3486         my $user_id;
3487         if($event->event_def->hook->core_type eq 'circ') {
3488             $user_id = $e->retrieve_action_circulation($event->target)->usr;
3489         } elsif($event->event_def->hook->core_type eq 'ahr') {
3490             $user_id = $e->retrieve_action_hold_request($event->target)->usr;
3491         } else {
3492             return 0;
3493         }
3494
3495         my $user = $e->retrieve_actor_user($user_id);
3496         return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3497
3498         if($self->api_name =~ /cancel/) {
3499             $event->state('invalid');
3500         } elsif($self->api_name =~ /reset/) {
3501             $event->clear_start_time;
3502             $event->clear_update_time;
3503             $event->state('pending');
3504         }
3505
3506         $e->update_action_trigger_event($event) or return $e->die_event;
3507         $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
3508     }
3509
3510     $e->commit;
3511     return {complete => 1};
3512 }
3513
3514
3515 __PACKAGE__->register_method (
3516         method          => 'really_delete_user',
3517         api_name    => 'open-ils.actor.user.delete',
3518     signature   => q/
3519         It anonymizes all personally identifiable information in actor.usr. By calling actor.usr_purge_data() 
3520         it also purges related data from other tables, sometimes by transferring it to a designated destination user.
3521         The usrname field (along with first_given_name and family_name) is updated to id '-PURGED-' now().
3522         dest_usr_id is only required when deleting a user that performs staff functions.
3523     /
3524 );
3525
3526 sub really_delete_user {
3527     my($self, $conn, $auth, $user_id, $dest_user_id) = @_;
3528     my $e = new_editor(authtoken => $auth, xact => 1);
3529     return $e->die_event unless $e->checkauth;
3530     my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3531     return $e->die_event unless $e->allowed('DELETE_USER', $user->home_ou);
3532     my $stat = $e->json_query(
3533         {from => ['actor.usr_delete', $user_id, $dest_user_id]})->[0] 
3534         or return $e->die_event;
3535     $e->commit;
3536     return 1;
3537 }
3538
3539
3540
3541 __PACKAGE__->register_method (
3542         method          => 'user_payments',
3543         api_name    => 'open-ils.actor.user.payments.retrieve',
3544     stream => 1,
3545     signature   => q/
3546         Returns all payments for a given user.  Default order is newest payments first.
3547         @param auth Authentication token
3548         @param user_id The user ID
3549         @param filters An optional hash of filters, including limit, offset, and order_by definitions
3550     /
3551 );
3552
3553 sub user_payments {
3554     my($self, $conn, $auth, $user_id, $filters) = @_;
3555     $filters ||= {};
3556
3557     my $e = new_editor(authtoken => $auth);
3558     return $e->die_event unless $e->checkauth;
3559
3560     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3561     return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
3562
3563     # Find all payments for all transactions for user $user_id
3564     my $query = {
3565         select => {mp => ['id']}, 
3566         from => 'mp', 
3567         where => {
3568             xact => {
3569                 in => {
3570                     select => {mbt => ['id']}, 
3571                     from => 'mbt', 
3572                     where => {usr => $user_id}
3573                 }   
3574             }
3575         },
3576         order_by => [{ # by default, order newest payments first
3577             class => 'mp', 
3578             field => 'payment_ts',
3579             direction => 'desc'
3580         }]
3581     };
3582
3583     for (qw/order_by limit offset/) {
3584         $query->{$_} = $filters->{$_} if defined $filters->{$_};
3585     }
3586
3587     if(defined $filters->{where}) {
3588         foreach (keys %{$filters->{where}}) {
3589             # don't allow the caller to expand the result set to other users
3590             $query->{where}->{$_} = $filters->{where}->{$_} unless $_ eq 'xact'; 
3591         }
3592     }
3593
3594     my $payment_ids = $e->json_query($query);
3595     for my $pid (@$payment_ids) {
3596         my $pay = $e->retrieve_money_payment([
3597             $pid->{id},
3598             {   flesh => 6,
3599                 flesh_fields => {
3600                     mp => ['xact'],
3601                     mbt => ['summary', 'circulation', 'grocery'],
3602                     circ => ['target_copy'],
3603                     acp => ['call_number'],
3604                     acn => ['record']
3605                 }
3606             }
3607         ]);
3608
3609         my $resp = {
3610             mp => $pay,
3611             xact_type => $pay->xact->summary->xact_type,
3612             last_billing_type => $pay->xact->summary->last_billing_type,
3613         };
3614
3615         if($pay->xact->summary->xact_type eq 'circulation') {
3616             $resp->{barcode} = $pay->xact->circulation->target_copy->barcode;
3617             $resp->{title} = $U->record_to_mvr($pay->xact->circulation->target_copy->call_number->record)->title;
3618         }
3619
3620         $pay->xact($pay->xact->id); # de-flesh
3621         $conn->respond($resp);
3622     }
3623
3624     return undef;
3625 }
3626
3627
3628
3629 __PACKAGE__->register_method (
3630         method          => 'negative_balance_users',
3631         api_name    => 'open-ils.actor.users.negative_balance',
3632     stream => 1,
3633     signature   => q/
3634         Returns all users that have an overall negative balance
3635         @param auth Authentication token
3636         @param org_id The context org unit as an ID or list of IDs.  This will be the home 
3637         library of the user.  If no org_unit is specified, no org unit filter is applied
3638     /
3639 );
3640
3641 sub negative_balance_users {
3642     my($self, $conn, $auth, $org_id) = @_;
3643
3644     my $e = new_editor(authtoken => $auth);
3645     return $e->die_event unless $e->checkauth;
3646     return $e->die_event unless $e->allowed('VIEW_USER', $org_id);
3647
3648     my $query = {
3649         select => { 
3650             mous => ['usr', 'balance_owed'], 
3651             au => ['home_ou'], 
3652             mbts => [
3653                 {column => 'last_billing_ts', transform => 'max', aggregate => 1},
3654                 {column => 'last_payment_ts', transform => 'max', aggregate => 1},
3655             ]
3656         }, 
3657         from => { 
3658             mous => { 
3659                 au => { 
3660                     fkey => 'usr', 
3661                     field => 'id', 
3662                     join => { 
3663                         mbts => { 
3664                             key => 'id', 
3665                             field => 'usr' 
3666                         } 
3667                     } 
3668                 } 
3669             } 
3670         }, 
3671         where => {'+mous' => {balance_owed => {'<' => 0}}} 
3672     };
3673
3674     $query->{from}->{mous}->{au}->{filter}->{home_ou} = $org_id if $org_id;
3675
3676     my $list = $e->json_query($query, {timeout => 600});
3677
3678     for my $data (@$list) {
3679         $conn->respond({
3680             usr => $e->retrieve_actor_user([$data->{usr}, {flesh => 1, flesh_fields => {au => ['card']}}]),
3681             balance_owed => $data->{balance_owed},
3682             last_billing_activity => max($data->{last_billing_ts}, $data->{last_payment_ts})
3683         });
3684     }
3685
3686     return undef;
3687 }
3688
3689 __PACKAGE__->register_method(
3690         method  => "request_password_reset",
3691         api_name        => "open-ils.actor.patron.password_reset.request",
3692         signature       => {
3693         desc => "Generates a UUID token usable with the open-ils.actor.patron.password_reset.commit " .
3694                 "method for changing a user's password.  The UUID token is distributed via A/T "      .
3695                 "templates (i.e. email to the user).",
3696         params => [
3697             { desc => 'user_id_type', type => 'string' },
3698             { desc => 'user_id', type => 'string' },
3699         ],
3700         return => {desc => '1 on success, Event on error'}
3701     }
3702 );
3703 sub request_password_reset {
3704     my($self, $conn, $user_id_type, $user_id) = @_;
3705
3706     # Check to see if password reset requests are already being throttled:
3707     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
3708
3709     my $e = new_editor(xact => 1);
3710     my $user;
3711
3712     # Get the user, if any, depending on the input value
3713     if ($user_id_type eq 'username') {
3714         $user = $e->search_actor_user({usrname => $user_id})->[0];
3715         if (!$user) {
3716             $e->die_event;
3717             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
3718         }
3719     } elsif ($user_id_type eq 'barcode') {
3720         my $card = $e->search_actor_card([
3721             {barcode => $user_id},
3722             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
3723         if (!$card) { 
3724             $e->die_event;
3725             return OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
3726         }
3727         $user = $card->usr;
3728     }
3729
3730     # If the user doesn't have an email address, we can't help them
3731     if (!$user->email) {
3732         $e->die_event;
3733         return OpenILS::Event->new('PATRON_NO_EMAIL_ADDRESS');
3734     }
3735     _reset_password_request($conn, $e, $user);
3736 }
3737
3738 # Once we have the user, we can issue the password reset request
3739 # XXX Add a wrapper method that accepts barcode + email input
3740 sub _reset_password_request {
3741     my ($conn, $e, $user) = @_;
3742
3743     # 1. Get throttle threshold and time-to-live from OU_settings
3744     my $aupr_throttle = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_throttle') || 1000;
3745     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
3746
3747     my $threshold_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl)->iso8601();
3748
3749     # 2. Get time of last request and number of active requests (num_active)
3750     my $active_requests = $e->json_query({
3751         from => 'aupr',
3752         select => {
3753             aupr => [
3754                 {
3755                     column => 'uuid',
3756                     transform => 'COUNT'
3757                 },
3758                 {
3759                     column => 'request_time',
3760                     transform => 'MAX'
3761                 }
3762             ]
3763         },
3764         where => {
3765             has_been_reset => { '=' => 'f' },
3766             request_time => { '>' => $threshold_time }
3767         }
3768     });
3769
3770     # Guard against no active requests
3771     if ($active_requests->[0]->{'request_time'}) {
3772         my $last_request = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($active_requests->[0]->{'request_time'}));
3773         my $now = DateTime::Format::ISO8601->new();
3774
3775         # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute)
3776         if (($active_requests->[0]->{'usr'} > $aupr_throttle) &&
3777             ($last_request->add_duration('1 minute') > $now)) {
3778             $cache->put_cache('open-ils.actor.password.throttle', DateTime::Format::ISO8601->new(), 60);
3779             $e->die_event;
3780             return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
3781         }
3782     }
3783
3784     # TODO Check to see if the user is in a password-reset-restricted group
3785
3786     # Otherwise, go ahead and try to get the user.
3787  
3788     # Check the number of active requests for this user
3789     $active_requests = $e->json_query({
3790         from => 'aupr',
3791         select => {
3792             aupr => [
3793                 {
3794                     column => 'usr',
3795                     transform => 'COUNT'
3796                 }
3797             ]
3798         },
3799         where => {
3800             usr => { '=' => $user->id },
3801             has_been_reset => { '=' => 'f' },
3802             request_time => { '>' => $threshold_time }
3803         }
3804     });
3805
3806     $logger->info("User " . $user->id . " has " . $active_requests->[0]->{'usr'} . " active password reset requests.");
3807
3808     # if less than or equal to per-user threshold, proceed; otherwise, return event
3809     my $aupr_per_user_limit = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_per_user_limit') || 3;
3810     if ($active_requests->[0]->{'usr'} > $aupr_per_user_limit) {
3811         $e->die_event;
3812         return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
3813     }
3814
3815     # Create the aupr object and insert into the database
3816     my $reset_request = Fieldmapper::actor::usr_password_reset->new;
3817     my $uuid = create_uuid_as_string(UUID_V4);
3818     $reset_request->uuid($uuid);
3819     $reset_request->usr($user->id);
3820
3821     my $aupr = $e->create_actor_usr_password_reset($reset_request) or return $e->die_event;
3822     $e->commit;
3823
3824     # Create an event to notify user of the URL to reset their password
3825
3826     # Can we stuff this in the user_data param for trigger autocreate?
3827     my $hostname = $U->ou_ancestor_setting_value($user->home_ou, 'lib.hostname') || 'localhost';
3828
3829     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3830     $ses->request('open-ils.trigger.event.autocreate', 'password.reset_request', $aupr, $user->home_ou);
3831
3832     # Trunk only
3833     # $U->create_trigger_event('password.reset_request', $aupr, $user->home_ou);
3834
3835     return 1;
3836 }
3837
3838 __PACKAGE__->register_method(
3839         method  => "commit_password_reset",
3840         api_name        => "open-ils.actor.patron.password_reset.commit",
3841         signature       => {
3842         desc => "Checks a UUID token generated by the open-ils.actor.patron.password_reset.request method for " .
3843                 "validity, and if valid, uses it as authorization for changing the associated user's password " .
3844                 "with the supplied password.",
3845         params => [
3846             { desc => 'uuid', type => 'string' },
3847             { desc => 'password', type => 'string' },
3848         ],
3849         return => {desc => '1 on success, Event on error'}
3850     }
3851 );
3852 sub commit_password_reset {
3853     my($self, $conn, $uuid, $password) = @_;
3854
3855     # Check to see if password reset requests are already being throttled:
3856     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
3857     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
3858     my $throttle = $cache->get_cache('open-ils.actor.password.throttle') || undef;
3859     if ($throttle) {
3860         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
3861     }
3862
3863     my $e = new_editor(xact => 1);
3864
3865     my $aupr = $e->search_actor_usr_password_reset({
3866         uuid => $uuid,
3867         has_been_reset => 0
3868     });
3869
3870     if (!$aupr->[0]) {
3871         $e->die_event;
3872         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
3873     }
3874     my $user_id = $aupr->[0]->usr;
3875     my $user = $e->retrieve_actor_user($user_id);
3876
3877     # Ensure we're still within the TTL for the request
3878     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
3879     my $threshold = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($aupr->[0]->request_time))->add(seconds => $aupr_ttl);
3880     if ($threshold < DateTime->now(time_zone => 'local')) {
3881         $e->die_event;
3882         $logger->info("Password reset request needed to be submitted before $threshold");
3883         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
3884     }
3885
3886     # Check complexity of password against OU-defined regex
3887     my $pw_regex = $U->ou_ancestor_setting_value($user->home_ou, 'global.password_regex');
3888
3889     my $is_strong = 0;
3890     if ($pw_regex) {
3891         # Calling JSON2perl on the $pw_regex causes failure, even before the fancy Unicode regex
3892         # ($pw_regex = OpenSRF::Utils::JSON->JSON2perl($pw_regex)) =~ s/\\u([0-9a-fA-F]{4})/\\x{$1}/gs;
3893         $is_strong = check_password_strength_custom($password, $pw_regex);
3894     } else {
3895         $is_strong = check_password_strength_default($password);
3896     }
3897
3898     if (!$is_strong) {
3899         $e->die_event;
3900         return OpenILS::Event->new('PATRON_PASSWORD_WAS_NOT_STRONG');
3901     }
3902
3903     # All is well; update the password
3904     $user->passwd($password);
3905     $e->update_actor_user($user);
3906
3907     # And flag that this password reset request has been honoured
3908     $aupr->[0]->has_been_reset('t');
3909     $e->update_actor_usr_password_reset($aupr->[0]);
3910     $e->commit;
3911
3912     return 1;
3913 }
3914
3915 sub check_password_strength_default {
3916     my $password = shift;
3917     # Use the default set of checks
3918     if ( (length($password) < 7) or 
3919             ($password !~ m/.*\d+.*/) or 
3920             ($password !~ m/.*[A-Za-z]+.*/)
3921        ) {
3922         return 0;
3923     }
3924     return 1;
3925 }
3926
3927 sub check_password_strength_custom {
3928     my ($password, $pw_regex) = @_;
3929
3930     $pw_regex = qr/$pw_regex/;
3931     if ($password !~  /$pw_regex/) {
3932         return 0;
3933     }
3934     return 1;
3935 }
3936
3937
3938
3939 __PACKAGE__->register_method(
3940     method    => "event_def_opt_in_settings",
3941     api_name  => "open-ils.actor.event_def.opt_in.settings",
3942     stream => 1,
3943     signature => {
3944         desc   => 'Streams the set of "cust" objects that are used as opt-in settings for event definitions',
3945         params => [
3946             { desc => 'Authentication token',  type => 'string'},
3947             { 
3948                 desc => 'Org Unit ID.  (optional).  If no org ID is present, the home_ou of the requesting user is used', 
3949                 type => 'number'
3950             },
3951         ],
3952         return => {
3953             desc => q/set of "cust" objects that are used as opt-in settings for event definitions at the specified org unit/,
3954             type => 'object',
3955             class => 'cust'
3956         }
3957     }
3958 );
3959
3960 sub event_def_opt_in_settings {
3961     my($self, $conn, $auth, $org_id) = @_;
3962     my $e = new_editor(authtoken => $auth);
3963     return $e->event unless $e->checkauth;
3964
3965     if(defined $org_id and $org_id != $e->requestor->home_ou) {
3966         return $e->event unless 
3967             $e->allowed(['VIEW_USER_SETTING_TYPE', 'ADMIN_USER_SETTING_TYPE'], $org_id);
3968     } else {
3969         $org_id = $e->requestor->home_ou;
3970     }
3971
3972     # find all config.user_setting_type's related to event_defs for the requested org unit
3973     my $types = $e->json_query({
3974         select => {cust => ['name']}, 
3975         from => {atevdef => 'cust'}, 
3976         where => {
3977             '+atevdef' => {
3978                 owner => $U->get_org_ancestors($org_id), # context org plus parents
3979                 active => 't'
3980             }
3981         }
3982     });
3983
3984     if(@$types) {
3985         $conn->respond($_) for 
3986             @{$e->search_config_usr_setting_type({name => [map {$_->{name}} @$types]})};
3987     }
3988
3989     return undef;
3990 }
3991
3992
3993 __PACKAGE__->register_method(
3994     method    => "user_visible_circs",
3995     api_name  => "open-ils.actor.history.circ.visible",
3996     stream => 1,
3997     signature => {
3998         desc   => 'Returns the set of opt-in visible circulations accompanied by circulation chain summaries',
3999         params => [
4000             { desc => 'Authentication token',  type => 'string'},
4001             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4002             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4003         ],
4004         return => {
4005             desc => q/An object with 2 fields: circulation and summary.  
4006                 circulation is the "circ" object.   summary is the related "accs" object/,
4007             type => 'object',
4008         }
4009     }
4010 );
4011
4012 __PACKAGE__->register_method(
4013     method    => "user_visible_circs",
4014     api_name  => "open-ils.actor.history.circ.visible.print",
4015     stream => 1,
4016     signature => {
4017         desc   => 'Returns printable output for the set of opt-in visible circulations',
4018         params => [
4019             { desc => 'Authentication token',  type => 'string'},
4020             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4021             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4022         ],
4023         return => {
4024             desc => q/An action_trigger.event object or error event./,
4025             type => 'object',
4026         }
4027     }
4028 );
4029
4030 __PACKAGE__->register_method(
4031     method    => "user_visible_circs",
4032     api_name  => "open-ils.actor.history.circ.visible.email",
4033     stream => 1,
4034     signature => {
4035         desc   => 'Emails the set of opt-in visible circulations to the requestor',
4036         params => [
4037             { desc => 'Authentication token',  type => 'string'},
4038             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4039             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4040         ],
4041         return => {
4042             desc => q/undef, or event on error/
4043         }
4044     }
4045 );
4046
4047 __PACKAGE__->register_method(
4048     method    => "user_visible_circs",
4049     api_name  => "open-ils.actor.history.hold.visible",
4050     stream => 1,
4051     signature => {
4052         desc   => 'Returns the set of opt-in visible holds',
4053         params => [
4054             { desc => 'Authentication token',  type => 'string'},
4055             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4056             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4057         ],
4058         return => {
4059             desc => q/An object with 1 field: "hold"/,
4060             type => 'object',
4061         }
4062     }
4063 );
4064
4065 __PACKAGE__->register_method(
4066     method    => "user_visible_circs",
4067     api_name  => "open-ils.actor.history.hold.visible.print",
4068     stream => 1,
4069     signature => {
4070         desc   => 'Returns printable output for the set of opt-in visible holds',
4071         params => [
4072             { desc => 'Authentication token',  type => 'string'},
4073             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4074             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4075         ],
4076         return => {
4077             desc => q/An action_trigger.event object or error event./,
4078             type => 'object',
4079         }
4080     }
4081 );
4082
4083 __PACKAGE__->register_method(
4084     method    => "user_visible_circs",
4085     api_name  => "open-ils.actor.history.hold.visible.email",
4086     stream => 1,
4087     signature => {
4088         desc   => 'Emails the set of opt-in visible holds to the requestor',
4089         params => [
4090             { desc => 'Authentication token',  type => 'string'},
4091             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4092             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4093         ],
4094         return => {
4095             desc => q/undef, or event on error/
4096         }
4097     }
4098 );
4099
4100 sub user_visible_circs {
4101     my($self, $conn, $auth, $user_id, $options) = @_;
4102
4103     my $is_hold = ($self->api_name =~ /hold/);
4104     my $for_print = ($self->api_name =~ /print/);
4105     my $for_email = ($self->api_name =~ /email/);
4106     my $e = new_editor(authtoken => $auth);
4107     return $e->event unless $e->checkauth;
4108
4109     $user_id ||= $e->requestor->id;
4110     $options ||= {};
4111     $options->{limit} ||= 50;
4112     $options->{offset} ||= 0;
4113
4114     if($user_id != $e->requestor->id) {
4115         my $perm = ($is_hold) ? 'VIEW_HOLD' : 'VIEW_CIRCULATIONS';
4116         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4117         return $e->event unless $e->allowed($perm, $user->home_ou);
4118     }
4119
4120     my $db_func = ($is_hold) ? 'action.usr_visible_holds' : 'action.usr_visible_circs';
4121
4122     my $data = $e->json_query({
4123         from => [$db_func, $user_id],
4124         limit => $$options{limit},
4125         offset => $$options{offset}
4126
4127         # TODO: I only want IDs. code below didn't get me there
4128         # {"select":{"au":[{"column":"id", "result_field":"id", 
4129         # "transform":"action.usr_visible_circs"}]}, "where":{"id":10}, "from":"au"}
4130     },{
4131         substream => 1
4132     });
4133
4134     return undef unless @$data;
4135
4136     if ($for_print) {
4137
4138         # collect the batch of objects
4139
4140         if($is_hold) {
4141
4142             my $hold_list = $e->search_action_hold_request({id => [map { $_->{id} } @$data]});
4143             return $U->fire_object_event(undef, 'ahr.format.history.print', $hold_list, $$hold_list[0]->request_lib);
4144
4145         } else {
4146
4147             my $circ_list = $e->search_action_circulation({id => [map { $_->{id} } @$data]});
4148             return $U->fire_object_event(undef, 'circ.format.history.print', $circ_list, $$circ_list[0]->circ_lib);
4149         }
4150
4151     } elsif ($for_email) {
4152
4153         $conn->respond_complete(1) if $for_email;  # no sense in waiting
4154
4155         foreach (@$data) {
4156
4157             my $id = $_->{id};
4158
4159             if($is_hold) {
4160
4161                 my $hold = $e->retrieve_action_hold_request($id);
4162                 $U->create_events_for_hook('ahr.format.history.email', $hold, $hold->request_lib, 1);
4163                 # events will be fired from action_trigger_runner
4164
4165             } else {
4166
4167                 my $circ = $e->retrieve_action_circulation($id);
4168                 $U->create_events_for_hook('circ.format.history.email', $circ, $circ->circ_lib, 1);
4169                 # events will be fired from action_trigger_runner
4170             }
4171         }
4172
4173     } else { # just give me the data please
4174
4175         foreach (@$data) {
4176
4177             my $id = $_->{id};
4178
4179             if($is_hold) {
4180
4181                 my $hold = $e->retrieve_action_hold_request($id);
4182                 $conn->respond({hold => $hold});
4183
4184             } else {
4185
4186                 my $circ = $e->retrieve_action_circulation($id);
4187                 $conn->respond({
4188                     circ => $circ,
4189                     summary => $U->create_circ_chain_summary($e, $id)
4190                 });
4191             }
4192         }
4193     }
4194
4195     return undef;
4196 }
4197
4198
4199
4200 1;