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