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