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