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