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