SIP2Gateway WIP
authorBill Erickson <berickxx@gmail.com>
Wed, 18 Mar 2020 15:41:24 +0000 (11:41 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 28 Oct 2020 18:57:39 +0000 (14:57 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Patron.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Mediator.pm [deleted file]

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm
new file mode 100644 (file)
index 0000000..1f4a3bf
--- /dev/null
@@ -0,0 +1,588 @@
+# ---------------------------------------------------------------
+# Copyright (C) 2020 King County Library System
+# Bill Erickson <berickxx@gmail.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# ---------------------------------------------------------------
+# Code borrows heavily and sometimes copies directly from from
+# ../SIP* and SIPServer*
+# ---------------------------------------------------------------
+package OpenILS::WWW::SIPSession;
+use strict; use warnings;
+use OpenSRF::Utils::Cache;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+my $U = 'OpenILS::Application::AppUtils';
+
+# Cache instances cannot be created until opensrf is connected.
+my $_cache;
+sub cache {
+    $_cache = OpenSRF::Utils::Cache->new unless $_cache;
+    return $_cache;
+}
+
+sub new {
+    my ($class, %args) = @_;
+    my $self = bless(\%args, $class);
+}
+
+# Create a new sessesion from cached data.
+sub from_cache {
+    my ($class, $seskey) = @_;
+
+    my $account = cache()->get_cache("sip2_$seskey");
+    return undef unless $account;
+
+    return $class->new(seskey => $seskey, account => $account);
+}
+
+sub seskey {
+    my $self = shift;
+    return $self->{seskey};
+}
+
+# Login account
+sub account {
+    my $self = shift;
+    return $self->{account};
+}
+
+# Logs in to Evergreen and caches the auth token/login with the SIP
+# account data.
+# Returns true on success, false on failure to authenticate.
+sub authenticate {
+    my ($self, $account) = @_;
+
+    my $seskey = $self->seskey;
+
+    my $auth = $U->simplereq(
+        'open-ils.auth',
+        'open-ils.auth.login', {
+        username => $account->{ils_username},
+        password => $account->{ils_password},
+        workstation => $account->{ils_workstation},
+        type => 'staff'
+    });
+
+    if ($auth->{textcode} ne 'SUCCESS') {
+        $logger->warn(
+            "SIP2 login failed for ils_username".$account->{ils_username});
+        return 0;
+    }
+
+    $account->{authtoken} = $auth->{payload}->{authtoken};
+
+    # cache the login user account as well
+    $account->{login} = $U->simplereq(
+        'open-ils.auth',
+        'open-ils.auth.session.retrieve',
+        $account->{authtoken}
+    );
+
+    cache()->put_cache("sip2_$seskey", $account);
+    return 1;
+}
+
+package OpenILS::WWW::SIP2Gateway;
+use strict; use warnings;
+use Apache2::Const -compile =>
+    qw(OK FORBIDDEN NOT_FOUND HTTP_INTERNAL_SERVER_ERROR HTTP_BAD_REQUEST);
+use Apache2::RequestRec;
+use CGI;
+use DateTime;
+use DateTime::Format::ISO8601;
+use JSON::XS;
+use OpenSRF::System;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenILS::Const qw/:const/;
+use OpenILS::WWW::SIP2Gateway::Patron;
+
+my $json = JSON::XS->new;
+$json->ascii(1);
+$json->allow_nonref(1);
+
+my $SIP_DATE_FORMAT = "%Y%m%d    %H%M%S";
+
+ # TODO: move to config / database
+my $config = {
+    options => {
+        # Allow 99 (sc status) message before successful 93 (login) message
+        allow_sc_status_before_login => 1
+    },
+    accounts => [{
+        sip_username => 'sip',
+        sip_password => 'sip',
+        ils_username => 'admin',
+        ils_password => 'demo123',
+        ils_workstation => 'BR1-gamma'
+    }],
+    institutions => [{
+        id => 'example',
+        currency => 'USD',
+        supports => [ # Supported Messages (BX)
+                       'Y', # patron status request,
+                       'Y', # checkout,
+                       'Y', # checkin,
+                       'N', # block patron,
+                       'Y', # acs status,
+                       'N', # request sc/acs resend,
+                       'Y', # login,
+                       'Y', # patron information,
+                       'N', # end patron session,
+                       'Y', # fee paid,
+                       'Y', # item information,
+                       'N', # item status update,
+                       'N', # patron enable,
+                       'N', # hold,
+                       'Y', # renew,
+                       'N', # renew all,
+        ],
+        options => {
+            due_date_use_sip_date_format => 0,
+            patron_status_permit_loans => 0,
+            patron_status_permit_all => 0,
+            msg64_hold_items_available => 0
+        }
+    }]
+};
+
+my $osrf_config;
+sub import {
+    $osrf_config = shift;
+    warn "OSRF CONFIG IS $osrf_config\n";
+}
+
+my $init_complete = 0;
+sub init {
+    return if $init_complete;
+    $init_complete = 1;
+
+    OpenSRF::System->bootstrap_client(config_file => $osrf_config);
+    OpenILS::Utils::CStoreEditor->init;
+
+    return Apache2::Const::OK;
+}
+
+sub sipdate {
+    my $date = shift || DateTime->now;
+    return $date->strftime($SIP_DATE_FORMAT);
+}
+
+sub sipbool {
+    my ($bool, $use_n) = @_;
+    return 'Y' if $bool;
+    return $use_n ? 'N' : ' ';
+}
+
+sub count4 {
+    my $value = shift;
+    return '    ' unless defined $value;
+    return sprintf("%04d", $value);
+}
+
+sub handler {
+    my $r = shift;
+    my $cgi = CGI->new;
+
+    init();
+
+    my $seskey = $cgi->param('session');
+    my $msg_json = $cgi->param('message');
+    my $message = $json->decode($msg_json);
+    my $msg_code = $message->{code};
+    my $response;
+
+    if ($msg_code eq '93') {
+        $response = handle_login($seskey, $message);
+
+    } elsif ($msg_code eq '99') {
+        $response = handle_sc_status($seskey, $message);
+
+    } else {
+
+        # A cached session means we have successfully logged in with
+        # the SIP credentials provided during a login request.  All
+        # message types following require authentication.
+        my $session = OpenILS::WWW::SIPSession->from_cache($seskey);
+
+        return Apache2::Const::FORBIDDEN unless $session;
+
+        if ($msg_code eq '63') {
+            $response = handle_patron_info($session, $message);
+        } elsif ($msg_code eq '17') {
+            $response = handle_item_info($session, $message);
+        }
+    }
+
+    unless ($response) {
+        $logger->error("SIP2: no response generated for: $msg_code");
+        return Apache2::Const::NOT_FOUND;
+    }
+
+    $r->content_type('application/json');
+    $r->print($json->encode($response));
+
+    return Apache2::Const::OK;
+}
+
+# Returns the value of the first occurrence of the requested SIP code.
+sub get_field_value {
+    my ($message, $code) = @_;
+    for my $field (@{$message->{fields}}) {
+        while (my ($c, $v) = each(%$field)) { # one pair per field
+            return $v if $c eq $code;
+        }
+    }
+
+    return undef;
+}
+
+# Returns the configuation chunk mapped to the requested institution.
+sub get_inst_config {
+    my $institution = shift;
+    my ($instconf) = grep {$_->{id} eq $institution} @{$config->{institutions}};
+
+    $logger->error(
+        "SIP2 has no configuration for institution: $institution")
+        unless $instconf;
+
+    return $instconf;
+}
+
+# Login to Evergreen and cache the login data.
+sub handle_login {
+    my ($seskey, $message) = @_;
+
+    my $response = {
+        code => '94',
+        fixed_fields => ['0'] # default to login failed.
+    };
+
+    my $sip_username = get_field_value($message, 'CN');
+    my $sip_password = get_field_value($message, 'CO');
+
+    my ($account) = grep {
+        $_->{sip_username} eq $sip_username &&
+        $_->{sip_password} eq $sip_password
+    } @{$config->{accounts}};
+
+    if ($account) {
+        my $session = OpenILS::WWW::SIPSession->new(seskey => $seskey);
+        $response->{fixed_fields}->[0] = '1' 
+            if $session->authenticate($account);
+
+    } else {
+        $logger->info("SIP2 login failed for user=$sip_username")
+    }
+
+    return $response;
+}
+
+sub handle_sc_status {
+    my ($seskey, $message) = @_;
+
+    return undef unless (
+        $config->{options}->{allow_sc_status_before_login} ||
+        OpenILS::WWW::SIPSession->from_cache($seskey)
+    );
+
+    # The SC Status message does not include an institution, but expects
+    # one in return.  Use the configuration for the first institution.
+    # Maybe the SIP server itself should track which institutoin its
+    # instance is configured to use?  That may multiple servers could
+    # run, one per institution.
+    my $instconf = $config->{institutions}->[0];
+    my $instname = $instconf->{id};
+
+    my $response = {
+        code => '98',
+        fixed_fields => [
+            sipbool(1),    # online_status
+            sipbool(1),    # checkin_ok
+            sipbool(1),    # checkout_ok
+            sipbool(1),    # acs_renewal_policy
+            sipbool(0, 1), # status_update_ok
+            sipbool(0, 1), # offline_ok
+            '999',         # timeout_period
+            '999',         # retries_allowed
+            sipdate(),     # transaction date
+            '2.00'         # protocol_version
+        ],
+        fields => [
+            {AO => $instname},
+            {BX => join('', @{$instconf->{supports}})}
+        ]
+    }
+}
+
+sub handle_item_info {
+    my ($session, $message) = @_;
+
+    my $account = $session->account;
+    my $institution = get_field_value($message, 'AO');
+    my $instconf = get_inst_config($institution) || return undef;
+    my $barcode = get_field_value($message, 'AB');
+    my $idetails = get_item_details($session, $instconf, $barcode);
+
+    if (!$idetails) {
+        # No matching item found, return a minimal response.
+        return {
+            code => '18',
+            fixed_fields => [
+                '01', # circ status: other/Unknown
+                '01', # security marker: other/unknown
+                '01', # fee type: other/unknown
+                sipdate()
+            ],
+            fields => [{AB => $barcode, AJ => ''}]
+        };
+    };
+
+    return {
+        code => '18',
+        fixed_fields => [
+            $idetails->{circ_status},
+            '02', # Security Marker, consistent with ../SIP*
+            $idetails->{fee_type},
+            sipdate()
+        ],
+        fields => [
+            {AB => $barcode},
+            {AJ => $idetails->{title}},
+            {CF => $idetails->{hold_queue_length}},
+            {AH => $idetails->{due_date}},
+            {CM => $idetails->{hold_pickup_date}},
+            {BG => $idetails->{item}->circ_lib->shortname},
+            {BH => $instconf->{currency}},
+            {BV => $idetails->{item}->deposit_amount},
+            {CK => $idetails->{media_type}},
+            {AQ => $idetails->{item}->circ_lib->shortname},
+            {AP => $idetails->{item}->circ_lib->shortname},
+        ]
+    };
+}
+
+sub get_item_details {
+    my ($session, $instconf, $barcode) = @_;
+    my $e = new_editor();
+
+    my $item = $e->search_asset_copy([{
+        barcode => $barcode,
+        deleted => 'f'
+    }, {
+        flesh => 3,
+        flesh_fields => {
+            acp => [qw/circ_lib call_number
+                status stat_cat_entry_copy_maps circ_modifier/],
+            acn => [qw/owning_lib record/],
+            bre => [qw/flat_display_entries/],
+            ascecm => [qw/stat_cat stat_cat_entry/],
+        }
+    }])->[0];
+
+    return undef unless $item;
+
+    my $details = {item => $item};
+
+    $details->{circ} = $e->search_action_circulation([{
+        target_copy => $item->id,
+        checkin_time => undef,
+        '-or' => [
+            {stop_fines => undef},
+            {stop_fines => [qw/MAXFINES LONGOVERDUE/]},
+        ]
+    }, {
+        flesh => 2,
+        flesh_fields => {circ => ['usr'], au => ['card']}
+    }])->[0];
+
+    if ($item->status->id == OILS_COPY_STATUS_IN_TRANSIT) {
+        $details->{transit} = $e->search_action_transit_copy([{
+            target_copy => $item->id,
+            dest_recv_time => undef,
+            cancel_time => undef
+        },{
+            flesh => 1,
+            flesh_fields => {atc => ['dest']}
+        }])->[0];
+    }
+
+    if ($item->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF || (
+        $details->{transit} &&
+        $details->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF)) {
+
+        $details->{hold} = $e->search_action_hold_request([{
+            current_copy        => $item->id,
+            capture_time        => {'!=' => undef},
+            cancel_time         => undef,
+            fulfillment_time    => undef
+        }, {
+            limit => 1,
+            flesh => 1,
+            flesh_fields => {ahr => ['pickup_lib']}
+        }])->[0];
+    }
+
+    my ($title_entry) = grep {$_->name eq 'title'}
+        @{$item->call_number->record->flat_display_entries};
+
+    $details->{title} = $title_entry ? $title_entry->value : '';
+
+    # Same as ../SIP*
+    $details->{hold_queue_length} = $details->{hold} ? 1 : 0;
+
+    $details->{circ_status} = circulation_status($item->status->id);
+
+    $details->{fee_type} =
+        ($item->deposit_amount > 0.0 && $item->deposit eq 'f') ?
+        '06' : '01';
+
+    my $cmod = $item->circ_modifier;
+    $details->{magnetic_media} = $cmod && $cmod->magnetic_media eq 't';
+    $details->{media_type} = $cmod ? $cmod->sip2_media_type : '001';
+
+    if ($details->{circ}) {
+
+        my $due_date = DateTime::Format::ISO8601->new->
+            parse_datetime(clean_ISO8601($details->{circ}->due_date));
+
+        $details->{due_date} =
+            $instconf->{due_date_use_sip_date_format} ?
+            sipdate($due_date) :
+            $due_date->strftime('%F %T');
+    }
+
+    if ($details->{hold}) {
+        my $pickup_date = $details->{hold}->shelf_expire_time;
+        $details->{hold_pickup_date} =
+            $pickup_date ? sipdate($pickup_date) : undef;
+    }
+
+    return $details;
+}
+
+# Maps item status to SIP circulation status constants.
+sub circulation_status {
+    my $stat = shift;
+
+    return '02' if $stat == OILS_COPY_STATUS_ON_ORDER;
+    return '03' if $stat == OILS_COPY_STATUS_AVAILABLE;
+    return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT;
+    return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS;
+    return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF;
+    return '09' if $stat == OILS_COPY_STATUS_RESHELVING;
+    return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT;
+    return '12' if (
+        $stat == OILS_COPY_STATUS_LOST ||
+        $stat == OILS_COPY_STATUS_LOST_AND_PAID
+    );
+    return '13' if $stat == OILS_COPY_STATUS_MISSING;
+
+    return '01';
+}
+
+sub handle_patron_info {
+    my ($session, $message) = @_;
+    my $account = $session->account;
+
+    my $institution = get_field_value($message, 'AO');
+    my $barcode = get_field_value($message, 'AA');
+    my $password = get_field_value($message, 'AD');
+    my $instconf = get_inst_config($institution) || return undef;
+    my $summary = $message->{fixed_fields}->[2];
+
+    my $pdetails = OpenILS::WWW::SIP2Gateway::Patron->get_patron_details(
+        session => $session,
+        instconf => $instconf,
+        barcode => $barcode,
+        password => $password,
+        summary_start_item => get_field_value($message, 'BP'),
+        summary_end_item => get_field_value($message, 'BQ'),
+        summary_list_items => patron_summary_list_items($summary)
+    );
+
+    if (!$pdetails) {
+        return {
+            code => '64',
+            fixed_fields => [
+                sipbool(1), # charge denied
+                sipbool(1), # renew denied
+                sipbool(1), # recall denied
+                sipbool(1), # holds denied
+                split('', (' ' x 10)),
+                '000', # language
+                sipdate()
+            ],
+            fields => [
+                {AO => $institution},
+                {AA => $barcode},
+                {BL => sipbool(0, 1)}, # valid patron
+                {CQ => sipbool(0, 1)}  # valid patron password
+            ]
+        };
+    }
+
+    return {
+        code => '64',
+        fixed_fields => [
+            sipbool($pdetails->{charge_denied}),
+            sipbool($pdetails->{renew_denied}),
+            sipbool($pdetails->{recall_denied}),
+            sipbool($pdetails->{holds_denied}),
+            sipbool($pdetails->{patron}->card->active eq 'f'),
+            sipbool(0), # too many charged
+            sipbool($pdetails->{too_may_overdue}),
+            sipbool(0), # too many renewals
+            sipbool(0), # too many claims retruned
+            sipbool(0), # too many lost
+            sipbool($pdetails->{too_many_fines}),
+            sipbool($pdetails->{too_many_fines}),
+            sipbool(0), # recall overdue
+            sipbool($pdetails->{too_many_fines}),
+            '000', # language
+            sipdate(),
+            count4($pdetails->{holds_count}),
+            count4($pdetails->{overdue_count}),
+            count4($pdetails->{out_count}),
+            count4($pdetails->{fine_count}),
+            count4($pdetails->{recall_count}),
+            count4($pdetails->{unavail_holds_count}),
+        ],
+        fields => [
+            {AO => $institution},
+            {AA => $barcode},
+            {BL => sipbool(1)}, # valid patron
+            {CQ => sipbool($password, 1)}  # password verified if exists
+        ]
+    };
+}
+
+
+# Determines which class of data the SIP client wants detailed
+# information on in the patron info request.
+sub patron_summary_list_items {
+    my $summary = shift;
+
+    my $idx = index($summary, 'Y');
+
+    return 'hold_items'        if $idx == 0;
+    return 'overdue_items'     if $idx == 1;
+    return 'charged_items'     if $idx == 2;
+    return 'fine_items'        if $idx == 3;
+    return 'recall_items'      if $idx == 4;
+    return 'unavailable_holds' if $idx == 5;
+}
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Patron.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Patron.pm
new file mode 100644 (file)
index 0000000..844222d
--- /dev/null
@@ -0,0 +1,209 @@
+# ---------------------------------------------------------------
+# Copyright (C) 2020 King County Library System
+# Bill Erickson <berickxx@gmail.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# ---------------------------------------------------------------
+# Code borrows heavily and sometimes copies directly from from
+# ../SIP* and SIPServer*
+# ---------------------------------------------------------------
+package OpenILS::WWW::SIP2Gateway::Patron;
+use strict; use warnings;
+use DateTime;
+use DateTime::Format::ISO8601;
+use OpenSRF::System;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenILS::Const qw/:const/;
+my $U = 'OpenILS::Application::AppUtils';
+
+sub get_patron_details {
+    my ($class, %params) = @_;
+
+    my $session = $params{session};
+    my $instconf = $params{instconf};
+    my $barcode = $params{barcode};
+    my $password = $params{password};
+
+    my $e = new_editor();
+    my $details = {};
+
+    my $card = $e->search_actor_card([{
+        barcode => $barcode
+    }, {
+        flesh => 3,
+        flesh_fields => {
+            ac => [qw/usr/],
+            au => [qw/
+                billing_address
+                mailing_address
+                profile
+                stat_cat_entries
+            /],
+            actscecm => [qw/stat_cat/]
+        }
+    }])->[0];
+
+    my $patron = $details->{patron} = $card->usr;
+    $patron->card($card);
+
+    # We only attempt to verify the password if one is provided.
+    return undef if defined $password &&
+        !$U->verify_migrated_user_password($e, $patron->id, $password);
+
+    my $penalties = get_patron_penalties($session, $patron);
+
+    set_patron_privileges($session, $instconf, $details, $penalties);
+
+    $details->{too_many_overdue} = 1 if
+        grep {$_->{id} == OILS_PENALTY_PATRON_EXCEEDS_OVERDUE_COUNT}
+        @$penalties;
+
+    $details->{too_many_fines} = 1 if
+        grep {$_->{id} == OILS_PENALTY_PATRON_EXCEEDS_FINES}
+        @$penalties;
+
+    set_patron_summary_items($session, $instconf, $details, %params);
+
+    return $details;
+}
+
+
+# Sets:
+#    holds_count
+#    overdue_count
+#    out_count
+#    fine_count
+#    recall_count
+#    unavail_holds_count
+sub set_patron_summary_items {
+    my ($session, $instconf, $details, %params) = @_;
+
+    my $patron = $details->{patron};
+    my $start_item = $params{summary_start_item} || 0;
+    my $end_item = $params{summary_end_item} || 10;
+    my $list_items = $params{summary_list_items};
+
+    my $e = new_editor();
+
+    my $holds_where = {
+        usr => $patron->id,
+        fulfillment_time => undef,
+        cancel_time => undef
+    };
+
+    $holds_where->{current_shelf_lib} = {'=' => {'+ahr' => 'pickup_lib'}} 
+        if $instconf->{msg64_hold_items_available};
+
+    my $hold_ids = $e->json_query({
+        select => {ahr => ['id']},
+        from => 'ahr',
+        where => {'+ahr' => $holds_where}
+    });
+
+    $details->{holds_count} = scalar(@$hold_ids);
+
+    my $circ_ids = $e->retrieve_action_open_circ_list($patron->id);
+    my $overdue_ids = [ grep {$_ > 0} split(',', $circ_ids->overdue) ];
+    my $out_ids = [ grep {$_ > 0} split(',', $circ_ids->out) ];
+
+    $details->{overdue_count} = scalar(@$overdue_ids);
+    $details->{out_count} = scalar(@$out_ids) + scalar(@$overdue_ids);
+
+    $details->{recall_count} = undef; # not supported
+
+    my $xacts = $U->simplereq(
+        'open-ils.actor',                                
+        'open-ils.actor.user.transactions.history.have_balance',               
+        $session->account->{authtoken},
+        $patron->id
+    );
+
+    $details->{fine_count} = scalar(@$xacts);
+
+    # TODO: unavail holds count; summary details request
+}
+
+sub set_patron_privileges {
+    my ($session, $instconf, $details, $penalties) = @_;
+    my $patron = $details->{patron};
+
+    my $expire = DateTime::Format::ISO8601->new
+        ->parse_datetime(clean_ISO8601($patron->expire_date));
+
+    if ($expire < DateTime->now) {
+        $logger->info(
+            "SIP2 Patron account is expired; all privileges blocked");
+        $details->{charge_denied} = 1;
+        $details->{recall_denied} = 1;
+        $details->{renew_denied} = 1;
+        $details->{holds_denied} = 1;
+        return;
+    }
+
+    # Non-expired patrons are allowed all privileges when 
+    # patron_status_permit_all is true.
+    return if $instconf->{patron_status_permit_all};
+
+    my $blocked = (
+           $patron->barred eq 't'
+        || $patron->active eq 'f'
+        || $patron->card->active eq 'f'
+    );
+
+    my @block_tags = map {$_->{block_list}} grep {$_->{block_list}} @$penalties;
+
+    return unless $blocked || @block_tags; # no blocks remain
+
+    $details->{holds_denied} = ($blocked || grep {$_ =~ /HOLD/} @block_tags);
+
+    # Ignore loan-related blocks?
+    return if $instconf->{patron_status_permit_loans};
+
+    $details->{charge_denied} = ($blocked || grep {$_ =~ /CIRC/} @block_tags);
+    $details->{renew_denied} = ($blocked || grep {$_ =~ /RENEW/} @block_tags);
+
+    # In evergreen, patrons cannot create Recall holds directly, but that
+    # doesn't mean they would not have said privilege if the functionality
+    # existed.  Base the ability to perform recalls on whether they have
+    # checkout and holds privilege, since both would be needed for recalls.
+    $details->{recall_denied} = 
+        ($details->{charge_denied} || $details->{holds_denied});
+}
+
+# Returns an array of penalty hashes with keys "id" and "block_list"
+sub get_patron_penalties {
+    my ($session, $patron) = @_;
+
+    return new_editor()->json_query({
+        select => {csp => ['id', 'block_list']},
+        from => {ausp => 'csp'},
+        where => {
+            '+ausp' => {
+                usr => $patron->id,
+                '-or' => [
+                    {stop_date => undef},
+                    {stop_date => {'>' => 'now'}}
+                ],
+                org_unit => 
+                    $U->get_org_full_path($session->account->{login}->ws_ou)
+            }
+        }
+    });
+}
+
+
+
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Mediator.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Mediator.pm
deleted file mode 100644 (file)
index 4fa1758..0000000
+++ /dev/null
@@ -1,735 +0,0 @@
-# ---------------------------------------------------------------
-# Copyright (C) 2020 King County Library System
-# Bill Erickson <berickxx@gmail.com>
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# ---------------------------------------------------------------
-# Code borrows heavily and sometimes copies directly from from
-# ../SIP* and SIPServer*
-# ---------------------------------------------------------------
-package OpenILS::WWW::SIPSession;
-use strict; use warnings;
-use OpenSRF::Utils::Cache;
-use OpenSRF::Utils::Logger q/$logger/;
-use OpenILS::Application::AppUtils;
-my $U = 'OpenILS::Application::AppUtils';
-
-# Cache instances cannot be created until opensrf is connected.
-my $_cache;
-sub cache {
-    $_cache = OpenSRF::Utils::Cache->new unless $_cache;
-    return $_cache;
-}
-
-sub new {
-    my ($class, %args) = @_;
-    my $self = bless(\%args, $class);
-}
-
-# Create a new sessesion from cached data.
-sub from_cache {
-    my ($class, $seskey) = @_;
-
-    my $account = cache()->get_cache("sip2_$seskey");
-    return undef unless $account;
-
-    return $class->new(seskey => $seskey, account => $account);
-}
-
-sub seskey {
-    my $self = shift;
-    return $self->{seskey};
-}
-
-# Login account
-sub account {
-    my $self = shift;
-    return $self->{account};
-}
-
-# Logs in to Evergreen and caches the auth token/login with the SIP
-# account data.
-# Returns true on success, false on failure to authenticate.
-sub authenticate {
-    my ($self, $account) = @_;
-
-    my $seskey = $self->seskey;
-
-    my $auth = $U->simplereq(
-        'open-ils.auth',
-        'open-ils.auth.login', {
-        username => $account->{ils_username},
-        password => $account->{ils_password},
-        workstation => $account->{ils_workstation},
-        type => 'staff'
-    });
-
-    if ($auth->{textcode} ne 'SUCCESS') {
-        $logger->warn(
-            "SIP2 login failed for ils_username".$account->{ils_username});
-        return 0;
-    }
-
-    $account->{authtoken} = $auth->{payload}->{authtoken};
-
-    # cache the login user account as well
-    $account->{login} = $U->simplereq(
-        'open-ils.auth',
-        'open-ils.auth.session.retrieve',
-        $account->{authtoken}
-    );
-
-    cache()->put_cache("sip2_$seskey", $account);
-    return 1;
-}
-
-package OpenILS::WWW::SIP2Mediator;
-use strict; use warnings;
-use Apache2::Const -compile =>
-    qw(OK FORBIDDEN NOT_FOUND HTTP_INTERNAL_SERVER_ERROR HTTP_BAD_REQUEST);
-use Apache2::RequestRec;
-use CGI;
-use DateTime;
-use DateTime::Format::ISO8601;
-use JSON::XS;
-use OpenSRF::System;
-use OpenILS::Utils::CStoreEditor q/:funcs/;
-use OpenSRF::Utils::Logger q/$logger/;
-use OpenILS::Application::AppUtils;
-use OpenILS::Utils::DateTime qw/:datetime/;
-use OpenILS::Const qw/:const/;
-
-my $json = JSON::XS->new;
-$json->ascii(1);
-$json->allow_nonref(1);
-
-my $SIP_DATE_FORMAT = "%Y%m%d    %H%M%S";
-
-my $config = { # TODO: move to external config / database settings
-    options => {
-        # Allow 99 (sc status) message before successful 93 (login) message
-        allow_sc_status_before_login => 1
-    },
-    accounts => [{
-        sip_username => 'sip',
-        sip_password => 'sip',
-        ils_username => 'admin',
-        ils_password => 'demo123',
-        ils_workstation => 'BR1-gamma'
-    }],
-    institutions => [{
-        id => 'example',
-        currency => 'USD',
-        supports => [ # Supported Messages (BX)
-                       'Y', # patron status request,
-                       'Y', # checkout,
-                       'Y', # checkin,
-                       'N', # block patron,
-                       'Y', # acs status,
-                       'N', # request sc/acs resend,
-                       'Y', # login,
-                       'Y', # patron information,
-                       'N', # end patron session,
-                       'Y', # fee paid,
-                       'Y', # item information,
-                       'N', # item status update,
-                       'N', # patron enable,
-                       'N', # hold,
-                       'Y', # renew,
-                       'N', # renew all,
-        ],
-        options => {
-            due_date_use_sip_date_format => 0,
-            patron_status_permit_loans => 0,
-            patron_status_permit_all => 0,
-            msg64_hold_items_available => 0
-        }
-    }]
-};
-
-my $osrf_config;
-sub import {
-    $osrf_config = shift;
-    warn "OSRF CONFIG IS $osrf_config\n";
-}
-
-my $init_complete = 0;
-sub init {
-    return if $init_complete;
-    $init_complete = 1;
-
-    OpenSRF::System->bootstrap_client(config_file => $osrf_config);
-    OpenILS::Utils::CStoreEditor->init;
-
-    return Apache2::Const::OK;
-}
-
-sub sipdate {
-    my $date = shift || DateTime->now;
-    return $date->strftime($SIP_DATE_FORMAT);
-}
-
-sub sipbool {
-    my ($bool, $use_n) = @_;
-    return 'Y' if $bool;
-    return $use_n ? 'N' : ' ';
-}
-
-sub count4 {
-    my $value = shift;
-    return '    ' unless defined $value;
-    return sprintf("%04d", $value);
-}
-
-sub handler {
-    my $r = shift;
-    my $cgi = CGI->new;
-
-    init();
-
-    my $seskey = $cgi->param('session');
-    my $msg_json = $cgi->param('message');
-    my $message = $json->decode($msg_json);
-    my $msg_code = $message->{code};
-    my $response;
-
-    if ($msg_code eq '93') {
-        $response = handle_login($seskey, $message);
-
-    } elsif ($msg_code eq '99') {
-        $response = handle_sc_status($seskey, $message);
-
-    } else {
-
-        # A cached session means we have successfully logged in with
-        # the SIP credentials provided during a login request.  All
-        # message types following require authentication.
-        my $session = OpenILS::WWW::SIPSession->from_cache($seskey);
-
-        return Apache2::Const::FORBIDDEN unless $session;
-
-        if ($msg_code eq '63') {
-            $response = handle_patron_info($session, $message);
-        } elsif ($msg_code eq '17') {
-            $response = handle_item_info($session, $message);
-        }
-    }
-
-    unless ($response) {
-        $logger->error("SIP2: no response generated for: $msg_code");
-        return Apache2::Const::NOT_FOUND;
-    }
-
-    $r->content_type('application/json');
-    $r->print($json->encode($response));
-
-    return Apache2::Const::OK;
-}
-
-# Returns the value of the first occurrence of the requested SIP code.
-sub get_field_value {
-    my ($message, $code) = @_;
-    for my $field (@{$message->{fields}}) {
-        while (my ($c, $v) = each(%$field)) { # one pair per field
-            return $v if $c eq $code;
-        }
-    }
-
-    return undef;
-}
-
-# Returns the configuation chunk mapped to the requested institution.
-sub get_inst_config {
-    my $institution = shift;
-    my ($instconf) = grep {$_->{id} eq $institution} @{$config->{institutions}};
-
-    $logger->error(
-        "SIP2 has no configuration for institution: $institution")
-        unless $instconf;
-
-    return $instconf;
-}
-
-# Login to Evergreen and cache the login data.
-sub handle_login {
-    my ($seskey, $message) = @_;
-
-    my $response = {
-        code => '94',
-        fixed_fields => ['0'] # default to login failed.
-    };
-
-    my $sip_username = get_field_value($message, 'CN');
-    my $sip_password = get_field_value($message, 'CO');
-
-    my ($account) = grep {
-        $_->{sip_username} eq $sip_username &&
-        $_->{sip_password} eq $sip_password
-    } @{$config->{accounts}};
-
-    if ($account) {
-        my $session = OpenILS::WWW::SIPSession->new(seskey => $seskey);
-        $response->{fixed_fields}->[0] = '1' 
-            if $session->authenticate($account);
-
-    } else {
-        $logger->info("SIP2 login failed for user=$sip_username")
-    }
-
-    return $response;
-}
-
-sub handle_sc_status {
-    my ($seskey, $message) = @_;
-
-    return undef unless (
-        $config->{options}->{allow_sc_status_before_login} ||
-        OpenILS::WWW::SIPSession->from_cache($seskey)
-    );
-
-    # The SC Status message does not include an institution, but expects
-    # one in return.  Use the configuration for the first institution.
-    # Maybe the SIP server itself should track which institutoin its
-    # instance is configured to use?  That may multiple servers could
-    # run, one per institution.
-    my $instconf = $config->{institutions}->[0];
-    my $instname = $instconf->{id};
-
-    my $response = {
-        code => '98',
-        fixed_fields => [
-            sipbool(1),    # online_status
-            sipbool(1),    # checkin_ok
-            sipbool(1),    # checkout_ok
-            sipbool(1),    # acs_renewal_policy
-            sipbool(0, 1), # status_update_ok
-            sipbool(0, 1), # offline_ok
-            '999',         # timeout_period
-            '999',         # retries_allowed
-            sipdate(),     # transaction date
-            '2.00'         # protocol_version
-        ],
-        fields => [
-            {AO => $instname},
-            {BX => join('', @{$instconf->{supports}})}
-        ]
-    }
-}
-
-sub handle_item_info {
-    my ($session, $message) = @_;
-
-    my $account = $session->account;
-    my $institution = get_field_value($message, 'AO');
-    my $instconf = get_inst_config($institution) || return undef;
-    my $barcode = get_field_value($message, 'AB');
-    my $idetails = get_item_details($session, $instconf, $barcode);
-
-    if (!$idetails) {
-        # No matching item found, return a minimal response.
-        return {
-            code => '18',
-            fixed_fields => [
-                '01', # circ status: other/Unknown
-                '01', # security marker: other/unknown
-                '01', # fee type: other/unknown
-                sipdate()
-            ],
-            fields => [{AB => $barcode, AJ => ''}]
-        };
-    };
-
-    return {
-        code => '18',
-        fixed_fields => [
-            $idetails->{circ_status},
-            '02', # Security Marker, consistent with ../SIP*
-            $idetails->{fee_type},
-            sipdate()
-        ],
-        fields => [
-            {AB => $barcode},
-            {AJ => $idetails->{title}},
-            {CF => $idetails->{hold_queue_length}},
-            {AH => $idetails->{due_date}},
-            {CM => $idetails->{hold_pickup_date}},
-            {BG => $idetails->{item}->circ_lib->shortname},
-            {BH => $instconf->{currency}},
-            {BV => $idetails->{item}->deposit_amount},
-            {CK => $idetails->{media_type}},
-            {AQ => $idetails->{item}->circ_lib->shortname},
-            {AP => $idetails->{item}->circ_lib->shortname},
-        ]
-    };
-}
-
-sub get_item_details {
-    my ($session, $instconf, $barcode) = @_;
-    my $e = new_editor();
-
-    my $item = $e->search_asset_copy([{
-        barcode => $barcode,
-        deleted => 'f'
-    }, {
-        flesh => 3,
-        flesh_fields => {
-            acp => [qw/circ_lib call_number
-                status stat_cat_entry_copy_maps circ_modifier/],
-            acn => [qw/owning_lib record/],
-            bre => [qw/flat_display_entries/],
-            ascecm => [qw/stat_cat stat_cat_entry/],
-        }
-    }])->[0];
-
-    return undef unless $item;
-
-    my $details = {item => $item};
-
-    $details->{circ} = $e->search_action_circulation([{
-        target_copy => $item->id,
-        checkin_time => undef,
-        '-or' => [
-            {stop_fines => undef},
-            {stop_fines => [qw/MAXFINES LONGOVERDUE/]},
-        ]
-    }, {
-        flesh => 2,
-        flesh_fields => {circ => ['usr'], au => ['card']}
-    }])->[0];
-
-    if ($item->status->id == OILS_COPY_STATUS_IN_TRANSIT) {
-        $details->{transit} = $e->search_action_transit_copy([{
-            target_copy => $item->id,
-            dest_recv_time => undef,
-            cancel_time => undef
-        },{
-            flesh => 1,
-            flesh_fields => {atc => ['dest']}
-        }])->[0];
-    }
-
-    if ($item->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF || (
-        $details->{transit} &&
-        $details->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF)) {
-
-        $details->{hold} = $e->search_action_hold_request([{
-            current_copy        => $item->id,
-            capture_time        => {'!=' => undef},
-            cancel_time         => undef,
-            fulfillment_time    => undef
-        }, {
-            limit => 1,
-            flesh => 1,
-            flesh_fields => {ahr => ['pickup_lib']}
-        }])->[0];
-    }
-
-    my ($title_entry) = grep {$_->name eq 'title'}
-        @{$item->call_number->record->flat_display_entries};
-
-    $details->{title} = $title_entry ? $title_entry->value : '';
-
-    # Same as ../SIP*
-    $details->{hold_queue_length} = $details->{hold} ? 1 : 0;
-
-    $details->{circ_status} = circulation_status($item->status->id);
-
-    $details->{fee_type} =
-        ($item->deposit_amount > 0.0 && $item->deposit eq 'f') ?
-        '06' : '01';
-
-    my $cmod = $item->circ_modifier;
-    $details->{magnetic_media} = $cmod && $cmod->magnetic_media eq 't';
-    $details->{media_type} = $cmod ? $cmod->sip2_media_type : '001';
-
-    if ($details->{circ}) {
-
-        my $due_date = DateTime::Format::ISO8601->new->
-            parse_datetime(clean_ISO8601($details->{circ}->due_date));
-
-        $details->{due_date} =
-            $instconf->{due_date_use_sip_date_format} ?
-            sipdate($due_date) :
-            $due_date->strftime('%F %T');
-    }
-
-    if ($details->{hold}) {
-        my $pickup_date = $details->{hold}->shelf_expire_time;
-        $details->{hold_pickup_date} =
-            $pickup_date ? sipdate($pickup_date) : undef;
-    }
-
-    return $details;
-}
-
-# Maps item status to SIP circulation status constants.
-sub circulation_status {
-    my $stat = shift;
-
-    return '02' if $stat == OILS_COPY_STATUS_ON_ORDER;
-    return '03' if $stat == OILS_COPY_STATUS_AVAILABLE;
-    return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT;
-    return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS;
-    return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF;
-    return '09' if $stat == OILS_COPY_STATUS_RESHELVING;
-    return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT;
-    return '12' if (
-        $stat == OILS_COPY_STATUS_LOST ||
-        $stat == OILS_COPY_STATUS_LOST_AND_PAID
-    );
-    return '13' if $stat == OILS_COPY_STATUS_MISSING;
-
-    return '01';
-}
-
-sub handle_patron_info {
-    my ($session, $message) = @_;
-    my $account = $session->account;
-
-    my $institution = get_field_value($message, 'AO');
-    my $instconf = get_inst_config($institution) || return undef;
-    my $barcode = get_field_value($message, 'AA');
-    my $password = get_field_value($message, 'AD');
-
-    my $pdetails =
-        get_patron_details($session, $instconf, $message, $barcode, $password);
-
-    if (!$pdetails) {
-        return {
-            code => '64',
-            fixed_fields => [
-                sipbool(1), # charge denied
-                sipbool(1), # renew denied
-                sipbool(1), # recall denied
-                sipbool(1), # holds denied
-                split('', (' ' x 10)),
-                '000', # language
-                sipdate()
-            ],
-            fields => [
-                {AO => $institution},
-                {AA => $barcode},
-                {BL => sipbool(0, 1)}, # valid patron
-                {CQ => sipbool(0, 1)}  # valid patron password
-            ]
-        };
-    }
-
-    return {
-        code => '64',
-        fixed_fields => [
-            sipbool($pdetails->{charge_denied}),
-            sipbool($pdetails->{renew_denied}),
-            sipbool($pdetails->{recall_denied}),
-            sipbool($pdetails->{holds_denied}),
-            sipbool($pdetails->{patron}->card->active eq 'f'),
-            sipbool(0), # too many charged
-            sipbool($pdetails->{too_may_overdue}),
-            sipbool(0), # too many renewals
-            sipbool(0), # too many claims retruned
-            sipbool(0), # too many lost
-            sipbool($pdetails->{too_many_fines}),
-            sipbool($pdetails->{too_many_fines}),
-            sipbool(0), # recall overdue
-            sipbool($pdetails->{too_many_fines}),
-            '000', # language
-            sipdate(),
-            count4($pdetails->{holds_count}),
-            count4($pdetails->{overdue_count}),
-            count4($pdetails->{out_count}),
-            count4($pdetails->{fine_count}),
-            count4($pdetails->{recall_count}),
-            count4($pdetails->{unavail_holds_count}),
-        ],
-        fields => [
-            {AO => $institution},
-            {AA => $barcode},
-            {BL => sipbool(1)}, # valid patron
-            {CQ => sipbool($password, 1)}  # password verified if exists
-        ]
-    };
-}
-
-sub get_patron_details {
-    my ($session, $instconf, $message, $barcode, $password) = @_;
-
-    my $e = new_editor();
-    my $details = {};
-
-    my $card = $e->search_actor_card([{
-        barcode => $barcode
-    }, {
-        flesh => 3,
-        flesh_fields => {
-            ac => [qw/usr/],
-            au => [qw/
-                billing_address
-                mailing_address
-                profile
-                stat_cat_entries
-            /],
-            actscecm => [qw/stat_cat/]
-        }
-    }])->[0];
-
-    my $patron = $details->{patron} = $card->usr;
-    $patron->card($card);
-
-    # We only attempt to verify the password if one is provided.
-    return undef if defined $password &&
-        !$U->verify_migrated_user_password($e, $patron->id, $password);
-
-    my $penalties = get_patron_penalties($session, $patron);
-
-    set_patron_privileges($session, $instconf, $details, $penalties);
-
-    $details->{too_many_overdue} = 1 if
-        grep {$_->{id} == OILS_PENALTY_PATRON_EXCEEDS_OVERDUE_COUNT}
-        @$penalties;
-
-    $details->{too_many_fines} = 1 if
-        grep {$_->{id} == OILS_PENALTY_PATRON_EXCEEDS_FINES}
-        @$penalties;
-
-    set_patron_summary_items($session, $instconf, $message, $details);
-
-    return $details;
-}
-
-
-# Sets:
-#    holds_count
-#    overdue_count
-#    out_count
-#    fine_count
-#    recall_count
-#    unavail_holds_count
-sub set_patron_summary_items {
-    my ($session, $instconf, $message, $details) = @_;
-
-    my $patron = $details->{patron};
-    my $start_item = get_field_value($message, 'BP');
-    my $end_item = get_field_value($message, 'BQ');
-    my @summary = @{$message->{fixed_fields}}[21 .. 30];
-
-    my $e = new_editor();
-
-    my $holds_where = {
-        usr => $patron->id,
-        fulfillment_time => undef,
-        cancel_time => undef
-    };
-
-    $holds_where->{current_shelf_lib} = {'=' => {'+ahr' => 'pickup_lib'}} 
-        if $instconf->{msg64_hold_items_available};
-
-    my $hold_ids = $e->json_query({
-        select => {ahr => ['id']},
-        from => 'ahr',
-        where => {'+ahr' => $holds_where}
-    });
-
-    $details->{holds_count} = scalar(@$hold_ids);
-
-    my $circ_ids = $e->retrieve_action_open_circ_list($patron->id);
-    my $overdue_ids = [ grep {$_ > 0} split(',', $circ_ids->overdue) ];
-    my $out_ids = [ grep {$_ > 0} split(',', $circ_ids->out) ];
-
-    $details->{overdue_count} = scalar(@$overdue_ids);
-    $details->{out_count} = scalar(@$out_ids) + scalar(@$overdue_ids);
-
-    $details->{recall_count} = undef; # not supported
-
-    my $xacts = $U->simplereq(
-        'open-ils.actor',                                
-        'open-ils.actor.user.transactions.history.have_balance',               
-        $session->account->{authtoken},
-        $patron->id
-    );
-
-    $details->{fine_count} = scalar(@$xacts);
-
-    # TODO: unavail holds count; summary details request
-}
-
-sub set_patron_privileges {
-    my ($session, $instconf, $details, $penalties) = @_;
-    my $patron = $details->{patron};
-
-    my $expire = DateTime::Format::ISO8601->new
-        ->parse_datetime(clean_ISO8601($patron->expire_date));
-
-    if ($expire < DateTime->now) {
-        $logger->info(
-            "SIP2 Patron account is expired; all privileges blocked");
-        $details->{charge_denied} = 1;
-        $details->{recall_denied} = 1;
-        $details->{renew_denied} = 1;
-        $details->{holds_denied} = 1;
-        return;
-    }
-
-    # Non-expired patrons are allowed all privileges when 
-    # patron_status_permit_all is true.
-    return if $instconf->{patron_status_permit_all};
-
-    my $blocked = (
-           $patron->barred eq 't'
-        || $patron->active eq 'f'
-        || $patron->card->active eq 'f'
-    );
-
-    my @block_tags = map {$_->{block_list}} grep {$_->{block_list}} @$penalties;
-
-    return unless $blocked || @block_tags; # no blocks remain
-
-    $details->{holds_denied} = ($blocked || grep {$_ =~ /HOLD/} @block_tags);
-
-    # Ignore loan-related blocks?
-    return if $instconf->{patron_status_permit_loans};
-
-    $details->{charge_denied} = ($blocked || grep {$_ =~ /CIRC/} @block_tags);
-    $details->{renew_denied} = ($blocked || grep {$_ =~ /RENEW/} @block_tags);
-
-    # In evergreen, patrons cannot create Recall holds directly, but that
-    # doesn't mean they would not have said privilege if the functionality
-    # existed.  Base the ability to perform recalls on whether they have
-    # checkout and holds privilege, since both would be needed for recalls.
-    $details->{recall_denied} = 
-        ($details->{charge_denied} || $details->{holds_denied});
-}
-
-# Returns an array of penalty hashes with keys "id" and "block_list"
-sub get_patron_penalties {
-    my ($session, $patron) = @_;
-
-    return new_editor()->json_query({
-        select => {csp => ['id', 'block_list']},
-        from => {ausp => 'csp'},
-        where => {
-            '+ausp' => {
-                usr => $patron->id,
-                '-or' => [
-                    {stop_date => undef},
-                    {stop_date => {'>' => 'now'}}
-                ],
-                org_unit => 
-                    $U->get_org_full_path($session->account->{login}->ws_ou)
-            }
-        }
-    });
-}
-
-
-
-
-1;