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