hold targeter reify experiment
authorBill Erickson <berickxx@gmail.com>
Tue, 7 Jun 2016 21:32:14 +0000 (17:32 -0400)
committerBill Erickson <berickxx@gmail.com>
Tue, 7 Jun 2016 21:32:14 +0000 (17:32 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm [new file with mode: 0644]
Open-ILS/src/support-scripts/test-scripts/hold_targeter.pl [new file with mode: 0755]

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm
new file mode 100644 (file)
index 0000000..792732d
--- /dev/null
@@ -0,0 +1,299 @@
+package OpenILS::Utils::HoldTargeter;
+use strict;
+use warnings;
+use DateTime;
+use OpenSRF::AppSession;
+use OpenSRF::Utils::Logger qw(:logger);
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+
+my $U = "OpenILS::Application::AppUtils";
+my $dt_parser = DateTime::Format::ISO8601->new;
+
+# avoid 'duplicate key value violates unique constraint "copy_once_per_hold"'
+# cache org unit settings per run
+# reduce memory requirements?
+# speed 
+#  -- better up-front copy filtering when finding potential copies
+
+sub new {
+    my $class = shift;
+    my $self = {
+        editor => new_editor(),
+        ou_setting_cache => {}
+    };
+    return bless($self, $class);
+}
+
+# Pre-fetch necessary data.
+sub init {
+    my $self = shift;
+
+    $self->{org_closed_dates} =
+        $self->{editor}->search_actor_org_unit_closed_date({
+            close_start => {'<=', 'now'},
+            close_end => {'>=', 'now'}
+    });
+}
+
+# Org unit setting fetch+cache
+sub get_ou_setting {
+    my ($self, $org_id, $setting) = @_;
+    my $c = $self->{ou_setting_cache};
+
+    $c->{$org_id} = {} unless $c->{$org_id};
+
+    if (not exists $c->{$org_id}->{$setting}) {
+        my $r = $U->ou_ancestor_setting($org_id, $setting, $self->{editor});
+        $c->{$org_id}->{$setting} = $r ? $r->{value} : undef;
+    }
+
+    return $c->{$org_id}->{$setting};
+}
+
+# TODO: stream messages back to caller
+sub exit_targeter {
+    my ($self, $msg) = @_;
+    $self->{editor}->rollback; # no-op if commit already occurred.
+    my $hold_id = $self->{hold_id};
+    $logger->info("targeter: exiting hold targeter for $hold_id : $msg");
+    return 0;
+}
+
+# Cancel expired holds and kick off the A/T no-target event.
+# Returns true if the hold is expired or an error occured.
+# False otherwise.
+sub handle_expired_hold {
+    my $self = shift;
+    my $hold = $self->{hold};
+
+    return 0 unless $hold->expire_time;
+
+    my $ex_time =
+        $dt_parser->parse_datetime(cleanse_ISO8601($hold->expire_time));
+    return 0 unless DateTime->compare($ex_time, DateTime->now) < 0;
+
+    # Hold is expired
+
+    $hold->cancel_time('now');
+    $hold->cancel_cause(1); # == un-targeted expiration
+
+    if (!$self->{editor}->update_action_hold_request($hold)) {
+        $self->exit_targeter("Error canceling hold");
+        return 1; # Tell the caller to exit
+    }
+
+    $self->{editor}->commit;
+
+    # Fire the A/T handler, but don't wait for a response.
+    OpenSRF::AppSession->create('open-ils.trigger')->request(
+        'open-ils.trigger.event.autocreate',
+        'hold_request.cancel.expire_no_target',
+        $hold, $hold->pickup_lib
+    );
+
+    return 1;
+}
+
+# Hold copy maps are only automatically deleted when the hold
+# is fulfilled or canceled.  Here, they have to be manually removed.
+# Returns event on error, undef on success.
+sub remove_copy_maps {
+    my $self = shift;
+    my $e = $self->{editor};
+    my $prev_maps = 
+        $e->search_action_hold_copy_map({hold => $self->{hold_id}});
+    for my $map (@$prev_maps) {
+        $e->delete_action_hold_copy_map($map) or return $e->die_event;
+    }
+    return undef;
+}
+
+sub get_hold_copies {
+    my $self = shift;
+    my $e = $self->{editor};
+    my $hold = $self->{hold};
+
+    my $hold_target = $hold->target;
+    my $hold_type   = $hold->hold_type;
+    my $org_unit    = $hold->selection_ou;
+    my $org_depth   = $hold->selection_depth || 0;
+
+    my $query = {
+        select => {acp => ['id']},
+        from => {acp => {}},
+        where => {
+            '+acp' => {
+                deleted => 'f',
+                circ_lib => {
+                    in => {
+                        select => {
+                            aou => [{
+                                transform => 'actor.org_unit_descendants',
+                                column => 'id',
+                                result_field => 'id',
+                                params => [$org_depth]
+                            }],
+                            },
+                        from => 'aou',
+                        where => {id => $org_unit}
+                    }
+                },
+            }
+        }
+    };
+
+    unless ($hold_type eq 'R' || $hold_type eq 'F') {
+        # Add the holdability filters to the copy query, unless
+        # we're processing a Recall or Force hold, which bypass most
+        # holdability checks.
+
+        $query->{from}->{acp} = {
+            acpl => {
+                field => 'id',
+                filter => {holdable => 't', deleted => 'f'},
+                fkey => 'location'
+            },
+            ccs => {
+                field => 'id',
+                filter => {holdable => 't'},
+                fkey => 'status'
+            }
+        };
+
+        $query->{where}->{'+acp'}->{circulate} = 't';
+        $query->{where}->{'+acp'}->{holdable} = 't';
+        $query->{where}->{'+acp'}->{mint_condition} = 't'
+            if $U->is_true($hold->mint_condition);
+    }
+
+    unless ($hold_type eq 'C' || $hold_type eq 'I' || $hold_type eq 'P') {
+        # TODO: Should this include 'R' holds?  The original hold targeter
+        # does not include them either.
+        # For volume and higher level holds, avoid targeting copies that
+        # act as instances of monograph parts.
+
+        $query->{from}->{acp}->{acpm} = {
+            type => 'left',
+            field => 'target_copy',
+            fkey => 'id'
+        };
+
+        $query->{where}->{'+acpm'}->{id} = undef;
+    }
+
+    if ($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
+
+        $query->{where}->{'+acp'}->{id} = $hold_target;
+
+    } elsif ($hold_type eq 'V') {
+
+        $query->{where}->{'+acp'}->{call_number} = $hold_target;
+
+    } elsif ($hold_type eq 'P') {
+
+        $query->{from}->{acp}->{acpm} = {
+            field  => 'target_copy',
+            fkey   => 'id',
+            filter => {part => $hold_target},
+        };
+
+    } elsif ($hold_type eq 'I') {
+
+        $query->{from}->{acp}->{sitem} = {
+            field  => 'unit',
+            fkey   => 'id',
+            filter => {issuance => $hold_target},
+        };
+
+    } elsif ($hold_type eq 'T') {
+
+        $query->{from}->{acp}->{acn} = {
+            field  => 'id',
+            fkey   => 'call_number',
+            'join' => {
+                bre => {
+                    field  => 'id',
+                    filter => {id => $hold_target},
+                    fkey   => 'record'
+                }
+            }
+        };
+
+    } else {
+
+        $query->{from}->{acp}->{acn} = {
+            field => 'id',
+            fkey  => 'call_number',
+            join  => {
+                bre => {
+                    field => 'id',
+                    fkey  => 'record',
+                    join  => {
+                        mmrsm => {
+                            field  => 'source',
+                            fkey   => 'id',
+                            filter => {metarecord => $hold_target},
+                        }
+                    }
+                }
+            }
+        };
+    }
+
+    my $res = $e->json_query($query);
+    return map {$_->{id}} @$res;
+}
+
+sub handle_no_copies {
+    my $self = shift;
+    my $e = $self->{editor};
+    my $hold = $self->{hold};
+
+    $hold->prev_check_time('now');
+    $hold->clear_current_copy;
+
+    if (!$e->update_action_hold_request($hold)) {
+        my $evt = $e->die_event;
+        return $self->exit_targeter(
+            "Error updating hold request: ".$evt->{textcode});
+    }
+
+    $e->commit;
+    return $self->exit_targeter("No copies available for targeting");
+}
+
+
+# Targets a single hold request
+sub target_hold {
+    my ($self, $hold_id) = @_;
+    my $e = $self->{editor};
+    $self->{hold_id} = $hold_id;
+
+    $e->xact_begin;
+
+    my $hold = $self->{hold} = 
+        $e->retrieve_action_hold_request($hold_id)
+            or return $self->exit_targeter("No hold found");
+
+    return $self->exit_targeter("Hold is not eligible for targeting")
+        if $hold->capture_time || $hold->cancel_time;
+
+    # Remove any existing hold copy maps so they can be replaced.
+    my $evt = $self->remove_copy_maps;
+    return $self->exit_targeter(
+        "Error deleting copy maps: ".$evt->{textcode}) if $evt;
+
+    return $self->exit_targeter("Hold is expired")
+        if $self->handle_expired_hold;
+
+    my @copy_ids = $self->get_hold_copies;
+
+    return $self->handle_no_copies unless @copy_ids;
+
+    $logger->info("targeter: Hold $hold_id has ".
+        scalar(@copy_ids)." potential copies");
+}
+
+
+
diff --git a/Open-ILS/src/support-scripts/test-scripts/hold_targeter.pl b/Open-ILS/src/support-scripts/test-scripts/hold_targeter.pl
new file mode 100755 (executable)
index 0000000..54fa863
--- /dev/null
@@ -0,0 +1,20 @@
+#!/usr/bin/perl
+#----------------------------------------------------------------
+# Simple cstore example
+#----------------------------------------------------------------
+
+require '../oils_header.pl';
+use strict; use warnings;
+use OpenSRF::AppSession;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Utils::HoldTargeter;
+
+my $config = shift; # path to opensrf_core.xml
+osrf_connect($config); # connect to jabber
+
+my $hold_id = shift;
+
+my $targeter = OpenILS::Utils::HoldTargeter->new;
+$targeter->init;
+$targeter->target_hold($hold_id);
+