</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>
<app_settings>
</app_settings>
</open-ils.curbside>
+
</apps>
</default>
<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>
--- /dev/null
+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;
+
+
+
+
--- /dev/null
+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;
--- /dev/null
+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
+
--- /dev/null
+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;
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) = @_;