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