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