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