hold targeter reify experiment
authorBill Erickson <berickxx@gmail.com>
Mon, 13 Jun 2016 21:37:44 +0000 (17:37 -0400)
committerBill Erickson <berickxx@gmail.com>
Mon, 13 Jun 2016 21:37:44 +0000 (17:37 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm

index 03e579a..d17f44b 100644 (file)
@@ -4,9 +4,10 @@ use warnings;
 use DateTime;
 use OpenSRF::AppSession;
 use OpenSRF::Utils::Logger qw(:logger);
+use OpenSRF::Utils::JSON;
+use OpenSRF::Utils qw/:datetime/;
 use OpenILS::Application::AppUtils;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
-use OpenSRF::Utils qw/:datetime/;
 
 # WIP notes:
 # avoid 'duplicate key value violates unique constraint "copy_once_per_hold"'
@@ -244,7 +245,6 @@ sub copy_prox_map {
 sub exit_targeter {
     my ($self, $msg, $is_error) = @_;
     $self->message($msg);
-    $self->{done} = 1;
 
     # Force a rollback when exiting.
     # This is a no-op if a commit or rollback have already occurred.
@@ -552,9 +552,14 @@ sub filter_closed_date_copies {
 # Returns true if filtering completes without error, false otherwise.
 sub filter_copies_by_status {
     my $self = shift;
+
     $self->copies([
         grep {$_->{status} == 0 || $_->{status} == 7} @{$self->copies}
     ]);
+
+    # Track checked out copies for later recall
+    $self->recall_copies([grep {$_->{status} == 1} @{$self->copies}]);
+
     return 1;
 }
 
@@ -602,14 +607,18 @@ sub inspect_previous_target {
 # should continue.  Otherwise, the hold is updated to reflect that 
 # there is no target and returns false, to stop targeting.
 sub handle_no_copies {
-    my $self = shift;
-    my $force = shift;
+    my ($self, %args) = @_;
 
-    if (!$force) {
+    if (!$args{force}) {
         # Force just says don't bother checking the copies, 
         # because other code already has.
         return 1 if @{$self->copies} || $self->{valid_previous_copy};
     }
+
+    if ($args{process_recalls}) {
+        # See if we have any copies to recall.
+        return unless $self->process_recalls;
+    }
     
     my $hold = $self->hold;
     $hold->clear_current_copy;
@@ -772,8 +781,7 @@ sub attempt_prev_copy_retarget {
     my $self = shift;
 
     # attempt_remote_copy_target() can in some cases cancel the hold.
-    # Check our global 'done' flag to confirm we're still going.
-    return undef if $self->{done};
+    return undef if $self->hold->cancel_time;
 
     my $prev_copy = $self->{valid_previous_copy};
     return undef unless $prev_copy;
@@ -901,6 +909,89 @@ sub log_unfulfilled_hold {
 
 sub process_recalls {
     my $self = shift;
+    my $e = $self->editor;
+
+    my $pu_lib = $self->hold->pickup_lib;
+
+    my $threshold = 
+        $self->parent->get_ou_setting($pu_lib, 'circ.holds.recall_threshold')
+        or return 1;
+
+    my $interval = 
+        $self->parent->get_ou_setting($pu_lib, 'circ.holds.recall_return_interval')
+        or return 1;
+
+    # Give me the ID of every checked out copy living at the hold
+    # pickup library.
+    my @copy_ids = map {$_->{id}} 
+        grep {$_->{circ_lib} eq $pu_lib} @{$self->recall_copies};
+
+    return 1 unless @copy_ids;
+
+    my $circ = $e->search_action_circulation([
+        {   target_copy => \@copy_ids,
+            checkin_time => undef,
+            duration => {'>' => $threshold}
+        }, {
+            order_by => 'due_date',
+            limit => 1
+        }
+    ])->[0];
+
+    return unless $circ;
+
+    $logger->info("targeter: recalling circ ".$circ->id);
+
+    # Give the user a new due date of either a full recall threshold,
+    # or the return interval, whichever is further in the future.
+    my $threshold_date = DateTime::Format::ISO8601
+        ->parse_datetime(cleanse_ISO8601($circ->xact_start))
+        ->add(seconds => interval_to_seconds($threshold))
+        ->iso8601();
+
+    my $return_date = DateTime->now(time_zone => 'local')->add(
+        seconds => interval_to_seconds($interval))->iso8601();
+
+    if (DateTime->compare(
+        DateTime::Format::ISO8601->parse_datetime($threshold_date), 
+        DateTime::Format::ISO8601->parse_datetime($return_date)) == 1) {
+        $return_date = $threshold_date;
+    }
+
+    my %update_fields = (
+        due_date => $return_date,
+        renewal_remaining => 0,
+    );
+
+    my $fine_rules = 
+        $self->parent->get_ou_setting($pu_lib, 'circ.holds.recall_fine_rules');
+
+    # If the OU hasn't defined new fine rules for recalls, keep them
+    # as they were
+    if ($fine_rules) {
+        $logger->info("targeter: applying recall fine rules: $fine_rules");
+        my $rules = OpenSRF::Utils::JSON->JSON2perl($fine_rules);
+        $update_fields{recurring_fine} = $rules->[0];
+        $update_fields{fine_interval} = $rules->[1];
+        $update_fields{max_fine} = $rules->[2];
+    }
+
+    # Copy updated fields into circ object.
+    $circ->$_($update_fields{$_}) for keys %update_fields;
+
+    if (!$e->update_action_circulation($circ)) {
+        my $evt = $e->die_event;
+        return $self->exit_targeter(
+            "Error updating circulation object in process_recalls: ". 
+            $evt->{textcode});
+    }
+
+    # Create trigger event for notifying current user
+    my $ses = OpenSRF::AppSession->create('open-ils.trigger');
+    $ses->request('open-ils.trigger.event.autocreate', 
+        'circ.recall.target', $circ, $circ->circ_lib);
+
+    return 1;
 }
 
 # Target a single hold request
@@ -929,30 +1020,34 @@ sub target {
     return unless $self->get_hold_copies;
     return unless $self->update_copy_maps;
 
-    # Confirm that we have something to work on.
+    # Confirm that we have something to work on.  If we have no 
+    # copies at this point, there's also nothing to recall.
     return unless $self->handle_no_copies;
 
+    # Trim the set of working copies down to those that are 
+    # currently targetable.
     return unless $self->filter_copies_by_status;
     return unless $self->filter_copies_in_use;
     return unless $self->filter_closed_date_copies;
+
+    # Set aside the previously targeted copy for later use as needed
     return unless $self->inspect_previous_target;
 
-    # Confirm again we have something to work on.
-    return unless $self->handle_no_copies;
+    # Confirm again we have something to work on.  If we have no
+    # targetable copies now, there may be a copy that can be recalled.
+    return unless $self->handle_no_copies(process_recalls => 1);
 
-    # At this point, we have trimmed the working set of copies
-    # down to those that are currently targetable. Clone this 
-    # list for later use by recalls processing if needed.
-    $self->recall_copies([@{$self->copies}]);
+    # At this point, the working list of copies has been trimmed to
+    # those that are currently targetable.
 
     my $copy = $self->attempt_force_recall_target ||
                $self->attempt_local_copy_target   ||
                $self->attempt_remote_copy_target  ||
                $self->attempt_prev_copy_retarget;
 
-    # Be sure none of the above attempt_* calls set the exit/done flag.
-    # This can happen if the hold has to be forceably canceled.
-    return if $self->{done};
+    # See if one of the above attempt* calls canceled the hold as a side
+    # effect of looking for a copy to target.
+    return if $hold->cancel_time;
 
     return unless $self->log_unfulfilled_hold;
 
@@ -961,8 +1056,7 @@ sub target {
     # No targetable copy was found.  Fire the no-copy 
     # handler to update the hold accordingly.
     
-    return unless $self->process_recalls;
-    $self->handle_no_copies(1);
+    $self->handle_no_copies(force => 1, process_recalls => 1);
 }