]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
flesh stat_cat_entries in the call number tree method used by Holdings Maintenance...
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Actor.pm
1 package OpenILS::Application::Actor;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
5 use Data::Dumper;
6 $Data::Dumper::Indent = 0;
7 use OpenILS::Event;
8
9 use Digest::MD5 qw(md5_hex);
10
11 use OpenSRF::EX qw(:try);
12 use OpenILS::Perm;
13
14 use OpenILS::Application::AppUtils;
15
16 use OpenILS::Utils::Fieldmapper;
17 use OpenILS::Utils::ModsParser;
18 use OpenSRF::Utils::Logger qw/$logger/;
19 use OpenSRF::Utils qw/:datetime/;
20 use OpenSRF::Utils::SettingsClient;
21
22 use OpenSRF::Utils::Cache;
23
24 use OpenSRF::Utils::JSON;
25 use DateTime;
26 use DateTime::Format::ISO8601;
27 use OpenILS::Const qw/:const/;
28
29 use OpenILS::Application::Actor::Container;
30 use OpenILS::Application::Actor::ClosedDates;
31 use OpenILS::Application::Actor::UserGroups;
32 use OpenILS::Application::Actor::Friends;
33 use OpenILS::Application::Actor::Stage;
34
35 use OpenILS::Utils::CStoreEditor qw/:funcs/;
36 use OpenILS::Utils::Penalty;
37 use List::Util qw/max reduce/;
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     my $home_ou = shift;
460
461     my $fields = [
462                 "cards",
463                 "card",
464                 "standing_penalties",
465                 "addresses",
466                 "billing_address",
467                 "mailing_address",
468                 "stat_cat_entries"
469     ];
470     push @$fields, "home_ou" if $home_ou;
471         return new_flesh_user($id, $fields, $e );
472 }
473
474
475
476
477
478
479 # clone and clear stuff that would break the database
480 sub _clone_patron {
481         my $patron = shift;
482
483         my $new_patron = $patron->clone;
484         # clear these
485         $new_patron->clear_billing_address();
486         $new_patron->clear_mailing_address();
487         $new_patron->clear_addresses();
488         $new_patron->clear_card();
489         $new_patron->clear_cards();
490         $new_patron->clear_id();
491         $new_patron->clear_isnew();
492         $new_patron->clear_ischanged();
493         $new_patron->clear_isdeleted();
494         $new_patron->clear_stat_cat_entries();
495         $new_patron->clear_permissions();
496         $new_patron->clear_standing_penalties();
497
498         return $new_patron;
499 }
500
501
502 sub _add_patron {
503
504         my $session             = shift;
505         my $patron              = shift;
506         my $user_obj    = shift;
507
508         my $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'CREATE_USER');
509         return (undef, $evt) if $evt;
510
511         my $ex = $session->request(
512                 'open-ils.storage.direct.actor.user.search.usrname', $patron->usrname())->gather(1);
513         if( $ex and @$ex ) {
514                 return (undef, OpenILS::Event->new('USERNAME_EXISTS'));
515         }
516
517         $logger->info("Creating new user in the DB with username: ".$patron->usrname());
518
519         my $id = $session->request(
520                 "open-ils.storage.direct.actor.user.create", $patron)->gather(1);
521         return (undef, $U->DB_UPDATE_FAILED($patron)) unless $id;
522
523         $logger->info("Successfully created new user [$id] in DB");
524
525         return ( $session->request( 
526                 "open-ils.storage.direct.actor.user.retrieve", $id)->gather(1), undef );
527 }
528
529
530 sub check_group_perm {
531         my( $session, $requestor, $patron ) = @_;
532         my $evt;
533
534         # first let's see if the requestor has 
535         # priveleges to update this user in any way
536         if( ! $patron->isnew ) {
537                 my $p = $session->request(
538                         'open-ils.storage.direct.actor.user.retrieve', $patron->id )->gather(1);
539
540                 # If we are the requestor (trying to update our own account)
541                 # and we are not trying to change our profile, we're good
542                 if( $p->id == $requestor->id and 
543                                 $p->profile == $patron->profile ) {
544                         return undef;
545                 }
546
547
548                 $evt = group_perm_failed($session, $requestor, $p);
549                 return $evt if $evt;
550         }
551
552         # They are allowed to edit this patron.. can they put the 
553         # patron into the group requested?
554         $evt = group_perm_failed($session, $requestor, $patron);
555         return $evt if $evt;
556         return undef;
557 }
558
559
560 sub group_perm_failed {
561         my( $session, $requestor, $patron ) = @_;
562
563         my $perm;
564         my $grp;
565         my $grpid = $patron->profile;
566
567         do {
568
569                 $logger->debug("user update looking for group perm for group $grpid");
570                 $grp = $session->request(
571                         'open-ils.storage.direct.permission.grp_tree.retrieve', $grpid )->gather(1);
572                 return OpenILS::Event->new('PERMISSION_GRP_TREE_NOT_FOUND') unless $grp;
573
574         } while( !($perm = $grp->application_perm) and ($grpid = $grp->parent) );
575
576         $logger->info("user update checking perm $perm on user ".
577                 $requestor->id." for update/create on user username=".$patron->usrname);
578
579         my $evt = $U->check_perms($requestor->id, $patron->home_ou, $perm);
580         return $evt if $evt;
581         return undef;
582 }
583
584
585
586 sub _update_patron {
587         my( $session, $patron, $user_obj, $noperm) = @_;
588
589         $logger->info("Updating patron ".$patron->id." in DB");
590
591         my $evt;
592
593         if(!$noperm) {
594                 $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'UPDATE_USER');
595                 return (undef, $evt) if $evt;
596         }
597
598         # update the password by itself to avoid the password protection magic
599         if( $patron->passwd ) {
600                 my $s = $session->request(
601                         'open-ils.storage.direct.actor.user.remote_update',
602                         {id => $patron->id}, {passwd => $patron->passwd})->gather(1);
603                 return (undef, $U->DB_UPDATE_FAILED($patron)) unless defined($s);
604                 $patron->clear_passwd;
605         }
606
607         if(!$patron->ident_type) {
608                 $patron->clear_ident_type;
609                 $patron->clear_ident_value;
610         }
611
612     $evt = verify_last_xact($session, $patron);
613     return (undef, $evt) if $evt;
614
615         my $stat = $session->request(
616                 "open-ils.storage.direct.actor.user.update",$patron )->gather(1);
617         return (undef, $U->DB_UPDATE_FAILED($patron)) unless defined($stat);
618
619         return ($patron);
620 }
621
622 sub verify_last_xact {
623     my( $session, $patron ) = @_;
624     return undef unless $patron->id and $patron->id > 0;
625     my $p = $session->request(
626         'open-ils.storage.direct.actor.user.retrieve', $patron->id)->gather(1);
627     my $xact = $p->last_xact_id;
628     return undef unless $xact;
629     $logger->info("user xact = $xact, saving with xact " . $patron->last_xact_id);
630     return OpenILS::Event->new('XACT_COLLISION')
631         if $xact != $patron->last_xact_id;
632     return undef;
633 }
634
635
636 sub _check_dup_ident {
637         my( $session, $patron ) = @_;
638
639         return undef unless $patron->ident_value;
640
641         my $search = {
642                 ident_type      => $patron->ident_type, 
643                 ident_value => $patron->ident_value,
644         };
645
646         $logger->debug("patron update searching for dup ident values: " . 
647                 $patron->ident_type . ':' . $patron->ident_value);
648
649         $search->{id} = {'!=' => $patron->id} if $patron->id and $patron->id > 0;
650
651         my $dups = $session->request(
652                 'open-ils.storage.direct.actor.user.search_where.atomic', $search )->gather(1);
653
654
655         return OpenILS::Event->new('PATRON_DUP_IDENT1', payload => $patron )
656                 if $dups and @$dups;
657
658         return undef;
659 }
660
661
662 sub _add_update_addresses {
663
664         my $session = shift;
665         my $patron = shift;
666         my $new_patron = shift;
667
668         my $evt;
669
670         my $current_id; # id of the address before creation
671
672         for my $address (@{$patron->addresses()}) {
673
674                 next unless ref $address;
675                 $current_id = $address->id();
676
677                 if( $patron->billing_address() and
678                         $patron->billing_address() == $current_id ) {
679                         $logger->info("setting billing addr to $current_id");
680                         $new_patron->billing_address($address->id());
681                         $new_patron->ischanged(1);
682                 }
683         
684                 if( $patron->mailing_address() and
685                         $patron->mailing_address() == $current_id ) {
686                         $new_patron->mailing_address($address->id());
687                         $logger->info("setting mailing addr to $current_id");
688                         $new_patron->ischanged(1);
689                 }
690
691
692                 if($address->isnew()) {
693
694                         $address->usr($new_patron->id());
695
696                         ($address, $evt) = _add_address($session,$address);
697                         return (undef, $evt) if $evt;
698
699                         # we need to get the new id
700                         if( $patron->billing_address() and 
701                                         $patron->billing_address() == $current_id ) {
702                                 $new_patron->billing_address($address->id());
703                                 $logger->info("setting billing addr to $current_id");
704                                 $new_patron->ischanged(1);
705                         }
706
707                         if( $patron->mailing_address() and
708                                         $patron->mailing_address() == $current_id ) {
709                                 $new_patron->mailing_address($address->id());
710                                 $logger->info("setting mailing addr to $current_id");
711                                 $new_patron->ischanged(1);
712                         }
713
714                 } elsif($address->ischanged() ) {
715
716                         ($address, $evt) = _update_address($session, $address);
717                         return (undef, $evt) if $evt;
718
719                 } elsif($address->isdeleted() ) {
720
721                         if( $address->id() == $new_patron->mailing_address() ) {
722                                 $new_patron->clear_mailing_address();
723                                 ($new_patron, $evt) = _update_patron($session, $new_patron);
724                                 return (undef, $evt) if $evt;
725                         }
726
727                         if( $address->id() == $new_patron->billing_address() ) {
728                                 $new_patron->clear_billing_address();
729                                 ($new_patron, $evt) = _update_patron($session, $new_patron);
730                                 return (undef, $evt) if $evt;
731                         }
732
733                         $evt = _delete_address($session, $address);
734                         return (undef, $evt) if $evt;
735                 } 
736         }
737
738         return ( $new_patron, undef );
739 }
740
741
742 # adds an address to the db and returns the address with new id
743 sub _add_address {
744         my($session, $address) = @_;
745         $address->clear_id();
746
747         $logger->info("Creating new address at street ".$address->street1);
748
749         # put the address into the database
750         my $id = $session->request(
751                 "open-ils.storage.direct.actor.user_address.create", $address )->gather(1);
752         return (undef, $U->DB_UPDATE_FAILED($address)) unless $id;
753
754         $address->id( $id );
755         return ($address, undef);
756 }
757
758
759 sub _update_address {
760         my( $session, $address ) = @_;
761
762         $logger->info("Updating address ".$address->id." in the DB");
763
764         my $stat = $session->request(
765                 "open-ils.storage.direct.actor.user_address.update", $address )->gather(1);
766
767         return (undef, $U->DB_UPDATE_FAILED($address)) unless defined($stat);
768         return ($address, undef);
769 }
770
771
772
773 sub _add_update_cards {
774
775         my $session = shift;
776         my $patron = shift;
777         my $new_patron = shift;
778
779         my $evt;
780
781         my $virtual_id; #id of the card before creation
782         for my $card (@{$patron->cards()}) {
783
784                 $card->usr($new_patron->id());
785
786                 if(ref($card) and $card->isnew()) {
787
788                         $virtual_id = $card->id();
789                         ( $card, $evt ) = _add_card($session,$card);
790                         return (undef, $evt) if $evt;
791
792                         #if(ref($patron->card)) { $patron->card($patron->card->id); }
793                         if($patron->card() == $virtual_id) {
794                                 $new_patron->card($card->id());
795                                 $new_patron->ischanged(1);
796                         }
797
798                 } elsif( ref($card) and $card->ischanged() ) {
799                         $evt = _update_card($session, $card);
800                         return (undef, $evt) if $evt;
801                 }
802         }
803
804         return ( $new_patron, undef );
805 }
806
807
808 # adds an card to the db and returns the card with new id
809 sub _add_card {
810         my( $session, $card ) = @_;
811         $card->clear_id();
812
813         $logger->info("Adding new patron card ".$card->barcode);
814
815         my $id = $session->request(
816                 "open-ils.storage.direct.actor.card.create", $card )->gather(1);
817         return (undef, $U->DB_UPDATE_FAILED($card)) unless $id;
818         $logger->info("Successfully created patron card $id");
819
820         $card->id($id);
821         return ( $card, undef );
822 }
823
824
825 # returns event on error.  returns undef otherwise
826 sub _update_card {
827         my( $session, $card ) = @_;
828         $logger->info("Updating patron card ".$card->id);
829
830         my $stat = $session->request(
831                 "open-ils.storage.direct.actor.card.update", $card )->gather(1);
832         return $U->DB_UPDATE_FAILED($card) unless defined($stat);
833         return undef;
834 }
835
836
837
838
839 # returns event on error.  returns undef otherwise
840 sub _delete_address {
841         my( $session, $address ) = @_;
842
843         $logger->info("Deleting address ".$address->id." from DB");
844
845         my $stat = $session->request(
846                 "open-ils.storage.direct.actor.user_address.delete", $address )->gather(1);
847
848         return $U->DB_UPDATE_FAILED($address) unless defined($stat);
849         return undef;
850 }
851
852
853
854 sub _add_survey_responses {
855         my ($session, $patron, $new_patron) = @_;
856
857         $logger->info( "Updating survey responses for patron ".$new_patron->id );
858
859         my $responses = $patron->survey_responses;
860
861         if($responses) {
862
863                 $_->usr($new_patron->id) for (@$responses);
864
865                 my $evt = $U->simplereq( "open-ils.circ", 
866                         "open-ils.circ.survey.submit.user_id", $responses );
867
868                 return (undef, $evt) if defined($U->event_code($evt));
869
870         }
871
872         return ( $new_patron, undef );
873 }
874
875
876 sub _create_stat_maps {
877
878         my($session, $user_session, $patron, $new_patron) = @_;
879
880         my $maps = $patron->stat_cat_entries();
881
882         for my $map (@$maps) {
883
884                 my $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.update";
885
886                 if ($map->isdeleted()) {
887                         $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.delete";
888
889                 } elsif ($map->isnew()) {
890                         $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.create";
891                         $map->clear_id;
892                 }
893
894
895                 $map->target_usr($new_patron->id);
896
897                 #warn "
898                 $logger->info("Updating stat entry with method $method and map $map");
899
900                 my $stat = $session->request($method, $map)->gather(1);
901                 return (undef, $U->DB_UPDATE_FAILED($map)) unless defined($stat);
902
903         }
904
905         return ($new_patron, undef);
906 }
907
908 sub _create_perm_maps {
909
910         my($session, $user_session, $patron, $new_patron) = @_;
911
912         my $maps = $patron->permissions;
913
914         for my $map (@$maps) {
915
916                 my $method = "open-ils.storage.direct.permission.usr_perm_map.update";
917                 if ($map->isdeleted()) {
918                         $method = "open-ils.storage.direct.permission.usr_perm_map.delete";
919                 } elsif ($map->isnew()) {
920                         $method = "open-ils.storage.direct.permission.usr_perm_map.create";
921                         $map->clear_id;
922                 }
923
924
925                 $map->usr($new_patron->id);
926
927                 #warn( "Updating permissions with method $method and session $user_session and map $map" );
928                 $logger->info( "Updating permissions with method $method and map $map" );
929
930                 my $stat = $session->request($method, $map)->gather(1);
931                 return (undef, $U->DB_UPDATE_FAILED($map)) unless defined($stat);
932
933         }
934
935         return ($new_patron, undef);
936 }
937
938
939 __PACKAGE__->register_method(
940     method   => "set_user_work_ous",
941     api_name => "open-ils.actor.user.work_ous.update",
942 );
943
944 sub set_user_work_ous {
945     my $self   = shift;
946     my $client = shift;
947     my $ses    = shift;
948     my $maps   = shift;
949
950         my( $requestor, $evt ) = $apputils->checksesperm( $ses, 'ASSIGN_WORK_ORG_UNIT' );
951         return $evt if $evt;
952
953         my $session = $apputils->start_db_session();
954
955         for my $map (@$maps) {
956
957                 my $method = "open-ils.storage.direct.permission.usr_work_ou_map.update";
958                 if ($map->isdeleted()) {
959                         $method = "open-ils.storage.direct.permission.usr_work_ou_map.delete";
960                 } elsif ($map->isnew()) {
961                         $method = "open-ils.storage.direct.permission.usr_work_ou_map.create";
962                         $map->clear_id;
963                 }
964
965                 #warn( "Updating permissions with method $method and session $ses and map $map" );
966                 $logger->info( "Updating work_ou map with method $method and map $map" );
967
968                 my $stat = $session->request($method, $map)->gather(1);
969                 $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
970
971         }
972
973         $apputils->commit_db_session($session);
974
975         return scalar(@$maps);
976 }
977
978
979 __PACKAGE__->register_method(
980     method   => "set_user_perms",
981     api_name => "open-ils.actor.user.permissions.update",
982 );
983
984 sub set_user_perms {
985         my $self = shift;
986         my $client = shift;
987         my $ses = shift;
988         my $maps = shift;
989
990         my $session = $apputils->start_db_session();
991
992         my( $user_obj, $evt ) = $U->checkses($ses);
993         return $evt if $evt;
994
995         my $perms = $session->request('open-ils.storage.permission.user_perms.atomic', $user_obj->id)->gather(1);
996
997         my $all = undef;
998         $all = 1 if ($U->is_true($user_obj->super_user()));
999     $all = 1 unless ($U->check_perms($user_obj->id, $user_obj->home_ou, 'EVERYTHING'));
1000
1001         for my $map (@$maps) {
1002
1003                 my $method = "open-ils.storage.direct.permission.usr_perm_map.update";
1004                 if ($map->isdeleted()) {
1005                         $method = "open-ils.storage.direct.permission.usr_perm_map.delete";
1006                 } elsif ($map->isnew()) {
1007                         $method = "open-ils.storage.direct.permission.usr_perm_map.create";
1008                         $map->clear_id;
1009                 }
1010
1011                 next if (!$all and !grep { $_->perm eq $map->perm and $U->is_true($_->grantable) and $_->depth <= $map->depth } @$perms);
1012                 #warn( "Updating permissions with method $method and session $ses and map $map" );
1013                 $logger->info( "Updating permissions with method $method and map $map" );
1014
1015                 my $stat = $session->request($method, $map)->gather(1);
1016                 $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
1017
1018         }
1019
1020         $apputils->commit_db_session($session);
1021
1022         return scalar(@$maps);
1023 }
1024
1025
1026 __PACKAGE__->register_method(
1027         method  => "user_retrieve_by_barcode",
1028     authoritative => 1,
1029         api_name        => "open-ils.actor.user.fleshed.retrieve_by_barcode",);
1030
1031 sub user_retrieve_by_barcode {
1032         my($self, $client, $auth, $barcode, $flesh_home_ou) = @_;
1033
1034     my $e = new_editor(authtoken => $auth);
1035     return $e->event unless $e->checkauth;
1036
1037     my $card = $e->search_actor_card({barcode => $barcode})->[0]
1038         or return $e->event;
1039
1040         my $user = flesh_user($card->usr, $e, $flesh_home_ou);
1041     return $e->event unless $e->allowed(
1042         "VIEW_USER", $flesh_home_ou ? $user->home_ou->id : $user->home_ou
1043     );
1044     return $user;
1045 }
1046
1047
1048
1049 __PACKAGE__->register_method(
1050     method        => "get_user_by_id",
1051     authoritative => 1,
1052     api_name      => "open-ils.actor.user.retrieve",
1053 );
1054
1055 sub get_user_by_id {
1056         my ($self, $client, $auth, $id) = @_;
1057         my $e = new_editor(authtoken=>$auth);
1058         return $e->event unless $e->checkauth;
1059         my $user = $e->retrieve_actor_user($id) or return $e->event;
1060         return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);       
1061         return $user;
1062 }
1063
1064
1065 __PACKAGE__->register_method(
1066     method   => "get_org_types",
1067     api_name => "open-ils.actor.org_types.retrieve",
1068 );
1069 sub get_org_types {
1070     return $U->get_org_types();
1071 }
1072
1073
1074 __PACKAGE__->register_method(
1075     method   => "get_user_ident_types",
1076     api_name => "open-ils.actor.user.ident_types.retrieve",
1077 );
1078 my $ident_types;
1079 sub get_user_ident_types {
1080         return $ident_types if $ident_types;
1081         return $ident_types = 
1082                 new_editor()->retrieve_all_config_identification_type();
1083 }
1084
1085
1086 __PACKAGE__->register_method(
1087     method   => "get_org_unit",
1088     api_name => "open-ils.actor.org_unit.retrieve",
1089 );
1090
1091 sub get_org_unit {
1092         my( $self, $client, $user_session, $org_id ) = @_;
1093         my $e = new_editor(authtoken => $user_session);
1094         if(!$org_id) {
1095                 return $e->event unless $e->checkauth;
1096                 $org_id = $e->requestor->ws_ou;
1097         }
1098         my $o = $e->retrieve_actor_org_unit($org_id)
1099                 or return $e->event;
1100         return $o;
1101 }
1102
1103 __PACKAGE__->register_method(
1104     method   => "search_org_unit",
1105     api_name => "open-ils.actor.org_unit_list.search",
1106 );
1107
1108 sub search_org_unit {
1109
1110         my( $self, $client, $field, $value ) = @_;
1111
1112         my $list = OpenILS::Application::AppUtils->simple_scalar_request(
1113                 "open-ils.cstore",
1114                 "open-ils.cstore.direct.actor.org_unit.search.atomic", 
1115                 { $field => $value } );
1116
1117         return $list;
1118 }
1119
1120
1121 # build the org tree
1122
1123 __PACKAGE__->register_method(
1124         method  => "get_org_tree",
1125         api_name        => "open-ils.actor.org_tree.retrieve",
1126         argc            => 0, 
1127         note            => "Returns the entire org tree structure",
1128 );
1129
1130 sub get_org_tree {
1131         my $self = shift;
1132         my $client = shift;
1133         return $U->get_org_tree($client->session->session_locale);
1134 }
1135
1136
1137 __PACKAGE__->register_method(
1138         method  => "get_org_descendants",
1139         api_name        => "open-ils.actor.org_tree.descendants.retrieve"
1140 );
1141
1142 # depth is optional.  org_unit is the id
1143 sub get_org_descendants {
1144         my( $self, $client, $org_unit, $depth ) = @_;
1145
1146     if(ref $org_unit eq 'ARRAY') {
1147         $depth ||= [];
1148         my @trees;
1149         for my $i (0..scalar(@$org_unit)-1) {
1150             my $list = $U->simple_scalar_request(
1151                             "open-ils.storage", 
1152                             "open-ils.storage.actor.org_unit.descendants.atomic",
1153                             $org_unit->[$i], $depth->[$i] );
1154             push(@trees, $U->build_org_tree($list));
1155         }
1156         return \@trees;
1157
1158     } else {
1159             my $orglist = $apputils->simple_scalar_request(
1160                             "open-ils.storage", 
1161                             "open-ils.storage.actor.org_unit.descendants.atomic",
1162                             $org_unit, $depth );
1163             return $U->build_org_tree($orglist);
1164     }
1165 }
1166
1167
1168 __PACKAGE__->register_method(
1169         method  => "get_org_ancestors",
1170         api_name        => "open-ils.actor.org_tree.ancestors.retrieve"
1171 );
1172
1173 # depth is optional.  org_unit is the id
1174 sub get_org_ancestors {
1175         my( $self, $client, $org_unit, $depth ) = @_;
1176         my $orglist = $apputils->simple_scalar_request(
1177                         "open-ils.storage", 
1178                         "open-ils.storage.actor.org_unit.ancestors.atomic",
1179                         $org_unit, $depth );
1180         return $U->build_org_tree($orglist);
1181 }
1182
1183
1184 __PACKAGE__->register_method(
1185         method  => "get_standings",
1186         api_name        => "open-ils.actor.standings.retrieve"
1187 );
1188
1189 my $user_standings;
1190 sub get_standings {
1191         return $user_standings if $user_standings;
1192         return $user_standings = 
1193                 $apputils->simple_scalar_request(
1194                         "open-ils.cstore",
1195                         "open-ils.cstore.direct.config.standing.search.atomic",
1196                         { id => { "!=" => undef } }
1197                 );
1198 }
1199
1200
1201 __PACKAGE__->register_method(
1202     method   => "get_my_org_path",
1203     api_name => "open-ils.actor.org_unit.full_path.retrieve"
1204 );
1205
1206 sub get_my_org_path {
1207         my( $self, $client, $auth, $org_id ) = @_;
1208         my $e = new_editor(authtoken=>$auth);
1209         return $e->event unless $e->checkauth;
1210         $org_id = $e->requestor->ws_ou unless defined $org_id;
1211
1212         return $apputils->simple_scalar_request(
1213                 "open-ils.storage",
1214                 "open-ils.storage.actor.org_unit.full_path.atomic",
1215                 $org_id );
1216 }
1217
1218
1219 __PACKAGE__->register_method(
1220     method   => "patron_adv_search",
1221     api_name => "open-ils.actor.patron.search.advanced"
1222 );
1223 sub patron_adv_search {
1224         my( $self, $client, $auth, $search_hash, 
1225         $search_limit, $search_sort, $include_inactive, $search_depth ) = @_;
1226
1227         my $e = new_editor(authtoken=>$auth);
1228         return $e->event unless $e->checkauth;
1229         return $e->event unless $e->allowed('VIEW_USER');
1230         return $U->storagereq(
1231                 "open-ils.storage.actor.user.crazy_search", $search_hash, 
1232             $search_limit, $search_sort, $include_inactive, $e->requestor->ws_ou, $search_depth);
1233 }
1234
1235
1236 __PACKAGE__->register_method(
1237     method    => "update_passwd",
1238     api_name  => "open-ils.actor.user.password.update",
1239     signature => {
1240         desc   => "Update the operator's password", 
1241         params => [
1242             { desc => 'Authentication token', type => 'string' },
1243             { desc => 'New password',         type => 'string' },
1244             { desc => 'Current password',     type => 'string' }
1245         ],
1246         return => {desc => '1 on success, Event on error or incorrect current password'}
1247     }
1248 );
1249
1250 __PACKAGE__->register_method(
1251     method    => "update_passwd",
1252     api_name  => "open-ils.actor.user.username.update",
1253     signature => {
1254         desc   => "Update the operator's username", 
1255         params => [
1256             { desc => 'Authentication token', type => 'string' },
1257             { desc => 'New username',         type => 'string' }
1258         ],
1259         return => {desc => '1 on success, Event on error'}
1260     }
1261 );
1262
1263 __PACKAGE__->register_method(
1264     method    => "update_passwd",
1265     api_name  => "open-ils.actor.user.email.update",
1266     signature => {
1267         desc   => "Update the operator's email address", 
1268         params => [
1269             { desc => 'Authentication token', type => 'string' },
1270             { desc => 'New email address',    type => 'string' }
1271         ],
1272         return => {desc => '1 on success, Event on error'}
1273     }
1274 );
1275
1276 sub update_passwd {
1277     my( $self, $conn, $auth, $new_val, $orig_pw ) = @_;
1278     my $e = new_editor(xact=>1, authtoken=>$auth);
1279     return $e->die_event unless $e->checkauth;
1280
1281     my $db_user = $e->retrieve_actor_user($e->requestor->id)
1282         or return $e->die_event;
1283     my $api = $self->api_name;
1284
1285     if( $api =~ /password/o ) {
1286         # make sure the original password matches the in-database password
1287         if (md5_hex($orig_pw) ne $db_user->passwd) {
1288             $e->rollback;
1289             return new OpenILS::Event('INCORRECT_PASSWORD');
1290         }
1291         $db_user->passwd($new_val);
1292
1293     } else {
1294
1295         # if we don't clear the password, the user will be updated with
1296         # a hashed version of the hashed version of their password
1297         $db_user->clear_passwd;
1298
1299         if( $api =~ /username/o ) {
1300
1301             # make sure no one else has this username
1302             my $exist = $e->search_actor_user({usrname=>$new_val},{idlist=>1}); 
1303             if (@$exist) {
1304                 $e->rollback;
1305                 return new OpenILS::Event('USERNAME_EXISTS');
1306             }
1307             $db_user->usrname($new_val);
1308
1309         } elsif( $api =~ /email/o ) {
1310             $db_user->email($new_val);
1311         }
1312     }
1313
1314     $e->update_actor_user($db_user) or return $e->die_event;
1315     $e->commit;
1316
1317     # update the cached user to pick up these changes
1318     $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1);
1319     return 1;
1320 }
1321
1322
1323
1324 __PACKAGE__->register_method(
1325     method   => "check_user_perms",
1326     api_name => "open-ils.actor.user.perm.check",
1327     notes    => <<"     NOTES");
1328         Takes a login session, user id, an org id, and an array of perm type strings.  For each
1329         perm type, if the user does *not* have the given permission it is added
1330         to a list which is returned from the method.  If all permissions
1331         are allowed, an empty list is returned
1332         if the logged in user does not match 'user_id', then the logged in user must
1333         have VIEW_PERMISSION priveleges.
1334         NOTES
1335
1336 sub check_user_perms {
1337         my( $self, $client, $login_session, $user_id, $org_id, $perm_types ) = @_;
1338
1339         my( $staff, $evt ) = $apputils->checkses($login_session);
1340         return $evt if $evt;
1341
1342         if($staff->id ne $user_id) {
1343                 if( $evt = $apputils->check_perms(
1344                         $staff->id, $org_id, 'VIEW_PERMISSION') ) {
1345                         return $evt;
1346                 }
1347         }
1348
1349         my @not_allowed;
1350         for my $perm (@$perm_types) {
1351                 if($apputils->check_perms($user_id, $org_id, $perm)) {
1352                         push @not_allowed, $perm;
1353                 }
1354         }
1355
1356         return \@not_allowed
1357 }
1358
1359 __PACKAGE__->register_method(
1360         method  => "check_user_perms2",
1361         api_name        => "open-ils.actor.user.perm.check.multi_org",
1362         notes           => q/
1363                 Checks the permissions on a list of perms and orgs for a user
1364                 @param authtoken The login session key
1365                 @param user_id The id of the user to check
1366                 @param orgs The array of org ids
1367                 @param perms The array of permission names
1368                 @return An array of  [ orgId, permissionName ] arrays that FAILED the check
1369                 if the logged in user does not match 'user_id', then the logged in user must
1370                 have VIEW_PERMISSION priveleges.
1371         /);
1372
1373 sub check_user_perms2 {
1374         my( $self, $client, $authtoken, $user_id, $orgs, $perms ) = @_;
1375
1376         my( $staff, $target, $evt ) = $apputils->checkses_requestor(
1377                 $authtoken, $user_id, 'VIEW_PERMISSION' );
1378         return $evt if $evt;
1379
1380         my @not_allowed;
1381         for my $org (@$orgs) {
1382                 for my $perm (@$perms) {
1383                         if($apputils->check_perms($user_id, $org, $perm)) {
1384                                 push @not_allowed, [ $org, $perm ];
1385                         }
1386                 }
1387         }
1388
1389         return \@not_allowed
1390 }
1391
1392
1393 __PACKAGE__->register_method(
1394         method => 'check_user_perms3',
1395         api_name        => 'open-ils.actor.user.perm.highest_org',
1396         notes           => q/
1397                 Returns the highest org unit id at which a user has a given permission
1398                 If the requestor does not match the target user, the requestor must have
1399                 'VIEW_PERMISSION' rights at the home org unit of the target user
1400                 @param authtoken The login session key
1401                 @param userid The id of the user in question
1402                 @param perm The permission to check
1403                 @return The org unit highest in the org tree within which the user has
1404                 the requested permission
1405         /);
1406
1407 sub check_user_perms3 {
1408         my($self, $client, $authtoken, $user_id, $perm) = @_;
1409         my $e = new_editor(authtoken=>$authtoken);
1410         return $e->event unless $e->checkauth;
1411
1412         my $tree = $U->get_org_tree();
1413
1414     unless($e->requestor->id == $user_id) {
1415         my $user = $e->retrieve_actor_user($user_id)
1416             or return $e->event;
1417         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1418             return $U->find_highest_perm_org($perm, $user_id, $user->home_ou, $tree );
1419     }
1420
1421     return $U->find_highest_perm_org($perm, $user_id, $e->requestor->ws_ou, $tree);
1422 }
1423
1424 __PACKAGE__->register_method(
1425         method => 'user_has_work_perm_at',
1426         api_name        => 'open-ils.actor.user.has_work_perm_at',
1427     authoritative => 1,
1428     signature => {
1429         desc => q/
1430             Returns a set of org unit IDs which represent the highest orgs in 
1431             the org tree where the user has the requested permission.  The
1432             purpose of this method is to return the smallest set of org units
1433             which represent the full expanse of the user's ability to perform
1434             the requested action.  The user whose perms this method should
1435             check is implied by the authtoken. /,
1436         params => [
1437                     {desc => 'authtoken', type => 'string'},
1438             {desc => 'permission name', type => 'string'},
1439             {desc => q/user id, optional.  If present, check perms for 
1440                 this user instead of the logged in user/, type => 'number'},
1441         ],
1442         return => {desc => 'An array of org IDs'}
1443     }
1444 );
1445
1446 sub user_has_work_perm_at {
1447     my($self, $conn, $auth, $perm, $user_id) = @_;
1448     my $e = new_editor(authtoken=>$auth);
1449     return $e->event unless $e->checkauth;
1450     if(defined $user_id) {
1451         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1452         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1453     }
1454     return $U->user_has_work_perm_at($e, $perm, undef, $user_id);
1455 }
1456
1457 __PACKAGE__->register_method(
1458         method => 'user_has_work_perm_at_batch',
1459         api_name        => 'open-ils.actor.user.has_work_perm_at.batch',
1460     authoritative => 1,
1461 );
1462
1463 sub user_has_work_perm_at_batch {
1464     my($self, $conn, $auth, $perms, $user_id) = @_;
1465     my $e = new_editor(authtoken=>$auth);
1466     return $e->event unless $e->checkauth;
1467     if(defined $user_id) {
1468         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1469         return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1470     }
1471     my $map = {};
1472     $map->{$_} = $U->user_has_work_perm_at($e, $_) for @$perms;
1473     return $map;
1474 }
1475
1476
1477
1478 __PACKAGE__->register_method(
1479         method => 'check_user_perms4',
1480         api_name        => 'open-ils.actor.user.perm.highest_org.batch',
1481         notes           => q/
1482                 Returns the highest org unit id at which a user has a given permission
1483                 If the requestor does not match the target user, the requestor must have
1484                 'VIEW_PERMISSION' rights at the home org unit of the target user
1485                 @param authtoken The login session key
1486                 @param userid The id of the user in question
1487                 @param perms An array of perm names to check 
1488                 @return An array of orgId's  representing the org unit 
1489                 highest in the org tree within which the user has the requested permission
1490                 The arrah of orgId's has matches the order of the perms array
1491         /);
1492
1493 sub check_user_perms4 {
1494         my( $self, $client, $authtoken, $userid, $perms ) = @_;
1495         
1496         my( $staff, $target, $org, $evt );
1497
1498         ( $staff, $target, $evt ) = $apputils->checkses_requestor(
1499                 $authtoken, $userid, 'VIEW_PERMISSION' );
1500         return $evt if $evt;
1501
1502         my @arr;
1503         return [] unless ref($perms);
1504         my $tree = $U->get_org_tree();
1505
1506         for my $p (@$perms) {
1507                 push( @arr, $U->find_highest_perm_org( $p, $userid, $target->home_ou, $tree ) );
1508         }
1509         return \@arr;
1510 }
1511
1512
1513 __PACKAGE__->register_method(
1514     method        => "user_fines_summary",
1515     api_name      => "open-ils.actor.user.fines.summary",
1516     authoritative => 1,
1517     signature     => {
1518         desc   => 'Returns a short summary of the users total open fines, '  .
1519                   'excluding voided fines Params are login_session, user_id' ,
1520         params => [
1521             {desc => 'Authentication token', type => 'string'},
1522             {desc => 'User ID',              type => 'string'}  # number?
1523         ],
1524         return => {
1525             desc => "a 'mous' object, event on error",
1526         }
1527     }
1528 );
1529
1530 sub user_fines_summary {
1531         my( $self, $client, $auth, $user_id ) = @_;
1532
1533         my $e = new_editor(authtoken=>$auth);
1534         return $e->event unless $e->checkauth;
1535
1536         if( $user_id ne $e->requestor->id ) {
1537             my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1538                 return $e->event unless 
1539                         $e->allowed('VIEW_USER_FINES_SUMMARY', $user->home_ou);
1540         }
1541
1542     return $e->search_money_open_user_summary({usr => $user_id})->[0];
1543 }
1544
1545
1546 __PACKAGE__->register_method(
1547     method        => "user_opac_vitals",
1548     api_name      => "open-ils.actor.user.opac.vital_stats",
1549     argc          => 1,
1550     authoritative => 1,
1551     signature     => {
1552         desc   => 'Returns a short summary of the users vital stats, including '  .
1553                   'identification information, accumulated balance, number of holds, ' .
1554                   'and current open circulation stats' ,
1555         params => [
1556             {desc => 'Authentication token',                          type => 'string'},
1557             {desc => 'Optional User ID, for use in the staff client', type => 'number'}  # number?
1558         ],
1559         return => {
1560             desc => "An object with four properties: user, fines, checkouts and holds."
1561         }
1562     }
1563 );
1564
1565 sub user_opac_vitals {
1566         my( $self, $client, $auth, $user_id ) = @_;
1567
1568         my $e = new_editor(authtoken=>$auth);
1569         return $e->event unless $e->checkauth;
1570
1571     $user_id ||= $e->requestor->id;
1572
1573     my $user = $e->retrieve_actor_user( $user_id );
1574
1575     my ($fines) = $self
1576         ->method_lookup('open-ils.actor.user.fines.summary')
1577         ->run($auth => $user_id);
1578     return $fines if (defined($U->event_code($fines)));
1579
1580     if (!$fines) {
1581         $fines = new Fieldmapper::money::open_user_summary ();
1582         $fines->balance_owed(0.00);
1583         $fines->total_owed(0.00);
1584         $fines->total_paid(0.00);
1585         $fines->usr($user_id);
1586     }
1587
1588     my ($holds) = $self
1589         ->method_lookup('open-ils.actor.user.hold_requests.count')
1590         ->run($auth => $user_id);
1591     return $holds if (defined($U->event_code($holds)));
1592
1593     my ($out) = $self
1594         ->method_lookup('open-ils.actor.user.checked_out.count')
1595         ->run($auth => $user_id);
1596     return $out if (defined($U->event_code($out)));
1597
1598     $out->{"total_out"} = reduce { $a + $out->{$b} } 0, qw/out overdue long_overdue/;
1599
1600     return {
1601         user => {
1602             first_given_name  => $user->first_given_name,
1603             second_given_name => $user->second_given_name,
1604             family_name       => $user->family_name,
1605             alias             => $user->alias,
1606             usrname           => $user->usrname
1607         },
1608         fines => $fines->to_bare_hash,
1609         checkouts => $out,
1610         holds => $holds
1611     };
1612 }
1613
1614
1615 ##### a small consolidation of related method registrations
1616 my $common_params = [
1617     { desc => 'Authentication token', type => 'string' },
1618     { desc => 'User ID',              type => 'string' },
1619     { desc => 'Transactions type (optional, defaults to all)', type => 'string' },
1620     { desc => 'Options hash.  May contain limit and offset for paged results.', type => 'object' },
1621 ];
1622 my %methods = (
1623     'open-ils.actor.user.transactions'                      => '',
1624     'open-ils.actor.user.transactions.fleshed'              => '',
1625     'open-ils.actor.user.transactions.have_charge'          => ' that have an initial charge',
1626     'open-ils.actor.user.transactions.have_charge.fleshed'  => ' that have an initial charge',
1627     'open-ils.actor.user.transactions.have_balance'         => ' that have an outstanding balance',
1628     'open-ils.actor.user.transactions.have_balance.fleshed' => ' that have an outstanding balance',
1629 );
1630
1631 foreach (keys %methods) {
1632     my %args = (
1633         method    => "user_transactions",
1634         api_name  => $_,
1635         signature => {
1636             desc   => 'For a given user, retrieve a list of '
1637                     . (/\.fleshed/ ? 'fleshed ' : '')
1638                     . 'transactions' . $methods{$_}
1639                     . ' optionally limited to transactions of a given type.',
1640             params => $common_params,
1641             return => {
1642                 desc => "List of objects, or event on error.  Each object is a hash containing: transaction, circ, record. "
1643                       . 'These represent the relevant (mbts) transaction, attached circulation and title pointed to in the circ, respectively.',
1644             }
1645         }
1646     );
1647     $args{authoritative} = 1;
1648     __PACKAGE__->register_method(%args);
1649 }
1650
1651 # Now for the counts
1652 %methods = (
1653     'open-ils.actor.user.transactions.count'              => '',
1654     'open-ils.actor.user.transactions.have_charge.count'  => ' that have an initial charge',
1655     'open-ils.actor.user.transactions.have_balance.count' => ' that have an outstanding balance',
1656 );
1657
1658 foreach (keys %methods) {
1659     my %args = (
1660         method    => "user_transactions",
1661         api_name  => $_,
1662         signature => {
1663             desc   => 'For a given user, retrieve a count of open '
1664                     . 'transactions' . $methods{$_}
1665                     . ' optionally limited to transactions of a given type.',
1666             params => $common_params,
1667             return => { desc => "Integer count of transactions, or event on error" }
1668         }
1669     );
1670     /\.have_balance/ and $args{authoritative} = 1;     # FIXME: I don't know why have_charge isn't authoritative
1671     __PACKAGE__->register_method(%args);
1672 }
1673
1674 __PACKAGE__->register_method(
1675     method        => "user_transactions",
1676     api_name      => "open-ils.actor.user.transactions.have_balance.total",
1677     authoritative => 1,
1678     signature     => {
1679         desc   => 'For a given user, retrieve the total balance owed for open transactions,'
1680                 . ' optionally limited to transactions of a given type.',
1681         params => $common_params,
1682         return => { desc => "Decimal balance value, or event on error" }
1683     }
1684 );
1685
1686
1687 sub user_transactions {
1688         my( $self, $client, $auth, $user_id, $type, $options ) = @_;
1689     $options ||= {};
1690
1691     my $e = new_editor(authtoken => $auth);
1692     return $e->event unless $e->checkauth;
1693
1694     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1695
1696     return $e->event unless 
1697         $e->requestor->id == $user_id or
1698         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
1699
1700     my $api = $self->api_name();
1701
1702     my $filter = ($api =~ /have_balance/o) ?
1703         { 'balance_owed' => { '<>' => 0 } }:
1704         { 'total_owed' => { '>' => 0 } };
1705
1706     my $method = 'open-ils.actor.user.transactions.history.still_open';
1707     $method = "$method.authoritative" if $api =~ /authoritative/;
1708     my ($trans) = $self->method_lookup($method)->run($auth, $user_id, $type, $filter, $options);
1709
1710         if($api =~ /total/o) { 
1711                 my $total = 0.0;
1712         $total += $_->balance_owed for @$trans;
1713                 return $total;
1714         }
1715
1716     ($api =~ /count/o  ) and return scalar @$trans;
1717     ($api !~ /fleshed/o) and return $trans;
1718
1719         my @resp;
1720         for my $t (@$trans) {
1721                         
1722                 if( $t->xact_type ne 'circulation' ) {
1723                         push @resp, {transaction => $t};
1724                         next;
1725                 }
1726
1727         my $circ_data = flesh_circ($e, $t->id);
1728                 push @resp, {transaction => $t, %$circ_data};
1729         }
1730
1731         return \@resp; 
1732
1733
1734
1735 __PACKAGE__->register_method(
1736     method   => "user_transaction_retrieve",
1737     api_name => "open-ils.actor.user.transaction.fleshed.retrieve",
1738     argc     => 1,
1739     authoritative => 1,
1740     notes    => "Returns a fleshed transaction record"
1741 );
1742
1743 __PACKAGE__->register_method(
1744     method   => "user_transaction_retrieve",
1745     api_name => "open-ils.actor.user.transaction.retrieve",
1746     argc     => 1,
1747     authoritative => 1,
1748     notes    => "Returns a transaction record"
1749 );
1750
1751 sub user_transaction_retrieve {
1752         my($self, $client, $auth, $bill_id) = @_;
1753
1754     my $e = new_editor(authtoken => $auth);
1755     return $e->event unless $e->checkauth;
1756
1757     my $trans = $e->retrieve_money_billable_transaction_summary(
1758         [$bill_id, {flesh => 1, flesh_fields => {mbts => ['usr']}}]) or return $e->event;
1759
1760     return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $trans->usr->home_ou);
1761
1762     $trans->usr($trans->usr->id); # de-flesh for backwards compat
1763
1764     return $trans unless $self->api_name =~ /flesh/;
1765     return {transaction => $trans} if $trans->xact_type ne 'circulation';
1766
1767     my $circ_data = flesh_circ($e, $trans->id, 1);
1768
1769         return {transaction => $trans, %$circ_data};
1770 }
1771
1772 sub flesh_circ {
1773     my $e = shift;
1774     my $circ_id = shift;
1775     my $flesh_copy = shift;
1776
1777     my $circ = $e->retrieve_action_circulation([
1778         $circ_id, {
1779             flesh => 3,
1780             flesh_fields => {
1781                 circ => ['target_copy'],
1782                 acp => ['call_number'],
1783                 acn => ['record']
1784             }
1785         }
1786     ]);
1787
1788         my $mods;
1789     my $copy = $circ->target_copy;
1790
1791     if($circ->target_copy->call_number->id == OILS_PRECAT_CALL_NUMBER) {
1792         $mods = new Fieldmapper::metabib::virtual_record;
1793         $mods->doc_id(OILS_PRECAT_RECORD);
1794         $mods->title($copy->dummy_title);
1795         $mods->author($copy->dummy_author);
1796
1797     } else {
1798         $mods = $U->record_to_mvr($circ->target_copy->call_number->record);
1799     }
1800
1801     # more de-fleshiing
1802     $circ->target_copy($circ->target_copy->id);
1803     $copy->call_number($copy->call_number->id);
1804
1805         return {circ => $circ, record => $mods, copy => ($flesh_copy) ? $copy : undef };
1806 }
1807
1808
1809 __PACKAGE__->register_method(
1810     method        => "hold_request_count",
1811     api_name      => "open-ils.actor.user.hold_requests.count",
1812     authoritative => 1,
1813     argc          => 1,
1814     notes         => 'Returns hold ready/total counts'
1815 );
1816         
1817 sub hold_request_count {
1818         my( $self, $client, $login_session, $userid ) = @_;
1819
1820         my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
1821                 $login_session, $userid, 'VIEW_HOLD' );
1822         return $evt if $evt;
1823         
1824
1825         my $holds = $apputils->simple_scalar_request(
1826                         "open-ils.cstore",
1827                         "open-ils.cstore.direct.action.hold_request.search.atomic",
1828                         { 
1829                                 usr => $userid,
1830                                 fulfillment_time => {"=" => undef },
1831                                 cancel_time => undef,
1832                         }
1833         );
1834
1835         my @ready;
1836         for my $h (@$holds) {
1837                 next unless $h->capture_time and $h->current_copy;
1838
1839                 my $copy = $apputils->simple_scalar_request(
1840                         "open-ils.cstore",
1841                         "open-ils.cstore.direct.asset.copy.retrieve",
1842                         $h->current_copy
1843                 );
1844
1845                 if ($copy and $copy->status == 8) {
1846                         push @ready, $h;
1847                 }
1848         }
1849
1850         return { total => scalar(@$holds), ready => scalar(@ready) };
1851 }
1852
1853 __PACKAGE__->register_method(
1854     method        => "checked_out",
1855     api_name      => "open-ils.actor.user.checked_out",
1856     authoritative => 1,
1857     argc          => 2,
1858         signature     => {
1859         desc => "For a given user, returns a structure of circulations objects sorted by out, overdue, lost, claims_returned, long_overdue. "
1860               . "A list of IDs are returned of each type.  Circs marked lost, long_overdue, and claims_returned will not be 'finished' "
1861               . "(i.e., outstanding balance or some other pending action on the circ). "
1862               . "The .count method also includes a 'total' field which sums all open circs.",
1863         params => [
1864             { desc => 'Authentication Token', type => 'string'},
1865             { desc => 'User ID',              type => 'string'},
1866         ],
1867         return => {
1868             desc => 'Returns event on error, or an object with ID lists, like: '
1869                   . '{"out":[12552,451232], "claims_returned":[], "long_overdue":[23421] "overdue":[], "lost":[]}'
1870         },
1871     }
1872 );
1873
1874 __PACKAGE__->register_method(
1875     method        => "checked_out",
1876     api_name      => "open-ils.actor.user.checked_out.count",
1877     authoritative => 1,
1878     argc          => 2,
1879     signature     => q/@see open-ils.actor.user.checked_out/
1880 );
1881
1882 sub checked_out {
1883         my( $self, $conn, $auth, $userid ) = @_;
1884
1885         my $e = new_editor(authtoken=>$auth);
1886         return $e->event unless $e->checkauth;
1887
1888         if( $userid ne $e->requestor->id ) {
1889         my $user = $e->retrieve_actor_user($userid) or return $e->event;
1890                 unless($e->allowed('VIEW_CIRCULATIONS', $user->home_ou)) {
1891
1892             # see if there is a friend link allowing circ.view perms
1893             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
1894                 $e, $userid, $e->requestor->id, 'circ.view');
1895             return $e->event unless $allowed;
1896         }
1897         }
1898
1899         my $count = $self->api_name =~ /count/;
1900         return _checked_out( $count, $e, $userid );
1901 }
1902
1903 sub _checked_out {
1904         my( $iscount, $e, $userid ) = @_;
1905
1906     my %result = (
1907         out => [],
1908         overdue => [],
1909         lost => [],
1910         claims_returned => [],
1911         long_overdue => []
1912     );
1913         my $meth = 'retrieve_action_open_circ_';
1914
1915     if ($iscount) {
1916             $meth .= 'count';
1917         %result = (
1918             out => 0,
1919             overdue => 0,
1920             lost => 0,
1921             claims_returned => 0,
1922             long_overdue => 0
1923         );
1924     } else {
1925             $meth .= 'list';
1926     }
1927
1928     my $data = $e->$meth($userid);
1929
1930     if ($data) {
1931         if ($iscount) {
1932             $result{$_} += $data->$_() for (keys %result);
1933             $result{total} += $data->$_() for (keys %result);
1934         } else {
1935             for my $k (keys %result) {
1936                 $result{$k} = [ grep { $_ > 0 } split( ',', $data->$k()) ];
1937             }
1938         }
1939     }
1940
1941     return \%result;
1942 }
1943
1944
1945
1946 __PACKAGE__->register_method(
1947     method        => "checked_in_with_fines",
1948     api_name      => "open-ils.actor.user.checked_in_with_fines",
1949     authoritative => 1,
1950     argc          => 2,
1951     signature     => q/@see open-ils.actor.user.checked_out/
1952 );
1953
1954 sub checked_in_with_fines {
1955         my( $self, $conn, $auth, $userid ) = @_;
1956
1957         my $e = new_editor(authtoken=>$auth);
1958         return $e->event unless $e->checkauth;
1959
1960         if( $userid ne $e->requestor->id ) {
1961                 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1962         }
1963
1964         # money is owed on these items and they are checked in
1965         my $open = $e->search_action_circulation(
1966                 {
1967                         usr                             => $userid, 
1968                         xact_finish             => undef,
1969                         checkin_time    => { "!=" => undef },
1970                 }
1971         );
1972
1973
1974         my( @lost, @cr, @lo );
1975         for my $c (@$open) {
1976                 push( @lost, $c->id ) if $c->stop_fines eq 'LOST';
1977                 push( @cr, $c->id ) if $c->stop_fines eq 'CLAIMSRETURNED';
1978                 push( @lo, $c->id ) if $c->stop_fines eq 'LONGOVERDUE';
1979         }
1980
1981         return {
1982                 lost            => \@lost,
1983                 claims_returned => \@cr,
1984                 long_overdue            => \@lo
1985         };
1986 }
1987
1988
1989 sub _sigmaker {
1990     my ($api, $desc, $auth) = @_;
1991     $desc = $desc ? (" " . $desc) : '';
1992     my $ids = ($api =~ /ids$/) ? 1 : 0;
1993     my @sig = (
1994         argc      => 1,
1995         method    => "user_transaction_history",
1996         api_name  => "open-ils.actor.user.transactions.$api",
1997         signature => {
1998             desc   => "For a given User ID, returns a list of billable transaction" .
1999                       ($ids ? " id" : '') .
2000                       "s$desc, optionally filtered by type and/or fields in money.billable_xact_summary.  " .
2001                       "The VIEW_USER_TRANSACTIONS permission is required to view another user's transactions",
2002             params => [
2003                 {desc => 'Authentication token',        type => 'string'},
2004                 {desc => 'User ID',                     type => 'number'},
2005                 {desc => 'Transaction type (optional)', type => 'number'},
2006                 {desc => 'Hash of Billable Transaction Summary filters (optional)', type => 'object'}
2007             ],
2008             return => {
2009                 desc => 'List of transaction' . ($ids ? " id" : '') . 's, Event on error'
2010             },
2011         }
2012     );
2013     $auth and push @sig, (authoritative => 1);
2014     return @sig;
2015 }
2016
2017 my %auth_hist_methods = (
2018     'history'             => '',
2019     'history.have_charge' => 'that have an initial charge',
2020     'history.still_open'  => 'that are not finished',
2021     'history.have_balance'         => 'that have a balance',
2022     'history.have_bill'            => 'that have billings',
2023     'history.have_bill_or_payment' => 'that have non-zero-sum billings or at least 1 payment',
2024     'history.have_payment' => 'that have at least 1 payment',
2025 );
2026
2027 foreach (keys %auth_hist_methods) {
2028     __PACKAGE__->register_method(_sigmaker($_,       $auth_hist_methods{$_}, 1));
2029     __PACKAGE__->register_method(_sigmaker("$_.ids", $auth_hist_methods{$_}, 1));
2030     __PACKAGE__->register_method(_sigmaker("$_.fleshed", $auth_hist_methods{$_}, 1));
2031 }
2032
2033 sub user_transaction_history {
2034         my( $self, $conn, $auth, $userid, $type, $filter, $options ) = @_;
2035     $filter ||= {};
2036     $options ||= {};
2037
2038         my $e = new_editor(authtoken=>$auth);
2039         return $e->die_event unless $e->checkauth;
2040
2041         if ($e->requestor->id ne $userid) {
2042         return $e->die_event unless $e->allowed('VIEW_USER_TRANSACTIONS');
2043         }
2044
2045         my $api = $self->api_name;
2046         my @xact_finish  = (xact_finish => undef ) if ($api =~ /history\.still_open$/);     # What about history.still_open.ids?
2047
2048         if(defined($type)) {
2049                 $filter->{'xact_type'} = $type;
2050         }
2051
2052         if($api =~ /have_bill_or_payment/o) {
2053
2054         # transactions that have a non-zero sum across all billings or at least 1 payment
2055         $filter->{'-or'} = {
2056             'balance_owed' => { '<>' => 0 },
2057             'last_payment_ts' => { '<>' => undef }
2058         };
2059
2060     } elsif($api =~ /have_payment/) {
2061
2062         $filter->{last_payment_ts} ||= {'<>' => undef};
2063
2064     } elsif( $api =~ /have_balance/o) {
2065
2066         # transactions that have a non-zero overall balance
2067         $filter->{'balance_owed'} = { '<>' => 0 };
2068
2069         } elsif( $api =~ /have_charge/o) {
2070
2071         # transactions that have at least 1 billing, regardless of whether it was voided
2072         $filter->{'last_billing_ts'} = { '<>' => undef };
2073
2074         } elsif( $api =~ /have_bill/o) {    # needs to be an elsif, or we double-match have_bill_or_payment!
2075
2076         # transactions that have non-zero sum across all billings.  This will exclude
2077         # xacts where all billings have been voided
2078         $filter->{'total_owed'} = { '<>' => 0 };
2079         }
2080
2081     my $options_clause = { order_by => { mbt => 'xact_start DESC' } };
2082     $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'}; 
2083     $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'}; 
2084
2085     my $mbts = $e->search_money_billable_transaction_summary(
2086         [   { usr => $userid, @xact_finish, %$filter },
2087             $options_clause
2088         ]
2089     );
2090
2091     return [map {$_->id} @$mbts] if $api =~ /\.ids/;
2092     return $mbts unless $api =~ /fleshed/;
2093
2094         my @resp;
2095         for my $t (@$mbts) {
2096                         
2097                 if( $t->xact_type ne 'circulation' ) {
2098                         push @resp, {transaction => $t};
2099                         next;
2100                 }
2101
2102         my $circ_data = flesh_circ($e, $t->id);
2103                 push @resp, {transaction => $t, %$circ_data};
2104         }
2105
2106         return \@resp; 
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 
3525         $e->requestor->id == $user_id or
3526         $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
3527
3528     # Find all payments for all transactions for user $user_id
3529     my $query = {
3530         select => {mp => ['id']}, 
3531         from => 'mp', 
3532         where => {
3533             xact => {
3534                 in => {
3535                     select => {mbt => ['id']}, 
3536                     from => 'mbt', 
3537                     where => {usr => $user_id}
3538                 }   
3539             }
3540         },
3541         order_by => [{ # by default, order newest payments first
3542             class => 'mp', 
3543             field => 'payment_ts',
3544             direction => 'desc'
3545         }]
3546     };
3547
3548     for (qw/order_by limit offset/) {
3549         $query->{$_} = $filters->{$_} if defined $filters->{$_};
3550     }
3551
3552     if(defined $filters->{where}) {
3553         foreach (keys %{$filters->{where}}) {
3554             # don't allow the caller to expand the result set to other users
3555             $query->{where}->{$_} = $filters->{where}->{$_} unless $_ eq 'xact'; 
3556         }
3557     }
3558
3559     my $payment_ids = $e->json_query($query);
3560     for my $pid (@$payment_ids) {
3561         my $pay = $e->retrieve_money_payment([
3562             $pid->{id},
3563             {   flesh => 6,
3564                 flesh_fields => {
3565                     mp => ['xact'],
3566                     mbt => ['summary', 'circulation', 'grocery'],
3567                     circ => ['target_copy'],
3568                     acp => ['call_number'],
3569                     acn => ['record']
3570                 }
3571             }
3572         ]);
3573
3574         my $resp = {
3575             mp => $pay,
3576             xact_type => $pay->xact->summary->xact_type,
3577             last_billing_type => $pay->xact->summary->last_billing_type,
3578         };
3579
3580         if($pay->xact->summary->xact_type eq 'circulation') {
3581             $resp->{barcode} = $pay->xact->circulation->target_copy->barcode;
3582             $resp->{title} = $U->record_to_mvr($pay->xact->circulation->target_copy->call_number->record)->title;
3583         }
3584
3585         $pay->xact($pay->xact->id); # de-flesh
3586         $conn->respond($resp);
3587     }
3588
3589     return undef;
3590 }
3591
3592
3593
3594 __PACKAGE__->register_method (
3595         method          => 'negative_balance_users',
3596         api_name    => 'open-ils.actor.users.negative_balance',
3597     stream => 1,
3598     signature   => q/
3599         Returns all users that have an overall negative balance
3600         @param auth Authentication token
3601         @param org_id The context org unit as an ID or list of IDs.  This will be the home 
3602         library of the user.  If no org_unit is specified, no org unit filter is applied
3603     /
3604 );
3605
3606 sub negative_balance_users {
3607     my($self, $conn, $auth, $org_id) = @_;
3608
3609     my $e = new_editor(authtoken => $auth);
3610     return $e->die_event unless $e->checkauth;
3611     return $e->die_event unless $e->allowed('VIEW_USER', $org_id);
3612
3613     my $query = {
3614         select => { 
3615             mous => ['usr', 'balance_owed'], 
3616             au => ['home_ou'], 
3617             mbts => [
3618                 {column => 'last_billing_ts', transform => 'max', aggregate => 1},
3619                 {column => 'last_payment_ts', transform => 'max', aggregate => 1},
3620             ]
3621         }, 
3622         from => { 
3623             mous => { 
3624                 au => { 
3625                     fkey => 'usr', 
3626                     field => 'id', 
3627                     join => { 
3628                         mbts => { 
3629                             key => 'id', 
3630                             field => 'usr' 
3631                         } 
3632                     } 
3633                 } 
3634             } 
3635         }, 
3636         where => {'+mous' => {balance_owed => {'<' => 0}}} 
3637     };
3638
3639     $query->{from}->{mous}->{au}->{filter}->{home_ou} = $org_id if $org_id;
3640
3641     my $list = $e->json_query($query, {timeout => 600});
3642
3643     for my $data (@$list) {
3644         $conn->respond({
3645             usr => $e->retrieve_actor_user([$data->{usr}, {flesh => 1, flesh_fields => {au => ['card']}}]),
3646             balance_owed => $data->{balance_owed},
3647             last_billing_activity => max($data->{last_billing_ts}, $data->{last_payment_ts})
3648         });
3649     }
3650
3651     return undef;
3652 }
3653
3654 __PACKAGE__->register_method(
3655         method  => "request_password_reset",
3656         api_name        => "open-ils.actor.patron.password_reset.request",
3657         signature       => {
3658         desc => "Generates a UUID token usable with the open-ils.actor.patron.password_reset.commit " .
3659                 "method for changing a user's password.  The UUID token is distributed via A/T "      .
3660                 "templates (i.e. email to the user).",
3661         params => [
3662             { desc => 'user_id_type', type => 'string' },
3663             { desc => 'user_id', type => 'string' },
3664             { desc => 'optional (based on library setting) matching email address for authorizing request', type => 'string' },
3665         ],
3666         return => {desc => '1 on success, Event on error'}
3667     }
3668 );
3669 sub request_password_reset {
3670     my($self, $conn, $user_id_type, $user_id, $email) = @_;
3671
3672     # Check to see if password reset requests are already being throttled:
3673     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
3674
3675     my $e = new_editor(xact => 1);
3676     my $user;
3677
3678     # Get the user, if any, depending on the input value
3679     if ($user_id_type eq 'username') {
3680         $user = $e->search_actor_user({usrname => $user_id})->[0];
3681         if (!$user) {
3682             $e->die_event;
3683             return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
3684         }
3685     } elsif ($user_id_type eq 'barcode') {
3686         my $card = $e->search_actor_card([
3687             {barcode => $user_id},
3688             {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
3689         if (!$card) { 
3690             $e->die_event;
3691             return OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
3692         }
3693         $user = $card->usr;
3694     }
3695     
3696     # If the user doesn't have an email address, we can't help them
3697     if (!$user->email) {
3698         $e->die_event;
3699         return OpenILS::Event->new('PATRON_NO_EMAIL_ADDRESS');
3700     }
3701     
3702     my $email_must_match = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_requires_matching_email');
3703     if ($email_must_match) {
3704         if ($user->email ne $email) {
3705             return OpenILS::Event->new('EMAIL_VERIFICATION_FAILED');
3706         }
3707     }
3708
3709     _reset_password_request($conn, $e, $user);
3710 }
3711
3712 # Once we have the user, we can issue the password reset request
3713 # XXX Add a wrapper method that accepts barcode + email input
3714 sub _reset_password_request {
3715     my ($conn, $e, $user) = @_;
3716
3717     # 1. Get throttle threshold and time-to-live from OU_settings
3718     my $aupr_throttle = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_throttle') || 1000;
3719     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
3720
3721     my $threshold_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl)->iso8601();
3722
3723     # 2. Get time of last request and number of active requests (num_active)
3724     my $active_requests = $e->json_query({
3725         from => 'aupr',
3726         select => {
3727             aupr => [
3728                 {
3729                     column => 'uuid',
3730                     transform => 'COUNT'
3731                 },
3732                 {
3733                     column => 'request_time',
3734                     transform => 'MAX'
3735                 }
3736             ]
3737         },
3738         where => {
3739             has_been_reset => { '=' => 'f' },
3740             request_time => { '>' => $threshold_time }
3741         }
3742     });
3743
3744     # Guard against no active requests
3745     if ($active_requests->[0]->{'request_time'}) {
3746         my $last_request = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($active_requests->[0]->{'request_time'}));
3747         my $now = DateTime::Format::ISO8601->new();
3748
3749         # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute)
3750         if (($active_requests->[0]->{'usr'} > $aupr_throttle) &&
3751             ($last_request->add_duration('1 minute') > $now)) {
3752             $cache->put_cache('open-ils.actor.password.throttle', DateTime::Format::ISO8601->new(), 60);
3753             $e->die_event;
3754             return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
3755         }
3756     }
3757
3758     # TODO Check to see if the user is in a password-reset-restricted group
3759
3760     # Otherwise, go ahead and try to get the user.
3761  
3762     # Check the number of active requests for this user
3763     $active_requests = $e->json_query({
3764         from => 'aupr',
3765         select => {
3766             aupr => [
3767                 {
3768                     column => 'usr',
3769                     transform => 'COUNT'
3770                 }
3771             ]
3772         },
3773         where => {
3774             usr => { '=' => $user->id },
3775             has_been_reset => { '=' => 'f' },
3776             request_time => { '>' => $threshold_time }
3777         }
3778     });
3779
3780     $logger->info("User " . $user->id . " has " . $active_requests->[0]->{'usr'} . " active password reset requests.");
3781
3782     # if less than or equal to per-user threshold, proceed; otherwise, return event
3783     my $aupr_per_user_limit = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_per_user_limit') || 3;
3784     if ($active_requests->[0]->{'usr'} > $aupr_per_user_limit) {
3785         $e->die_event;
3786         return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
3787     }
3788
3789     # Create the aupr object and insert into the database
3790     my $reset_request = Fieldmapper::actor::usr_password_reset->new;
3791     my $uuid = create_uuid_as_string(UUID_V4);
3792     $reset_request->uuid($uuid);
3793     $reset_request->usr($user->id);
3794
3795     my $aupr = $e->create_actor_usr_password_reset($reset_request) or return $e->die_event;
3796     $e->commit;
3797
3798     # Create an event to notify user of the URL to reset their password
3799
3800     # Can we stuff this in the user_data param for trigger autocreate?
3801     my $hostname = $U->ou_ancestor_setting_value($user->home_ou, 'lib.hostname') || 'localhost';
3802
3803     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3804     $ses->request('open-ils.trigger.event.autocreate', 'password.reset_request', $aupr, $user->home_ou);
3805
3806     # Trunk only
3807     # $U->create_trigger_event('password.reset_request', $aupr, $user->home_ou);
3808
3809     return 1;
3810 }
3811
3812 __PACKAGE__->register_method(
3813         method  => "commit_password_reset",
3814         api_name        => "open-ils.actor.patron.password_reset.commit",
3815         signature       => {
3816         desc => "Checks a UUID token generated by the open-ils.actor.patron.password_reset.request method for " .
3817                 "validity, and if valid, uses it as authorization for changing the associated user's password " .
3818                 "with the supplied password.",
3819         params => [
3820             { desc => 'uuid', type => 'string' },
3821             { desc => 'password', type => 'string' },
3822         ],
3823         return => {desc => '1 on success, Event on error'}
3824     }
3825 );
3826 sub commit_password_reset {
3827     my($self, $conn, $uuid, $password) = @_;
3828
3829     # Check to see if password reset requests are already being throttled:
3830     # 0. Check cache to see if we're in throttle mode (avoid hitting database)
3831     $cache ||= OpenSRF::Utils::Cache->new("global", 0);
3832     my $throttle = $cache->get_cache('open-ils.actor.password.throttle') || undef;
3833     if ($throttle) {
3834         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
3835     }
3836
3837     my $e = new_editor(xact => 1);
3838
3839     my $aupr = $e->search_actor_usr_password_reset({
3840         uuid => $uuid,
3841         has_been_reset => 0
3842     });
3843
3844     if (!$aupr->[0]) {
3845         $e->die_event;
3846         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
3847     }
3848     my $user_id = $aupr->[0]->usr;
3849     my $user = $e->retrieve_actor_user($user_id);
3850
3851     # Ensure we're still within the TTL for the request
3852     my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
3853     my $threshold = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($aupr->[0]->request_time))->add(seconds => $aupr_ttl);
3854     if ($threshold < DateTime->now(time_zone => 'local')) {
3855         $e->die_event;
3856         $logger->info("Password reset request needed to be submitted before $threshold");
3857         return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
3858     }
3859
3860     # Check complexity of password against OU-defined regex
3861     my $pw_regex = $U->ou_ancestor_setting_value($user->home_ou, 'global.password_regex');
3862
3863     my $is_strong = 0;
3864     if ($pw_regex) {
3865         # Calling JSON2perl on the $pw_regex causes failure, even before the fancy Unicode regex
3866         # ($pw_regex = OpenSRF::Utils::JSON->JSON2perl($pw_regex)) =~ s/\\u([0-9a-fA-F]{4})/\\x{$1}/gs;
3867         $is_strong = check_password_strength_custom($password, $pw_regex);
3868     } else {
3869         $is_strong = check_password_strength_default($password);
3870     }
3871
3872     if (!$is_strong) {
3873         $e->die_event;
3874         return OpenILS::Event->new('PATRON_PASSWORD_WAS_NOT_STRONG');
3875     }
3876
3877     # All is well; update the password
3878     $user->passwd($password);
3879     $e->update_actor_user($user);
3880
3881     # And flag that this password reset request has been honoured
3882     $aupr->[0]->has_been_reset('t');
3883     $e->update_actor_usr_password_reset($aupr->[0]);
3884     $e->commit;
3885
3886     return 1;
3887 }
3888
3889 sub check_password_strength_default {
3890     my $password = shift;
3891     # Use the default set of checks
3892     if ( (length($password) < 7) or 
3893             ($password !~ m/.*\d+.*/) or 
3894             ($password !~ m/.*[A-Za-z]+.*/)
3895        ) {
3896         return 0;
3897     }
3898     return 1;
3899 }
3900
3901 sub check_password_strength_custom {
3902     my ($password, $pw_regex) = @_;
3903
3904     $pw_regex = qr/$pw_regex/;
3905     if ($password !~  /$pw_regex/) {
3906         return 0;
3907     }
3908     return 1;
3909 }
3910
3911
3912
3913 __PACKAGE__->register_method(
3914     method    => "event_def_opt_in_settings",
3915     api_name  => "open-ils.actor.event_def.opt_in.settings",
3916     stream => 1,
3917     signature => {
3918         desc   => 'Streams the set of "cust" objects that are used as opt-in settings for event definitions',
3919         params => [
3920             { desc => 'Authentication token',  type => 'string'},
3921             { 
3922                 desc => 'Org Unit ID.  (optional).  If no org ID is present, the home_ou of the requesting user is used', 
3923                 type => 'number'
3924             },
3925         ],
3926         return => {
3927             desc => q/set of "cust" objects that are used as opt-in settings for event definitions at the specified org unit/,
3928             type => 'object',
3929             class => 'cust'
3930         }
3931     }
3932 );
3933
3934 sub event_def_opt_in_settings {
3935     my($self, $conn, $auth, $org_id) = @_;
3936     my $e = new_editor(authtoken => $auth);
3937     return $e->event unless $e->checkauth;
3938
3939     if(defined $org_id and $org_id != $e->requestor->home_ou) {
3940         return $e->event unless 
3941             $e->allowed(['VIEW_USER_SETTING_TYPE', 'ADMIN_USER_SETTING_TYPE'], $org_id);
3942     } else {
3943         $org_id = $e->requestor->home_ou;
3944     }
3945
3946     # find all config.user_setting_type's related to event_defs for the requested org unit
3947     my $types = $e->json_query({
3948         select => {cust => ['name']}, 
3949         from => {atevdef => 'cust'}, 
3950         where => {
3951             '+atevdef' => {
3952                 owner => $U->get_org_ancestors($org_id), # context org plus parents
3953                 active => 't'
3954             }
3955         }
3956     });
3957
3958     if(@$types) {
3959         $conn->respond($_) for 
3960             @{$e->search_config_usr_setting_type({name => [map {$_->{name}} @$types]})};
3961     }
3962
3963     return undef;
3964 }
3965
3966
3967 __PACKAGE__->register_method(
3968     method    => "user_visible_circs",
3969     api_name  => "open-ils.actor.history.circ.visible",
3970     stream => 1,
3971     signature => {
3972         desc   => 'Returns the set of opt-in visible circulations accompanied by circulation chain summaries',
3973         params => [
3974             { desc => 'Authentication token',  type => 'string'},
3975             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
3976             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
3977         ],
3978         return => {
3979             desc => q/An object with 2 fields: circulation and summary.  
3980                 circulation is the "circ" object.   summary is the related "accs" object/,
3981             type => 'object',
3982         }
3983     }
3984 );
3985
3986 __PACKAGE__->register_method(
3987     method    => "user_visible_circs",
3988     api_name  => "open-ils.actor.history.circ.visible.print",
3989     stream => 1,
3990     signature => {
3991         desc   => 'Returns printable output for the set of opt-in visible circulations',
3992         params => [
3993             { desc => 'Authentication token',  type => 'string'},
3994             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
3995             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
3996         ],
3997         return => {
3998             desc => q/An action_trigger.event object or error event./,
3999             type => 'object',
4000         }
4001     }
4002 );
4003
4004 __PACKAGE__->register_method(
4005     method    => "user_visible_circs",
4006     api_name  => "open-ils.actor.history.circ.visible.email",
4007     stream => 1,
4008     signature => {
4009         desc   => 'Emails the set of opt-in visible circulations to the requestor',
4010         params => [
4011             { desc => 'Authentication token',  type => 'string'},
4012             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4013             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4014         ],
4015         return => {
4016             desc => q/undef, or event on error/
4017         }
4018     }
4019 );
4020
4021 __PACKAGE__->register_method(
4022     method    => "user_visible_circs",
4023     api_name  => "open-ils.actor.history.hold.visible",
4024     stream => 1,
4025     signature => {
4026         desc   => 'Returns the set of opt-in visible holds',
4027         params => [
4028             { desc => 'Authentication token',  type => 'string'},
4029             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4030             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4031         ],
4032         return => {
4033             desc => q/An object with 1 field: "hold"/,
4034             type => 'object',
4035         }
4036     }
4037 );
4038
4039 __PACKAGE__->register_method(
4040     method    => "user_visible_circs",
4041     api_name  => "open-ils.actor.history.hold.visible.print",
4042     stream => 1,
4043     signature => {
4044         desc   => 'Returns printable output for the set of opt-in visible holds',
4045         params => [
4046             { desc => 'Authentication token',  type => 'string'},
4047             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4048             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4049         ],
4050         return => {
4051             desc => q/An action_trigger.event object or error event./,
4052             type => 'object',
4053         }
4054     }
4055 );
4056
4057 __PACKAGE__->register_method(
4058     method    => "user_visible_circs",
4059     api_name  => "open-ils.actor.history.hold.visible.email",
4060     stream => 1,
4061     signature => {
4062         desc   => 'Emails the set of opt-in visible holds to the requestor',
4063         params => [
4064             { desc => 'Authentication token',  type => 'string'},
4065             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
4066             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
4067         ],
4068         return => {
4069             desc => q/undef, or event on error/
4070         }
4071     }
4072 );
4073
4074 sub user_visible_circs {
4075     my($self, $conn, $auth, $user_id, $options) = @_;
4076
4077     my $is_hold = ($self->api_name =~ /hold/);
4078     my $for_print = ($self->api_name =~ /print/);
4079     my $for_email = ($self->api_name =~ /email/);
4080     my $e = new_editor(authtoken => $auth);
4081     return $e->event unless $e->checkauth;
4082
4083     $user_id ||= $e->requestor->id;
4084     $options ||= {};
4085     $options->{limit} ||= 50;
4086     $options->{offset} ||= 0;
4087
4088     if($user_id != $e->requestor->id) {
4089         my $perm = ($is_hold) ? 'VIEW_HOLD' : 'VIEW_CIRCULATIONS';
4090         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4091         return $e->event unless $e->allowed($perm, $user->home_ou);
4092     }
4093
4094     my $db_func = ($is_hold) ? 'action.usr_visible_holds' : 'action.usr_visible_circs';
4095
4096     my $data = $e->json_query({
4097         from => [$db_func, $user_id],
4098         limit => $$options{limit},
4099         offset => $$options{offset}
4100
4101         # TODO: I only want IDs. code below didn't get me there
4102         # {"select":{"au":[{"column":"id", "result_field":"id", 
4103         # "transform":"action.usr_visible_circs"}]}, "where":{"id":10}, "from":"au"}
4104     },{
4105         substream => 1
4106     });
4107
4108     return undef unless @$data;
4109
4110     if ($for_print) {
4111
4112         # collect the batch of objects
4113
4114         if($is_hold) {
4115
4116             my $hold_list = $e->search_action_hold_request({id => [map { $_->{id} } @$data]});
4117             return $U->fire_object_event(undef, 'ahr.format.history.print', $hold_list, $$hold_list[0]->request_lib);
4118
4119         } else {
4120
4121             my $circ_list = $e->search_action_circulation({id => [map { $_->{id} } @$data]});
4122             return $U->fire_object_event(undef, 'circ.format.history.print', $circ_list, $$circ_list[0]->circ_lib);
4123         }
4124
4125     } elsif ($for_email) {
4126
4127         $conn->respond_complete(1) if $for_email;  # no sense in waiting
4128
4129         foreach (@$data) {
4130
4131             my $id = $_->{id};
4132
4133             if($is_hold) {
4134
4135                 my $hold = $e->retrieve_action_hold_request($id);
4136                 $U->create_events_for_hook('ahr.format.history.email', $hold, $hold->request_lib, undef, undef, 1);
4137                 # events will be fired from action_trigger_runner
4138
4139             } else {
4140
4141                 my $circ = $e->retrieve_action_circulation($id);
4142                 $U->create_events_for_hook('circ.format.history.email', $circ, $circ->circ_lib, undef, undef, 1);
4143                 # events will be fired from action_trigger_runner
4144             }
4145         }
4146
4147     } else { # just give me the data please
4148
4149         foreach (@$data) {
4150
4151             my $id = $_->{id};
4152
4153             if($is_hold) {
4154
4155                 my $hold = $e->retrieve_action_hold_request($id);
4156                 $conn->respond({hold => $hold});
4157
4158             } else {
4159
4160                 my $circ = $e->retrieve_action_circulation($id);
4161                 $conn->respond({
4162                     circ => $circ,
4163                     summary => $U->create_circ_chain_summary($e, $id)
4164                 });
4165             }
4166         }
4167     }
4168
4169     return undef;
4170 }
4171
4172 __PACKAGE__->register_method(
4173     method     => "user_saved_search_cud",
4174     api_name   => "open-ils.actor.user.saved_search.cud",
4175     stream     => 1,
4176     signature  => {
4177         desc   => 'Create/Update/Delete Access to user saved searches',
4178         params => [
4179             { desc => 'Authentication token', type => 'string' },
4180             { desc => 'Saved Search Object', type => 'object', class => 'auss' }
4181         ],
4182         return => {
4183             desc   => q/The retrieved or updated saved search object, or id of a deleted object; Event on error/,
4184             class  => 'auss'
4185         }   
4186     }
4187 );
4188
4189 __PACKAGE__->register_method(
4190     method     => "user_saved_search_cud",
4191     api_name   => "open-ils.actor.user.saved_search.retrieve",
4192     stream     => 1,
4193     signature  => {
4194         desc   => 'Retrieve a saved search object',
4195         params => [
4196             { desc => 'Authentication token', type => 'string' },
4197             { desc => 'Saved Search ID', type => 'number' }
4198         ],
4199         return => {
4200             desc   => q/The saved search object, Event on error/,
4201             class  => 'auss'
4202         }   
4203     }
4204 );
4205
4206 sub user_saved_search_cud {
4207     my( $self, $client, $auth, $search ) = @_;
4208     my $e = new_editor( authtoken=>$auth );
4209     return $e->die_event unless $e->checkauth;
4210
4211     my $o_search;      # prior version of the object, if any
4212     my $res;           # to be returned
4213
4214     # branch on the operation type
4215
4216     if( $self->api_name =~ /retrieve/ ) {                    # Retrieve
4217
4218         # Get the old version, to check ownership
4219         $o_search = $e->retrieve_actor_usr_saved_search( $search )
4220             or return $e->die_event;
4221
4222         # You can't read somebody else's search
4223         return OpenILS::Event->new('BAD_PARAMS')
4224             unless $o_search->owner == $e->requestor->id;
4225
4226         $res = $o_search;
4227
4228     } else {
4229
4230         $e->xact_begin;               # start an editor transaction
4231
4232         if( $search->isnew ) {                               # Create
4233
4234             # You can't create a search for somebody else
4235             return OpenILS::Event->new('BAD_PARAMS')
4236                 unless $search->owner == $e->requestor->id;
4237
4238             $e->create_actor_usr_saved_search( $search )
4239                 or return $e->die_event;
4240
4241             $res = $search->id;
4242
4243         } elsif( $search->ischanged ) {                      # Update
4244
4245             # You can't change ownership of a search
4246             return OpenILS::Event->new('BAD_PARAMS')
4247                 unless $search->owner == $e->requestor->id;
4248
4249             # Get the old version, to check ownership
4250             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4251                 or return $e->die_event;
4252
4253             # You can't update somebody else's search
4254             return OpenILS::Event->new('BAD_PARAMS')
4255                 unless $o_search->owner == $e->requestor->id;
4256
4257             # Do the update
4258             $e->update_actor_usr_saved_search( $search )
4259                 or return $e->die_event;
4260
4261             $res = $search;
4262
4263         } elsif( $search->isdeleted ) {                      # Delete
4264
4265             # Get the old version, to check ownership
4266             $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4267                 or return $e->die_event;
4268
4269             # You can't delete somebody else's search
4270             return OpenILS::Event->new('BAD_PARAMS')
4271                 unless $o_search->owner == $e->requestor->id;
4272
4273             # Do the delete
4274             $e->delete_actor_usr_saved_search( $o_search )
4275                 or return $e->die_event;
4276
4277             $res = $search->id;
4278         }
4279
4280         $e->commit;
4281     }
4282
4283     return $res;
4284 }
4285
4286
4287
4288 1;