1 package OpenILS::Application::Actor;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
6 $Data::Dumper::Indent = 0;
9 use Digest::MD5 qw(md5_hex);
11 use OpenSRF::EX qw(:try);
14 use OpenILS::Application::AppUtils;
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;
22 use OpenSRF::Utils::Cache;
24 use OpenSRF::Utils::JSON;
26 use DateTime::Format::ISO8601;
27 use OpenILS::Const qw/:const/;
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;
35 use OpenILS::Utils::CStoreEditor qw/:funcs/;
36 use OpenILS::Utils::Penalty;
37 use List::Util qw/max/;
39 use UUID::Tiny qw/:std/;
42 OpenILS::Application::Actor::Container->initialize();
43 OpenILS::Application::Actor::UserGroups->initialize();
44 OpenILS::Application::Actor::ClosedDates->initialize();
47 my $apputils = "OpenILS::Application::AppUtils";
50 sub _d { warn "Patron:\n" . Dumper(shift()); }
53 my $set_user_settings;
57 #__PACKAGE__->register_method(
58 # method => "allowed_test",
59 # api_name => "open-ils.actor.allowed_test",
62 # my($self, $conn, $auth, $orgid, $permcode) = @_;
63 # my $e = new_editor(authtoken => $auth);
64 # return $e->die_event unless $e->checkauth;
68 # permcode => $permcode,
69 # result => $e->allowed($permcode, $orgid)
73 __PACKAGE__->register_method(
74 method => "update_user_setting",
75 api_name => "open-ils.actor.patron.settings.update",
77 sub update_user_setting {
78 my($self, $conn, $auth, $user_id, $settings) = @_;
79 my $e = new_editor(xact => 1, authtoken => $auth);
80 return $e->die_event unless $e->checkauth;
82 $user_id = $e->requestor->id unless defined $user_id;
84 unless($e->requestor->id == $user_id) {
85 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
86 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
89 for my $name (keys %$settings) {
90 my $val = $$settings{$name};
91 my $set = $e->search_actor_user_setting({usr => $user_id, name => $name})->[0];
94 $val = OpenSRF::Utils::JSON->perl2JSON($val);
97 $e->update_actor_user_setting($set) or return $e->die_event;
99 $set = Fieldmapper::actor::user_setting->new;
103 $e->create_actor_user_setting($set) or return $e->die_event;
106 $e->delete_actor_user_setting($set) or return $e->die_event;
115 __PACKAGE__->register_method(
116 method => "set_ou_settings",
117 api_name => "open-ils.actor.org_unit.settings.update",
119 desc => "Updates the value for a given org unit setting. The permission to update " .
120 "an org unit setting is either the UPDATE_ORG_UNIT_SETTING_ALL, or a specific " .
121 "permission specified in the update_perm column of the config.org_unit_setting_type " .
122 "table's row corresponding to the setting being changed." ,
124 {desc => 'Authentication token', type => 'string'},
125 {desc => 'Org unit ID', type => 'number'},
126 {desc => 'Hash of setting name-value pairs', type => 'object'}
128 return => {desc => '1 on success, Event on error'}
132 sub set_ou_settings {
133 my( $self, $client, $auth, $org_id, $settings ) = @_;
135 my $e = new_editor(authtoken => $auth, xact => 1);
136 return $e->die_event unless $e->checkauth;
138 my $all_allowed = $e->allowed("UPDATE_ORG_UNIT_SETTING_ALL", $org_id);
140 for my $name (keys %$settings) {
141 my $val = $$settings{$name};
143 my $type = $e->retrieve_config_org_unit_setting_type([
145 {flesh => 1, flesh_fields => {'coust' => ['update_perm']}}
146 ]) or return $e->die_event;
147 my $set = $e->search_actor_org_unit_setting({org_unit => $org_id, name => $name})->[0];
149 # If there is no relevant permission, the default assumption will
150 # be, "no, the caller cannot change that value."
151 return $e->die_event unless ($all_allowed ||
152 ($type->update_perm && $e->allowed($type->update_perm->code, $org_id)));
155 $val = OpenSRF::Utils::JSON->perl2JSON($val);
158 $e->update_actor_org_unit_setting($set) or return $e->die_event;
160 $set = Fieldmapper::actor::org_unit_setting->new;
161 $set->org_unit($org_id);
164 $e->create_actor_org_unit_setting($set) or return $e->die_event;
167 $e->delete_actor_org_unit_setting($set) or return $e->die_event;
175 __PACKAGE__->register_method(
176 method => "user_settings",
177 api_name => "open-ils.actor.patron.settings.retrieve",
180 my( $self, $client, $auth, $user_id, $setting ) = @_;
182 my $e = new_editor(authtoken => $auth);
183 return $e->event unless $e->checkauth;
184 $user_id = $e->requestor->id unless defined $user_id;
186 my $patron = $e->retrieve_actor_user($user_id) or return $e->event;
187 if($e->requestor->id != $user_id) {
188 return $e->event unless $e->allowed('VIEW_USER', $patron->home_ou);
192 my($e, $user_id, $setting) = @_;
193 my $val = $e->search_actor_user_setting({usr => $user_id, name => $setting})->[0];
194 return undef unless $val; # XXX this should really return undef, but needs testing
195 return OpenSRF::Utils::JSON->JSON2perl($val->value);
199 if(ref $setting eq 'ARRAY') {
201 $settings{$_} = get_setting($e, $user_id, $_) for @$setting;
204 return get_setting($e, $user_id, $setting);
207 my $s = $e->search_actor_user_setting({usr => $user_id});
208 return { map { ( $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) ) } @$s };
213 __PACKAGE__->register_method(
214 method => "ranged_ou_settings",
215 api_name => "open-ils.actor.org_unit_setting.values.ranged.retrieve",
217 desc => "Retrieves all org unit settings for the given org_id, up to whatever limit " .
218 "is implied for retrieving OU settings by the authenticated users' permissions.",
220 {desc => 'Authentication token', type => 'string'},
221 {desc => 'Org unit ID', type => 'number'},
223 return => {desc => 'A hashref of "ranged" settings, event on error'}
226 sub ranged_ou_settings {
227 my( $self, $client, $auth, $org_id ) = @_;
229 my $e = new_editor(authtoken => $auth);
230 return $e->event unless $e->checkauth;
233 my $org_list = $U->get_org_ancestors($org_id);
234 my $settings = $e->search_actor_org_unit_setting({org_unit => $org_list});
235 $org_list = [ reverse @$org_list ];
237 # start at the context org and capture the setting value
238 # without clobbering settings we've already captured
239 for my $this_org_id (@$org_list) {
241 my @sets = grep { $_->org_unit == $this_org_id } @$settings;
243 for my $set (@sets) {
244 my $type = $e->retrieve_config_org_unit_setting_type([
246 {flesh => 1, flesh_fields => {coust => ['view_perm']}}
249 # If there is no relevant permission, the default assumption will
250 # be, "yes, the caller can have that value."
251 if ($type && $type->view_perm) {
252 next if not $e->allowed($type->view_perm->code, $org_id);
255 $ranged_settings{$set->name} = OpenSRF::Utils::JSON->JSON2perl($set->value)
256 unless defined $ranged_settings{$set->name};
260 return \%ranged_settings;
265 __PACKAGE__->register_method(
266 api_name => 'open-ils.actor.ou_setting.ancestor_default',
267 method => 'ou_ancestor_setting',
269 desc => 'Get the org unit setting value associated with the setting name as seen from the specified org unit. ' .
270 'IF AND ONLY IF an authentication token is provided, this method will make sure that the given ' .
271 'user has permission to view that setting, if there is a permission associated with the setting.' ,
273 { desc => 'Org unit ID', type => 'number' },
274 { desc => 'setting name', type => 'string' },
275 { desc => 'authtoken (optional)', type => 'string' }
277 return => {desc => 'A value for the org unit setting, or undef'}
281 # ------------------------------------------------------------------
282 # Attempts to find the org setting value for a given org. if not
283 # found at the requested org, searches up the org tree until it
284 # finds a parent that has the requested setting.
285 # when found, returns { org => $id, value => $value }
286 # otherwise, returns NULL
287 # ------------------------------------------------------------------
288 sub ou_ancestor_setting {
289 my( $self, $client, $orgid, $name, $auth ) = @_;
290 return $U->ou_ancestor_setting($orgid, $name, undef, $auth);
293 __PACKAGE__->register_method(
294 api_name => 'open-ils.actor.ou_setting.ancestor_default.batch',
295 method => 'ou_ancestor_setting_batch',
297 desc => 'Get org unit setting name => value pairs for a list of names, as seen from the specified org unit. ' .
298 'IF AND ONLY IF an authentication token is provided, this method will make sure that the given ' .
299 'user has permission to view that setting, if there is a permission associated with the setting.' ,
301 { desc => 'Org unit ID', type => 'number' },
302 { desc => 'setting name list', type => 'array' },
303 { desc => 'authtoken (optional)', type => 'string' }
305 return => {desc => 'A hash with name => value pairs for the org unit settings'}
308 sub ou_ancestor_setting_batch {
309 my( $self, $client, $orgid, $name_list, $auth ) = @_;
311 $values{$_} = $U->ou_ancestor_setting($orgid, $_, undef, $auth) for @$name_list;
317 __PACKAGE__->register_method(
318 method => "update_patron",
319 api_name => "open-ils.actor.patron.update",
322 Update an existing user, or create a new one. Related objects,
323 like cards, addresses, survey responses, and stat cats,
324 can be updated by attaching them to the user object in their
325 respective fields. For examples, the billing address object
326 may be inserted into the 'billing_address' field, etc. For each
327 attached object, indicate if the object should be created,
328 updated, or deleted using the built-in 'isnew', 'ischanged',
329 and 'isdeleted' fields on the object.
332 { desc => 'Authentication token', type => 'string' },
333 { desc => 'Patron data object', type => 'object' }
335 return => {desc => 'A fleshed user object, event on error'}
340 my( $self, $client, $user_session, $patron ) = @_;
342 my $session = $apputils->start_db_session();
344 $logger->info($patron->isnew ? "Creating new patron..." : "Updating Patron: " . $patron->id);
346 my( $user_obj, $evt ) = $U->checkses($user_session);
349 $evt = check_group_perm($session, $user_obj, $patron);
353 # $new_patron is the patron in progress. $patron is the original patron
354 # passed in with the method. new_patron will change as the components
355 # of patron are added/updated.
359 # unflesh the real items on the patron
360 $patron->card( $patron->card->id ) if(ref($patron->card));
361 $patron->billing_address( $patron->billing_address->id )
362 if(ref($patron->billing_address));
363 $patron->mailing_address( $patron->mailing_address->id )
364 if(ref($patron->mailing_address));
366 # create/update the patron first so we can use his id
367 if($patron->isnew()) {
368 ( $new_patron, $evt ) = _add_patron($session, _clone_patron($patron), $user_obj);
370 } else { $new_patron = $patron; }
372 ( $new_patron, $evt ) = _add_update_addresses($session, $patron, $new_patron, $user_obj);
375 ( $new_patron, $evt ) = _add_update_cards($session, $patron, $new_patron, $user_obj);
378 ( $new_patron, $evt ) = _add_survey_responses($session, $patron, $new_patron, $user_obj);
381 # re-update the patron if anything has happened to him during this process
382 if($new_patron->ischanged()) {
383 ( $new_patron, $evt ) = _update_patron($session, $new_patron, $user_obj);
387 ($new_patron, $evt) = _create_stat_maps($session, $user_session, $patron, $new_patron, $user_obj);
390 ($new_patron, $evt) = _create_perm_maps($session, $user_session, $patron, $new_patron, $user_obj);
393 $apputils->commit_db_session($session);
395 $evt = apply_invalid_addr_penalty($patron);
398 my $tses = OpenSRF::AppSession->create('open-ils.trigger');
400 $tses->request('open-ils.trigger.event.autocreate', 'au.create', $new_patron, $new_patron->home_ou);
402 $tses->request('open-ils.trigger.event.autocreate', 'au.update', $new_patron, $new_patron->home_ou);
405 return flesh_user($new_patron->id(), new_editor(requestor => $user_obj, xact => 1));
408 sub apply_invalid_addr_penalty {
410 my $e = new_editor(xact => 1);
412 # grab the invalid address penalty if set
413 my $penalties = OpenILS::Utils::Penalty->retrieve_usr_penalties($e, $patron->id, $patron->home_ou);
415 my ($addr_penalty) = grep
416 { $_->standing_penalty->name eq 'INVALID_PATRON_ADDRESS' } @$penalties;
418 # do we enforce invalid address penalty
419 my $enforce = $U->ou_ancestor_setting_value(
420 $patron->home_ou, 'circ.patron_invalid_address_apply_penalty') || 0;
422 my $addrs = $e->search_actor_user_address(
423 {usr => $patron->id, valid => 'f', id => {'>' => 0}}, {idlist => 1});
424 my $addr_count = scalar(@$addrs);
426 if($addr_count == 0 and $addr_penalty) {
428 # regardless of any settings, remove the penalty when the user has no invalid addresses
429 $e->delete_actor_user_standing_penalty($addr_penalty) or return $e->die_event;
432 } elsif($enforce and $addr_count > 0 and !$addr_penalty) {
434 my $ptype = $e->retrieve_config_standing_penalty(29) or return $e->die_event;
435 my $depth = $ptype->org_depth;
436 my $ctx_org = $U->org_unit_ancestor_at_depth($patron->home_ou, $depth) if defined $depth;
437 $ctx_org = $patron->home_ou unless defined $ctx_org;
439 my $penalty = Fieldmapper::actor::user_standing_penalty->new;
440 $penalty->usr($patron->id);
441 $penalty->org_unit($ctx_org);
442 $penalty->standing_penalty(OILS_PENALTY_INVALID_PATRON_ADDRESS);
444 $e->create_actor_user_standing_penalty($penalty) or return $e->die_event;
458 return new_flesh_user($id, [
461 "standing_penalties",
465 "stat_cat_entries" ], $e );
473 # clone and clear stuff that would break the database
477 my $new_patron = $patron->clone;
479 $new_patron->clear_billing_address();
480 $new_patron->clear_mailing_address();
481 $new_patron->clear_addresses();
482 $new_patron->clear_card();
483 $new_patron->clear_cards();
484 $new_patron->clear_id();
485 $new_patron->clear_isnew();
486 $new_patron->clear_ischanged();
487 $new_patron->clear_isdeleted();
488 $new_patron->clear_stat_cat_entries();
489 $new_patron->clear_permissions();
490 $new_patron->clear_standing_penalties();
500 my $user_obj = shift;
502 my $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'CREATE_USER');
503 return (undef, $evt) if $evt;
505 my $ex = $session->request(
506 'open-ils.storage.direct.actor.user.search.usrname', $patron->usrname())->gather(1);
508 return (undef, OpenILS::Event->new('USERNAME_EXISTS'));
511 $logger->info("Creating new user in the DB with username: ".$patron->usrname());
513 my $id = $session->request(
514 "open-ils.storage.direct.actor.user.create", $patron)->gather(1);
515 return (undef, $U->DB_UPDATE_FAILED($patron)) unless $id;
517 $logger->info("Successfully created new user [$id] in DB");
519 return ( $session->request(
520 "open-ils.storage.direct.actor.user.retrieve", $id)->gather(1), undef );
524 sub check_group_perm {
525 my( $session, $requestor, $patron ) = @_;
528 # first let's see if the requestor has
529 # priveleges to update this user in any way
530 if( ! $patron->isnew ) {
531 my $p = $session->request(
532 'open-ils.storage.direct.actor.user.retrieve', $patron->id )->gather(1);
534 # If we are the requestor (trying to update our own account)
535 # and we are not trying to change our profile, we're good
536 if( $p->id == $requestor->id and
537 $p->profile == $patron->profile ) {
542 $evt = group_perm_failed($session, $requestor, $p);
546 # They are allowed to edit this patron.. can they put the
547 # patron into the group requested?
548 $evt = group_perm_failed($session, $requestor, $patron);
554 sub group_perm_failed {
555 my( $session, $requestor, $patron ) = @_;
559 my $grpid = $patron->profile;
563 $logger->debug("user update looking for group perm for group $grpid");
564 $grp = $session->request(
565 'open-ils.storage.direct.permission.grp_tree.retrieve', $grpid )->gather(1);
566 return OpenILS::Event->new('PERMISSION_GRP_TREE_NOT_FOUND') unless $grp;
568 } while( !($perm = $grp->application_perm) and ($grpid = $grp->parent) );
570 $logger->info("user update checking perm $perm on user ".
571 $requestor->id." for update/create on user username=".$patron->usrname);
573 my $evt = $U->check_perms($requestor->id, $patron->home_ou, $perm);
581 my( $session, $patron, $user_obj, $noperm) = @_;
583 $logger->info("Updating patron ".$patron->id." in DB");
588 $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'UPDATE_USER');
589 return (undef, $evt) if $evt;
592 # update the password by itself to avoid the password protection magic
593 if( $patron->passwd ) {
594 my $s = $session->request(
595 'open-ils.storage.direct.actor.user.remote_update',
596 {id => $patron->id}, {passwd => $patron->passwd})->gather(1);
597 return (undef, $U->DB_UPDATE_FAILED($patron)) unless defined($s);
598 $patron->clear_passwd;
601 if(!$patron->ident_type) {
602 $patron->clear_ident_type;
603 $patron->clear_ident_value;
606 $evt = verify_last_xact($session, $patron);
607 return (undef, $evt) if $evt;
609 my $stat = $session->request(
610 "open-ils.storage.direct.actor.user.update",$patron )->gather(1);
611 return (undef, $U->DB_UPDATE_FAILED($patron)) unless defined($stat);
616 sub verify_last_xact {
617 my( $session, $patron ) = @_;
618 return undef unless $patron->id and $patron->id > 0;
619 my $p = $session->request(
620 'open-ils.storage.direct.actor.user.retrieve', $patron->id)->gather(1);
621 my $xact = $p->last_xact_id;
622 return undef unless $xact;
623 $logger->info("user xact = $xact, saving with xact " . $patron->last_xact_id);
624 return OpenILS::Event->new('XACT_COLLISION')
625 if $xact != $patron->last_xact_id;
630 sub _check_dup_ident {
631 my( $session, $patron ) = @_;
633 return undef unless $patron->ident_value;
636 ident_type => $patron->ident_type,
637 ident_value => $patron->ident_value,
640 $logger->debug("patron update searching for dup ident values: " .
641 $patron->ident_type . ':' . $patron->ident_value);
643 $search->{id} = {'!=' => $patron->id} if $patron->id and $patron->id > 0;
645 my $dups = $session->request(
646 'open-ils.storage.direct.actor.user.search_where.atomic', $search )->gather(1);
649 return OpenILS::Event->new('PATRON_DUP_IDENT1', payload => $patron )
656 sub _add_update_addresses {
660 my $new_patron = shift;
664 my $current_id; # id of the address before creation
666 for my $address (@{$patron->addresses()}) {
668 next unless ref $address;
669 $current_id = $address->id();
671 if( $patron->billing_address() and
672 $patron->billing_address() == $current_id ) {
673 $logger->info("setting billing addr to $current_id");
674 $new_patron->billing_address($address->id());
675 $new_patron->ischanged(1);
678 if( $patron->mailing_address() and
679 $patron->mailing_address() == $current_id ) {
680 $new_patron->mailing_address($address->id());
681 $logger->info("setting mailing addr to $current_id");
682 $new_patron->ischanged(1);
686 if($address->isnew()) {
688 $address->usr($new_patron->id());
690 ($address, $evt) = _add_address($session,$address);
691 return (undef, $evt) if $evt;
693 # we need to get the new id
694 if( $patron->billing_address() and
695 $patron->billing_address() == $current_id ) {
696 $new_patron->billing_address($address->id());
697 $logger->info("setting billing addr to $current_id");
698 $new_patron->ischanged(1);
701 if( $patron->mailing_address() and
702 $patron->mailing_address() == $current_id ) {
703 $new_patron->mailing_address($address->id());
704 $logger->info("setting mailing addr to $current_id");
705 $new_patron->ischanged(1);
708 } elsif($address->ischanged() ) {
710 ($address, $evt) = _update_address($session, $address);
711 return (undef, $evt) if $evt;
713 } elsif($address->isdeleted() ) {
715 if( $address->id() == $new_patron->mailing_address() ) {
716 $new_patron->clear_mailing_address();
717 ($new_patron, $evt) = _update_patron($session, $new_patron);
718 return (undef, $evt) if $evt;
721 if( $address->id() == $new_patron->billing_address() ) {
722 $new_patron->clear_billing_address();
723 ($new_patron, $evt) = _update_patron($session, $new_patron);
724 return (undef, $evt) if $evt;
727 $evt = _delete_address($session, $address);
728 return (undef, $evt) if $evt;
732 return ( $new_patron, undef );
736 # adds an address to the db and returns the address with new id
738 my($session, $address) = @_;
739 $address->clear_id();
741 $logger->info("Creating new address at street ".$address->street1);
743 # put the address into the database
744 my $id = $session->request(
745 "open-ils.storage.direct.actor.user_address.create", $address )->gather(1);
746 return (undef, $U->DB_UPDATE_FAILED($address)) unless $id;
749 return ($address, undef);
753 sub _update_address {
754 my( $session, $address ) = @_;
756 $logger->info("Updating address ".$address->id." in the DB");
758 my $stat = $session->request(
759 "open-ils.storage.direct.actor.user_address.update", $address )->gather(1);
761 return (undef, $U->DB_UPDATE_FAILED($address)) unless defined($stat);
762 return ($address, undef);
767 sub _add_update_cards {
771 my $new_patron = shift;
775 my $virtual_id; #id of the card before creation
776 for my $card (@{$patron->cards()}) {
778 $card->usr($new_patron->id());
780 if(ref($card) and $card->isnew()) {
782 $virtual_id = $card->id();
783 ( $card, $evt ) = _add_card($session,$card);
784 return (undef, $evt) if $evt;
786 #if(ref($patron->card)) { $patron->card($patron->card->id); }
787 if($patron->card() == $virtual_id) {
788 $new_patron->card($card->id());
789 $new_patron->ischanged(1);
792 } elsif( ref($card) and $card->ischanged() ) {
793 $evt = _update_card($session, $card);
794 return (undef, $evt) if $evt;
798 return ( $new_patron, undef );
802 # adds an card to the db and returns the card with new id
804 my( $session, $card ) = @_;
807 $logger->info("Adding new patron card ".$card->barcode);
809 my $id = $session->request(
810 "open-ils.storage.direct.actor.card.create", $card )->gather(1);
811 return (undef, $U->DB_UPDATE_FAILED($card)) unless $id;
812 $logger->info("Successfully created patron card $id");
815 return ( $card, undef );
819 # returns event on error. returns undef otherwise
821 my( $session, $card ) = @_;
822 $logger->info("Updating patron card ".$card->id);
824 my $stat = $session->request(
825 "open-ils.storage.direct.actor.card.update", $card )->gather(1);
826 return $U->DB_UPDATE_FAILED($card) unless defined($stat);
833 # returns event on error. returns undef otherwise
834 sub _delete_address {
835 my( $session, $address ) = @_;
837 $logger->info("Deleting address ".$address->id." from DB");
839 my $stat = $session->request(
840 "open-ils.storage.direct.actor.user_address.delete", $address )->gather(1);
842 return $U->DB_UPDATE_FAILED($address) unless defined($stat);
848 sub _add_survey_responses {
849 my ($session, $patron, $new_patron) = @_;
851 $logger->info( "Updating survey responses for patron ".$new_patron->id );
853 my $responses = $patron->survey_responses;
857 $_->usr($new_patron->id) for (@$responses);
859 my $evt = $U->simplereq( "open-ils.circ",
860 "open-ils.circ.survey.submit.user_id", $responses );
862 return (undef, $evt) if defined($U->event_code($evt));
866 return ( $new_patron, undef );
870 sub _create_stat_maps {
872 my($session, $user_session, $patron, $new_patron) = @_;
874 my $maps = $patron->stat_cat_entries();
876 for my $map (@$maps) {
878 my $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.update";
880 if ($map->isdeleted()) {
881 $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.delete";
883 } elsif ($map->isnew()) {
884 $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.create";
889 $map->target_usr($new_patron->id);
892 $logger->info("Updating stat entry with method $method and map $map");
894 my $stat = $session->request($method, $map)->gather(1);
895 return (undef, $U->DB_UPDATE_FAILED($map)) unless defined($stat);
899 return ($new_patron, undef);
902 sub _create_perm_maps {
904 my($session, $user_session, $patron, $new_patron) = @_;
906 my $maps = $patron->permissions;
908 for my $map (@$maps) {
910 my $method = "open-ils.storage.direct.permission.usr_perm_map.update";
911 if ($map->isdeleted()) {
912 $method = "open-ils.storage.direct.permission.usr_perm_map.delete";
913 } elsif ($map->isnew()) {
914 $method = "open-ils.storage.direct.permission.usr_perm_map.create";
919 $map->usr($new_patron->id);
921 #warn( "Updating permissions with method $method and session $user_session and map $map" );
922 $logger->info( "Updating permissions with method $method and map $map" );
924 my $stat = $session->request($method, $map)->gather(1);
925 return (undef, $U->DB_UPDATE_FAILED($map)) unless defined($stat);
929 return ($new_patron, undef);
933 __PACKAGE__->register_method(
934 method => "set_user_work_ous",
935 api_name => "open-ils.actor.user.work_ous.update",
938 sub set_user_work_ous {
944 my( $requestor, $evt ) = $apputils->checksesperm( $ses, 'ASSIGN_WORK_ORG_UNIT' );
947 my $session = $apputils->start_db_session();
949 for my $map (@$maps) {
951 my $method = "open-ils.storage.direct.permission.usr_work_ou_map.update";
952 if ($map->isdeleted()) {
953 $method = "open-ils.storage.direct.permission.usr_work_ou_map.delete";
954 } elsif ($map->isnew()) {
955 $method = "open-ils.storage.direct.permission.usr_work_ou_map.create";
959 #warn( "Updating permissions with method $method and session $ses and map $map" );
960 $logger->info( "Updating work_ou map with method $method and map $map" );
962 my $stat = $session->request($method, $map)->gather(1);
963 $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
967 $apputils->commit_db_session($session);
969 return scalar(@$maps);
973 __PACKAGE__->register_method(
974 method => "set_user_perms",
975 api_name => "open-ils.actor.user.permissions.update",
984 my $session = $apputils->start_db_session();
986 my( $user_obj, $evt ) = $U->checkses($ses);
989 my $perms = $session->request('open-ils.storage.permission.user_perms.atomic', $user_obj->id)->gather(1);
992 $all = 1 if ($U->is_true($user_obj->super_user()));
993 $all = 1 unless ($U->check_perms($user_obj->id, $user_obj->home_ou, 'EVERYTHING'));
995 for my $map (@$maps) {
997 my $method = "open-ils.storage.direct.permission.usr_perm_map.update";
998 if ($map->isdeleted()) {
999 $method = "open-ils.storage.direct.permission.usr_perm_map.delete";
1000 } elsif ($map->isnew()) {
1001 $method = "open-ils.storage.direct.permission.usr_perm_map.create";
1005 next if (!$all and !grep { $_->perm eq $map->perm and $U->is_true($_->grantable) and $_->depth <= $map->depth } @$perms);
1006 #warn( "Updating permissions with method $method and session $ses and map $map" );
1007 $logger->info( "Updating permissions with method $method and map $map" );
1009 my $stat = $session->request($method, $map)->gather(1);
1010 $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
1014 $apputils->commit_db_session($session);
1016 return scalar(@$maps);
1020 __PACKAGE__->register_method(
1021 method => "user_retrieve_by_barcode",
1023 api_name => "open-ils.actor.user.fleshed.retrieve_by_barcode",);
1025 sub user_retrieve_by_barcode {
1026 my($self, $client, $user_session, $barcode) = @_;
1028 $logger->debug("Searching for user with barcode $barcode");
1029 my ($user_obj, $evt) = $apputils->checkses($user_session);
1030 return $evt if $evt;
1032 my $card = OpenILS::Application::AppUtils->simple_scalar_request(
1034 "open-ils.cstore.direct.actor.card.search.atomic",
1035 { barcode => $barcode }
1038 if(!$card || !$card->[0]) {
1039 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
1043 my $user = flesh_user($card->usr(), new_editor(requestor => $user_obj));
1045 $evt = $U->check_perms($user_obj->id, $user->home_ou, 'VIEW_USER');
1046 return $evt if $evt;
1048 if(!$user) { return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ); }
1055 __PACKAGE__->register_method(
1056 method => "get_user_by_id",
1058 api_name => "open-ils.actor.user.retrieve",
1061 sub get_user_by_id {
1062 my ($self, $client, $auth, $id) = @_;
1063 my $e = new_editor(authtoken=>$auth);
1064 return $e->event unless $e->checkauth;
1065 my $user = $e->retrieve_actor_user($id)
1066 or return $e->event;
1067 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
1072 __PACKAGE__->register_method(
1073 method => "get_org_types",
1074 api_name => "open-ils.actor.org_types.retrieve",
1077 return $U->get_org_types();
1081 __PACKAGE__->register_method(
1082 method => "get_user_ident_types",
1083 api_name => "open-ils.actor.user.ident_types.retrieve",
1086 sub get_user_ident_types {
1087 return $ident_types if $ident_types;
1088 return $ident_types =
1089 new_editor()->retrieve_all_config_identification_type();
1093 __PACKAGE__->register_method(
1094 method => "get_org_unit",
1095 api_name => "open-ils.actor.org_unit.retrieve",
1099 my( $self, $client, $user_session, $org_id ) = @_;
1100 my $e = new_editor(authtoken => $user_session);
1102 return $e->event unless $e->checkauth;
1103 $org_id = $e->requestor->ws_ou;
1105 my $o = $e->retrieve_actor_org_unit($org_id)
1106 or return $e->event;
1110 __PACKAGE__->register_method(
1111 method => "search_org_unit",
1112 api_name => "open-ils.actor.org_unit_list.search",
1115 sub search_org_unit {
1117 my( $self, $client, $field, $value ) = @_;
1119 my $list = OpenILS::Application::AppUtils->simple_scalar_request(
1121 "open-ils.cstore.direct.actor.org_unit.search.atomic",
1122 { $field => $value } );
1128 # build the org tree
1130 __PACKAGE__->register_method(
1131 method => "get_org_tree",
1132 api_name => "open-ils.actor.org_tree.retrieve",
1134 note => "Returns the entire org tree structure",
1140 return $U->get_org_tree($client->session->session_locale);
1144 __PACKAGE__->register_method(
1145 method => "get_org_descendants",
1146 api_name => "open-ils.actor.org_tree.descendants.retrieve"
1149 # depth is optional. org_unit is the id
1150 sub get_org_descendants {
1151 my( $self, $client, $org_unit, $depth ) = @_;
1153 if(ref $org_unit eq 'ARRAY') {
1156 for my $i (0..scalar(@$org_unit)-1) {
1157 my $list = $U->simple_scalar_request(
1159 "open-ils.storage.actor.org_unit.descendants.atomic",
1160 $org_unit->[$i], $depth->[$i] );
1161 push(@trees, $U->build_org_tree($list));
1166 my $orglist = $apputils->simple_scalar_request(
1168 "open-ils.storage.actor.org_unit.descendants.atomic",
1169 $org_unit, $depth );
1170 return $U->build_org_tree($orglist);
1175 __PACKAGE__->register_method(
1176 method => "get_org_ancestors",
1177 api_name => "open-ils.actor.org_tree.ancestors.retrieve"
1180 # depth is optional. org_unit is the id
1181 sub get_org_ancestors {
1182 my( $self, $client, $org_unit, $depth ) = @_;
1183 my $orglist = $apputils->simple_scalar_request(
1185 "open-ils.storage.actor.org_unit.ancestors.atomic",
1186 $org_unit, $depth );
1187 return $U->build_org_tree($orglist);
1191 __PACKAGE__->register_method(
1192 method => "get_standings",
1193 api_name => "open-ils.actor.standings.retrieve"
1198 return $user_standings if $user_standings;
1199 return $user_standings =
1200 $apputils->simple_scalar_request(
1202 "open-ils.cstore.direct.config.standing.search.atomic",
1203 { id => { "!=" => undef } }
1208 __PACKAGE__->register_method(
1209 method => "get_my_org_path",
1210 api_name => "open-ils.actor.org_unit.full_path.retrieve"
1213 sub get_my_org_path {
1214 my( $self, $client, $auth, $org_id ) = @_;
1215 my $e = new_editor(authtoken=>$auth);
1216 return $e->event unless $e->checkauth;
1217 $org_id = $e->requestor->ws_ou unless defined $org_id;
1219 return $apputils->simple_scalar_request(
1221 "open-ils.storage.actor.org_unit.full_path.atomic",
1226 __PACKAGE__->register_method(
1227 method => "patron_adv_search",
1228 api_name => "open-ils.actor.patron.search.advanced"
1230 sub patron_adv_search {
1231 my( $self, $client, $auth, $search_hash,
1232 $search_limit, $search_sort, $include_inactive, $search_depth ) = @_;
1234 my $e = new_editor(authtoken=>$auth);
1235 return $e->event unless $e->checkauth;
1236 return $e->event unless $e->allowed('VIEW_USER');
1237 return $U->storagereq(
1238 "open-ils.storage.actor.user.crazy_search", $search_hash,
1239 $search_limit, $search_sort, $include_inactive, $e->requestor->ws_ou, $search_depth);
1243 __PACKAGE__->register_method(
1244 method => "update_passwd",
1245 api_name => "open-ils.actor.user.password.update",
1247 desc => "Update the operator's password",
1249 { desc => 'Authentication token', type => 'string' },
1250 { desc => 'New password', type => 'string' },
1251 { desc => 'Current password', type => 'string' }
1253 return => {desc => '1 on success, Event on error or incorrect current password'}
1257 __PACKAGE__->register_method(
1258 method => "update_passwd",
1259 api_name => "open-ils.actor.user.username.update",
1261 desc => "Update the operator's username",
1263 { desc => 'Authentication token', type => 'string' },
1264 { desc => 'New username', type => 'string' }
1266 return => {desc => '1 on success, Event on error'}
1270 __PACKAGE__->register_method(
1271 method => "update_passwd",
1272 api_name => "open-ils.actor.user.email.update",
1274 desc => "Update the operator's email address",
1276 { desc => 'Authentication token', type => 'string' },
1277 { desc => 'New email address', type => 'string' }
1279 return => {desc => '1 on success, Event on error'}
1284 my( $self, $conn, $auth, $new_val, $orig_pw ) = @_;
1285 my $e = new_editor(xact=>1, authtoken=>$auth);
1286 return $e->die_event unless $e->checkauth;
1288 my $db_user = $e->retrieve_actor_user($e->requestor->id)
1289 or return $e->die_event;
1290 my $api = $self->api_name;
1292 if( $api =~ /password/o ) {
1294 # make sure the original password matches the in-database password
1295 return OpenILS::Event->new('INCORRECT_PASSWORD')
1296 if md5_hex($orig_pw) ne $db_user->passwd;
1297 $db_user->passwd($new_val);
1301 # if we don't clear the password, the user will be updated with
1302 # a hashed version of the hashed version of their password
1303 $db_user->clear_passwd;
1305 if( $api =~ /username/o ) {
1307 # make sure no one else has this username
1308 my $exist = $e->search_actor_user({usrname=>$new_val},{idlist=>1});
1309 return OpenILS::Event->new('USERNAME_EXISTS') if @$exist;
1310 $db_user->usrname($new_val);
1312 } elsif( $api =~ /email/o ) {
1313 $db_user->email($new_val);
1317 $e->update_actor_user($db_user) or return $e->die_event;
1324 __PACKAGE__->register_method(
1325 method => "check_user_perms",
1326 api_name => "open-ils.actor.user.perm.check",
1327 notes => <<" NOTES");
1328 Takes a login session, user id, an org id, and an array of perm type strings. For each
1329 perm type, if the user does *not* have the given permission it is added
1330 to a list which is returned from the method. If all permissions
1331 are allowed, an empty list is returned
1332 if the logged in user does not match 'user_id', then the logged in user must
1333 have VIEW_PERMISSION priveleges.
1336 sub check_user_perms {
1337 my( $self, $client, $login_session, $user_id, $org_id, $perm_types ) = @_;
1339 my( $staff, $evt ) = $apputils->checkses($login_session);
1340 return $evt if $evt;
1342 if($staff->id ne $user_id) {
1343 if( $evt = $apputils->check_perms(
1344 $staff->id, $org_id, 'VIEW_PERMISSION') ) {
1350 for my $perm (@$perm_types) {
1351 if($apputils->check_perms($user_id, $org_id, $perm)) {
1352 push @not_allowed, $perm;
1356 return \@not_allowed
1359 __PACKAGE__->register_method(
1360 method => "check_user_perms2",
1361 api_name => "open-ils.actor.user.perm.check.multi_org",
1363 Checks the permissions on a list of perms and orgs for a user
1364 @param authtoken The login session key
1365 @param user_id The id of the user to check
1366 @param orgs The array of org ids
1367 @param perms The array of permission names
1368 @return An array of [ orgId, permissionName ] arrays that FAILED the check
1369 if the logged in user does not match 'user_id', then the logged in user must
1370 have VIEW_PERMISSION priveleges.
1373 sub check_user_perms2 {
1374 my( $self, $client, $authtoken, $user_id, $orgs, $perms ) = @_;
1376 my( $staff, $target, $evt ) = $apputils->checkses_requestor(
1377 $authtoken, $user_id, 'VIEW_PERMISSION' );
1378 return $evt if $evt;
1381 for my $org (@$orgs) {
1382 for my $perm (@$perms) {
1383 if($apputils->check_perms($user_id, $org, $perm)) {
1384 push @not_allowed, [ $org, $perm ];
1389 return \@not_allowed
1393 __PACKAGE__->register_method(
1394 method => 'check_user_perms3',
1395 api_name => 'open-ils.actor.user.perm.highest_org',
1397 Returns the highest org unit id at which a user has a given permission
1398 If the requestor does not match the target user, the requestor must have
1399 'VIEW_PERMISSION' rights at the home org unit of the target user
1400 @param authtoken The login session key
1401 @param userid The id of the user in question
1402 @param perm The permission to check
1403 @return The org unit highest in the org tree within which the user has
1404 the requested permission
1407 sub check_user_perms3 {
1408 my($self, $client, $authtoken, $user_id, $perm) = @_;
1409 my $e = new_editor(authtoken=>$authtoken);
1410 return $e->event unless $e->checkauth;
1412 my $tree = $U->get_org_tree();
1414 unless($e->requestor->id == $user_id) {
1415 my $user = $e->retrieve_actor_user($user_id)
1416 or return $e->event;
1417 return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1418 return $U->find_highest_perm_org($perm, $user_id, $user->home_ou, $tree );
1421 return $U->find_highest_perm_org($perm, $user_id, $e->requestor->ws_ou, $tree);
1424 __PACKAGE__->register_method(
1425 method => 'user_has_work_perm_at',
1426 api_name => 'open-ils.actor.user.has_work_perm_at',
1430 Returns a set of org unit IDs which represent the highest orgs in
1431 the org tree where the user has the requested permission. The
1432 purpose of this method is to return the smallest set of org units
1433 which represent the full expanse of the user's ability to perform
1434 the requested action. The user whose perms this method should
1435 check is implied by the authtoken. /,
1437 {desc => 'authtoken', type => 'string'},
1438 {desc => 'permission name', type => 'string'},
1439 {desc => q/user id, optional. If present, check perms for
1440 this user instead of the logged in user/, type => 'number'},
1442 return => {desc => 'An array of org IDs'}
1446 sub user_has_work_perm_at {
1447 my($self, $conn, $auth, $perm, $user_id) = @_;
1448 my $e = new_editor(authtoken=>$auth);
1449 return $e->event unless $e->checkauth;
1450 if(defined $user_id) {
1451 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1452 return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1454 return $U->user_has_work_perm_at($e, $perm, undef, $user_id);
1457 __PACKAGE__->register_method(
1458 method => 'user_has_work_perm_at_batch',
1459 api_name => 'open-ils.actor.user.has_work_perm_at.batch',
1463 sub user_has_work_perm_at_batch {
1464 my($self, $conn, $auth, $perms, $user_id) = @_;
1465 my $e = new_editor(authtoken=>$auth);
1466 return $e->event unless $e->checkauth;
1467 if(defined $user_id) {
1468 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1469 return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1472 $map->{$_} = $U->user_has_work_perm_at($e, $_) for @$perms;
1478 __PACKAGE__->register_method(
1479 method => 'check_user_perms4',
1480 api_name => 'open-ils.actor.user.perm.highest_org.batch',
1482 Returns the highest org unit id at which a user has a given permission
1483 If the requestor does not match the target user, the requestor must have
1484 'VIEW_PERMISSION' rights at the home org unit of the target user
1485 @param authtoken The login session key
1486 @param userid The id of the user in question
1487 @param perms An array of perm names to check
1488 @return An array of orgId's representing the org unit
1489 highest in the org tree within which the user has the requested permission
1490 The arrah of orgId's has matches the order of the perms array
1493 sub check_user_perms4 {
1494 my( $self, $client, $authtoken, $userid, $perms ) = @_;
1496 my( $staff, $target, $org, $evt );
1498 ( $staff, $target, $evt ) = $apputils->checkses_requestor(
1499 $authtoken, $userid, 'VIEW_PERMISSION' );
1500 return $evt if $evt;
1503 return [] unless ref($perms);
1504 my $tree = $U->get_org_tree();
1506 for my $p (@$perms) {
1507 push( @arr, $U->find_highest_perm_org( $p, $userid, $target->home_ou, $tree ) );
1513 __PACKAGE__->register_method(
1514 method => "user_fines_summary",
1515 api_name => "open-ils.actor.user.fines.summary",
1518 desc => 'Returns a short summary of the users total open fines, ' .
1519 'excluding voided fines Params are login_session, user_id' ,
1521 {desc => 'Authentication token', type => 'string'},
1522 {desc => 'User ID', type => 'string'} # number?
1525 desc => "a 'mous' object, event on error",
1530 sub user_fines_summary {
1531 my( $self, $client, $auth, $user_id ) = @_;
1533 my $e = new_editor(authtoken=>$auth);
1534 return $e->event unless $e->checkauth;
1536 if( $user_id ne $e->requestor->id ) {
1537 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1538 return $e->event unless
1539 $e->allowed('VIEW_USER_FINES_SUMMARY', $user->home_ou);
1542 return $e->search_money_open_user_summary({usr => $user_id})->[0];
1546 ##### a small consolidation of related method registrations
1547 my $common_params = [
1548 { desc => 'Authentication token', type => 'string' },
1549 { desc => 'User ID', type => 'string' },
1550 { desc => 'Transactions type (optional, defaults to all)', type => 'string' },
1551 { desc => 'Options hash. May contain limit and offset for paged results.', type => 'object' },
1554 'open-ils.actor.user.transactions' => '',
1555 'open-ils.actor.user.transactions.fleshed' => '',
1556 'open-ils.actor.user.transactions.have_charge' => ' that have an initial charge',
1557 'open-ils.actor.user.transactions.have_charge.fleshed' => ' that have an initial charge',
1558 'open-ils.actor.user.transactions.have_balance' => ' that have an outstanding balance',
1559 'open-ils.actor.user.transactions.have_balance.fleshed' => ' that have an outstanding balance',
1562 foreach (keys %methods) {
1564 method => "user_transactions",
1567 desc => 'For a given user, retrieve a list of '
1568 . (/\.fleshed/ ? 'fleshed ' : '')
1569 . 'transactions' . $methods{$_}
1570 . ' optionally limited to transactions of a given type.',
1571 params => $common_params,
1573 desc => "List of objects, or event on error. Each object is a hash containing: transaction, circ, record. "
1574 . 'These represent the relevant (mbts) transaction, attached circulation and title pointed to in the circ, respectively.',
1578 /\.have_balance/ and $args{authoritative} = 1; # FIXME: I don't know why have_charge isn't authoritative
1579 __PACKAGE__->register_method(%args);
1582 # Now for the counts
1584 'open-ils.actor.user.transactions.count' => '',
1585 'open-ils.actor.user.transactions.have_charge.count' => ' that have an initial charge',
1586 'open-ils.actor.user.transactions.have_balance.count' => ' that have an outstanding balance',
1589 foreach (keys %methods) {
1591 method => "user_transactions",
1594 desc => 'For a given user, retrieve a count of open '
1595 . 'transactions' . $methods{$_}
1596 . ' optionally limited to transactions of a given type.',
1597 params => $common_params,
1598 return => { desc => "Integer count of transactions, or event on error" }
1601 /\.have_balance/ and $args{authoritative} = 1; # FIXME: I don't know why have_charge isn't authoritative
1602 __PACKAGE__->register_method(%args);
1605 __PACKAGE__->register_method(
1606 method => "user_transactions",
1607 api_name => "open-ils.actor.user.transactions.have_balance.total",
1610 desc => 'For a given user, retrieve the total balance owed for open transactions,'
1611 . ' optionally limited to transactions of a given type.',
1612 params => $common_params,
1613 return => { desc => "Decimal balance value, or event on error" }
1618 sub user_transactions {
1619 my( $self, $client, $login_session, $user_id, $type, $options ) = @_;
1622 my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
1623 $login_session, $user_id, 'VIEW_USER_TRANSACTIONS' );
1624 return $evt if $evt;
1626 my $api = $self->api_name();
1628 my $filter = ($api =~ /have_balance/o) ?
1629 { 'balance_owed' => { '<>' => 0 } }:
1630 { 'total_owed' => { '>' => 0 } };
1632 my ($trans) = $self->method_lookup(
1633 'open-ils.actor.user.transactions.history.still_open')
1634 ->run( $login_session, $user_id, $type, $filter, $options );
1636 if($api =~ /total/o) {
1638 for my $t (@$trans) {
1639 $total += $t->balance_owed;
1642 $logger->debug("Total balance owed by user $user_id: $total");
1646 ($api =~ /count/o ) and return scalar @$trans;
1647 ($api !~ /fleshed/o) and return $trans;
1650 for my $t (@$trans) {
1652 if( $t->xact_type ne 'circulation' ) {
1653 push @resp, {transaction => $t};
1657 my $circ = $apputils->simple_scalar_request(
1659 "open-ils.cstore.direct.action.circulation.retrieve",
1664 my $title = $apputils->simple_scalar_request(
1666 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
1667 $circ->target_copy );
1671 my $u = OpenILS::Utils::ModsParser->new();
1672 $u->start_mods_batch($title->marc());
1673 my $mods = $u->finish_mods_batch();
1674 $mods->doc_id($title->id) if $mods;
1676 push @resp, {transaction => $t, circ => $circ, record => $mods };
1684 __PACKAGE__->register_method(
1685 method => "user_transaction_retrieve",
1686 api_name => "open-ils.actor.user.transaction.fleshed.retrieve",
1688 notes => "Returns a fleshed transaction record"
1691 __PACKAGE__->register_method(
1692 method => "user_transaction_retrieve",
1693 api_name => "open-ils.actor.user.transaction.retrieve",
1695 notes => "Returns a transaction record"
1698 sub user_transaction_retrieve {
1699 my( $self, $client, $login_session, $bill_id ) = @_;
1701 # I think I'm deprecated... make sure. phasefx says, "No, I'll use you :)
1703 my $trans = $apputils->simple_scalar_request(
1705 "open-ils.cstore.direct.money.billable_transaction_summary.retrieve",
1709 my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
1710 $login_session, $trans->usr, 'VIEW_USER_TRANSACTIONS' );
1711 return $evt if $evt;
1713 my $api = $self->api_name();
1714 if($api !~ /fleshed/o) { return $trans; }
1716 if( $trans->xact_type ne 'circulation' ) {
1717 $logger->debug("Returning non-circ transaction");
1718 return {transaction => $trans};
1721 my $circ = $apputils->simple_scalar_request(
1723 "open-ils.cstore.direct.action.circulation.retrieve",
1726 return {transaction => $trans} unless $circ;
1727 $logger->debug("Found the circ transaction");
1729 my $title = $apputils->simple_scalar_request(
1731 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
1732 $circ->target_copy );
1734 return {transaction => $trans, circ => $circ } unless $title;
1735 $logger->debug("Found the circ title");
1738 my $copy = $apputils->simple_scalar_request(
1740 "open-ils.cstore.direct.asset.copy.retrieve",
1741 $circ->target_copy );
1744 my $u = OpenILS::Utils::ModsParser->new();
1745 $u->start_mods_batch($title->marc());
1746 $mods = $u->finish_mods_batch();
1748 if ($title->id == OILS_PRECAT_RECORD) {
1749 $mods = new Fieldmapper::metabib::virtual_record;
1750 $mods->doc_id(OILS_PRECAT_RECORD);
1751 $mods->title($copy->dummy_title);
1752 $mods->author($copy->dummy_author);
1756 $logger->debug("MODSized the circ title");
1758 return {transaction => $trans, circ => $circ, record => $mods, copy => $copy };
1762 __PACKAGE__->register_method(
1763 method => "hold_request_count",
1764 api_name => "open-ils.actor.user.hold_requests.count",
1767 notes => 'Returns hold ready/total counts'
1770 sub hold_request_count {
1771 my( $self, $client, $login_session, $userid ) = @_;
1773 my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
1774 $login_session, $userid, 'VIEW_HOLD' );
1775 return $evt if $evt;
1778 my $holds = $apputils->simple_scalar_request(
1780 "open-ils.cstore.direct.action.hold_request.search.atomic",
1783 fulfillment_time => {"=" => undef },
1784 cancel_time => undef,
1789 for my $h (@$holds) {
1790 next unless $h->capture_time and $h->current_copy;
1792 my $copy = $apputils->simple_scalar_request(
1794 "open-ils.cstore.direct.asset.copy.retrieve",
1798 if ($copy and $copy->status == 8) {
1803 return { total => scalar(@$holds), ready => scalar(@ready) };
1807 __PACKAGE__->register_method(
1808 method => "checkedout_count",
1809 api_name => "open-ils.actor.user.checked_out.count__",
1812 desc => "[DEPRECATED] For a given user, returns the total number of items checked out, "
1813 . ' and the total number of items overdue',
1815 { desc => 'Authentication Token', type => 'string'},
1816 { desc => 'User ID', type => 'string'},
1819 desc => 'Counts of total checked out and overdue, like { total => $i, overdue => $j }',
1826 sub checkedout_count {
1827 my( $self, $client, $login_session, $userid ) = @_;
1829 my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
1830 $login_session, $userid, 'VIEW_CIRCULATIONS' );
1831 return $evt if $evt;
1833 my $circs = $apputils->simple_scalar_request(
1835 "open-ils.cstore.direct.action.circulation.search.atomic",
1836 { usr => $userid, stop_fines => undef }
1837 #{ usr => $userid, checkin_time => {"=" => undef } }
1840 my $parser = DateTime::Format::ISO8601->new;
1843 for my $c (@$circs) {
1844 my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $c->due_date ) );
1845 if ($due_dt->epoch < DateTime->today->epoch) {
1850 return { total => scalar(@$circs), overdue => $overdue };
1854 __PACKAGE__->register_method(
1855 method => "checked_out",
1856 api_name => "open-ils.actor.user.checked_out",
1860 desc => "For a given user, returns a structure of circulations objects sorted by out, overdue, lost, claims_returned, long_overdue. "
1861 . "A list of IDs are returned of each type. Circs marked lost, long_overdue, and claims_returned will not be 'finished' "
1862 . "(i.e., outstanding balance or some other pending action on the circ). "
1863 . "The .count method also includes a 'total' field which sums all open circs.",
1865 { desc => 'Authentication Token', type => 'string'},
1866 { desc => 'User ID', type => 'string'},
1869 desc => 'Returns event on error, or an object with ID lists, like: '
1870 . '{"out":[12552,451232], "claims_returned":[], "long_overdue":[23421] "overdue":[], "lost":[]}'
1875 __PACKAGE__->register_method(
1876 method => "checked_out",
1877 api_name => "open-ils.actor.user.checked_out.count",
1880 signature => q/@see open-ils.actor.user.checked_out/
1884 my( $self, $conn, $auth, $userid ) = @_;
1886 my $e = new_editor(authtoken=>$auth);
1887 return $e->event unless $e->checkauth;
1889 if( $userid ne $e->requestor->id ) {
1890 my $user = $e->retrieve_actor_user($userid) or return $e->event;
1891 unless($e->allowed('VIEW_CIRCULATIONS', $user->home_ou)) {
1893 # see if there is a friend link allowing circ.view perms
1894 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
1895 $e, $userid, $e->requestor->id, 'circ.view');
1896 return $e->event unless $allowed;
1900 my $count = $self->api_name =~ /count/;
1901 return _checked_out( $count, $e, $userid );
1905 my( $iscount, $e, $userid ) = @_;
1906 my $meth = 'open-ils.storage.actor.user.checked_out';
1907 $meth = "$meth.count" if $iscount;
1908 return $U->storagereq($meth, $userid);
1912 sub _checked_out_WHAT {
1913 my( $iscount, $e, $userid ) = @_;
1915 my $circs = $e->search_action_circulation(
1916 { usr => $userid, stop_fines => undef });
1918 my $mcircs = $e->search_action_circulation(
1921 checkin_time => undef,
1922 xact_finish => undef,
1926 push( @$circs, @$mcircs );
1928 my $parser = DateTime::Format::ISO8601->new;
1930 # split the circs up into overdue and not-overdue circs
1932 for my $c (@$circs) {
1933 if( $c->due_date ) {
1934 my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $c->due_date ) );
1935 my $due = $due_dt->epoch;
1936 if ($due < DateTime->today->epoch) {
1937 push @overdue, $c->id;
1946 # grab all of the lost, claims-returned, and longoverdue circs
1947 #my $open = $e->search_action_circulation(
1948 # {usr => $userid, stop_fines => { '!=' => undef }, xact_finish => undef });
1951 # these items have stop_fines, but no xact_finish, so money
1952 # is owed on them and they have not been checked in
1953 my $open = $e->search_action_circulation(
1956 stop_fines => { in => [ qw/LOST CLAIMSRETURNED LONGOVERDUE/ ] },
1957 xact_finish => undef,
1958 checkin_time => undef,
1963 my( @lost, @cr, @lo );
1964 for my $c (@$open) {
1965 push( @lost, $c->id ) if $c->stop_fines eq 'LOST';
1966 push( @cr, $c->id ) if $c->stop_fines eq 'CLAIMSRETURNED';
1967 push( @lo, $c->id ) if $c->stop_fines eq 'LONGOVERDUE';
1973 total => @$circs + @lost + @cr + @lo,
1974 out => scalar(@out),
1975 overdue => scalar(@overdue),
1976 lost => scalar(@lost),
1977 claims_returned => scalar(@cr),
1978 long_overdue => scalar(@lo)
1984 overdue => \@overdue,
1986 claims_returned => \@cr,
1987 long_overdue => \@lo
1993 __PACKAGE__->register_method(
1994 method => "checked_in_with_fines",
1995 api_name => "open-ils.actor.user.checked_in_with_fines",
1998 signature => q/@see open-ils.actor.user.checked_out/
2001 sub checked_in_with_fines {
2002 my( $self, $conn, $auth, $userid ) = @_;
2004 my $e = new_editor(authtoken=>$auth);
2005 return $e->event unless $e->checkauth;
2007 if( $userid ne $e->requestor->id ) {
2008 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2011 # money is owed on these items and they are checked in
2012 my $open = $e->search_action_circulation(
2015 xact_finish => undef,
2016 checkin_time => { "!=" => undef },
2021 my( @lost, @cr, @lo );
2022 for my $c (@$open) {
2023 push( @lost, $c->id ) if $c->stop_fines eq 'LOST';
2024 push( @cr, $c->id ) if $c->stop_fines eq 'CLAIMSRETURNED';
2025 push( @lo, $c->id ) if $c->stop_fines eq 'LONGOVERDUE';
2030 claims_returned => \@cr,
2031 long_overdue => \@lo
2037 my ($api, $desc, $auth) = @_;
2038 $desc = $desc ? (" " . $desc) : '';
2039 my $ids = ($api =~ /ids$/) ? 1 : 0;
2042 method => "user_transaction_history",
2043 api_name => "open-ils.actor.user.transactions.$api",
2045 desc => "For a given User ID, returns a list of billable transaction" .
2046 ($ids ? " id" : '') .
2047 "s$desc, optionally filtered by type and/or fields in money.billable_xact_summary. " .
2048 "The VIEW_USER_TRANSACTIONS permission is required to view another user's transactions",
2050 {desc => 'Authentication token', type => 'string'},
2051 {desc => 'User ID', type => 'number'},
2052 {desc => 'Transaction type (optional)', type => 'number'},
2053 {desc => 'Hash of Billable Transaction Summary filters (optional)', type => 'object'}
2056 desc => 'List of transaction' . ($ids ? " id" : '') . 's, Event on error'
2060 $auth and push @sig, (authoritative => 1);
2064 my %hist_methods = (
2066 'history.have_charge' => 'that have an initial charge',
2067 'history.still_open' => 'that are not finished',
2069 my %auth_hist_methods = (
2070 'history.have_balance' => 'that have a balance',
2071 'history.have_bill' => 'that have billings',
2072 'history.have_bill_or_payment' => 'that have non-zero-sum billings or at least 1 payment',
2074 foreach (keys %hist_methods) {
2075 __PACKAGE__->register_method(_sigmaker($_, $hist_methods{$_}));
2076 __PACKAGE__->register_method(_sigmaker("$_.ids", $hist_methods{$_}));
2078 foreach (keys %auth_hist_methods) {
2079 __PACKAGE__->register_method(_sigmaker($_, $auth_hist_methods{$_}, 1));
2080 __PACKAGE__->register_method(_sigmaker("$_.ids", $auth_hist_methods{$_}, 1));
2083 sub user_transaction_history {
2084 my( $self, $conn, $auth, $userid, $type, $filter, $options ) = @_;
2088 # run inside of a transaction to prevent replication delays
2089 my $e = new_editor(authtoken=>$auth);
2090 return $e->die_event unless $e->checkauth;
2092 if ($e->requestor->id ne $userid) {
2093 return $e->die_event unless $e->allowed('VIEW_USER_TRANSACTIONS');
2096 my $api = $self->api_name;
2097 my @xact_finish = (xact_finish => undef ) if ($api =~ /history\.still_open$/); # What about history.still_open.ids?
2099 if(defined($type)) {
2100 $filter->{'xact_type'} = $type;
2103 if($api =~ /have_bill_or_payment/o) {
2105 # transactions that have a non-zero sum across all billings or at least 1 payment
2106 $filter->{'-or'} = {
2107 'balance_owed' => { '<>' => 0 },
2108 'last_payment_ts' => { '<>' => undef }
2111 } elsif( $api =~ /have_balance/o) {
2113 # transactions that have a non-zero overall balance
2114 $filter->{'balance_owed'} = { '<>' => 0 };
2116 } elsif( $api =~ /have_charge/o) {
2118 # transactions that have at least 1 billing, regardless of whether it was voided
2119 $filter->{'last_billing_ts'} = { '<>' => undef };
2121 } elsif( $api =~ /have_bill/o) { # needs to be an elsif, or we double-match have_bill_or_payment!
2123 # transactions that have non-zero sum across all billings. This will exclude
2124 # xacts where all billings have been voided
2125 $filter->{'total_owed'} = { '<>' => 0 };
2128 my $options_clause = { order_by => { mbt => 'xact_start DESC' } };
2129 $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'};
2130 $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'};
2132 my $mbts = $e->search_money_billable_transaction_summary(
2134 { usr => $userid, @xact_finish, %$filter },
2139 if ($api =~ /\.ids/) {
2140 return [map {$_->id} @$mbts];
2148 __PACKAGE__->register_method(
2149 method => "user_perms",
2150 api_name => "open-ils.actor.permissions.user_perms.retrieve",
2152 notes => "Returns a list of permissions"
2156 my( $self, $client, $authtoken, $user ) = @_;
2158 my( $staff, $evt ) = $apputils->checkses($authtoken);
2159 return $evt if $evt;
2161 $user ||= $staff->id;
2163 if( $user != $staff->id and $evt = $apputils->check_perms( $staff->id, $staff->home_ou, 'VIEW_PERMISSION') ) {
2167 return $apputils->simple_scalar_request(
2169 "open-ils.storage.permission.user_perms.atomic",
2173 __PACKAGE__->register_method(
2174 method => "retrieve_perms",
2175 api_name => "open-ils.actor.permissions.retrieve",
2176 notes => "Returns a list of permissions"
2178 sub retrieve_perms {
2179 my( $self, $client ) = @_;
2180 return $apputils->simple_scalar_request(
2182 "open-ils.cstore.direct.permission.perm_list.search.atomic",
2183 { id => { '!=' => undef } }
2187 __PACKAGE__->register_method(
2188 method => "retrieve_groups",
2189 api_name => "open-ils.actor.groups.retrieve",
2190 notes => "Returns a list of user groups"
2192 sub retrieve_groups {
2193 my( $self, $client ) = @_;
2194 return new_editor()->retrieve_all_permission_grp_tree();
2197 __PACKAGE__->register_method(
2198 method => "retrieve_org_address",
2199 api_name => "open-ils.actor.org_unit.address.retrieve",
2200 notes => <<' NOTES');
2201 Returns an org_unit address by ID
2202 @param An org_address ID
2204 sub retrieve_org_address {
2205 my( $self, $client, $id ) = @_;
2206 return $apputils->simple_scalar_request(
2208 "open-ils.cstore.direct.actor.org_address.retrieve",
2213 __PACKAGE__->register_method(
2214 method => "retrieve_groups_tree",
2215 api_name => "open-ils.actor.groups.tree.retrieve",
2216 notes => "Returns a list of user groups"
2219 sub retrieve_groups_tree {
2220 my( $self, $client ) = @_;
2221 return new_editor()->search_permission_grp_tree(
2226 flesh_fields => { pgt => ["children"] },
2227 order_by => { pgt => 'name'}
2234 __PACKAGE__->register_method(
2235 method => "add_user_to_groups",
2236 api_name => "open-ils.actor.user.set_groups",
2237 notes => "Adds a user to one or more permission groups"
2240 sub add_user_to_groups {
2241 my( $self, $client, $authtoken, $userid, $groups ) = @_;
2243 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2244 $authtoken, $userid, 'CREATE_USER_GROUP_LINK' );
2245 return $evt if $evt;
2247 ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2248 $authtoken, $userid, 'REMOVE_USER_GROUP_LINK' );
2249 return $evt if $evt;
2251 $apputils->simplereq(
2253 'open-ils.storage.direct.permission.usr_grp_map.mass_delete', { usr => $userid } );
2255 for my $group (@$groups) {
2256 my $link = Fieldmapper::permission::usr_grp_map->new;
2258 $link->usr($userid);
2260 my $id = $apputils->simplereq(
2262 'open-ils.storage.direct.permission.usr_grp_map.create', $link );
2268 __PACKAGE__->register_method(
2269 method => "get_user_perm_groups",
2270 api_name => "open-ils.actor.user.get_groups",
2271 notes => "Retrieve a user's permission groups."
2275 sub get_user_perm_groups {
2276 my( $self, $client, $authtoken, $userid ) = @_;
2278 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2279 $authtoken, $userid, 'VIEW_PERM_GROUPS' );
2280 return $evt if $evt;
2282 return $apputils->simplereq(
2284 'open-ils.cstore.direct.permission.usr_grp_map.search.atomic', { usr => $userid } );
2288 __PACKAGE__->register_method(
2289 method => "get_user_work_ous",
2290 api_name => "open-ils.actor.user.get_work_ous",
2291 notes => "Retrieve a user's work org units."
2294 __PACKAGE__->register_method(
2295 method => "get_user_work_ous",
2296 api_name => "open-ils.actor.user.get_work_ous.ids",
2297 notes => "Retrieve a user's work org units."
2300 sub get_user_work_ous {
2301 my( $self, $client, $auth, $userid ) = @_;
2302 my $e = new_editor(authtoken=>$auth);
2303 return $e->event unless $e->checkauth;
2304 $userid ||= $e->requestor->id;
2306 if($e->requestor->id != $userid) {
2307 my $user = $e->retrieve_actor_user($userid)
2308 or return $e->event;
2309 return $e->event unless $e->allowed('ASSIGN_WORK_ORG_UNIT', $user->home_ou);
2312 return $e->search_permission_usr_work_ou_map({usr => $userid})
2313 unless $self->api_name =~ /.ids$/;
2315 # client just wants a list of org IDs
2316 return $U->get_user_work_ou_ids($e, $userid);
2321 __PACKAGE__->register_method(
2322 method => 'register_workstation',
2323 api_name => 'open-ils.actor.workstation.register.override',
2324 signature => q/@see open-ils.actor.workstation.register/
2327 __PACKAGE__->register_method(
2328 method => 'register_workstation',
2329 api_name => 'open-ils.actor.workstation.register',
2331 Registers a new workstion in the system
2332 @param authtoken The login session key
2333 @param name The name of the workstation id
2334 @param owner The org unit that owns this workstation
2335 @return The workstation id on success, WORKSTATION_NAME_EXISTS
2336 if the name is already in use.
2340 sub register_workstation {
2341 my( $self, $conn, $authtoken, $name, $owner ) = @_;
2343 my $e = new_editor(authtoken=>$authtoken, xact=>1);
2344 return $e->die_event unless $e->checkauth;
2345 return $e->die_event unless $e->allowed('REGISTER_WORKSTATION', $owner);
2346 my $existing = $e->search_actor_workstation({name => $name})->[0];
2350 if( $self->api_name =~ /override/o ) {
2351 # workstation with the given name exists.
2353 if($owner ne $existing->owning_lib) {
2354 # if necessary, update the owning_lib of the workstation
2356 $logger->info("changing owning lib of workstation ".$existing->id.
2357 " from ".$existing->owning_lib." to $owner");
2358 return $e->die_event unless
2359 $e->allowed('UPDATE_WORKSTATION', $existing->owning_lib);
2361 return $e->die_event unless $e->allowed('UPDATE_WORKSTATION', $owner);
2363 $existing->owning_lib($owner);
2364 return $e->die_event unless $e->update_actor_workstation($existing);
2370 "attempt to register an existing workstation. returning existing ID");
2373 return $existing->id;
2376 return OpenILS::Event->new('WORKSTATION_NAME_EXISTS')
2380 my $ws = Fieldmapper::actor::workstation->new;
2381 $ws->owning_lib($owner);
2383 $e->create_actor_workstation($ws) or return $e->die_event;
2385 return $ws->id; # note: editor sets the id on the new object for us
2388 __PACKAGE__->register_method(
2389 method => 'workstation_list',
2390 api_name => 'open-ils.actor.workstation.list',
2392 Returns a list of workstations registered at the given location
2393 @param authtoken The login session key
2394 @param ids A list of org_unit.id's for the workstation owners
2398 sub workstation_list {
2399 my( $self, $conn, $authtoken, @orgs ) = @_;
2401 my $e = new_editor(authtoken=>$authtoken);
2402 return $e->event unless $e->checkauth;
2407 unless $e->allowed('REGISTER_WORKSTATION', $o);
2408 $results{$o} = $e->search_actor_workstation({owning_lib=>$o});
2414 __PACKAGE__->register_method(
2415 method => 'fetch_patron_note',
2416 api_name => 'open-ils.actor.note.retrieve.all',
2419 Returns a list of notes for a given user
2420 Requestor must have VIEW_USER permission if pub==false and
2421 @param authtoken The login session key
2422 @param args Hash of params including
2423 patronid : the patron's id
2424 pub : true if retrieving only public notes
2428 sub fetch_patron_note {
2429 my( $self, $conn, $authtoken, $args ) = @_;
2430 my $patronid = $$args{patronid};
2432 my($reqr, $evt) = $U->checkses($authtoken);
2433 return $evt if $evt;
2436 ($patron, $evt) = $U->fetch_user($patronid);
2437 return $evt if $evt;
2440 if( $patronid ne $reqr->id ) {
2441 $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
2442 return $evt if $evt;
2444 return $U->cstorereq(
2445 'open-ils.cstore.direct.actor.usr_note.search.atomic',
2446 { usr => $patronid, pub => 't' } );
2449 $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
2450 return $evt if $evt;
2452 return $U->cstorereq(
2453 'open-ils.cstore.direct.actor.usr_note.search.atomic', { usr => $patronid } );
2456 __PACKAGE__->register_method(
2457 method => 'create_user_note',
2458 api_name => 'open-ils.actor.note.create',
2460 Creates a new note for the given user
2461 @param authtoken The login session key
2462 @param note The note object
2465 sub create_user_note {
2466 my( $self, $conn, $authtoken, $note ) = @_;
2467 my $e = new_editor(xact=>1, authtoken=>$authtoken);
2468 return $e->die_event unless $e->checkauth;
2470 my $user = $e->retrieve_actor_user($note->usr)
2471 or return $e->die_event;
2473 return $e->die_event unless
2474 $e->allowed('UPDATE_USER',$user->home_ou);
2476 $note->creator($e->requestor->id);
2477 $e->create_actor_usr_note($note) or return $e->die_event;
2483 __PACKAGE__->register_method(
2484 method => 'delete_user_note',
2485 api_name => 'open-ils.actor.note.delete',
2487 Deletes a note for the given user
2488 @param authtoken The login session key
2489 @param noteid The note id
2492 sub delete_user_note {
2493 my( $self, $conn, $authtoken, $noteid ) = @_;
2495 my $e = new_editor(xact=>1, authtoken=>$authtoken);
2496 return $e->die_event unless $e->checkauth;
2497 my $note = $e->retrieve_actor_usr_note($noteid)
2498 or return $e->die_event;
2499 my $user = $e->retrieve_actor_user($note->usr)
2500 or return $e->die_event;
2501 return $e->die_event unless
2502 $e->allowed('UPDATE_USER', $user->home_ou);
2504 $e->delete_actor_usr_note($note) or return $e->die_event;
2510 __PACKAGE__->register_method(
2511 method => 'update_user_note',
2512 api_name => 'open-ils.actor.note.update',
2514 @param authtoken The login session key
2515 @param note The note
2519 sub update_user_note {
2520 my( $self, $conn, $auth, $note ) = @_;
2521 my $e = new_editor(authtoken=>$auth, xact=>1);
2522 return $e->event unless $e->checkauth;
2523 my $patron = $e->retrieve_actor_user($note->usr)
2524 or return $e->event;
2525 return $e->event unless
2526 $e->allowed('UPDATE_USER', $patron->home_ou);
2527 $e->update_actor_user_note($note)
2528 or return $e->event;
2535 __PACKAGE__->register_method(
2536 method => 'create_closed_date',
2537 api_name => 'open-ils.actor.org_unit.closed_date.create',
2539 Creates a new closing entry for the given org_unit
2540 @param authtoken The login session key
2541 @param note The closed_date object
2544 sub create_closed_date {
2545 my( $self, $conn, $authtoken, $cd ) = @_;
2547 my( $user, $evt ) = $U->checkses($authtoken);
2548 return $evt if $evt;
2550 $evt = $U->check_perms($user->id, $cd->org_unit, 'CREATE_CLOSEING');
2551 return $evt if $evt;
2553 $logger->activity("user ".$user->id." creating library closing for ".$cd->org_unit);
2555 my $id = $U->storagereq(
2556 'open-ils.storage.direct.actor.org_unit.closed_date.create', $cd );
2557 return $U->DB_UPDATE_FAILED($cd) unless $id;
2562 __PACKAGE__->register_method(
2563 method => 'delete_closed_date',
2564 api_name => 'open-ils.actor.org_unit.closed_date.delete',
2566 Deletes a closing entry for the given org_unit
2567 @param authtoken The login session key
2568 @param noteid The close_date id
2571 sub delete_closed_date {
2572 my( $self, $conn, $authtoken, $cd ) = @_;
2574 my( $user, $evt ) = $U->checkses($authtoken);
2575 return $evt if $evt;
2578 ($cd_obj, $evt) = fetch_closed_date($cd);
2579 return $evt if $evt;
2581 $evt = $U->check_perms($user->id, $cd->org_unit, 'DELETE_CLOSEING');
2582 return $evt if $evt;
2584 $logger->activity("user ".$user->id." deleting library closing for ".$cd->org_unit);
2586 my $stat = $U->storagereq(
2587 'open-ils.storage.direct.actor.org_unit.closed_date.delete', $cd );
2588 return $U->DB_UPDATE_FAILED($cd) unless $stat;
2593 __PACKAGE__->register_method(
2594 method => 'usrname_exists',
2595 api_name => 'open-ils.actor.username.exists',
2597 desc => 'Check if a username is already taken (by an undeleted patron)',
2599 {desc => 'Authentication token', type => 'string'},
2600 {desc => 'Username', type => 'string'}
2603 desc => 'id of existing user if username exists, undef otherwise. Event on error'
2608 sub usrname_exists {
2609 my( $self, $conn, $auth, $usrname ) = @_;
2610 my $e = new_editor(authtoken=>$auth);
2611 return $e->event unless $e->checkauth;
2612 my $a = $e->search_actor_user({usrname => $usrname, deleted=>'f'}, {idlist=>1});
2613 return $$a[0] if $a and @$a;
2617 __PACKAGE__->register_method(
2618 method => 'barcode_exists',
2619 api_name => 'open-ils.actor.barcode.exists',
2621 signature => 'Returns 1 if the requested barcode exists, returns 0 otherwise'
2624 sub barcode_exists {
2625 my( $self, $conn, $auth, $barcode ) = @_;
2626 my $e = new_editor(authtoken=>$auth);
2627 return $e->event unless $e->checkauth;
2628 my $card = $e->search_actor_card({barcode => $barcode});
2634 #return undef unless @$card;
2635 #return $card->[0]->usr;
2639 __PACKAGE__->register_method(
2640 method => 'retrieve_net_levels',
2641 api_name => 'open-ils.actor.net_access_level.retrieve.all',
2644 sub retrieve_net_levels {
2645 my( $self, $conn, $auth ) = @_;
2646 my $e = new_editor(authtoken=>$auth);
2647 return $e->event unless $e->checkauth;
2648 return $e->retrieve_all_config_net_access_level();
2651 # Retain the old typo API name just in case
2652 __PACKAGE__->register_method(
2653 method => 'fetch_org_by_shortname',
2654 api_name => 'open-ils.actor.org_unit.retrieve_by_shorname',
2656 __PACKAGE__->register_method(
2657 method => 'fetch_org_by_shortname',
2658 api_name => 'open-ils.actor.org_unit.retrieve_by_shortname',
2660 sub fetch_org_by_shortname {
2661 my( $self, $conn, $sname ) = @_;
2662 my $e = new_editor();
2663 my $org = $e->search_actor_org_unit({ shortname => uc($sname)})->[0];
2664 return $e->event unless $org;
2669 __PACKAGE__->register_method(
2670 method => 'session_home_lib',
2671 api_name => 'open-ils.actor.session.home_lib',
2674 sub session_home_lib {
2675 my( $self, $conn, $auth ) = @_;
2676 my $e = new_editor(authtoken=>$auth);
2677 return undef unless $e->checkauth;
2678 my $org = $e->retrieve_actor_org_unit($e->requestor->home_ou);
2679 return $org->shortname;
2682 __PACKAGE__->register_method(
2683 method => 'session_safe_token',
2684 api_name => 'open-ils.actor.session.safe_token',
2686 Returns a hashed session ID that is safe for export to the world.
2687 This safe token will expire after 1 hour of non-use.
2688 @param auth Active authentication token
2692 sub session_safe_token {
2693 my( $self, $conn, $auth ) = @_;
2694 my $e = new_editor(authtoken=>$auth);
2695 return undef unless $e->checkauth;
2697 my $safe_token = md5_hex($auth);
2699 $cache ||= OpenSRF::Utils::Cache->new("global", 0);
2701 # Add more like the following if needed...
2703 "safe-token-home_lib-shortname-$safe_token",
2704 $e->retrieve_actor_org_unit(
2705 $e->requestor->home_ou
2714 __PACKAGE__->register_method(
2715 method => 'safe_token_home_lib',
2716 api_name => 'open-ils.actor.safe_token.home_lib.shortname',
2718 Returns the home library shortname from the session
2719 asscociated with a safe token from generated by
2720 open-ils.actor.session.safe_token.
2721 @param safe_token Active safe token
2725 sub safe_token_home_lib {
2726 my( $self, $conn, $safe_token ) = @_;
2728 $cache ||= OpenSRF::Utils::Cache->new("global", 0);
2729 return $cache->get_cache( 'safe-token-home_lib-shortname-'. $safe_token );
2734 __PACKAGE__->register_method(
2735 method => 'slim_tree',
2736 api_name => "open-ils.actor.org_tree.slim_hash.retrieve",
2739 my $tree = new_editor()->search_actor_org_unit(
2741 {"parent_ou" => undef },
2744 flesh_fields => { aou => ['children'] },
2745 order_by => { aou => 'name'},
2746 select => { aou => ["id","shortname", "name"]},
2751 return trim_tree($tree);
2757 return undef unless $tree;
2759 code => $tree->shortname,
2760 name => $tree->name,
2762 if( $tree->children and @{$tree->children} ) {
2763 $htree->{children} = [];
2764 for my $c (@{$tree->children}) {
2765 push( @{$htree->{children}}, trim_tree($c) );
2773 __PACKAGE__->register_method(
2774 method => "update_penalties",
2775 api_name => "open-ils.actor.user.penalties.update"
2778 sub update_penalties {
2779 my($self, $conn, $auth, $user_id) = @_;
2780 my $e = new_editor(authtoken=>$auth, xact => 1);
2781 return $e->die_event unless $e->checkauth;
2782 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
2783 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2784 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $e->requestor->ws_ou);
2785 return $evt if $evt;
2791 __PACKAGE__->register_method(
2792 method => "apply_penalty",
2793 api_name => "open-ils.actor.user.penalty.apply"
2797 my($self, $conn, $auth, $penalty) = @_;
2799 my $e = new_editor(authtoken=>$auth, xact => 1);
2800 return $e->die_event unless $e->checkauth;
2802 my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
2803 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2805 my $ptype = $e->retrieve_config_standing_penalty($penalty->standing_penalty) or return $e->die_event;
2808 (defined $ptype->org_depth) ?
2809 $U->org_unit_ancestor_at_depth($penalty->org_unit, $ptype->org_depth) :
2812 $penalty->org_unit($ctx_org);
2813 $penalty->staff($e->requestor->id);
2814 $e->create_actor_user_standing_penalty($penalty) or return $e->die_event;
2817 return $penalty->id;
2820 __PACKAGE__->register_method(
2821 method => "remove_penalty",
2822 api_name => "open-ils.actor.user.penalty.remove"
2825 sub remove_penalty {
2826 my($self, $conn, $auth, $penalty) = @_;
2827 my $e = new_editor(authtoken=>$auth, xact => 1);
2828 return $e->die_event unless $e->checkauth;
2829 my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
2830 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2832 $e->delete_actor_user_standing_penalty($penalty) or return $e->die_event;
2837 __PACKAGE__->register_method(
2838 method => "update_penalty_note",
2839 api_name => "open-ils.actor.user.penalty.note.update"
2842 sub update_penalty_note {
2843 my($self, $conn, $auth, $penalty_ids, $note) = @_;
2844 my $e = new_editor(authtoken=>$auth, xact => 1);
2845 return $e->die_event unless $e->checkauth;
2846 for my $penalty_id (@$penalty_ids) {
2847 my $penalty = $e->search_actor_user_standing_penalty( { id => $penalty_id } )->[0];
2848 if (! $penalty ) { return $e->die_event; }
2849 my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
2850 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2852 $penalty->note( $note ); $penalty->ischanged( 1 );
2854 $e->update_actor_user_standing_penalty($penalty) or return $e->die_event;
2860 __PACKAGE__->register_method(
2861 method => "ranged_penalty_thresholds",
2862 api_name => "open-ils.actor.grp_penalty_threshold.ranged.retrieve",
2866 sub ranged_penalty_thresholds {
2867 my($self, $conn, $auth, $context_org) = @_;
2868 my $e = new_editor(authtoken=>$auth);
2869 return $e->event unless $e->checkauth;
2870 return $e->event unless $e->allowed('VIEW_GROUP_PENALTY_THRESHOLD', $context_org);
2871 my $list = $e->search_permission_grp_penalty_threshold([
2872 {org_unit => $U->get_org_ancestors($context_org)},
2873 {order_by => {pgpt => 'id'}}
2875 $conn->respond($_) for @$list;
2881 __PACKAGE__->register_method(
2882 method => "user_retrieve_fleshed_by_id",
2884 api_name => "open-ils.actor.user.fleshed.retrieve",
2887 sub user_retrieve_fleshed_by_id {
2888 my( $self, $client, $auth, $user_id, $fields ) = @_;
2889 my $e = new_editor(authtoken => $auth);
2890 return $e->event unless $e->checkauth;
2892 if( $e->requestor->id != $user_id ) {
2893 return $e->event unless $e->allowed('VIEW_USER');
2899 "standing_penalties",
2903 "stat_cat_entries" ];
2904 return new_flesh_user($user_id, $fields, $e);
2908 sub new_flesh_user {
2911 my $fields = shift || [];
2914 my $fetch_penalties = 0;
2915 if(grep {$_ eq 'standing_penalties'} @$fields) {
2916 $fields = [grep {$_ ne 'standing_penalties'} @$fields];
2917 $fetch_penalties = 1;
2920 my $user = $e->retrieve_actor_user(
2925 "flesh_fields" => { "au" => $fields }
2928 ) or return $e->event;
2931 if( grep { $_ eq 'addresses' } @$fields ) {
2933 $user->addresses([]) unless @{$user->addresses};
2934 # don't expose "replaced" addresses by default
2935 $user->addresses([grep {$_->id >= 0} @{$user->addresses}]);
2937 if( ref $user->billing_address ) {
2938 unless( grep { $user->billing_address->id == $_->id } @{$user->addresses} ) {
2939 push( @{$user->addresses}, $user->billing_address );
2943 if( ref $user->mailing_address ) {
2944 unless( grep { $user->mailing_address->id == $_->id } @{$user->addresses} ) {
2945 push( @{$user->addresses}, $user->mailing_address );
2950 if($fetch_penalties) {
2951 # grab the user penalties ranged for this location
2952 $user->standing_penalties(
2953 $e->search_actor_user_standing_penalty([
2956 {stop_date => undef},
2957 {stop_date => {'>' => 'now'}}
2959 org_unit => $U->get_org_ancestors($e->requestor->ws_ou)
2962 flesh_fields => {ausp => ['standing_penalty']}
2969 $user->clear_passwd();
2976 __PACKAGE__->register_method(
2977 method => "user_retrieve_parts",
2978 api_name => "open-ils.actor.user.retrieve.parts",
2981 sub user_retrieve_parts {
2982 my( $self, $client, $auth, $user_id, $fields ) = @_;
2983 my $e = new_editor(authtoken => $auth);
2984 return $e->event unless $e->checkauth;
2985 $user_id ||= $e->requestor->id;
2986 if( $e->requestor->id != $user_id ) {
2987 return $e->event unless $e->allowed('VIEW_USER');
2990 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
2991 push(@resp, $user->$_()) for(@$fields);
2997 __PACKAGE__->register_method(
2998 method => 'user_opt_in_enabled',
2999 api_name => 'open-ils.actor.user.org_unit_opt_in.enabled',
3000 signature => '@return 1 if user opt-in is globally enabled, 0 otherwise.'
3003 sub user_opt_in_enabled {
3004 my($self, $conn) = @_;
3005 my $sc = OpenSRF::Utils::SettingsClient->new;
3006 return 1 if lc($sc->config_value(share => user => 'opt_in')) eq 'true';
3011 __PACKAGE__->register_method(
3012 method => 'user_opt_in_at_org',
3013 api_name => 'open-ils.actor.user.org_unit_opt_in.check',
3015 @param $auth The auth token
3016 @param user_id The ID of the user to test
3017 @return 1 if the user has opted in at the specified org,
3018 event on error, and 0 otherwise. /
3020 sub user_opt_in_at_org {
3021 my($self, $conn, $auth, $user_id) = @_;
3023 # see if we even need to enforce the opt-in value
3024 return 1 unless user_opt_in_enabled($self);
3026 my $e = new_editor(authtoken => $auth);
3027 return $e->event unless $e->checkauth;
3028 my $org_id = $e->requestor->ws_ou;
3030 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3031 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3033 # user is automatically opted-in at the home org
3034 return 1 if $user->home_ou eq $org_id;
3036 my $vals = $e->search_actor_usr_org_unit_opt_in(
3037 {org_unit=>$org_id, usr=>$user_id},{idlist=>1});
3043 __PACKAGE__->register_method(
3044 method => 'create_user_opt_in_at_org',
3045 api_name => 'open-ils.actor.user.org_unit_opt_in.create',
3047 @param $auth The auth token
3048 @param user_id The ID of the user to test
3049 @return The ID of the newly created object, event on error./
3052 sub create_user_opt_in_at_org {
3053 my($self, $conn, $auth, $user_id) = @_;
3055 my $e = new_editor(authtoken => $auth, xact=>1);
3056 return $e->die_event unless $e->checkauth;
3057 my $org_id = $e->requestor->ws_ou;
3059 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3060 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3062 my $opt_in = Fieldmapper::actor::usr_org_unit_opt_in->new;
3064 $opt_in->org_unit($org_id);
3065 $opt_in->usr($user_id);
3066 $opt_in->staff($e->requestor->id);
3067 $opt_in->opt_in_ts('now');
3068 $opt_in->opt_in_ws($e->requestor->wsid);
3070 $opt_in = $e->create_actor_usr_org_unit_opt_in($opt_in)
3071 or return $e->die_event;
3079 __PACKAGE__->register_method (
3080 method => 'retrieve_org_hours',
3081 api_name => 'open-ils.actor.org_unit.hours_of_operation.retrieve',
3083 Returns the hours of operation for a specified org unit
3084 @param authtoken The login session key
3085 @param org_id The org_unit ID
3089 sub retrieve_org_hours {
3090 my($self, $conn, $auth, $org_id) = @_;
3091 my $e = new_editor(authtoken => $auth);
3092 return $e->die_event unless $e->checkauth;
3093 $org_id ||= $e->requestor->ws_ou;
3094 return $e->retrieve_actor_org_unit_hours_of_operation($org_id);
3098 __PACKAGE__->register_method (
3099 method => 'verify_user_password',
3100 api_name => 'open-ils.actor.verify_user_password',
3102 Given a barcode or username and the MD5 encoded password,
3103 returns 1 if the password is correct. Returns 0 otherwise.
3107 sub verify_user_password {
3108 my($self, $conn, $auth, $barcode, $username, $password) = @_;
3109 my $e = new_editor(authtoken => $auth);
3110 return $e->die_event unless $e->checkauth;
3112 my $user_by_barcode;
3113 my $user_by_username;
3115 my $card = $e->search_actor_card([
3116 {barcode => $barcode},
3117 {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0] or return 0;
3118 $user_by_barcode = $card->usr;
3119 $user = $user_by_barcode;
3122 $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return 0;
3123 $user = $user_by_username;
3125 return 0 if (!$user);
3126 return 0 if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3127 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3128 return 1 if $user->passwd eq $password;
3132 __PACKAGE__->register_method (
3133 method => 'retrieve_usr_id_via_barcode_or_usrname',
3134 api_name => "open-ils.actor.user.retrieve_id_by_barcode_or_username",
3136 Given a barcode or username returns the id for the user or
3141 sub retrieve_usr_id_via_barcode_or_usrname {
3142 my($self, $conn, $auth, $barcode, $username) = @_;
3143 my $e = new_editor(authtoken => $auth);
3144 return $e->die_event unless $e->checkauth;
3145 my $id_as_barcode= OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.actor' => app_settings => 'id_as_barcode');
3147 my $user_by_barcode;
3148 my $user_by_username;
3149 $logger->info("$id_as_barcode is the ID as BARCODE");
3151 my $card = $e->search_actor_card([
3152 {barcode => $barcode},
3153 {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
3154 if ($id_as_barcode =~ /^t/i) {
3156 $user = $e->retrieve_actor_user($barcode);
3157 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$user);
3159 $user_by_barcode = $card->usr;
3160 $user = $user_by_barcode;
3163 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$card);
3164 $user_by_barcode = $card->usr;
3165 $user = $user_by_barcode;
3170 $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return OpenILS::Event->new( 'ACTOR_USR_NOT_FOUND' );
3172 $user = $user_by_username;
3174 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if (!$user);
3175 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3176 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3181 __PACKAGE__->register_method (
3182 method => 'merge_users',
3183 api_name => 'open-ils.actor.user.merge',
3186 Given a list of source users and destination user, transfer all data from the source
3187 to the dest user and delete the source user. All user related data is
3188 transferred, including circulations, holds, bookbags, etc.
3194 my($self, $conn, $auth, $master_id, $user_ids, $options) = @_;
3195 my $e = new_editor(xact => 1, authtoken => $auth);
3196 return $e->die_event unless $e->checkauth;
3198 # disallow the merge if any subordinate accounts are in collections
3199 my $colls = $e->search_money_collections_tracker({usr => $user_ids}, {idlist => 1});
3200 return OpenILS::Event->new('MERGED_USER_IN_COLLECTIONS', payload => $user_ids) if @$colls;
3202 my $master_user = $e->retrieve_actor_user($master_id) or return $e->die_event;
3203 my $del_addrs = ($U->ou_ancestor_setting_value(
3204 $master_user->home_ou, 'circ.user_merge.delete_addresses', $e)) ? 't' : 'f';
3205 my $del_cards = ($U->ou_ancestor_setting_value(
3206 $master_user->home_ou, 'circ.user_merge.delete_cards', $e)) ? 't' : 'f';
3207 my $deactivate_cards = ($U->ou_ancestor_setting_value(
3208 $master_user->home_ou, 'circ.user_merge.deactivate_cards', $e)) ? 't' : 'f';
3210 for my $src_id (@$user_ids) {
3211 my $src_user = $e->retrieve_actor_user($src_id) or return $e->die_event;
3213 return $e->die_event unless $e->allowed('MERGE_USERS', $src_user->home_ou);
3214 if($src_user->home_ou ne $master_user->home_ou) {
3215 return $e->die_event unless $e->allowed('MERGE_USERS', $master_user->home_ou);
3218 return $e->die_event unless
3219 $e->json_query({from => [
3234 __PACKAGE__->register_method (
3235 method => 'approve_user_address',
3236 api_name => 'open-ils.actor.user.pending_address.approve',
3243 sub approve_user_address {
3244 my($self, $conn, $auth, $addr) = @_;
3245 my $e = new_editor(xact => 1, authtoken => $auth);
3246 return $e->die_event unless $e->checkauth;
3248 # if the caller passes an address object, assume they want to
3249 # update it first before approving it
3250 $e->update_actor_user_address($addr) or return $e->die_event;
3252 $addr = $e->retrieve_actor_user_address($addr) or return $e->die_event;
3254 my $user = $e->retrieve_actor_user($addr->usr);
3255 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3256 my $result = $e->json_query({from => ['actor.approve_pending_address', $addr->id]})->[0]
3257 or return $e->die_event;
3259 return [values %$result]->[0];
3263 __PACKAGE__->register_method (
3264 method => 'retrieve_friends',
3265 api_name => 'open-ils.actor.friends.retrieve',
3268 returns { confirmed: [], pending_out: [], pending_in: []}
3269 pending_out are users I'm requesting friendship with
3270 pending_in are users requesting friendship with me
3275 sub retrieve_friends {
3276 my($self, $conn, $auth, $user_id, $options) = @_;
3277 my $e = new_editor(authtoken => $auth);
3278 return $e->event unless $e->checkauth;
3279 $user_id ||= $e->requestor->id;
3281 if($user_id != $e->requestor->id) {
3282 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3283 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3286 return OpenILS::Application::Actor::Friends->retrieve_friends(
3287 $e, $user_id, $options);
3292 __PACKAGE__->register_method (
3293 method => 'apply_friend_perms',
3294 api_name => 'open-ils.actor.friends.perms.apply',
3300 sub apply_friend_perms {
3301 my($self, $conn, $auth, $user_id, $delegate_id, @perms) = @_;
3302 my $e = new_editor(authtoken => $auth, xact => 1);
3303 return $e->event unless $e->checkauth;
3305 if($user_id != $e->requestor->id) {
3306 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3307 return $e->die_event unless $e->allowed('VIEW_USER', $user->home_ou);
3310 for my $perm (@perms) {
3312 OpenILS::Application::Actor::Friends->apply_friend_perm(
3313 $e, $user_id, $delegate_id, $perm);
3314 return $evt if $evt;
3322 __PACKAGE__->register_method (
3323 method => 'update_user_pending_address',
3324 api_name => 'open-ils.actor.user.address.pending.cud'
3327 sub update_user_pending_address {
3328 my($self, $conn, $auth, $addr) = @_;
3329 my $e = new_editor(authtoken => $auth, xact => 1);
3330 return $e->event unless $e->checkauth;
3332 if($addr->usr != $e->requestor->id) {
3333 my $user = $e->retrieve_actor_user($addr->usr) or return $e->die_event;
3334 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3338 $e->create_actor_user_address($addr) or return $e->die_event;
3339 } elsif($addr->isdeleted) {
3340 $e->delete_actor_user_address($addr) or return $e->die_event;
3342 $e->update_actor_user_address($addr) or return $e->die_event;
3350 __PACKAGE__->register_method (
3351 method => 'user_events',
3352 api_name => 'open-ils.actor.user.events.circ',
3355 __PACKAGE__->register_method (
3356 method => 'user_events',
3357 api_name => 'open-ils.actor.user.events.ahr',
3362 my($self, $conn, $auth, $user_id, $filters) = @_;
3363 my $e = new_editor(authtoken => $auth);
3364 return $e->event unless $e->checkauth;
3366 (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3367 my $user_field = 'usr';
3370 $filters->{target} = {
3371 select => { $obj_type => ['id'] },
3373 where => {usr => $user_id}
3376 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3377 if($e->requestor->id != $user_id) {
3378 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3381 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3382 my $req = $ses->request('open-ils.trigger.events_by_target',
3383 $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3385 while(my $resp = $req->recv) {
3386 my $val = $resp->content;
3387 my $tgt = $val->target;
3389 if($obj_type eq 'circ') {
3390 $tgt->target_copy($e->retrieve_asset_copy($tgt->target_copy));
3392 } elsif($obj_type eq 'ahr') {
3393 $tgt->current_copy($e->retrieve_asset_copy($tgt->current_copy))
3394 if $tgt->current_copy;
3397 $conn->respond($val) if $val;
3403 __PACKAGE__->register_method (
3404 method => 'copy_events',
3405 api_name => 'open-ils.actor.copy.events.circ',
3408 __PACKAGE__->register_method (
3409 method => 'copy_events',
3410 api_name => 'open-ils.actor.copy.events.ahr',
3415 my($self, $conn, $auth, $copy_id, $filters) = @_;
3416 my $e = new_editor(authtoken => $auth);
3417 return $e->event unless $e->checkauth;
3419 (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3421 my $copy = $e->retrieve_asset_copy($copy_id) or return $e->event;
3423 my $copy_field = 'target_copy';
3424 $copy_field = 'current_copy' if $obj_type eq 'ahr';
3427 $filters->{target} = {
3428 select => { $obj_type => ['id'] },
3430 where => {$copy_field => $copy_id}
3434 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3435 my $req = $ses->request('open-ils.trigger.events_by_target',
3436 $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3438 while(my $resp = $req->recv) {
3439 my $val = $resp->content;
3440 my $tgt = $val->target;
3442 my $user = $e->retrieve_actor_user($tgt->usr);
3443 if($e->requestor->id != $user->id) {
3444 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3447 $tgt->$copy_field($copy);
3450 $conn->respond($val) if $val;
3459 __PACKAGE__->register_method (
3460 method => 'update_events',
3461 api_name => 'open-ils.actor.user.event.cancel.batch',
3464 __PACKAGE__->register_method (
3465 method => 'update_events',
3466 api_name => 'open-ils.actor.user.event.reset.batch',
3471 my($self, $conn, $auth, $event_ids) = @_;
3472 my $e = new_editor(xact => 1, authtoken => $auth);
3473 return $e->die_event unless $e->checkauth;
3476 for my $id (@$event_ids) {
3478 # do a little dance to determine what user we are ultimately affecting
3479 my $event = $e->retrieve_action_trigger_event([
3482 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
3484 ]) or return $e->die_event;
3487 if($event->event_def->hook->core_type eq 'circ') {
3488 $user_id = $e->retrieve_action_circulation($event->target)->usr;
3489 } elsif($event->event_def->hook->core_type eq 'ahr') {
3490 $user_id = $e->retrieve_action_hold_request($event->target)->usr;
3495 my $user = $e->retrieve_actor_user($user_id);
3496 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3498 if($self->api_name =~ /cancel/) {
3499 $event->state('invalid');
3500 } elsif($self->api_name =~ /reset/) {
3501 $event->clear_start_time;
3502 $event->clear_update_time;
3503 $event->state('pending');
3506 $e->update_action_trigger_event($event) or return $e->die_event;
3507 $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
3511 return {complete => 1};
3515 __PACKAGE__->register_method (
3516 method => 'really_delete_user',
3517 api_name => 'open-ils.actor.user.delete',
3519 It anonymizes all personally identifiable information in actor.usr. By calling actor.usr_purge_data()
3520 it also purges related data from other tables, sometimes by transferring it to a designated destination user.
3521 The usrname field (along with first_given_name and family_name) is updated to id '-PURGED-' now().
3522 dest_usr_id is only required when deleting a user that performs staff functions.
3526 sub really_delete_user {
3527 my($self, $conn, $auth, $user_id, $dest_user_id) = @_;
3528 my $e = new_editor(authtoken => $auth, xact => 1);
3529 return $e->die_event unless $e->checkauth;
3530 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3531 return $e->die_event unless $e->allowed('DELETE_USER', $user->home_ou);
3532 my $stat = $e->json_query(
3533 {from => ['actor.usr_delete', $user_id, $dest_user_id]})->[0]
3534 or return $e->die_event;
3541 __PACKAGE__->register_method (
3542 method => 'user_payments',
3543 api_name => 'open-ils.actor.user.payments.retrieve',
3546 Returns all payments for a given user. Default order is newest payments first.
3547 @param auth Authentication token
3548 @param user_id The user ID
3549 @param filters An optional hash of filters, including limit, offset, and order_by definitions
3554 my($self, $conn, $auth, $user_id, $filters) = @_;
3557 my $e = new_editor(authtoken => $auth);
3558 return $e->die_event unless $e->checkauth;
3560 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3561 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
3563 # Find all payments for all transactions for user $user_id
3565 select => {mp => ['id']},
3570 select => {mbt => ['id']},
3572 where => {usr => $user_id}
3576 order_by => [{ # by default, order newest payments first
3578 field => 'payment_ts',
3583 for (qw/order_by limit offset/) {
3584 $query->{$_} = $filters->{$_} if defined $filters->{$_};
3587 if(defined $filters->{where}) {
3588 foreach (keys %{$filters->{where}}) {
3589 # don't allow the caller to expand the result set to other users
3590 $query->{where}->{$_} = $filters->{where}->{$_} unless $_ eq 'xact';
3594 my $payment_ids = $e->json_query($query);
3595 for my $pid (@$payment_ids) {
3596 my $pay = $e->retrieve_money_payment([
3601 mbt => ['summary', 'circulation', 'grocery'],
3602 circ => ['target_copy'],
3603 acp => ['call_number'],
3611 xact_type => $pay->xact->summary->xact_type,
3612 last_billing_type => $pay->xact->summary->last_billing_type,
3615 if($pay->xact->summary->xact_type eq 'circulation') {
3616 $resp->{barcode} = $pay->xact->circulation->target_copy->barcode;
3617 $resp->{title} = $U->record_to_mvr($pay->xact->circulation->target_copy->call_number->record)->title;
3620 $pay->xact($pay->xact->id); # de-flesh
3621 $conn->respond($resp);
3629 __PACKAGE__->register_method (
3630 method => 'negative_balance_users',
3631 api_name => 'open-ils.actor.users.negative_balance',
3634 Returns all users that have an overall negative balance
3635 @param auth Authentication token
3636 @param org_id The context org unit as an ID or list of IDs. This will be the home
3637 library of the user. If no org_unit is specified, no org unit filter is applied
3641 sub negative_balance_users {
3642 my($self, $conn, $auth, $org_id) = @_;
3644 my $e = new_editor(authtoken => $auth);
3645 return $e->die_event unless $e->checkauth;
3646 return $e->die_event unless $e->allowed('VIEW_USER', $org_id);
3650 mous => ['usr', 'balance_owed'],
3653 {column => 'last_billing_ts', transform => 'max', aggregate => 1},
3654 {column => 'last_payment_ts', transform => 'max', aggregate => 1},
3671 where => {'+mous' => {balance_owed => {'<' => 0}}}
3674 $query->{from}->{mous}->{au}->{filter}->{home_ou} = $org_id if $org_id;
3676 my $list = $e->json_query($query, {timeout => 600});
3678 for my $data (@$list) {
3680 usr => $e->retrieve_actor_user([$data->{usr}, {flesh => 1, flesh_fields => {au => ['card']}}]),
3681 balance_owed => $data->{balance_owed},
3682 last_billing_activity => max($data->{last_billing_ts}, $data->{last_payment_ts})
3689 __PACKAGE__->register_method(
3690 method => "request_password_reset",
3691 api_name => "open-ils.actor.patron.password_reset.request",
3693 desc => "Generates a UUID token usable with the open-ils.actor.patron.password_reset.commit " .
3694 "method for changing a user's password. The UUID token is distributed via A/T " .
3695 "templates (i.e. email to the user).",
3697 { desc => 'user_id_type', type => 'string' },
3698 { desc => 'user_id', type => 'string' },
3700 return => {desc => '1 on success, Event on error'}
3703 sub request_password_reset {
3704 my($self, $conn, $user_id_type, $user_id) = @_;
3706 # Check to see if password reset requests are already being throttled:
3707 # 0. Check cache to see if we're in throttle mode (avoid hitting database)
3709 my $e = new_editor(xact => 1);
3712 # Get the user, if any, depending on the input value
3713 if ($user_id_type eq 'username') {
3714 $user = $e->search_actor_user({usrname => $user_id})->[0];
3717 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
3719 } elsif ($user_id_type eq 'barcode') {
3720 my $card = $e->search_actor_card([
3721 {barcode => $user_id},
3722 {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
3725 return OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
3730 # If the user doesn't have an email address, we can't help them
3731 if (!$user->email) {
3733 return OpenILS::Event->new('PATRON_NO_EMAIL_ADDRESS');
3735 _reset_password_request($conn, $e, $user);
3738 # Once we have the user, we can issue the password reset request
3739 # XXX Add a wrapper method that accepts barcode + email input
3740 sub _reset_password_request {
3741 my ($conn, $e, $user) = @_;
3743 # 1. Get throttle threshold and time-to-live from OU_settings
3744 my $aupr_throttle = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_throttle') || 1000;
3745 my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
3747 my $threshold_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl)->iso8601();
3749 # 2. Get time of last request and number of active requests (num_active)
3750 my $active_requests = $e->json_query({
3756 transform => 'COUNT'
3759 column => 'request_time',
3765 has_been_reset => { '=' => 'f' },
3766 request_time => { '>' => $threshold_time }
3770 # Guard against no active requests
3771 if ($active_requests->[0]->{'request_time'}) {
3772 my $last_request = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($active_requests->[0]->{'request_time'}));
3773 my $now = DateTime::Format::ISO8601->new();
3775 # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute)
3776 if (($active_requests->[0]->{'usr'} > $aupr_throttle) &&
3777 ($last_request->add_duration('1 minute') > $now)) {
3778 $cache->put_cache('open-ils.actor.password.throttle', DateTime::Format::ISO8601->new(), 60);
3780 return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
3784 # TODO Check to see if the user is in a password-reset-restricted group
3786 # Otherwise, go ahead and try to get the user.
3788 # Check the number of active requests for this user
3789 $active_requests = $e->json_query({
3795 transform => 'COUNT'
3800 usr => { '=' => $user->id },
3801 has_been_reset => { '=' => 'f' },
3802 request_time => { '>' => $threshold_time }
3806 $logger->info("User " . $user->id . " has " . $active_requests->[0]->{'usr'} . " active password reset requests.");
3808 # if less than or equal to per-user threshold, proceed; otherwise, return event
3809 my $aupr_per_user_limit = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_per_user_limit') || 3;
3810 if ($active_requests->[0]->{'usr'} > $aupr_per_user_limit) {
3812 return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
3815 # Create the aupr object and insert into the database
3816 my $reset_request = Fieldmapper::actor::usr_password_reset->new;
3817 my $uuid = create_uuid_as_string(UUID_V4);
3818 $reset_request->uuid($uuid);
3819 $reset_request->usr($user->id);
3821 my $aupr = $e->create_actor_usr_password_reset($reset_request) or return $e->die_event;
3824 # Create an event to notify user of the URL to reset their password
3826 # Can we stuff this in the user_data param for trigger autocreate?
3827 my $hostname = $U->ou_ancestor_setting_value($user->home_ou, 'lib.hostname') || 'localhost';
3829 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3830 $ses->request('open-ils.trigger.event.autocreate', 'password.reset_request', $aupr, $user->home_ou);
3833 # $U->create_trigger_event('password.reset_request', $aupr, $user->home_ou);
3838 __PACKAGE__->register_method(
3839 method => "commit_password_reset",
3840 api_name => "open-ils.actor.patron.password_reset.commit",
3842 desc => "Checks a UUID token generated by the open-ils.actor.patron.password_reset.request method for " .
3843 "validity, and if valid, uses it as authorization for changing the associated user's password " .
3844 "with the supplied password.",
3846 { desc => 'uuid', type => 'string' },
3847 { desc => 'password', type => 'string' },
3849 return => {desc => '1 on success, Event on error'}
3852 sub commit_password_reset {
3853 my($self, $conn, $uuid, $password) = @_;
3855 # Check to see if password reset requests are already being throttled:
3856 # 0. Check cache to see if we're in throttle mode (avoid hitting database)
3857 $cache ||= OpenSRF::Utils::Cache->new("global", 0);
3858 my $throttle = $cache->get_cache('open-ils.actor.password.throttle') || undef;
3860 return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
3863 my $e = new_editor(xact => 1);
3865 my $aupr = $e->search_actor_usr_password_reset({
3872 return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
3874 my $user_id = $aupr->[0]->usr;
3875 my $user = $e->retrieve_actor_user($user_id);
3877 # Ensure we're still within the TTL for the request
3878 my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
3879 my $threshold = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($aupr->[0]->request_time))->add(seconds => $aupr_ttl);
3880 if ($threshold > DateTime->now(time_zone => 'local')) {
3882 return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
3885 # Check complexity of password against OU-defined regex
3886 my $pw_regex = $U->ou_ancestor_setting_value($user->home_ou, 'global.password_regex');
3890 # Calling JSON2perl on the $pw_regex causes failure, even before the fancy Unicode regex
3891 # ($pw_regex = OpenSRF::Utils::JSON->JSON2perl($pw_regex)) =~ s/\\u([0-9a-fA-F]{4})/\\x{$1}/gs;
3892 $is_strong = check_password_strength_custom($password, $pw_regex);
3894 $is_strong = check_password_strength_default($password);
3899 return OpenILS::Event->new('PATRON_PASSWORD_WAS_NOT_STRONG');
3902 # All is well; update the password
3903 $user->passwd($password);
3904 $e->update_actor_user($user);
3906 # And flag that this password reset request has been honoured
3907 $aupr->[0]->has_been_reset('t');
3908 $e->update_actor_usr_password_reset($aupr->[0]);
3914 sub check_password_strength_default {
3915 my $password = shift;
3916 # Use the default set of checks
3917 if ( (length($password) < 7) or
3918 ($password !~ m/.*\d+.*/) or
3919 ($password !~ m/.*[A-Za-z]+.*/)
3926 sub check_password_strength_custom {
3927 my ($password, $pw_regex) = @_;
3929 $pw_regex = qr/$pw_regex/;
3930 if ($password !~ /$pw_regex/) {
3938 __PACKAGE__->register_method(
3939 method => "event_def_opt_in_settings",
3940 api_name => "open-ils.actor.event_def.opt_in.settings",
3943 desc => 'Streams the set of "cust" objects that are used as opt-in settings for event definitions',
3945 { desc => 'Authentication token', type => 'string'},
3947 desc => 'Org Unit ID. (optional). If no org ID is present, the home_ou of the requesting user is used',
3952 desc => q/set of "cust" objects that are used as opt-in settings for event definitions at the specified org unit/,
3959 sub event_def_opt_in_settings {
3960 my($self, $conn, $auth, $org_id) = @_;
3961 my $e = new_editor(authtoken => $auth);
3962 return $e->event unless $e->checkauth;
3964 if(defined $org_id and $org_id != $e->requestor->home_ou) {
3965 return $e->event unless
3966 $e->allowed(['VIEW_USER_SETTING_TYPE', 'ADMIN_USER_SETTING_TYPE'], $org_id);
3968 $org_id = $e->requestor->home_ou;
3971 # find all config.user_setting_type's related to event_defs for the requested org unit
3972 my $types = $e->json_query({
3973 select => {cust => ['name']},
3974 from => {atevdef => 'cust'},
3977 owner => $U->get_org_ancestors($org_id), # context org plus parents
3984 $conn->respond($_) for
3985 @{$e->search_config_usr_setting_type({name => [map {$_->{name}} @$types]})};
3992 __PACKAGE__->register_method(
3993 method => "user_visible_circs",
3994 api_name => "open-ils.actor.history.circ.visible",
3997 desc => 'Returns the set of opt-in visible circulations accompanied by circulation chain summaries',
3999 { desc => 'Authentication token', type => 'string'},
4000 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4001 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4004 desc => q/An object with 2 fields: circulation and summary.
4005 circulation is the "circ" object. summary is the related "accs" object/,
4011 __PACKAGE__->register_method(
4012 method => "user_visible_circs",
4013 api_name => "open-ils.actor.history.circ.visible.print",
4016 desc => 'Returns printable output for the set of opt-in visible circulations',
4018 { desc => 'Authentication token', type => 'string'},
4019 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4020 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4023 desc => q/An action_trigger.event object or error event./,
4029 __PACKAGE__->register_method(
4030 method => "user_visible_circs",
4031 api_name => "open-ils.actor.history.circ.visible.email",
4034 desc => 'Emails the set of opt-in visible circulations to the requestor',
4036 { desc => 'Authentication token', type => 'string'},
4037 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4038 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4041 desc => q/undef, or event on error/
4046 __PACKAGE__->register_method(
4047 method => "user_visible_circs",
4048 api_name => "open-ils.actor.history.hold.visible",
4051 desc => 'Returns the set of opt-in visible holds',
4053 { desc => 'Authentication token', type => 'string'},
4054 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4055 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4058 desc => q/An object with 1 field: "hold"/,
4064 __PACKAGE__->register_method(
4065 method => "user_visible_circs",
4066 api_name => "open-ils.actor.history.hold.visible.print",
4069 desc => 'Returns printable output for the set of opt-in visible holds',
4071 { desc => 'Authentication token', type => 'string'},
4072 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4073 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4076 desc => q/An action_trigger.event object or error event./,
4082 __PACKAGE__->register_method(
4083 method => "user_visible_circs",
4084 api_name => "open-ils.actor.history.hold.visible.email",
4087 desc => 'Emails the set of opt-in visible holds to the requestor',
4089 { desc => 'Authentication token', type => 'string'},
4090 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4091 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4094 desc => q/undef, or event on error/
4099 sub user_visible_circs {
4100 my($self, $conn, $auth, $user_id, $options) = @_;
4102 my $is_hold = ($self->api_name =~ /hold/);
4103 my $for_print = ($self->api_name =~ /print/);
4104 my $for_email = ($self->api_name =~ /email/);
4105 my $e = new_editor(authtoken => $auth);
4106 return $e->event unless $e->checkauth;
4108 $user_id ||= $e->requestor->id;
4110 $options->{limit} ||= 50;
4111 $options->{offset} ||= 0;
4113 if($user_id != $e->requestor->id) {
4114 my $perm = ($is_hold) ? 'VIEW_HOLD' : 'VIEW_CIRCULATIONS';
4115 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4116 return $e->event unless $e->allowed($perm, $user->home_ou);
4119 my $db_func = ($is_hold) ? 'action.usr_visible_holds' : 'action.usr_visible_circs';
4121 my $data = $e->json_query({
4122 from => [$db_func, $user_id],
4123 limit => $$options{limit},
4124 offset => $$options{offset}
4126 # TODO: I only want IDs. code below didn't get me there
4127 # {"select":{"au":[{"column":"id", "result_field":"id",
4128 # "transform":"action.usr_visible_circs"}]}, "where":{"id":10}, "from":"au"}
4133 return undef unless @$data;
4137 # collect the batch of objects
4141 my $hold_list = $e->search_action_hold_request({id => [map { $_->{id} } @$data]});
4142 return $U->fire_object_event(undef, 'ahr.format.history.print', $hold_list, $$hold_list[0]->request_lib);
4146 my $circ_list = $e->search_action_circulation({id => [map { $_->{id} } @$data]});
4147 return $U->fire_object_event(undef, 'circ.format.history.print', $circ_list, $$circ_list[0]->circ_lib);
4150 } elsif ($for_email) {
4152 $conn->respond_complete(1) if $for_email; # no sense in waiting
4160 my $hold = $e->retrieve_action_hold_request($id);
4161 $U->create_events_for_hook('ahr.format.history.email', $hold, $hold->request_lib, 1);
4162 # events will be fired from action_trigger_runner
4166 my $circ = $e->retrieve_action_circulation($id);
4167 $U->create_events_for_hook('circ.format.history.email', $circ, $circ->circ_lib, 1);
4168 # events will be fired from action_trigger_runner
4172 } else { # just give me the data please
4180 my $hold = $e->retrieve_action_hold_request($id);
4181 $conn->respond({hold => $hold});
4185 my $circ = $e->retrieve_action_circulation($id);
4188 summary => $U->create_circ_chain_summary($e, $id)