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