try-catching any calls to hold reset reasons in circulation module so that it'll...
authorLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Mon, 26 Sep 2022 18:38:32 +0000 (14:38 -0400)
committerLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Thu, 16 Mar 2023 17:00:24 +0000 (13:00 -0400)
sql for reset reasons

make proximity adjustments based on reset reasons if circ.holds.retarget_previous_targets_interval greater than 0. For each previous copy on a hold, reset reasons with MANUAL_RESET will increase the proximity by an amount equal to the maximum proximity + 1 while TIMED_OUT will apply a +1 prox adjustment per occurence within that interval.

log retarget only if it was successful

Don't create reset reason in hold targeter if hold arg is defined. Try catch any errors from hold reset note in hold-targeter application. run reset reason entry search within eval in case of failure.

Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/HoldTargeter.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm
Open-ILS/src/sql/Pg/upgrade/xxxx.hold_reset_reasons.sql [new file with mode: 0644]

index 29dec09..1a8e2a9 100644 (file)
@@ -1868,15 +1868,22 @@ sub handle_checkout_holds {
     
         $logger->info("circulator: un-targeting hold ".$hold->id.
             " because copy ".$copy->id." is getting checked out");
-
+        try {
+            $U->simplereq('open-ils.circ', 
+            'open-ils.circ.hold_reset_reason_entry.create',
+            $e->authtoken,
+            $hold->id,
+            OILS_HOLD_CHECK_OUT,
+            "Checked out to patron #".$patron->id);
+        } catch Error with {
+            $logger->error("circulate: create reset reason failed with ".shift());
+        };
         $hold->clear_prev_check_time; 
         $hold->clear_current_copy;
         $hold->clear_capture_time;
         $hold->clear_shelf_time;
         $hold->clear_shelf_expire_time;
         $hold->clear_current_shelf_lib;
-        $U->simplereq('open-ils.circ', 
-            'open-ils.circ.hold_reset_reason_entry.create',$e->authtoken,$hold->id,OILS_HOLD_CHECK_OUT,"Checked out to patron #".$patron->id);
         return $self->bail_on_event($e->event)
             unless $e->update_action_hold_request($hold);
 
@@ -2651,8 +2658,13 @@ sub checkin_retarget {
                 next if ($_->{hold_type} eq 'P');
             }
             # So much for easy stuff, attempt a retarget!
-            $U->simplereq('open-ils.circ', 
-            'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $_->{id},OILS_HOLD_BETTER_HOLD);
+            try{
+                $U->simplereq('open-ils.circ', 
+                'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $_->{id},OILS_HOLD_BETTER_HOLD);
+            }
+            catch Error with{
+                $logger->error("circulate: create reset reason failed with ".shift());
+            };
             my $tresult = $U->simplereq(
                 'open-ils.hold-targeter',
                 'open-ils.hold-targeter.target', 
@@ -3306,8 +3318,13 @@ sub attempt_checkin_hold_capture {
     $hold->clear_cancel_time;
     $hold->clear_prev_check_time unless $hold->prev_check_time;
     
-    $U->simplereq('open-ils.circ', 
-    'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $hold->id, OILS_HOLD_CHECK_IN);
+    try{
+        $U->simplereq('open-ils.circ', 
+        'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $hold->id, OILS_HOLD_CHECK_IN);
+    }
+    catch Error with{
+        $logger->error("circulate: create reset reason failed with ".shift());
+    };
     $self->bail_on_events($self->editor->event)
         unless $self->editor->update_action_hold_request($hold);
     $self->hold($hold);
@@ -3451,9 +3468,14 @@ sub retarget_holds {
     my $self = shift;
     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
     my $ses = OpenSRF::AppSession->create('open-ils.hold-targeter');
-    my $cses = OpenSRF::AppSession->create('open-ils.circ');            
-    $cses->request('open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $self->retarget,OILS_HOLD_BETTER_HOLD);
-    $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
+    $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});   
+    try{
+        my $cses = OpenSRF::AppSession->create('open-ils.circ');
+        $cses->request('open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $self->retarget,OILS_HOLD_BETTER_HOLD);
+    }
+    catch Error with{
+        $logger->error("circulate: create reset reason failed with ".shift());
+    };
     # no reason to wait for the return value
     return;
 }
index 0dd2ad4..e16f44e 100644 (file)
@@ -1107,20 +1107,20 @@ sub cancel_hold {
     $hold->cancel_time('now');
     $hold->cancel_cause($cause);
     $hold->cancel_note($note);
-       my $note_body = "";
-       if($cause){
-               my $cancel_reason = "ID $cause";
-               my $cancel_cause = $e->retrieve_action_hold_request_cancel_cause($cause);
-               if($cancel_cause){
-                       $cancel_reason = $cancel_cause->label;
-               }               
-               $note_body .= "Cancel Cause: $cancel_reason";           
-       }
-       else{
-               $note_body .= "Cancel reason unknown";
-       }
-       $note_body .= "," unless $note_body eq "" || $note eq "";
-       $note_body .= " Cancel Note: \"$note\"" unless $note eq "";
+    my $note_body = "";
+    if($cause){
+        my $cancel_reason = "ID $cause";
+        my $cancel_cause = $e->retrieve_action_hold_request_cancel_cause($cause);
+        if($cancel_cause){
+            $cancel_reason = $cancel_cause->label;
+        }       
+        $note_body .= "Cancel Cause: $cancel_reason";       
+    }
+    else{
+        $note_body .= "Cancel reason unknown";
+    }
+    $note_body .= "," unless $note_body eq "" || $note eq "";
+    $note_body .= " Cancel Note: \"$note\"" unless $note eq "";
     _create_reset_reason_entry($e,$hold,OILS_HOLD_CANCELED,$note_body);
     $e->update_action_hold_request($hold)
         or return $e->die_event;
@@ -1350,7 +1350,7 @@ sub update_hold_impl {
    
     if($U->is_true($hold->frozen)) {
         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
-               _create_reset_reason_entry($e,$hold,OILS_HOLD_FROZEN,$note_body) unless $U->is_true($orig_hold->frozen);
+        _create_reset_reason_entry($e,$hold,OILS_HOLD_FROZEN,$note_body) unless $U->is_true($orig_hold->frozen);
         $hold->clear_current_copy;
         $hold->clear_prev_check_time;
         # Clear expire_time to prevent frozen holds from expiring.
@@ -2247,8 +2247,8 @@ sub create_reset_reason_entry
 {
     my($self, $conn, $auth, $hold, $reset_reason, $note) = @_;
     my $e = new_editor(authtoken => $auth, xact => 1);
-       #checkauth to set the requestor (if available)
-       $e->checkauth;
+    #checkauth to set the requestor (if available)
+    $e->checkauth;
     my @holds;
     if(ref $hold eq 'ARRAY'){
         @holds = @{$hold};
@@ -2257,9 +2257,13 @@ sub create_reset_reason_entry
         @holds = ($hold);
     }
     for my $holdid (@holds){
-        my ($hold, $evt) = $U->fetch_hold($holdid);
-        return $evt if $evt;   
-        _create_reset_reason_entry($e, $hold, $reset_reason, $note);
+        try{
+            my ($hold, $evt) = $U->fetch_hold($holdid);  
+            _create_reset_reason_entry($e, $hold, $reset_reason, $note) unless $evt;
+        }
+        catch Error with{
+            $logger->error("holds: create reset reason failed with ".shift());
+        };
     }
     $e->commit;
     return 1;
@@ -2267,18 +2271,18 @@ sub create_reset_reason_entry
 
 sub _create_reset_reason_entry
 {
-    my($e, $hold, $reset_reason,$note) = @_;
+    my($e, $hold, $reset_reason,$note,$previous_copy) = @_;
     my $ts = DateTime->now;
     my $entry = Fieldmapper::action::hold_request_reset_reason_entry->new; 
     $logger->info("Creating reset reason entry for hold #" . $hold->id);    
-    my $last_copy = $hold->current_copy;
+    my $last_copy = defined $previous_copy ? $previous_copy : $hold->current_copy;
     $entry->hold($hold->id);
     $entry->reset_reason($reset_reason);
     $entry->reset_time('now');
-    $entry->requestor($e->requestor->id) if defined $e->requestor;
-    $entry->requestor_workstation($e->requestor->wsid)  if defined $e->requestor;
     $entry->previous_copy($last_copy);
     $entry->note($note);
+    $entry->requestor($e->requestor->id) if defined $e->requestor;
+    $entry->requestor_workstation($e->requestor->wsid)  if defined $e->requestor;
     $e->create_action_hold_request_reset_reason_entry($entry) or return $e->die_event;
     return 1;
 }
index 7b54b39..7305015 100644 (file)
@@ -6,6 +6,7 @@ use base qw/OpenILS::Application/;
 use OpenILS::Utils::HoldTargeter;
 use OpenILS::Const qw/:const/;
 use OpenSRF::Utils::Logger qw(:logger);
+use OpenSRF::EX qw(:try);
 
 __PACKAGE__->register_method(
     method    => 'hold_targeter',
@@ -89,16 +90,6 @@ sub hold_targeter {
         my $single = 
             OpenILS::Utils::HoldTargeter::Single->new(parent => $targeter);
 
-        # If targeter is issued without a hold
-        # it will retarget all of the holds that need it
-        # so we shoot off a RRE for all them.                  
-               $hold_ses->request(
-        "open-ils.circ.hold_reset_reason_entry.create",
-               $single->editor()->authtoken,
-        $hold_id,
-        OILS_HOLD_TIMED_OUT) 
-        unless defined $args->{hold};
-
         # Don't let an explosion on a single hold stop processing
         eval { $single->target($hold_id) };
 
@@ -108,6 +99,24 @@ sub hold_targeter {
             $logger->error($msg);
             $single->message($msg) unless $single->message;
         }
+        else{
+            try{
+                # create a TIMED_OUT reset reason 
+                # other types of resets are handled
+                # at their sources. 
+                $hold_ses->request(
+                    "open-ils.circ.hold_reset_reason_entry.create",
+                    $single->editor()->authtoken,
+                    $hold_id,
+                    OILS_HOLD_TIMED_OUT,
+                    $single->{previous_copy_id}
+                ) unless defined $args->{hold};
+            } catch Error with {
+                $logger->error(
+                    "hold-targeter: create reset reason failed with ".shift()
+                );          
+            }
+        }
 
         if (($count % $throttle) == 0) { 
             # Time to reply to the caller.  Return either the number
index 31b8852..c3a185c 100644 (file)
@@ -265,6 +265,7 @@ use DateTime;
 use OpenSRF::AppSession;
 use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::Logger qw(:logger);
+use OpenILS::Const qw/:const/;
 use OpenILS::Application::AppUtils;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 
@@ -367,6 +368,20 @@ sub result {
     };
 }
 
+sub retarget_previous_targets_interval {
+    my ($self) = @_;
+    if (!defined($self->{retarget_previous_targets_interval}) || !$self->{retarget_previous_targets_interval}) { 
+        # See if we have a global flag value for the interval
+        my $rri = $self->editor->search_config_global_flag({
+            name => 'circ.holds.retarget_previous_targets_interval',
+            enabled => 't'
+        })->[0];
+        # If no flag is present, default to 0 so feature is disabled
+        $self->{retarget_previous_targets_interval} = $rri ? $rri->value : 0;
+    }    
+    return $self->{retarget_previous_targets_interval};
+}
+
 # List of potential copies in the form of slim hashes.  This list
 # evolves as copies are filtered as they are deemed non-targetable.
 sub copies {
@@ -735,6 +750,42 @@ sub get_copy_circ_libs {
 sub compile_weighted_proximity_map {
     my $self = shift;
 
+    my %copy_reset_map = {}; 
+    my %copy_timeout_map = {}; 
+    eval{
+        my $pt_interval = $self->retarget_previous_targets_interval();
+        if($pt_interval){
+            my $reset_cutoff_time = DateTime->now(time_zone => 'local')
+                    ->subtract(days => $pt_interval);
+
+            # Collect reset reason info and previous copies.
+            # for this hold within the last time interval
+            my $reset_entries = $self->editor->json_query({
+                select => {ahrrre => ['reset_reason','reset_time','previous_copy']},
+                from => 'ahrrre',
+                where => {
+                    hold => $self->hold_id, 
+                    previous_copy => {'!=' => undef},
+                    reset_time => {'>=' => $reset_cutoff_time->strftime('%F %T%z')},
+                    reset_reason => [OILS_HOLD_TIMED_OUT, OILS_HOLD_MANUAL_RESET]
+                }
+            });
+         
+            # count how many times each copy 
+            # was reset or timed out
+            for(@$reset_entries){
+                my $pc = $_->{previous_copy};
+                my $rr = $_->{reset_reason};
+                if($rr == OILS_HOLD_MANUAL_RESET){
+                    $copy_reset_map{$pc} += 1;
+                }
+                elsif($rr == OILS_HOLD_TIMED_OUT){
+                    $copy_timeout_map{$pc} += 1;
+                }
+            }
+        }
+    };
+    
     # Collect copy proximity info (generated via DB trigger)
     # from our newly create copy maps.
     my $hold_copy_maps = $self->editor->json_query({
@@ -745,7 +796,14 @@ sub compile_weighted_proximity_map {
 
     my %copy_prox_map =
         map {$_->{target_copy} => $_->{proximity}} @$hold_copy_maps;
-
+        
+    # calculate the maximum proximity to make adjustments
+    my $max_prox = 0;
+    foreach(@$hold_copy_maps){
+        my $mp = $_->{proximity} + 1;
+        $max_prox = $mp unless $max_prox >= $mp;
+    }
+    
     # Pre-fetch the org setting value for all circ libs so that
     # later calls can reference the cached value.
     $self->parent->precache_batch_ou_settings($self->get_copy_circ_libs, 
@@ -753,7 +811,18 @@ sub compile_weighted_proximity_map {
 
     my %prox_map;
     for my $copy_hash (@{$self->copies}) {
-        my $prox = $copy_prox_map{$copy_hash->{id}};
+        my $copy_id = $copy_hash->{id};
+        my $prox = $copy_prox_map{$copy_id};
+        my $reset_count = $copy_reset_map{$copy_id};
+        my $timeout_count = $copy_timeout_map{$copy_id};
+
+        # make adjustments to proximity based on reset reason.
+        # manual resets get +max_prox each time
+        # this moves them to the end of the hold copy map.
+        # timeout resets only add one level of proximity 
+        # so that copies can be inspected again later.
+        $prox += (($reset_count || 0) * $max_prox) + (($timeout_count || 0));
+        
         $copy_hash->{proximity} = $prox;
         $prox_map{$prox} ||= [];
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/xxxx.hold_reset_reasons.sql b/Open-ILS/src/sql/Pg/upgrade/xxxx.hold_reset_reasons.sql
new file mode 100644 (file)
index 0000000..1de4750
--- /dev/null
@@ -0,0 +1,66 @@
+CREATE TABLE action.hold_request_reset_reason
+(
+ id serial NOT NULL,
+ manual  boolean,
+ name text,
+CONSTRAINT hold_request_reset_reason_pkey PRIMARY KEY (id),
+CONSTRAINT hold_request_reset_reason_name_key UNIQUE (name)
+) WITH (
+  OIDS=FALSE
+);
+
+INSERT INTO action.hold_request_reset_reason (id, name, manual) VALUES
+(1,'HOLD_TIMED_OUT',false),
+(2,'HOLD_MANUAL_RESET',true),
+(3,'HOLD_BETTER_HOLD',false),
+(4,'HOLD_FROZEN',true),
+(5,'HOLD_UNFROZEN',true),
+(6,'HOLD_CANCELED',true),
+(7,'HOLD_UNCANCELED',true),
+(8,'HOLD_UPDATED',true),
+(9,'HOLD_CHECKED_OUT',true),
+(10,'HOLD_CHECKED_IN',true);
+
+CREATE TABLE action.hold_request_reset_reason_entry
+(
+  id serial NOT NULL,
+  hold int,
+  reset_reason int,
+  note text,
+  reset_time timestamp with time zone,
+  previous_copy bigint,
+  requestor int,
+  requestor_workstation int,
+  CONSTRAINT hold_request_reset_reason_entry_pkey PRIMARY KEY (id),
+  CONSTRAINT action_hold_request_reset_reason_entry_reason_fkey FOREIGN KEY (reset_reason)
+      REFERENCES action.hold_request_reset_reason (id) MATCH SIMPLE
+      ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,  
+CONSTRAINT action_hold_request_reset_reason_entry_previous_copy_fkey FOREIGN KEY (previous_copy)
+      REFERENCES asset.copy (id) MATCH SIMPLE
+      ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+  CONSTRAINT action_hold_request_reset_reason_entry_requestor_fkey FOREIGN KEY (requestor)
+      REFERENCES actor.usr (id) MATCH SIMPLE
+      ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+  CONSTRAINT action_hold_request_reset_reason_entry_requestor_workstation_fkey FOREIGN KEY (requestor_workstation)
+      REFERENCES actor.workstation (id) MATCH SIMPLE
+      ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+  CONSTRAINT action_hold_request_reset_reason_entry_hold_fkey FOREIGN KEY (hold)
+      REFERENCES action.hold_request (id) MATCH SIMPLE
+      ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED
+)
+
+WITH (
+  OIDS=FALSE
+);
+
+INSERT INTO config.global_flag (name, label, enabled)
+    VALUES (
+        'circ.holds.retarget_previous_targets_interval',
+        oils_i18n_gettext(
+            'circ.holds.retarget_previous_targets_interval',
+            'Hold targeter will create proximity adjustments for previously targeted copies within this time interval (in days).',
+            'cgf',
+            'label'
+        ),
+        TRUE
+    );