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