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