migrating top sip2 service
authorBill Erickson <berickxx@gmail.com>
Thu, 3 Sep 2020 19:06:32 +0000 (15:06 -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/examples/opensrf.xml.example
Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Common.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Item.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Session.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm

index 36a4396..5eecd00 100644 (file)
@@ -1289,6 +1289,26 @@ vim:et:ts=4:sw=4:
                 </unix_config>
             </open-ils.courses>
 
+            <open-ils.sip2>
+                <keepalive>5</keepalive>
+                <stateless>1</stateless>
+                <language>perl</language>
+                <implementation>OpenILS::Application::SIP2</implementation>
+                <max_requests>100</max_requests>
+                <unix_config>
+                    <unix_sock>sip2_unix.sock</unix_sock>
+                    <unix_pid>sip2_unix.pid</unix_pid>
+                    <unix_log>sip2_unix.log</unix_log>
+                    <max_requests>1000</max_requests>
+                    <min_children>1</min_children>
+                    <max_children>15</max_children>
+                    <min_spare_children>1</min_spare_children>
+                    <max_spare_children>5</max_spare_children>
+                </unix_config>
+                <app_settings>
+                </app_settings>
+            </open-ils.sip2>
+
             <open-ils.curbside>
                 <keepalive>5</keepalive>
                 <stateless>1</stateless>
@@ -1308,6 +1328,7 @@ vim:et:ts=4:sw=4:
                 <app_settings>
                 </app_settings>
             </open-ils.curbside>
+
         </apps>
     </default>
 
@@ -1355,8 +1376,12 @@ vim:et:ts=4:sw=4:
                 <appname>open-ils.serial</appname>  
                 <appname>open-ils.hold-targeter</appname>  
                 <appname>open-ils.ebook_api</appname>
+<<<<<<< HEAD
                 <appname>open-ils.courses</appname>
                 <appname>open-ils.curbside</appname>
+=======
+                <appname>open-ils.sip2</appname>
+>>>>>>> migrating top sip2 service
             </activeapps>
         </localhost>
     </hosts>
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm
new file mode 100644 (file)
index 0000000..e765d5b
--- /dev/null
@@ -0,0 +1,194 @@
+package OpenILS::Application::SIP2;
+use strict; use warnings;
+use base 'OpenILS::Application';
+use OpenSRF::Utils::Cache;
+use OpenILS::Application;
+use OpenILS::Event;
+use OpenILS::Utils::Fieldmapper;
+use OpenSRF::Utils::Logger qw(:logger);
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Application::SIP2::Common;
+use OpenILS::Application::SIP2::Session;
+use OpenILS::Application::SIP2::Item;
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+
+
+__PACKAGE__->register_method(
+    method    => 'dispatch_sip2_request',
+    api_name  => 'open-ils.sip2.request', 
+    api_level => 1,
+    argc      => 2,
+    signature => {
+        desc     => q/
+            Takes a SIP2 JSON message and handles the request/,
+        params   => [{   
+            name => 'seskey',
+            desc => 'The SIP2 session key',
+            type => 'string'
+        }, {
+            name => 'message',
+            desc => 'SIP2 JSON message',
+            type => q/SIP JSON object/
+        }],
+        return => {
+            desc => q/SIP2 JSON message on success, Event on error/,
+            type => 'object'
+        }
+    }
+);
+
+sub dispatch_sip2_request {
+    my ($self, $client, $seskey, $message) = @_;
+
+    return OpenILS::Event->new('SIP2_SESSION_REQUIRED') unless $seskey;
+    my $msg_code = $message->{code};
+
+    return handle_login($seskey, $message) if $msg_code eq '93';
+    return handle_sc_status($seskey, $message) if $msg_code eq '99';
+
+    # 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::Application::SIPSession->from_cache($seskey);
+    return OpenILS::Event->new('SIP2_SESSION_REQUIRED') unless $session;
+
+    my $MESSAGE_MAP = {
+        '17' => &handle_item_info,
+        '23' => &handle_patron_status,
+        '63' => &handle_patron_info
+    };
+
+    return OpenILS::Event->new('SIP2_NOT_IMPLEMENTED', {payload => $message})
+        unless exists $MESSAGE_MAP->{$msg_code};
+
+    return $MESSAGE_MAP->{$msg_code}->($session, $message);
+}
+
+# Login to Evergreen and cache the login data.
+sub handle_login {
+    my ($seskey, $message) = @_;
+    my $e = new_editor();
+
+    # Default to login-failed
+    my $response = {code => '94', fixed_fields => ['0']};
+
+    my $sip_username = get_field_value($message, 'CN');
+    my $sip_password = get_field_value($message, 'CO');
+    my $sip_account = $e->search_config_sip_account([
+        {sip_username => $sip_username, enabled => 't'}, 
+        {flesh => 1, flesh_fields => {csa => ['workstation']}}
+    ])->[0];
+
+    if (!$sip_account) {
+        $logger->warn("SIP2: No such SIP account: $sip_username");
+        return $response;
+    }
+
+    if ($U->verify_user_password($e, $sip_account->usr, $sip_password, 'sip2')) {
+    
+        my $session = OpenILS::Application::SIPSession->new(
+            seskey => $seskey,
+            sip_account => $sip_account
+        );
+        $response->{fixed_fields}->[0] = '1' if $session->set_ils_account;
+
+    } else {
+        $logger->info("SIP2: login failed for user=$sip_username")
+    }
+
+    return $response;
+}
+
+sub handle_sc_status {
+    my ($seskey, $message) = @_;
+
+    my $session = OpenILS::Application::SIPSession->from_cache($seskey);
+
+    my $config;
+
+    if ($session) {
+        $config = $session->config;
+
+    } else {
+        # TODO: where should the 'allow_sc_status_before_login' setting 
+        # live, since we don't yet have an institution configuration loaded?
+        # TODO: Do we need a 'default institution' setting?
+        $config = {id => 'NONE', supports => [], settings => {}};
+    }
+
+    my $response = {
+        code => '98',
+        fixed_fields => [
+            $SC->sipbool(1),    # online_status
+            $SC->sipbool(1),    # checkin_ok
+            $SC->sipbool(1),    # checkout_ok
+            $SC->sipbool(1),    # acs_renewal_policy
+            $SC->sipbool(0),    # status_update_ok
+            $SC->sipbool(0),    # offline_ok
+            '999',              # timeout_period
+            '999',              # retries_allowed
+            $SC->sipdate,       # transaction date
+            '2.00'              # protocol_version
+        ],
+        fields => [
+            {AO => $config->{id}},
+            {BX => join('', @{$config->{supports}})}
+        ]
+    }
+}
+
+sub handle_item_info {
+    my ($session, $message) = @_;
+
+    my $barcode = get_field_value($message, 'AB');
+    my $config = $session->config;
+
+    my $idetails = OpenILS::Application::SIP2::Item->get_item_details(
+        $session, barcode => $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
+                $SC->sipdate
+            ],
+            fields => [{AB => $barcode, AJ => ''}]
+        };
+    };
+
+    return {
+        code => '18',
+        fixed_fields => [
+            $idetails->{circ_status},
+            '02', # Security Marker, consistent with ../SIP*
+            $idetails->{fee_type},
+            $SC->sipdate
+        ],
+        fields => [
+            {AB => $barcode},
+            {AH => $idetails->{due_date}},
+            {AJ => $idetails->{title}},
+            {AP => $idetails->{item}->circ_lib->shortname},
+            {AQ => $idetails->{item}->circ_lib->shortname},
+            {BG => $idetails->{item}->circ_lib->shortname},
+            {BH => $config->{settings}->{currency}},
+            {BV => $idetails->{item}->deposit_amount},
+            {CF => $idetails->{hold_queue_length}},
+            {CK => $idetails->{media_type}},
+            {CM => $idetails->{hold_pickup_date}}
+        ]
+    };
+}
+
+1;
+
+
+
+
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Common.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Common.pm
new file mode 100644 (file)
index 0000000..424fa52
--- /dev/null
@@ -0,0 +1,42 @@
+package OpenILS::Application::SIP2::Common;
+use strict; use warnings;
+
+use constant SIP_DATE_FORMAT => "%Y%m%d    %H%M%S";
+
+sub sipdate {
+    my ($class, $date) = @_;
+    $date ||= DateTime->now;
+    return $date->strftime(SIP_DATE_FORMAT);
+}
+
+# False == 'N'
+sub sipbool {
+    my ($class, $bool) = @_;
+    return $bool ? 'Y' : 'N';
+}
+
+# False == ' '
+sub spacebool {
+    my $bool = shift;
+    return $bool ? 'Y' : ' ';
+}
+
+sub count4 {
+    my $value = shift;
+    return '    ' unless defined $value;
+    return sprintf("%04d", $value);
+}
+
+# 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;
+}
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Item.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Item.pm
new file mode 100644 (file)
index 0000000..1def432
--- /dev/null
@@ -0,0 +1,141 @@
+package OpenILS::Application::SIP2::Item;
+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/;
+use OpenILS::Application::SIP2::Common;
+use OpenILS::Application::SIP2::Session;
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+
+sub get_item_details {
+    my ($class, $session, %params) = @_;
+
+    my $config = $session->config;
+    my $barcode = $params{barcode};
+    my $e = $session->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 ($details->{circ}) {
+
+        my $due_date = DateTime::Format::ISO8601->new->
+            parse_datetime(clean_ISO8601($details->{circ}->due_date));
+
+        $details->{due_date} =
+            $config->{due_date_use_sip_date_format} ?
+            $SC->sipdate($due_date) :
+            $due_date->strftime('%F %T');
+    }
+
+
+    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];
+    }
+
+
+    if ($details->{hold}) {
+        my $pickup_date = $details->{hold}->shelf_expire_time;
+        $details->{hold_pickup_date} =
+            $pickup_date ? $SC->sipdate($pickup_date) : undef;
+    }
+
+    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';
+
+    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';
+}
+
+1
+
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Session.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Session.pm
new file mode 100644 (file)
index 0000000..12f36bf
--- /dev/null
@@ -0,0 +1,166 @@
+package OpenILS::Application::SIPSession;
+use strict; use warnings;
+use JSON::XS;
+use OpenSRF::Utils::Cache;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+my $U = 'OpenILS::Application::AppUtils';
+my $json = JSON::XS->new;
+$json->ascii(1);
+$json->allow_nonref(1);
+
+# Supported Messages (BX)
+# Currently hard-coded, since it's based on availabilty of functionality
+# in the code, but it could be moved into the database to limit access for 
+# specific institutions.
+use constant INSTITUTION_SUPPORTS => [ 
+    '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,
+];
+
+# 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) = @_;
+    return bless(\%args, $class);
+}
+
+sub config {
+    my $self = shift;
+    return $self->{config} if $self->{config};
+
+    my $inst = $self->sip_account->institution;
+
+    my $config = {
+        id => $inst,
+        settings => {
+            currency => 'USD' # TODO add db setting
+        },
+        supports => INSTITUTION_SUPPORTS
+    };
+
+    # Institution "*" provides default values for all institution configs.
+    my $settings = 
+        $self->editor->search_config_sip_setting({institution => ['*', $inst]});
+
+    # Institution specific settings.
+    for my $set (grep {$_->institution eq $inst} @$settings) {
+        $config->{settings}->{$set->name} = $json->decode($set->value);
+    }
+
+    # Apply values for global settings without replacing 
+    # institution-specific values.
+    for my $set (grep {$_->institution eq '*'} @$settings) {
+        my $name = $set->name;
+        my $value = $json->decode($set->value);
+
+        $config->{settings}->{$name} = $value 
+            unless exists $config->{settings}->{$name};
+    }
+
+    return $self->{config} = $config;
+}
+
+# Create a new sessesion from cached data.
+sub from_cache {
+    my ($class, $seskey) = @_;
+
+    my $ses = cache()->get_cache("sip2_$seskey");
+
+    if ($ses) {
+
+        my $session = $class->new(
+            seskey => $seskey, 
+            sip_account => $ses->{sip_account}
+        );
+
+        $session->editor->authtoken($ses->{ils_authtoken});
+
+        return $session if $session->set_ils_account;
+
+        return undef;
+
+    } else {
+
+        $logger->warn("SIP2: No session found in cache for key $seskey");
+        return undef;
+    }
+}
+
+# The editor contains the authtoken and ILS user account (requestor).
+sub editor {
+    my $self = shift;
+    $self->{editor} = new_editor() unless $self->{editor};
+    return $self->{editor};
+}
+
+sub seskey {
+    my $self = shift;
+    return $self->{seskey};
+}
+
+# SIP account
+sub sip_account {
+    my $self = shift;
+    return $self->{sip_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 set_ils_account {
+    my $self = shift;
+
+    # Verify previously applied authtoken is still valid.
+    return 1 if $self->editor->authtoken && $self->editor->checkauth;
+
+    my $seskey = $self->seskey;
+
+    my $auth = $U->simplereq(
+        'open-ils.auth_internal',
+        'open-ils.auth_internal.session.create', {
+        user_id => $self->sip_account->usr,
+        workstation => $self->sip_account->workstation->name,
+        login_type => 'staff'
+    });
+
+    if ($auth->{textcode} ne 'SUCCESS') {
+        $logger->warn(
+            "SIP2 failed to create an internal login session for ILS user: ".
+            $self->sip_account->usr);
+        return 0;
+    }
+
+    my $ses = {
+        sip_account => $self->sip_account, 
+        ils_authtoken => $auth->{payload}->{authtoken}
+    };
+
+    $self->editor->authtoken($ses->{ils_authtoken});
+    $self->editor->checkauth;
+
+    cache()->put_cache("sip2_$seskey", $ses);
+    return 1;
+}
+
+1;
index 5e070a6..86510c5 100644 (file)
@@ -315,18 +315,6 @@ sub get_field_value {
     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) = @_;