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