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