1 # ---------------------------------------------------------------
2 # Copyright (C) 2005 Georgia Public Library Service
3 # Bill Erickson <highfalutin@gmail.com>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
23 use OpenSRF::EX qw(:try);
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::CStoreEditor q/:funcs/;
29 use OpenILS::Utils::PermitHold;
30 use OpenSRF::Utils::SettingsClient;
31 use OpenILS::Const qw/:const/;
32 use OpenILS::Application::Circ::Transit;
33 use OpenILS::Application::Actor::Friends;
35 use DateTime::Format::ISO8601;
36 use OpenSRF::Utils qw/:datetime/;
37 my $apputils = "OpenILS::Application::AppUtils";
41 __PACKAGE__->register_method(
42 method => "create_hold",
43 api_name => "open-ils.circ.holds.create",
45 desc => "Create a new hold for an item. From a permissions perspective, " .
46 "the login session is used as the 'requestor' of the hold. " .
47 "The hold recipient is determined by the 'usr' setting within the hold object. " .
48 'First we verify the requestor has holds request permissions. ' .
49 'Then we verify that the recipient is allowed to make the given hold. ' .
50 'If not, we see if the requestor has "override" capabilities. If not, ' .
51 'a permission exception is returned. If permissions allow, we cycle ' .
52 'through the set of holds objects and create. ' .
53 'If the recipient does not have permission to place multiple holds ' .
54 'on a single title and said operation is attempted, a permission ' .
55 'exception is returned',
57 { desc => 'Authentication token', type => 'string' },
58 { desc => 'Hold object for hold to be created', type => 'object' }
61 desc => 'Undef on success, -1 on missing arg, event (or ref to array of events) on error(s)',
66 __PACKAGE__->register_method(
67 method => "create_hold",
68 api_name => "open-ils.circ.holds.create.override",
69 notes => '@see open-ils.circ.holds.create',
71 desc => "If the recipient is not allowed to receive the requested hold, " .
72 "call this method to attempt the override",
74 { desc => 'Authentication token', type => 'string' },
75 { desc => 'Hold object for hold to be created', type => 'object' }
78 desc => 'Undef on success, -1 on missing arg, event (or ref to array of events) on error(s)',
84 my( $self, $conn, $auth, $hold ) = @_;
85 my $e = new_editor(authtoken=>$auth, xact=>1);
86 return $e->event unless $e->checkauth;
88 return -1 unless $hold;
89 my $override = 1 if $self->api_name =~ /override/;
93 my $requestor = $e->requestor;
94 my $recipient = $requestor;
96 if( $requestor->id ne $hold->usr ) {
97 # Make sure the requestor is allowed to place holds for
98 # the recipient if they are not the same people
99 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
100 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
103 # Now make sure the recipient is allowed to receive the specified hold
104 my $porg = $recipient->home_ou;
105 my $rid = $e->requestor->id;
106 my $t = $hold->hold_type;
108 # See if a duplicate hold already exists
110 usr => $recipient->id,
112 fulfillment_time => undef,
113 target => $hold->target,
114 cancel_time => undef,
117 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
119 my $existing = $e->search_action_hold_request($sargs);
120 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
122 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
123 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
125 if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
126 return $e->event unless $e->allowed('MR_HOLDS', $porg);
127 } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
128 return $e->event unless $e->allowed('TITLE_HOLDS', $porg);
129 } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
130 return $e->event unless $e->allowed('VOLUME_HOLDS', $porg);
131 } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
132 return $e->event unless $e->allowed('COPY_HOLDS', $porg);
136 $override or return \@events;
137 for my $evt (@events) {
139 my $name = $evt->{textcode};
140 return $e->event unless $e->allowed("$name.override", $porg);
144 # set the configured expire time
145 unless($hold->expire_time) {
146 my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
148 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
149 $hold->expire_time($U->epoch2ISO8601($date->epoch));
153 $hold->requestor($e->requestor->id);
154 $hold->request_lib($e->requestor->ws_ou);
155 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
156 $hold = $e->create_action_hold_request($hold) or return $e->event;
160 $conn->respond_complete($hold->id);
163 'open-ils.storage.action.hold_request.copy_targeter',
164 undef, $hold->id ) unless $U->is_true($hold->frozen);
170 my( $self, $client, $login_session, @holds) = @_;
172 if(!@holds){return 0;}
173 my( $user, $evt ) = $apputils->checkses($login_session);
176 my $holdsref = (ref($holds[0]) eq 'ARRAY') ? $holds[0] : [ @holds ];
178 $logger->debug("Iterating over " . scalar(@$holdsref) . " holds requests...");
180 for my $hold (@$holdsref) {
182 my $type = $hold->hold_type;
184 $logger->activity("User " . $user->id .
185 " creating new hold of type $type for user " . $hold->usr);
188 if($user->id ne $hold->usr) {
189 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
195 # am I allowed to place holds for this user?
196 if($hold->requestor ne $hold->usr) {
197 my $perm = _check_request_holds_perm($user->id, $user->home_ou);
198 return $perm if $perm;
201 # is this user allowed to have holds of this type?
202 my $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
203 return $perm if $perm;
205 #enforce the fact that the login is the one requesting the hold
206 $hold->requestor($user->id);
207 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
209 my $resp = $apputils->simplereq(
211 'open-ils.storage.direct.action.hold_request.create', $hold );
214 return OpenSRF::EX::ERROR ("Error creating hold");
221 # makes sure that a user has permission to place the type of requested hold
222 # returns the Perm exception if not allowed, returns undef if all is well
223 sub _check_holds_perm {
224 my($type, $user_id, $org_id) = @_;
228 $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS" );
229 } elsif ($type eq "T") {
230 $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
231 } elsif($type eq "V") {
232 $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
233 } elsif($type eq "C") {
234 $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS" );
241 # tests if the given user is allowed to place holds on another's behalf
242 sub _check_request_holds_perm {
245 if (my $evt = $apputils->check_perms(
246 $user_id, $org_id, "REQUEST_HOLDS")) {
251 my $ses_is_req_note = 'The login session is the requestor. If the requestor is different from the user, ' .
252 'then the requestor must have VIEW_HOLD permissions';
254 __PACKAGE__->register_method(
255 method => "retrieve_holds_by_id",
256 api_name => "open-ils.circ.holds.retrieve_by_id",
258 desc => "Retrieve the hold, with hold transits attached, for the specified ID. $ses_is_req_note",
260 { desc => 'Authentication token', type => 'string' },
261 { desc => 'Hold ID', type => 'number' }
264 desc => 'Hold object with transits attached, event on error',
270 sub retrieve_holds_by_id {
271 my($self, $client, $auth, $hold_id) = @_;
272 my $e = new_editor(authtoken=>$auth);
273 $e->checkauth or return $e->event;
274 $e->allowed('VIEW_HOLD') or return $e->event;
276 my $holds = $e->search_action_hold_request(
278 { id => $hold_id , fulfillment_time => undef },
280 order_by => { ahr => "request_time" },
282 flesh_fields => {ahr => ['notes']}
287 flesh_hold_transits($holds);
288 flesh_hold_notices($holds, $e);
293 __PACKAGE__->register_method(
294 method => "retrieve_holds",
295 api_name => "open-ils.circ.holds.retrieve",
297 desc => "Retrieves all the holds, with hold transits attached, for the specified user. $ses_is_req_note",
299 { desc => 'Authentication token', type => 'string' },
300 { desc => 'User ID', type => 'integer' }
303 desc => 'list of holds, event on error',
308 __PACKAGE__->register_method(
309 method => "retrieve_holds",
310 api_name => "open-ils.circ.holds.id_list.retrieve",
313 desc => "Retrieves all the hold IDs, for the specified user. $ses_is_req_note",
315 { desc => 'Authentication token', type => 'string' },
316 { desc => 'User ID', type => 'integer' }
319 desc => 'list of holds, event on error',
324 __PACKAGE__->register_method(
325 method => "retrieve_holds",
326 api_name => "open-ils.circ.holds.canceled.retrieve",
329 desc => "Retrieves all the cancelled holds for the specified user. $ses_is_req_note",
331 { desc => 'Authentication token', type => 'string' },
332 { desc => 'User ID', type => 'integer' }
335 desc => 'list of holds, event on error',
340 __PACKAGE__->register_method(
341 method => "retrieve_holds",
342 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
345 desc => "Retrieves list of cancelled hold IDs for the specified user. $ses_is_req_note",
347 { desc => 'Authentication token', type => 'string' },
348 { desc => 'User ID', type => 'integer' }
351 desc => 'list of hold IDs, event on error',
358 my ($self, $client, $auth, $user_id) = @_;
360 my $e = new_editor(authtoken=>$auth);
361 return $e->event unless $e->checkauth;
362 $user_id = $e->requestor->id unless defined $user_id;
364 unless($user_id == $e->requestor->id) {
365 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
366 unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
367 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
368 $e, $user_id, $e->requestor->id, 'hold.view');
369 return $e->event unless $allowed;
375 if($self->api_name !~ /canceled/) {
377 # Fetch the active holds
379 $holds = $e->search_action_hold_request([
381 fulfillment_time => undef,
382 cancel_time => undef,
384 {order_by => {ahr => "request_time"}}
389 # Fetch the canceled holds
392 my $cancel_count = $U->ou_ancestor_setting_value(
393 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
395 unless($cancel_count) {
396 $cancel_age = $U->ou_ancestor_setting_value(
397 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
400 if($cancel_count) { # limit by count
402 # find at most cancel_count canceled holds
403 $holds = $e->search_action_hold_request([
405 fulfillment_time => undef,
406 cancel_time => {'!=' => undef},
408 {order_by => {ahr => "cancel_time desc"}, limit => $cancel_count}
411 } elsif($cancel_age) { # limit by age
413 # find all of the canceled holds that were canceled within the configured time frame
414 my $date = DateTime->now->subtract(seconds => OpenSRF::Utils::interval_to_seconds($cancel_age));
415 $date = $U->epoch2ISO8601($date->epoch);
417 $holds = $e->search_action_hold_request([
419 fulfillment_time => undef,
420 cancel_time => {'>=' => $date},
422 {order_by => {ahr => "cancel_time desc"}}
427 if( ! $self->api_name =~ /id_list/ ) {
428 for my $hold ( @$holds ) {
430 $e->search_action_hold_transit_copy([
432 {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
438 return [ map { $_->id } @$holds ];
442 __PACKAGE__->register_method(
443 method => 'user_hold_count',
444 api_name => 'open-ils.circ.hold.user.count'
447 sub user_hold_count {
448 my ( $self, $conn, $auth, $userid ) = @_;
449 my $e = new_editor( authtoken => $auth );
450 return $e->event unless $e->checkauth;
451 my $patron = $e->retrieve_actor_user($userid)
453 return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
454 return __user_hold_count( $self, $e, $userid );
457 sub __user_hold_count {
458 my ( $self, $e, $userid ) = @_;
459 my $holds = $e->search_action_hold_request(
462 fulfillment_time => undef,
463 cancel_time => undef,
468 return scalar(@$holds);
472 __PACKAGE__->register_method(
473 method => "retrieve_holds_by_pickup_lib",
474 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
476 "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
479 __PACKAGE__->register_method(
480 method => "retrieve_holds_by_pickup_lib",
481 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
482 notes => "Retrieves all the hold ids for the specified pickup_ou id. "
485 sub retrieve_holds_by_pickup_lib {
486 my ($self, $client, $login_session, $ou_id) = @_;
488 #FIXME -- put an appropriate permission check here
489 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
490 # $login_session, $user_id, 'VIEW_HOLD' );
491 #return $evt if $evt;
493 my $holds = $apputils->simplereq(
495 "open-ils.cstore.direct.action.hold_request.search.atomic",
497 pickup_lib => $ou_id ,
498 fulfillment_time => undef,
501 { order_by => { ahr => "request_time" } }
504 if ( ! $self->api_name =~ /id_list/ ) {
505 flesh_hold_transits($holds);
509 return [ map { $_->id } @$holds ];
513 __PACKAGE__->register_method(
514 method => "uncancel_hold",
515 api_name => "open-ils.circ.hold.uncancel"
519 my($self, $client, $auth, $hold_id) = @_;
520 my $e = new_editor(authtoken=>$auth, xact=>1);
521 return $e->event unless $e->checkauth;
523 my $hold = $e->retrieve_action_hold_request($hold_id)
524 or return $e->die_event;
525 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
527 return 0 if $hold->fulfillment_time;
528 return 1 unless $hold->cancel_time;
530 # if configured to reset the request time, also reset the expire time
531 if($U->ou_ancestor_setting_value(
532 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
534 $hold->request_time('now');
535 my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
537 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
538 $hold->expire_time($U->epoch2ISO8601($date->epoch));
542 $hold->clear_cancel_time;
543 $hold->clear_cancel_cause;
544 $hold->clear_cancel_note;
545 $e->update_action_hold_request($hold) or return $e->die_event;
548 $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
554 __PACKAGE__->register_method(
555 method => "cancel_hold",
556 api_name => "open-ils.circ.hold.cancel",
558 desc => 'Cancels the specified hold. The login session is the requestor. If the requestor is different from the usr field ' .
559 'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
561 {desc => 'Authentication token', type => 'string'},
562 {desc => 'Hold ID', type => 'number'},
563 {desc => 'Cause of Cancellation', type => 'string'},
564 {desc => 'Note', type => 'string'}
567 desc => '1 on success, event on error'
573 my($self, $client, $auth, $holdid, $cause, $note) = @_;
575 my $e = new_editor(authtoken=>$auth, xact=>1);
576 return $e->event unless $e->checkauth;
578 my $hold = $e->retrieve_action_hold_request($holdid)
581 if( $e->requestor->id ne $hold->usr ) {
582 return $e->event unless $e->allowed('CANCEL_HOLDS');
585 return 1 if $hold->cancel_time;
587 # If the hold is captured, reset the copy status
588 if( $hold->capture_time and $hold->current_copy ) {
590 my $copy = $e->retrieve_asset_copy($hold->current_copy)
593 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
594 $logger->info("canceling hold $holdid whose item is on the holds shelf");
595 # $logger->info("setting copy to status 'reshelving' on hold cancel");
596 # $copy->status(OILS_COPY_STATUS_RESHELVING);
597 # $copy->editor($e->requestor->id);
598 # $copy->edit_date('now');
599 # $e->update_asset_copy($copy) or return $e->event;
601 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
604 $logger->warn("! canceling hold [$hid] that is in transit");
605 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
608 my $trans = $e->retrieve_action_transit_copy($transid);
609 # Leave the transit alive, but set the copy status to
610 # reshelving so it will be properly reshelved when it gets back home
612 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
613 $e->update_action_transit_copy($trans) or return $e->die_event;
619 $hold->cancel_time('now');
620 $hold->cancel_cause($cause);
621 $hold->cancel_note($note);
622 $e->update_action_hold_request($hold)
625 delete_hold_copy_maps($self, $e, $hold->id);
631 sub delete_hold_copy_maps {
636 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
638 $editor->delete_action_hold_copy_map($_)
639 or return $editor->event;
645 my $update_hold_desc = 'The login session is the requestor. ' .
646 'If the requestor is different from the usr field on the hold, ' .
647 'the requestor must have UPDATE_HOLDS permissions. ' .
648 'If supplying a hash of hold data, "id" must be included. ' .
649 'The hash is ignored if a hold object is supplied, ' .
650 'so you should supply only one kind of hold data argument.' ;
652 __PACKAGE__->register_method(
653 method => "update_hold",
654 api_name => "open-ils.circ.hold.update",
656 desc => "Updates the specified hold. $update_hold_desc",
658 {desc => 'Authentication token', type => 'string'},
659 {desc => 'Hold Object', type => 'object'},
660 {desc => 'Hash of values to be applied', type => 'object'}
663 desc => 'Hold ID on success, event on error',
669 __PACKAGE__->register_method(
670 method => "batch_update_hold",
671 api_name => "open-ils.circ.hold.update.batch",
674 desc => "Updates the specified hold(s). $update_hold_desc",
676 {desc => 'Authentication token', type => 'string'},
677 {desc => 'Array of hold obejcts', type => 'array' },
678 {desc => 'Array of hashes of values to be applied', type => 'array' }
681 desc => 'Hold ID per success, event per error',
687 my($self, $client, $auth, $hold, $values) = @_;
688 my $e = new_editor(authtoken=>$auth, xact=>1);
689 return $e->die_event unless $e->checkauth;
690 my $resp = update_hold_impl($self, $e, $hold, $values);
691 return $resp if $U->event_code($resp);
692 $e->commit; # FIXME: update_hold_impl already does $e->commit ??
696 sub batch_update_hold {
697 my($self, $client, $auth, $hold_list, $values_list) = @_;
698 my $e = new_editor(authtoken=>$auth);
699 return $e->die_event unless $e->checkauth;
701 my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list); # FIXME: we don't know for sure that we got $values_list. we could have neither list.
703 $values_list ||= []; # FIXME: either move this above $count declaration, or send an event if both lists undef. Probably the latter.
705 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
706 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
708 for my $idx (0..$count-1) {
710 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
711 $e->xact_commit unless $U->event_code($resp);
712 $client->respond($resp);
716 return undef; # not in the register return type, assuming we should always have at least one list populated
719 sub update_hold_impl {
720 my($self, $e, $hold, $values) = @_;
723 $hold = $e->retrieve_action_hold_request($values->{id})
724 or return $e->die_event;
725 $hold->$_($values->{$_}) for keys %$values;
728 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
729 or return $e->die_event;
731 # don't allow the user to be changed
732 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
734 if($hold->usr ne $e->requestor->id) {
735 # if the hold is for a different user, make sure the
736 # requestor has the appropriate permissions
737 my $usr = $e->retrieve_actor_user($hold->usr)
738 or return $e->die_event;
739 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
743 # --------------------------------------------------------------
744 # Changing the request time is like playing God
745 # --------------------------------------------------------------
746 if($hold->request_time ne $orig_hold->request_time) {
747 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
748 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
751 # --------------------------------------------------------------
752 # if the hold is on the holds shelf or in transit and the pickup
753 # lib changes we need to create a new transit.
754 # --------------------------------------------------------------
755 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
757 my $status = _hold_status($e, $hold);
759 if($status == 3) { # in transit
761 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
762 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
764 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
766 # update the transit to reflect the new pickup location
767 my $transit = $e->search_action_hold_transit_copy(
768 {hold=>$hold->id, dest_recv_time => undef})->[0]
769 or return $e->die_event;
771 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
772 $transit->dest($hold->pickup_lib);
773 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
775 } elsif($status == 4) { # on holds shelf
777 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
778 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
780 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
782 # create the new transit
783 my $evt = transit_hold($e, $orig_hold, $hold, $e->retrieve_asset_copy($hold->current_copy));
788 update_hold_if_frozen($self, $e, $hold, $orig_hold);
789 $e->update_action_hold_request($hold) or return $e->die_event;
792 # a change to mint-condition changes the set of potential copies, so retarget the hold;
793 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
794 _reset_hold($self, $e->requestor, $hold)
801 my($e, $orig_hold, $hold, $copy) = @_;
802 my $src = $orig_hold->pickup_lib;
803 my $dest = $hold->pickup_lib;
805 $logger->info("putting hold into transit on pickup_lib update");
807 my $transit = Fieldmapper::action::hold_transit_copy->new;
808 $transit->hold($hold->id);
809 $transit->source($src);
810 $transit->dest($dest);
811 $transit->target_copy($copy->id);
812 $transit->source_send_time('now');
813 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
815 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
816 $copy->editor($e->requestor->id);
817 $copy->edit_date('now');
819 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
820 $e->update_asset_copy($copy) or return $e->die_event;
824 # if the hold is frozen, this method ensures that the hold is not "targeted",
825 # that is, it clears the current_copy and prev_check_time to essentiallly
826 # reset the hold. If it is being activated, it runs the targeter in the background
827 sub update_hold_if_frozen {
828 my($self, $e, $hold, $orig_hold) = @_;
829 return if $hold->capture_time;
831 if($U->is_true($hold->frozen)) {
832 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
833 $hold->clear_current_copy;
834 $hold->clear_prev_check_time;
837 if($U->is_true($orig_hold->frozen)) {
838 $logger->info("Running targeter on activated hold ".$hold->id);
839 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
844 __PACKAGE__->register_method(
845 method => "hold_note_CUD",
846 api_name => "open-ils.circ.hold_request.note.cud"
850 my($self, $conn, $auth, $note) = @_;
852 my $e = new_editor(authtoken => $auth, xact => 1);
853 return $e->die_event unless $e->checkauth;
855 my $hold = $e->retrieve_action_hold_request($note->hold)
856 or return $e->die_event;
858 if($hold->usr ne $e->requestor->id) {
859 my $usr = $e->retrieve_actor_user($hold->usr);
860 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
861 $note->staff('t') if $note->isnew;
865 $e->create_action_hold_request_note($note) or return $e->die_event;
866 } elsif($note->ischanged) {
867 $e->update_action_hold_request_note($note) or return $e->die_event;
868 } elsif($note->isdeleted) {
869 $e->delete_action_hold_request_note($note) or return $e->die_event;
877 __PACKAGE__->register_method(
878 method => "retrieve_hold_status",
879 api_name => "open-ils.circ.hold.status.retrieve",
881 desc => 'Calculates the current status of the hold. The requestor must have ' .
882 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
884 { desc => 'Hold ID', type => 'number' }
887 # type => 'number', # event sometimes
888 desc => <<'END_OF_DESC'
889 Returns event on error or:
890 -1 on error (for now),
891 1 for 'waiting for copy to become available',
892 2 for 'waiting for copy capture',
895 5 for 'hold-shelf-delay'
901 sub retrieve_hold_status {
902 my($self, $client, $auth, $hold_id) = @_;
904 my $e = new_editor(authtoken => $auth);
905 return $e->event unless $e->checkauth;
906 my $hold = $e->retrieve_action_hold_request($hold_id)
909 if( $e->requestor->id != $hold->usr ) {
910 return $e->event unless $e->allowed('VIEW_HOLD');
913 return _hold_status($e, $hold);
919 return 1 unless $hold->current_copy;
920 return 2 unless $hold->capture_time;
922 my $copy = $hold->current_copy;
923 unless( ref $copy ) {
924 $copy = $e->retrieve_asset_copy($hold->current_copy)
928 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
930 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
932 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
933 return 4 unless $hs_wait_interval;
935 # if a hold_shelf_status_delay interval is defined and start_time plus
936 # the interval is greater than now, consider the hold to be in the virtual
937 # "on its way to the holds shelf" status. Return 5.
939 my $transit = $e->search_action_hold_transit_copy({hold => $hold->id})->[0];
940 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
941 $start_time = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
942 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
944 return 5 if $end_time > DateTime->now;
953 __PACKAGE__->register_method(
954 method => "retrieve_hold_queue_stats",
955 api_name => "open-ils.circ.hold.queue_stats.retrieve",
957 desc => q/Returns object with total_holds count, queue_position, potential_copies count, and status code/,
961 sub retrieve_hold_queue_stats {
962 my($self, $conn, $auth, $hold_id) = @_;
963 my $e = new_editor(authtoken => $auth);
964 return $e->event unless $e->checkauth;
965 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
966 if($e->requestor->id != $hold->usr) {
967 return $e->event unless $e->allowed('VIEW_HOLD');
969 return retrieve_hold_queue_status_impl($e, $hold);
972 sub retrieve_hold_queue_status_impl {
976 # The holds queue is defined as the distinct set of holds that share at
977 # least one potential copy with the context hold, plus any holds that
978 # share the same hold type and target. The latter part exists to
979 # accomodate holds that currently have no potential copies
980 my $q_holds = $e->json_query({
982 # fetch request_time since it's in the order_by and we're asking for distinct values
983 select => {ahr => ['id', 'request_time']},
986 ahcm => {type => 'left'} # there may be no copy maps
989 order_by => {ahr => ['request_time']},
997 select => {ahcm => ['target_copy']},
999 where => {hold => $hold->id}
1006 hold_type => $hold->hold_type,
1007 target => $hold->target
1015 for my $h (@$q_holds) {
1016 last if $h->{id} == $hold->id;
1020 # total count of potential copies
1021 my $num_potentials = $e->json_query({
1022 select => {ahcm => [{column => 'id', transform => 'count', alias => 'count'}]},
1024 where => {hold => $hold->id}
1027 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1028 my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
1029 my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
1032 total_holds => scalar(@$q_holds),
1033 queue_position => $qpos,
1034 potential_copies => $num_potentials->{count},
1035 status => _hold_status( $e, $hold ),
1036 estimated_wait => int($estimated_wait)
1041 sub fetch_open_hold_by_current_copy {
1044 my $hold = $apputils->simplereq(
1046 'open-ils.cstore.direct.action.hold_request.search.atomic',
1047 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1048 return $hold->[0] if ref($hold);
1052 sub fetch_related_holds {
1055 return $apputils->simplereq(
1057 'open-ils.cstore.direct.action.hold_request.search.atomic',
1058 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1062 __PACKAGE__->register_method(
1063 method => "hold_pull_list",
1064 api_name => "open-ils.circ.hold_pull_list.retrieve",
1066 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1067 'The location is determined by the login session.',
1069 { desc => 'Limit (optional)', type => 'number'},
1070 { desc => 'Offset (optional)', type => 'number'},
1073 desc => 'reference to a list of holds, or event on failure',
1078 __PACKAGE__->register_method(
1079 method => "hold_pull_list",
1080 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1082 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1083 'The location is determined by the login session.',
1085 { desc => 'Limit (optional)', type => 'number'},
1086 { desc => 'Offset (optional)', type => 'number'},
1089 desc => 'reference to a list of holds, or event on failure',
1094 __PACKAGE__->register_method(
1095 method => "hold_pull_list",
1096 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1098 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1099 'The location is determined by the login session.',
1101 { desc => 'Limit (optional)', type => 'number'},
1102 { desc => 'Offset (optional)', type => 'number'},
1105 desc => 'Holds count (integer), or event on failure',
1112 sub hold_pull_list {
1113 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1114 my( $reqr, $evt ) = $U->checkses($authtoken);
1115 return $evt if $evt;
1117 my $org = $reqr->ws_ou || $reqr->home_ou;
1118 # the perm locaiton shouldn't really matter here since holds
1119 # will exist all over and VIEW_HOLDS should be universal
1120 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1121 return $evt if $evt;
1123 if($self->api_name =~ /count/) {
1125 my $count = $U->storagereq(
1126 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1127 $org, $limit, $offset );
1129 $logger->info("Grabbing pull list for org unit $org with $count items");
1132 } elsif( $self->api_name =~ /id_list/ ) {
1133 return $U->storagereq(
1134 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1135 $org, $limit, $offset );
1138 return $U->storagereq(
1139 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1140 $org, $limit, $offset );
1144 __PACKAGE__->register_method(
1145 method => 'fetch_hold_notify',
1146 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
1149 Returns a list of hold notification objects based on hold id.
1150 @param authtoken The loggin session key
1151 @param holdid The id of the hold whose notifications we want to retrieve
1152 @return An array of hold notification objects, event on error.
1156 sub fetch_hold_notify {
1157 my( $self, $conn, $authtoken, $holdid ) = @_;
1158 my( $requestor, $evt ) = $U->checkses($authtoken);
1159 return $evt if $evt;
1160 my ($hold, $patron);
1161 ($hold, $evt) = $U->fetch_hold($holdid);
1162 return $evt if $evt;
1163 ($patron, $evt) = $U->fetch_user($hold->usr);
1164 return $evt if $evt;
1166 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1167 return $evt if $evt;
1169 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1170 return $U->cstorereq(
1171 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1175 __PACKAGE__->register_method(
1176 method => 'create_hold_notify',
1177 api_name => 'open-ils.circ.hold_notification.create',
1179 Creates a new hold notification object
1180 @param authtoken The login session key
1181 @param notification The hold notification object to create
1182 @return ID of the new object on success, Event on error
1186 sub create_hold_notify {
1187 my( $self, $conn, $auth, $note ) = @_;
1188 my $e = new_editor(authtoken=>$auth, xact=>1);
1189 return $e->die_event unless $e->checkauth;
1191 my $hold = $e->retrieve_action_hold_request($note->hold)
1192 or return $e->die_event;
1193 my $patron = $e->retrieve_actor_user($hold->usr)
1194 or return $e->die_event;
1196 return $e->die_event unless
1197 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1199 $note->notify_staff($e->requestor->id);
1200 $e->create_action_hold_notification($note) or return $e->die_event;
1205 __PACKAGE__->register_method(
1206 method => 'create_hold_note',
1207 api_name => 'open-ils.circ.hold_note.create',
1209 Creates a new hold request note object
1210 @param authtoken The login session key
1211 @param note The hold note object to create
1212 @return ID of the new object on success, Event on error
1216 sub create_hold_note {
1217 my( $self, $conn, $auth, $note ) = @_;
1218 my $e = new_editor(authtoken=>$auth, xact=>1);
1219 return $e->die_event unless $e->checkauth;
1221 my $hold = $e->retrieve_action_hold_request($note->hold)
1222 or return $e->die_event;
1223 my $patron = $e->retrieve_actor_user($hold->usr)
1224 or return $e->die_event;
1226 return $e->die_event unless
1227 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1229 $e->create_action_hold_request_note($note) or return $e->die_event;
1234 __PACKAGE__->register_method(
1235 method => 'reset_hold',
1236 api_name => 'open-ils.circ.hold.reset',
1238 Un-captures and un-targets a hold, essentially returning
1239 it to the state it was in directly after it was placed,
1240 then attempts to re-target the hold
1241 @param authtoken The login session key
1242 @param holdid The id of the hold
1248 my( $self, $conn, $auth, $holdid ) = @_;
1250 my ($hold, $evt) = $U->fetch_hold($holdid);
1251 return $evt if $evt;
1252 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1253 return $evt if $evt;
1254 $evt = _reset_hold($self, $reqr, $hold);
1255 return $evt if $evt;
1260 __PACKAGE__->register_method(
1261 method => 'reset_hold_batch',
1262 api_name => 'open-ils.circ.hold.reset.batch'
1265 sub reset_hold_batch {
1266 my($self, $conn, $auth, $hold_ids) = @_;
1268 my $e = new_editor(authtoken => $auth);
1269 return $e->event unless $e->checkauth;
1271 for my $hold_id ($hold_ids) {
1273 my $hold = $e->retrieve_action_hold_request(
1274 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1275 or return $e->event;
1277 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1278 _reset_hold($self, $e->requestor, $hold);
1286 my ($self, $reqr, $hold) = @_;
1288 my $e = new_editor(xact =>1, requestor => $reqr);
1290 $logger->info("reseting hold ".$hold->id);
1292 my $hid = $hold->id;
1294 if( $hold->capture_time and $hold->current_copy ) {
1296 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1297 or return $e->event;
1299 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1300 $logger->info("setting copy to status 'reshelving' on hold retarget");
1301 $copy->status(OILS_COPY_STATUS_RESHELVING);
1302 $copy->editor($e->requestor->id);
1303 $copy->edit_date('now');
1304 $e->update_asset_copy($copy) or return $e->event;
1306 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1308 # We don't want the copy to remain "in transit"
1309 $copy->status(OILS_COPY_STATUS_RESHELVING);
1310 $logger->warn("! reseting hold [$hid] that is in transit");
1311 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1314 my $trans = $e->retrieve_action_transit_copy($transid);
1316 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1317 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1318 $logger->info("Transit abort completed with result $evt");
1319 return $evt unless "$evt" eq 1;
1325 $hold->clear_capture_time;
1326 $hold->clear_current_copy;
1327 $hold->clear_shelf_time;
1328 $hold->clear_shelf_expire_time;
1330 $e->update_action_hold_request($hold) or return $e->event;
1334 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1340 __PACKAGE__->register_method(
1341 method => 'fetch_open_title_holds',
1342 api_name => 'open-ils.circ.open_holds.retrieve',
1344 Returns a list ids of un-fulfilled holds for a given title id
1345 @param authtoken The login session key
1346 @param id the id of the item whose holds we want to retrieve
1347 @param type The hold type - M, T, V, C
1351 sub fetch_open_title_holds {
1352 my( $self, $conn, $auth, $id, $type, $org ) = @_;
1353 my $e = new_editor( authtoken => $auth );
1354 return $e->event unless $e->checkauth;
1357 $org ||= $e->requestor->ws_ou;
1359 # return $e->search_action_hold_request(
1360 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1362 # XXX make me return IDs in the future ^--
1363 my $holds = $e->search_action_hold_request(
1366 cancel_time => undef,
1368 fulfillment_time => undef
1372 flesh_hold_transits($holds);
1377 sub flesh_hold_transits {
1379 for my $hold ( @$holds ) {
1381 $apputils->simplereq(
1383 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1384 { hold => $hold->id },
1385 { order_by => { ahtc => 'id desc' }, limit => 1 }
1391 sub flesh_hold_notices {
1392 my( $holds, $e ) = @_;
1393 $e ||= new_editor();
1395 for my $hold (@$holds) {
1396 my $notices = $e->search_action_hold_notification(
1398 { hold => $hold->id },
1399 { order_by => { anh => 'notify_time desc' } },
1404 $hold->notify_count(scalar(@$notices));
1406 my $n = $e->retrieve_action_hold_notification($$notices[0])
1407 or return $e->event;
1408 $hold->notify_time($n->notify_time);
1414 __PACKAGE__->register_method(
1415 method => 'fetch_captured_holds',
1416 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1419 Returns a list of un-fulfilled holds for a given title id
1420 @param authtoken The login session key
1421 @param org The org id of the location in question
1425 __PACKAGE__->register_method(
1426 method => 'fetch_captured_holds',
1427 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1430 Returns a list ids of un-fulfilled holds for a given title id
1431 @param authtoken The login session key
1432 @param org The org id of the location in question
1436 sub fetch_captured_holds {
1437 my( $self, $conn, $auth, $org ) = @_;
1439 my $e = new_editor(authtoken => $auth);
1440 return $e->event unless $e->checkauth;
1441 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1443 $org ||= $e->requestor->ws_ou;
1445 my $hold_ids = $e->json_query(
1447 select => { ahr => ['id'] },
1452 fkey => 'current_copy'
1457 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1459 capture_time => { "!=" => undef },
1460 current_copy => { "!=" => undef },
1461 fulfillment_time => undef,
1463 cancel_time => undef,
1469 for my $hold_id (@$hold_ids) {
1470 if($self->api_name =~ /id_list/) {
1471 $conn->respond($hold_id->{id});
1475 $e->retrieve_action_hold_request([
1479 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1480 order_by => {anh => 'notify_time desc'}
1491 __PACKAGE__->register_method(
1492 method => "check_title_hold",
1493 api_name => "open-ils.circ.title_hold.is_possible",
1495 desc => 'Determines if a hold were to be placed by a given user, ' .
1496 'whether or not said hold would have any potential copies to fulfill it.' .
1497 'The named paramaters of the second argument include: ' .
1498 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
1499 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
1501 { desc => 'Authentication token', type => 'string'},
1502 { desc => 'Hash of named parameters', type => 'object'},
1505 desc => 'List of new message IDs (empty if none)',
1511 =head3 check_title_hold (token, hash)
1513 The named fields in the hash are:
1515 patronid - ID of the hold recipient (required)
1516 depth - hold range depth (default 0)
1517 pickup_lib - destination for hold, fallback value for selection_ou
1518 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
1519 titleid - ID (BRN) of the title to be held, required for Title level hold
1520 volume_id - required for Volume level hold
1521 copy_id - required for Copy level hold
1522 mrid - required for Meta-record level hold
1523 hold_type - T,C,V or M for Title, Copy, Volume or Meta-record (default "T")
1525 All key/value pairs are passed on to do_possibility_checks.
1529 # FIXME: better params checking. what other params are required, if any?
1530 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
1531 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
1532 # used in conditionals, where it may be undefined, causing a warning.
1533 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
1535 sub check_title_hold {
1536 my( $self, $client, $authtoken, $params ) = @_;
1537 my $e = new_editor(authtoken=>$authtoken);
1538 return $e->event unless $e->checkauth;
1540 my %params = %$params;
1541 my $depth = $params{depth} || 0;
1542 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
1544 my $patron = $e->retrieve_actor_user($params{patronid})
1545 or return $e->event;
1547 if( $e->requestor->id ne $patron->id ) {
1548 return $e->event unless
1549 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1552 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1554 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1555 or return $e->event;
1557 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1558 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1560 if(defined $soft_boundary and $depth < $soft_boundary) {
1561 # work up the tree and as soon as we find a potential copy, use that depth
1562 # also, make sure we don't go past the hard boundary if it exists
1564 # our min boundary is the greater of user-specified boundary or hard boundary
1565 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
1566 $hard_boundary : $depth;
1568 my $depth = $soft_boundary;
1569 while($depth >= $min_depth) {
1570 $logger->info("performing hold possibility check with soft boundary $depth");
1571 my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1572 return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1575 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
1576 # there is no soft boundary, enforce the hard boundary if it exists
1577 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1578 my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1580 return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1583 # no boundaries defined, fall back to user specifed boundary or no boundary
1584 $logger->info("performing hold possibility check with no boundary");
1585 my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1587 return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1590 return {success => 0};
1593 sub do_possibility_checks {
1594 my($e, $patron, $request_lib, $depth, %params) = @_;
1596 my $titleid = $params{titleid} || "";
1597 my $volid = $params{volume_id};
1598 my $copyid = $params{copy_id};
1599 my $mrid = $params{mrid} || "";
1600 my $pickup_lib = $params{pickup_lib};
1601 my $hold_type = $params{hold_type} || 'T';
1602 my $selection_ou = $params{selection_ou} || $pickup_lib;
1609 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1611 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
1612 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
1613 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
1615 return verify_copy_for_hold(
1616 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib
1619 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1621 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
1622 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
1624 return _check_volume_hold_is_possible(
1625 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
1628 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1630 return _check_title_hold_is_possible(
1631 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
1634 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1636 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1637 my @recs = map { $_->source } @$maps;
1638 for my $rec (@recs) {
1639 my @status = _check_title_hold_is_possible(
1640 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1641 return @status if $status[1];
1645 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
1649 sub create_ranged_org_filter {
1650 my($e, $selection_ou, $depth) = @_;
1652 # find the orgs from which this hold may be fulfilled,
1653 # based on the selection_ou and depth
1655 my $top_org = $e->search_actor_org_unit([
1656 {parent_ou => undef},
1657 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1660 return () if $depth == $top_org->ou_type->depth;
1662 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1663 %org_filter = (circ_lib => []);
1664 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1666 $logger->info("hold org filter at depth $depth and selection_ou ".
1667 "$selection_ou created list of @{$org_filter{circ_lib}}");
1673 sub _check_title_hold_is_possible {
1674 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1676 my $e = new_editor();
1677 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1679 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1680 my $copies = $e->json_query(
1682 select => { acp => ['id', 'circ_lib'] },
1687 fkey => 'call_number',
1691 filter => { id => $titleid },
1696 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1697 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1701 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1706 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1707 return (0) unless @$copies;
1709 # -----------------------------------------------------------------------
1710 # sort the copies into buckets based on their circ_lib proximity to
1711 # the patron's home_ou.
1712 # -----------------------------------------------------------------------
1714 my $home_org = $patron->home_ou;
1715 my $req_org = $request_lib->id;
1717 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1719 $prox_cache{$home_org} =
1720 $e->search_actor_org_unit_proximity({from_org => $home_org})
1721 unless $prox_cache{$home_org};
1722 my $home_prox = $prox_cache{$home_org};
1725 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1726 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1728 my @keys = sort { $a <=> $b } keys %buckets;
1731 if( $home_org ne $req_org ) {
1732 # -----------------------------------------------------------------------
1733 # shove the copies close to the request_lib into the primary buckets
1734 # directly before the farthest away copies. That way, they are not
1735 # given priority, but they are checked before the farthest copies.
1736 # -----------------------------------------------------------------------
1737 $prox_cache{$req_org} =
1738 $e->search_actor_org_unit_proximity({from_org => $req_org})
1739 unless $prox_cache{$req_org};
1740 my $req_prox = $prox_cache{$req_org};
1743 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1744 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1746 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1747 my $new_key = $highest_key - 0.5; # right before the farthest prox
1748 my @keys2 = sort { $a <=> $b } keys %buckets2;
1749 for my $key (@keys2) {
1750 last if $key >= $highest_key;
1751 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1755 @keys = sort { $a <=> $b } keys %buckets;
1759 for my $key (@keys) {
1760 my @cps = @{$buckets{$key}};
1762 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1764 for my $copyid (@cps) {
1766 next if $seen{$copyid};
1767 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1768 my $copy = $e->retrieve_asset_copy($copyid);
1769 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1771 unless($title) { # grab the title if we don't already have it
1772 my $vol = $e->retrieve_asset_call_number(
1773 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1774 $title = $vol->record;
1777 my @status = verify_copy_for_hold(
1778 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1780 return @status if $status[0];
1788 sub _check_volume_hold_is_possible {
1789 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1790 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1791 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1792 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1793 for my $copy ( @$copies ) {
1794 my @status = verify_copy_for_hold(
1795 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1796 return @status if $status[0];
1803 sub verify_copy_for_hold {
1804 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1805 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1806 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1807 { patron => $patron,
1808 requestor => $requestor,
1811 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1812 pickup_lib => $pickup_lib,
1813 request_lib => $request_lib,
1821 ($copy->circ_lib == $pickup_lib) and
1822 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1829 sub find_nearest_permitted_hold {
1832 my $editor = shift; # CStoreEditor object
1833 my $copy = shift; # copy to target
1834 my $user = shift; # staff
1835 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1837 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1839 my $bc = $copy->barcode;
1841 # find any existing holds that already target this copy
1842 my $old_holds = $editor->search_action_hold_request(
1843 { current_copy => $copy->id,
1844 cancel_time => undef,
1845 capture_time => undef
1849 # hold->type "R" means we need this copy
1850 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1853 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1855 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1856 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1858 # search for what should be the best holds for this copy to fulfill
1859 my $best_holds = $U->storagereq(
1860 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1861 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1863 unless(@$best_holds) {
1865 if( my $hold = $$old_holds[0] ) {
1866 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1870 $logger->info("circulator: no suitable holds found for copy $bc");
1871 return (undef, $evt);
1877 # for each potential hold, we have to run the permit script
1878 # to make sure the hold is actually permitted.
1879 for my $holdid (@$best_holds) {
1880 next unless $holdid;
1881 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1883 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1884 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1885 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1887 # see if this hold is permitted
1888 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1889 { patron_id => $hold->usr,
1892 pickup_lib => $hold->pickup_lib,
1893 request_lib => $rlib,
1904 unless( $best_hold ) { # no "good" permitted holds were found
1905 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1906 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1911 $logger->info("circulator: no suitable holds found for copy $bc");
1912 return (undef, $evt);
1915 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1917 # indicate a permitted hold was found
1918 return $best_hold if $check_only;
1920 # we've found a permitted hold. we need to "grab" the copy
1921 # to prevent re-targeted holds (next part) from re-grabbing the copy
1922 $best_hold->current_copy($copy->id);
1923 $editor->update_action_hold_request($best_hold)
1924 or return (undef, $editor->event);
1929 # re-target any other holds that already target this copy
1930 for my $old_hold (@$old_holds) {
1931 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1932 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1933 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1934 $old_hold->clear_current_copy;
1935 $old_hold->clear_prev_check_time;
1936 $editor->update_action_hold_request($old_hold)
1937 or return (undef, $editor->event);
1938 push(@retarget, $old_hold->id);
1941 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1949 __PACKAGE__->register_method(
1950 method => 'all_rec_holds',
1951 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1955 my( $self, $conn, $auth, $title_id, $args ) = @_;
1957 my $e = new_editor(authtoken=>$auth);
1958 $e->checkauth or return $e->event;
1959 $e->allowed('VIEW_HOLD') or return $e->event;
1962 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
1963 $args->{cancel_time} = undef;
1965 my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1967 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1969 $resp->{metarecord_holds} = $e->search_action_hold_request(
1970 { hold_type => OILS_HOLD_TYPE_METARECORD,
1971 target => $mr_map->metarecord,
1977 $resp->{title_holds} = $e->search_action_hold_request(
1979 hold_type => OILS_HOLD_TYPE_TITLE,
1980 target => $title_id,
1984 my $vols = $e->search_asset_call_number(
1985 { record => $title_id, deleted => 'f' }, {idlist=>1});
1987 return $resp unless @$vols;
1989 $resp->{volume_holds} = $e->search_action_hold_request(
1991 hold_type => OILS_HOLD_TYPE_VOLUME,
1996 my $copies = $e->search_asset_copy(
1997 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1999 return $resp unless @$copies;
2001 $resp->{copy_holds} = $e->search_action_hold_request(
2003 hold_type => OILS_HOLD_TYPE_COPY,
2015 __PACKAGE__->register_method(
2016 method => 'uber_hold',
2018 api_name => 'open-ils.circ.hold.details.retrieve'
2022 my($self, $client, $auth, $hold_id) = @_;
2023 my $e = new_editor(authtoken=>$auth);
2024 $e->checkauth or return $e->event;
2025 return uber_hold_impl($e, $hold_id);
2028 __PACKAGE__->register_method(
2029 method => 'batch_uber_hold',
2032 api_name => 'open-ils.circ.hold.details.batch.retrieve'
2035 sub batch_uber_hold {
2036 my($self, $client, $auth, $hold_ids) = @_;
2037 my $e = new_editor(authtoken=>$auth);
2038 $e->checkauth or return $e->event;
2039 $client->respond(uber_hold_impl($e, $_)) for @$hold_ids;
2043 sub uber_hold_impl {
2044 my($e, $hold_id) = @_;
2048 my $hold = $e->retrieve_action_hold_request(
2053 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
2056 ) or return $e->event;
2058 if($hold->usr->id ne $e->requestor->id) {
2059 # A user is allowed to see his/her own holds
2060 $e->allowed('VIEW_HOLD') or return $e->event;
2063 my $user = $hold->usr;
2064 $hold->usr($user->id);
2066 my $card = $e->retrieve_actor_card($user->card)
2067 or return $e->event;
2069 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
2071 flesh_hold_notices([$hold], $e);
2072 flesh_hold_transits([$hold]);
2074 my $details = retrieve_hold_queue_status_impl($e, $hold);
2081 patron_first => $user->first_given_name,
2082 patron_last => $user->family_name,
2083 patron_barcode => $card->barcode,
2090 # -----------------------------------------------------
2091 # Returns the MVR object that represents what the
2093 # -----------------------------------------------------
2095 my( $e, $hold ) = @_;
2101 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2102 my $mr = $e->retrieve_metabib_metarecord($hold->target)
2103 or return $e->event;
2104 $tid = $mr->master_record;
2106 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
2107 $tid = $hold->target;
2109 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2110 $volume = $e->retrieve_asset_call_number($hold->target)
2111 or return $e->event;
2112 $tid = $volume->record;
2114 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
2115 $copy = $e->retrieve_asset_copy($hold->target)
2116 or return $e->event;
2117 $volume = $e->retrieve_asset_call_number($copy->call_number)
2118 or return $e->event;
2119 $tid = $volume->record;
2122 if(!$copy and ref $hold->current_copy ) {
2123 $copy = $hold->current_copy;
2124 $hold->current_copy($copy->id);
2127 if(!$volume and $copy) {
2128 $volume = $e->retrieve_asset_call_number($copy->call_number);
2131 # TODO return metarcord mvr for M holds
2132 my $title = $e->retrieve_biblio_record_entry($tid);
2133 return ( $U->record_to_mvr($title), $volume, $copy );
2137 __PACKAGE__->register_method(
2138 method => 'clear_shelf_process',
2140 api_name => 'open-ils.circ.hold.clear_shelf.process',
2143 1. Find all holds that have expired on the holds shelf
2145 3. If a clear-shelf status is configured, put targeted copies into this status
2146 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
2147 that are needed for holds. No subsequent action is taken on the holds
2148 or items after grouping.
2153 sub clear_shelf_process {
2154 my($self, $client, $auth, $org_id) = @_;
2156 my $e = new_editor(authtoken=>$auth, xact => 1);
2157 $e->checkauth or return $e->die_event;
2159 $org_id ||= $e->requestor->ws_ou;
2160 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
2162 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
2164 # Find holds on the shelf that have been there too long
2165 my $hold_ids = $e->search_action_hold_request(
2166 { shelf_expire_time => {'<' => 'now'},
2167 pickup_lib => $org_id,
2168 cancel_time => undef,
2169 fulfillment_time => undef,
2170 shelf_time => {'!=' => undef}
2177 for my $hold_id (@$hold_ids) {
2179 $logger->info("Clear shelf processing hold $hold_id");
2181 my $hold = $e->retrieve_action_hold_request([
2184 flesh_fields => {ahr => ['current_copy']}
2188 $hold->cancel_time('now');
2189 $hold->cancel_cause(2); # Hold Shelf expiration
2190 $e->update_action_hold_request($hold) or return $e->die_event;
2192 my $copy = $hold->current_copy;
2195 # if a clear-shelf copy status is defined, update the copy
2196 $copy->status($copy_status);
2197 $copy->edit_date('now');
2198 $copy->editor($e->requestor->id);
2199 $e->update_asset_copy($copy) or return $e->die_event;
2202 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
2206 # copy is needed for a hold
2207 $client->respond({action => 'hold', copy => $copy, hold_id => $hold->id});
2209 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
2211 # copy needs to transit
2212 $client->respond({action => 'transit', copy => $copy, hold_id => $hold->id});
2216 # copy needs to go back to the shelf
2217 $client->respond({action => 'shelf', copy => $copy, hold_id => $hold->id});
2220 push(@holds, $hold);
2225 # tell the client we're done
2226 $client->resopnd_complete;
2228 # fire off the hold cancelation trigger
2229 my $trigger = OpenSRF::AppSession->connect('open-ils.trigger');
2231 for my $hold (@holds) {
2233 my $req = $trigger->request(
2234 'open-ils.trigger.event.autocreate',
2235 'hold_request.cancel.expire_holds_shelf',
2238 # wait for response so don't flood the service
2242 $trigger->disconnect;
2246 __PACKAGE__->register_method(
2247 method => 'usr_hold_summary',
2248 api_name => 'open-ils.circ.holds.user_summary',
2250 Returns a summary of holds statuses for a given user
2254 sub usr_hold_summary {
2255 my($self, $conn, $auth, $user_id) = @_;
2257 my $e = new_editor(authtoken=>$auth);
2258 $e->checkauth or return $e->event;
2259 $e->allowed('VIEW_HOLD') or return $e->event;
2261 my $holds = $e->search_action_hold_request(
2264 fulfillment_time => undef,
2265 cancel_time => undef,
2269 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
2270 $summary{_hold_status($e, $_)} += 1 for @$holds;
2276 __PACKAGE__->register_method(
2277 method => 'hold_has_copy_at',
2278 api_name => 'open-ils.circ.hold.has_copy_at',
2281 Returns the ID of the found copy and name of the shelving location if there is
2282 an available copy at the specified org unit. Returns empty hash otherwise.
2283 The anticipated use for this method is to determine whether an item is
2284 available at the library where the user is placing the hold (or, alternatively,
2285 at the pickup library) to encourage bypassing the hold placement and just
2286 checking out the item.
2289 { desc => 'Authentication Token', type => 'string' },
2291 Method Arguments. Options include:
2292 hold_type : the hold type code (T, V, C, M, ...)
2293 hold_target : the identifier of the hold target object
2294 org_unit : org unit ID
2300 desc => q/{ "copy" : copy_id, "location" : location_name }/,
2306 sub hold_has_copy_at {
2307 my($self, $conn, $auth, $args) = @_;
2309 my $e = new_editor(authtoken=>$auth);
2310 $e->checkauth or return $e->event;
2312 my $hold_type = $$args{hold_type};
2313 my $hold_target = $$args{hold_target};
2314 my $org_unit = $$args{org_unit};
2317 select => {acp => ['id'], acpl => ['name']},
2320 acpl => {field => 'id', filter => { holdable => 't'}, fkey => 'location'},
2321 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2324 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit}},
2328 if($hold_type eq 'C') {
2330 $query->{where}->{'+acp'}->{id} = $hold_target;
2332 } elsif($hold_type eq 'V') {
2334 $query->{where}->{'+acp'}->{call_number} = $hold_target;
2336 } elsif($hold_type eq 'T') {
2338 $query->{from}->{acp}->{acn} = {
2340 fkey => 'call_number',
2344 filter => {id => $hold_target},
2352 $query->{from}->{acp}->{acn} = {
2354 fkey => 'call_number',
2363 filter => {metarecord => $hold_target},
2371 my $res = $e->json_query($query)->[0] or return {};
2372 return {copy => $res->{id}, location => $res->{name}} if $res;
2376 # returns true if the user already has an item checked out
2377 # that could be used to fulfill the requested hold.
2378 sub hold_item_is_checked_out {
2379 my($e, $user_id, $hold_type, $hold_target) = @_;
2382 select => {acp => ['id']},
2383 from => {acp => {}},
2387 in => { # copies for circs the user has checked out
2388 select => {circ => ['target_copy']},
2392 checkin_time => undef,
2394 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2395 {stop_fines => undef}
2405 if($hold_type eq 'C') {
2407 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
2409 } elsif($hold_type eq 'V') {
2411 $query->{where}->{'+acp'}->{call_number} = $hold_target;
2413 } elsif($hold_type eq 'T') {
2415 $query->{from}->{acp}->{acn} = {
2417 fkey => 'call_number',
2421 filter => {id => $hold_target},
2429 $query->{from}->{acp}->{acn} = {
2431 fkey => 'call_number',
2440 filter => {metarecord => $hold_target},
2448 return $e->json_query($query)->[0];
2451 __PACKAGE__->register_method(
2452 method => 'change_hold_title',
2453 api_name => 'open-ils.circ.hold.change_title',
2456 Updates all title level holds targeting the specified bibs to point a new bib./,
2458 { desc => 'Authentication Token', type => 'string' },
2459 { desc => 'New Target Bib Id', type => 'number' },
2460 { desc => 'Old Target Bib Ids', type => 'array' },
2462 return => { desc => '1 on success' }
2466 sub change_hold_title {
2467 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
2469 my $e = new_editor(authtoken=>$auth, xact=>1);
2470 return $e->event unless $e->checkauth;
2472 my $holds = $e->search_action_hold_request(
2475 cancel_time => undef,
2476 fulfillment_time => undef,
2482 flesh_fields => { ahr => ['usr'] }
2488 for my $hold (@$holds) {
2489 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
2490 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
2491 $hold->target( $new_bib_id );
2492 $e->update_action_hold_request($hold) or return $e->die_event;