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