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