adding openncip / opensip SIP2 service

Signed-off-by: Chris Cormack <crc@liblime.com>
Signed-off-by: Joshua Ferraro <jmf@liblime.com>
This commit is contained in:
Ryan Higgins 2007-11-05 17:13:56 -06:00 committed by Joshua Ferraro
parent fa99edd8b3
commit 7b9b36bd2e
44 changed files with 7348 additions and 0 deletions

498
C4/SIP/ILS.pm Normal file
View File

@ -0,0 +1,498 @@
#
# ILS.pm: Test ILS interface module
#
package ILS;
use warnings;
use strict;
use Sys::Syslog qw(syslog);
use ILS::Item;
use ILS::Patron;
use ILS::Transaction;
use ILS::Transaction::Checkout;
use ILS::Transaction::Checkin;
use ILS::Transaction::FeePayment;
use ILS::Transaction::Hold;
use ILS::Transaction::Renew;
use ILS::Transaction::RenewAll;
my %supports = (
'magnetic media' => 0,
'security inhibit' => 0,
'offline operation' => 0,
"patron status request" => 1,
"checkout" => 1,
"checkin" => 1,
"block patron" => 1,
"acs status" => 1,
"login" => 1,
"patron information" => 1,
"end patron session" => 1,
"fee paid" => 0,
"item information" => 1,
"item status update" => 0,
"patron enable" => 1,
"hold" => 1,
"renew" => 1,
"renew all" => 0,
);
sub new {
my ($class, $institution) = @_;
my $type = ref($class) || $class;
my $self = {};
syslog("LOG_DEBUG", "new ILS '%s'", $institution->{id});
$self->{institution} = $institution;
return bless $self, $type;
}
sub find_patron {
my $self = shift;
return ILS::Patron->new(@_);
}
sub find_item {
my $self = shift;
return ILS::Item->new(@_);
}
sub institution {
my $self = shift;
return $self->{institution}->{id};
}
sub supports {
my ($self, $op) = @_;
return (exists($supports{$op}) && $supports{$op});
}
sub check_inst_id {
my ($self, $id, $whence) = @_;
if ($id ne $self->{institution}->{id}) {
syslog("LOG_WARNING", "%s: received institution '%s', expected '%s'",
$whence, $id, $self->{institution}->{id});
}
}
sub to_bool {
my $bool = shift;
# If it's defined, and matches a true sort of string, or is
# a non-zero number, then it's true.
return defined($bool) && (($bool =~ /true|y|yes/i) || $bool != 0);
}
sub checkout_ok {
my $self = shift;
return (exists($self->{policy}->{checkout})
&& to_bool($self->{policy}->{checkout}));
}
sub checkin_ok {
my $self = shift;
return (exists($self->{policy}->{checkin})
&& to_bool($self->{policy}->{checkin}));
}
sub status_update_ok {
my $self = shift;
return (exists($self->{policy}->{status_update})
&& to_bool($self->{policy}->{status_update}));
}
sub offline_ok {
my $self = shift;
return (exists($self->{policy}->{offline})
&& to_bool($self->{policy}->{offline}));
}
#
# Checkout(patron_id, item_id, sc_renew):
# patron_id & item_id are the identifiers send by the terminal
# sc_renew is the renewal policy configured on the terminal
# returns a status opject that can be queried for the various bits
# of information that the protocol (SIP or NCIP) needs to generate
# the response.
#
sub checkout {
my ($self, $patron_id, $item_id, $sc_renew) = @_;
my ($patron, $item, $circ);
$circ = new ILS::Transaction::Checkout;
# BEGIN TRANSACTION
$circ->patron($patron = new ILS::Patron $patron_id);
$circ->item($item = new ILS::Item $item_id);
if (!$patron) {
$circ->screen_msg("Invalid Patron");
} elsif (!$patron->charge_ok) {
$circ->screen_msg("Patron Blocked");
} elsif (!$item) {
$circ->screen_msg("Invalid Item");
} elsif (@{$item->hold_queue} && ($patron_id ne $item->hold_queue->[0])) {
$circ->screen_msg("Item on Hold for Another User");
} elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
# I can't deal with this right now
$circ->screen_msg("Item checked out to another patron");
} else {
$circ->ok(1);
# If the item is already associated with this patron, then
# we're renewing it.
$circ->renew_ok($item->{patron} && ($item->{patron} eq $patron_id));
$item->{patron} = $patron_id;
$item->{due_date} = time + (14*24*60*60); # two weeks
push(@{$patron->{items}}, $item_id);
$circ->desensitize(!$item->magnetic);
syslog("LOG_DEBUG", "ILS::Checkout: patron %s has checked out %s",
$patron_id, join(', ', @{$patron->{items}}));
}
# END TRANSACTION
return $circ;
}
sub checkin {
my ($self, $item_id, $trans_date, $return_date,
$current_loc, $item_props, $cancel) = @_;
my ($patron, $item, $circ);
$circ = new ILS::Transaction::Checkin;
# BEGIN TRANSACTION
$circ->item($item = new ILS::Item $item_id);
# It's ok to check it in if it exists, and if it was checked out
$circ->ok($item && $item->{patron});
if ($circ->ok) {
$circ->patron($patron = new ILS::Patron $item->{patron});
delete $item->{patron};
delete $item->{due_date};
$patron->{items} = [ grep {$_ ne $item_id} @{$patron->{items}} ];
}
# END TRANSACTION
return $circ;
}
# If the ILS caches patron information, this lets it free
# it up
sub end_patron_session {
my ($self, $patron_id) = @_;
# success?, screen_msg, print_line
return (1, 'Thank you for using Evergreen!', '');
}
sub pay_fee {
my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
$pay_type, $fee_id, $trans_id, $currency) = @_;
my $trans;
my $patron;
$trans = new ILS::Transaction::FeePayment;
$patron = new ILS::Patron $patron_id;
$trans->transaction_id($trans_id);
$trans->patron($patron);
$trans->ok(1);
return $trans;
}
sub add_hold {
my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
$expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
my ($patron, $item);
my $hold;
my $trans;
$trans = new ILS::Transaction::Hold;
# BEGIN TRANSACTION
$patron = new ILS::Patron $patron_id;
if (!$patron
|| (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
$trans->screen_msg("Invalid Patron.");
return $trans;
}
$item = new ILS::Item ($item_id || $title_id);
if (!$item) {
$trans->screen_msg("No such item.");
# END TRANSACTION (conditionally)
return $trans;
} elsif ($item->fee && ($fee_ack ne 'Y')) {
$trans->screen_msg = "Fee required to place hold.";
# END TRANSACTION (conditionally)
return $trans;
}
$hold = {
item_id => $item->id,
patron_id => $patron->id,
expiration_date => $expiry_date,
pickup_location => $pickup_location,
hold_type => $hold_type,
};
$trans->ok(1);
$trans->patron($patron);
$trans->item($item);
$trans->pickup_location($pickup_location);
push(@{$item->hold_queue}, $hold);
push(@{$patron->{hold_items}}, $hold);
# END TRANSACTION
return $trans;
}
sub cancel_hold {
my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
my ($patron, $item, $hold);
my $trans;
$trans = new ILS::Transaction::Hold;
# BEGIN TRANSACTION
$patron = new ILS::Patron $patron_id;
if (!$patron) {
$trans->screen_msg("Invalid patron barcode.");
return $trans;
} elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
$trans->screen_msg('Invalid patron password.');
return $trans;
}
$item = new ILS::Item ($item_id || $title_id);
if (!$item) {
$trans->screen_msg("No such item.");
# END TRANSACTION (conditionally)
return $trans;
}
# Remove the hold from the patron's record first
$trans->ok($patron->drop_hold($item_id));
if (!$trans->ok) {
# We didn't find it on the patron record
$trans->screen_msg("No such hold on patron record.");
# END TRANSACTION (conditionally)
return $trans;
}
# Now, remove it from the item record. If it was on the patron
# record but not on the item record, we'll treat that as success.
foreach my $i (0 .. scalar @{$item->hold_queue}) {
$hold = $item->hold_queue->[$i];
if ($hold->{patron_id} eq $patron->id) {
# found it: delete it.
splice @{$item->hold_queue}, $i, 1;
last;
}
}
$trans->screen_msg("Hold Cancelled.");
$trans->patron($patron);
$trans->item($item);
return $trans;
}
# The patron and item id's can't be altered, but the
# date, location, and type can.
sub alter_hold {
my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
$expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
my ($patron, $item);
my $hold;
my $trans;
$trans = new ILS::Transaction::Hold;
# BEGIN TRANSACTION
$patron = new ILS::Patron $patron_id;
if (!$patron) {
$trans->screen_msg("Invalid patron barcode.");
return $trans;
}
foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
$hold = $patron->{hold_items}[$i];
if ($hold->{item_id} eq $item_id) {
# Found it. So fix it.
$hold->{expiration_date} = $expiry_date if $expiry_date;
$hold->{pickup_location} = $pickup_location if $pickup_location;
$hold->{hold_type} = $hold_type if $hold_type;
$trans->ok(1);
$trans->screen_msg("Hold updated.");
$trans->patron($patron);
$trans->item(new ILS::Item $hold->{item_id});
last;
}
}
# The same hold structure is linked into both the patron's
# list of hold items and into the queue of outstanding holds
# for the item, so we don't need to search the hold queue for
# the item, since it's already been updated by the patron code.
if (!$trans->ok) {
$trans->screen_msg("No such outstanding hold.");
}
return $trans;
}
sub renew {
my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
$no_block, $nb_due_date, $third_party,
$item_props, $fee_ack) = @_;
my ($patron, $item);
my $trans;
$trans = new ILS::Transaction::Renew;
$trans->patron($patron = new ILS::Patron $patron_id);
if (!$patron) {
$trans->screen_msg("Invalid patron barcode.");
return $trans;
} elsif (!$patron->renew_ok) {
$trans->screen_msg("Renewals not allowed.");
return $trans;
}
if (defined($title_id)) {
# renewing a title, rather than an item (sort of)
# This is gross, but in a real ILS it would be better
foreach my $i (@{$patron->{items}}) {
$item = new ILS::Item $i;
last if ($title_id eq $item->title_id);
$item = undef;
}
} else {
foreach my $i (@{$patron->{items}}) {
if ($i == $item_id) {
# We have it checked out
$item = new ILS::Item $item_id;
last;
}
}
}
$trans->item($item);
if (!defined($item)) {
# It's not checked out to $patron_id
$trans->screen_msg("Item not checked out to " . $patron->name);
} elsif (!$item->available($patron_id)) {
$trans->screen_msg("Item has outstanding holds");
} else {
$trans->renewal_ok(1);
$trans->desensitize(0); # It's already checked out
if ($no_block eq 'Y') {
$item->{due_date} = $nb_due_date;
} else {
$item->{due_date} = time + (14*24*60*60); # two weeks
}
if ($item_props) {
$item->{sip_item_properties} = $item_props;
}
$trans->ok(1);
$trans->renewal_ok(1);
return $trans;
}
return $trans;
}
sub renew_all {
my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
my ($patron, $item_id);
my $trans;
$trans = new ILS::Transaction::RenewAll;
$trans->patron($patron = new ILS::Patron $patron_id);
if (defined $patron) {
syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
$patron->name, $patron->renew_ok);
} else {
syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
$patron_id);
}
if (!defined($patron)) {
$trans->screen_msg("Invalid patron barcode.");
return $trans;
} elsif (!$patron->renew_ok) {
$trans->screen_msg("Renewals not allowed.");
return $trans;
} elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
$trans->screen_msg("Invalid patron password.");
return $trans;
}
foreach $item_id (@{$patron->{items}}) {
my $item = new ILS::Item $item_id;
if (!defined($item)) {
syslog("LOG_WARNING",
"renew_all: Invalid item id associated with patron '%s'",
$patron->id);
next;
}
if (@{$item->hold_queue}) {
# Can't renew if there are outstanding holds
push @{$trans->unrenewed}, $item_id;
} else {
$item->{due_date} = time + (14*24*60*60); # two weeks hence
push @{$trans->renewed}, $item_id;
}
}
$trans->ok(1);
return $trans;
}
1;

486
C4/SIP/ILS.pod Normal file
View File

@ -0,0 +1,486 @@
=head1 NAME
ILS - Portability layer to interface between Open-SIP and ILS
=head1 SYNOPSIS
use ILS;
# Initialize connection between SIP and the ILS
my $ils = new ILS (institution => 'Foo Public Library');
# Basic object access methods
$inst_name = $self->institution;
$bool = $self->support($operation);
$self->check_inst_id($inst_name, "error message");
# Check to see if certain protocol options are permitted
$bool = $self->checkout_ok;
$bool = $self->checkin_ok;
$bool = $self->status_update_ok;
$bool = $self->offline_ok;
$status = $ils->checkout($patron_id, $item_id, $sc_renew);
$status = $ils->checkin($item_id, $trans_date, $return_date,
$current_loc, $item_props, $cancel);
$status = $ils->end_patron_session($patron_id);
$status = $ils->pay_fee($patron_id, $patron_pwd, $fee_amt,
$fee_type, $pay_type, $fee_id, $trans_id,
$currency);
$status = $ils->add_hold($patron_id, $patron_pwd, $item_id,
$title_id, $expiry_date,
$pickup_locn, $hold_type, $fee_ack);
$status = $ils->cancel_hold($patron_id, $patron_pwd,
$item_id, $title_id);
$status = $ils->alter_hold($patron_id, $patron_pwd, $item_id,
$title_id, $expiry_date,
$pickup_locn, $hold_type,
$fee_ack);
$status = $ils->renew($patron_id, $patron_pwd, $item_id,
$title_id, $no_block, $nb_due_date,
$third_party, $item_props, $fee_ack);
$status = $ils->renew_all($patron_id, $patron_pwd, $fee_ack);
=head1 INTRODUCTION
The ILS module defines a basic portability layer between the SIP
server and the rest of the integrated library system. It is the
responsibility of the ILS vendor to implement the functions
defined by this interface. This allows the SIP server to be
reasonably portable between ILS systems (of course, we won't know
exactly I<how> portable the interface is until it's been used by
a second ILS.
Because no business logic is embedded in the SIP server code
itself, the SIP protocol handler functions do almost nothing
except decode the network messages and pass the parameters to the
ILS module or one of its submodules, C<ILS::Patron> and
C<ILS::Item>. The SIP protocol query messages (Patron
Information, or Item Status, for example), are implemented within
the SIP server code by fetching a Patron, or Item, record and
then retrieving the relevant information from that record. See
L<ILS::Patron> and L<ILS::Item> for the details.
=head1 INITIALIZATION
The first thing the SIP server does, after a terminal has
successfully logged in, is initialize the ILS module by calling
$ils = new ILS $institution
where C<$institution> is an object of type
C<Sip::Configuration::Institution>, describing the institution to
which the terminal belongs. In general, this will be the single
institution that the ILS supports, but it may be that in a
consortial setting, the SIP server may support connecting to
different ILSs based on the C<$institution> of the terminal.
=head1 BASIC OBJECT ACCESS AND PROTOCOL SUPPORT
The C<$ils> object supports a small set of simple access methods
and methods that allow the SIP server to determine if certain
protocol operations are permitted to the remote terminals.
=head2 C<$inst_name = $self-E<gt>institution;>
Returns the institution ID as a string, suitable for
incorporating into a SIP response message.
=head2 C<$bool = $self-E<gt>support($operation);>
Reports whether this ILS implementation supports certain
operations that are necessary to report information to the SIP
terminal. The argument C<$operation> is a string from this list:
=over
=item C<'magnetic media'>
Can the ILS properly report whether an item is (or contains)
magnetic media, such as a videotape or a book with a floppy disk?
=item C<'security inhibit'>
Is the ILS capable of directing the terminal to ignore the
security status of an item?
=item C<'offline operation'>
Does the ILS allow self-check units to operate when unconnected
to the ILS? That is, can a self-check unit check out items to
patrons without checking the status of the items and patrons in
real time?
=back
=head2 C<$bool = $self-E<gt>checkout_ok;>
Are the self service terminals permitted to check items out to
patrons?
=head2 C<$bool = $self-E<gt>checkin_ok;>
Are the self service terminals permitted to check items in?
=head2 C<$bool = $self-E<gt>status_update_ok;>
Are the self service terminals permitted to update patron status
information. For example, can terminals block patrons?
=head2 C<$bool = $self-E<gt>offline_ok>;
Are the self service terminals permitted to operate off-line.
That is, can they perform their core self service operations when
not in communication with the ILS?
=head1 THE TRANSACTIONS
In general, every protocol transaction that changes the status of
some ILS object (Patron or Item) has a corresponding C<ILS>
method. Operations like C<Check In>, which are a function of
both a patron and an item are C<ILS> functions, while others,
like C<Patron Status> or C<Item Status>, which only depend on one
type of object, are methods of the corresponding sub-module.
In the stub implementation provided with the SIP system, the
C<$status> objects returned by the various C<ILS> transactions
are objects that are subclasses of a virtual C<ILS::Transaction>
object, but this is not required of the SIP code, as long as the
status objects support the appropriate methods.
=head2 CORE TRANSACTION STATUS METHODS
The C<$status> objects returned by all transactions must support
the following common methods:
=over
=item C<ok>
Returns C<true> if the transaction was successful and C<false> if
not. Other methods can be used to find out what went wrong.
=item C<item>
Returns an C<ILS::Item> object corresponding to the item with the
barcode C<$item_id>, or C<undef> if the barcode is invalid.
=item C<patron>
Returns a C<ILS::Patron> object corresponding to the patron with
the barcode C<$patron_id>, or C<undef> if the barcode is invalid
(ie, nonexistent, as opposed to "expired" or "delinquent").
=item C<screen_msg>
Optional. Returns a message that is to be displayed on the
terminal's screen. Some self service terminals read the value of
this string and act based on it. The configuration of the
terminal, and the ILS implementation of this method will have to
be coordinated.
=item C<print_line>
Optional. Returns a message that is to be printed on the
terminal's receipt printer. This message is distinct from the
basic transactional information that the terminal will be
printing anyway (such as, the basic checkout information like the
title and due date).
=back
=head2 C<$status = $ils-E<gt>checkout($patron_id, $item_id, $sc_renew)>
Check out (or possibly renew) item with barcode C<$item_id> to
the patron with barcode C<$patron_id>. If C<$sc_renew> is true,
then the self-check terminal has been configured to allow
self-renewal of items, and the ILS may take this into account
when deciding how to handle the case where C<$item_id> is already
checked out to C<$patron_id>.
The C<$status> object returned by C<checkout> must support the
following methods:
=over
=item C<renewal_ok>
Is this transaction actually a renewal? That is, did C<$patron_id>
already have C<$item_id> checked out?
=item C<desensitize>
Should the terminal desensitize the item? This will be false for
magnetic media, like videocassettes, and for "in library" items
that are checked out to the patron, but not permitted to leave the
building.
=item C<security_inhibit>
Should self checkout unit ignore the security status of this
item?
This method will only be used if
$ils->supports('security inhibit')
returns C<true>.
=item C<fee_amount>
If there is a fee associated with the use of C<$item_id>, then
this method should return the amount of the fee, otherwise it
should return zero. See also the C<sip_currency> and
C<sip_fee_type> methods.
=item C<sip_currency>
The ISO currency code for the currency in which the fee
associated with this item is denominated. For example, 'USD' or
'CAD'.
=item C<sip_fee_type>
A code indicating the type of fee associated with this item. See
the table in the protocol specification for the complete list of
standard values that this function can return.
=back
=head2 C<$status = $ils-E<gt>checkin($item_id, $trans_date, $return_date, $current_loc, $item_props, $cancel)>
Check in item identified by barcode C<$item_id>. This
transaction took place at time C<$trans_date> and was effective
C<$return_date> (to allow for backdating of items to when the
branch closed, for example). The self check unit which received
the item is located at C<$current_loc>, and the item has
properties C<$item_props>. The parameters C<$current_loc> and
C<$item_props> are opaque strings passed from the self service
unit to the ILS untranslated. The configuration of the terminal,
and the ILS implementation of this method will have to be
coordinated.
The C<$status> object returned by the C<checkin> operation must
support the following methods:
=over
=item C<resensitize>
Does the item need to be resensitized by the self check unit?
=item C<alert>
Should the self check unit generate an audible alert to notify
staff that the item has been returned?
=item C<sort_bin>
Certain self checkin units provide for automated sorting of the
returned items. This function returns the bin number into which
the received item should be placed. This function may return the
empty string, or C<undef>, to indicate that no sort bin has been
specified.
=back
=head2 C<($status, $screen_msg, $print_line) = $ils-E<gt>end_patron_session($patron_id)>
This function informs the ILS that the current patron's session
has ended. This allows the ILS to free up any internal state
that it may be preserving between messages from the self check
unit. The function returns a boolean C<$status>, where C<true>
indicates success, and two strings: a screen message to display
on the self check unit's console, and a print line to be printed
on the unit's receipt printer.
=head2 C<$status = $ils-E<gt>pay_fee($patron_id, $patron_pwd, $fee_amt, $fee_type, $pay_type, $fee_id, $trans_id, $currency)>
Reports that the self check terminal handled fee payment from
patron C<$patron_id> (who has password C<$patron_pwd>, which is
an optional parameter). The other parameters are:
=over
=item C<$fee_amt>
The amount of the fee.
=item C<$fee_type>
The type of fee, according a table in the SIP protocol
specification.
=item C<$pay_type>
The payment method. Defined in the SIP protocol specification.
=item C<$fee_id>
Optional. Identifies which particular fee was paid. This
identifier would have been sent from the ILS to the Self Check
unit by a previous "Patron Information Response" message.
=item C<$trans_id>
Optional. A transaction identifier set by the payment device.
This should be recorded by the ILS for financial tracking
purposes.
=item C<$currency>
An ISO currency code indicating the currency in which the fee was
paid.
=back
The status object returned by the C<pay_fee> must support the
following methods:
=over
=item C<transaction_id>
Transaction identifier of the transaction. This parallels the
optional C<$trans_id> sent from the terminal to the ILS. This
may return an empty string.
=back
=head2 C<$status = $ils-E<gt>add_hold($patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack);>
Places a hold for C<$patron_id> (optionally, with password
C<$patron_pwd>) on the item described by either C<$item_id> or
C<$title_id>. The other parameters are:
=over
=item C<$expiry_date>
The date on which the hold should be cancelled. This date is a
SIP protocol standard format timestamp:
YYYYMMDDZZZZHHMMSS
where the 'Z' characters indicate spaces.
=item C<$pickup_location>
The location at which the patron wishes to pick up the item when
it's available. The configuration of the terminal, and the ILS
implementation of this parameter will have to be coordinated.
=item C<$hold_type>
The type of hold being placed: any copy, a specific copy, any
copy from a particular branch or location. See the SIP protocol
specification for the exact values that this parameter might
take.
=item C<$fee_ack>
Boolean. If true, the patron has acknowleged that she is willing
to pay the fee associated with placing a hold on this item. If
C<$fee_ack> is false, then the ILS should refuse to place the
hold.
=back
=head2 C<$status = $ils-E<gt>cancel_hold($patron_id, $patron_pwd, $item_id, $title_id);>
Cancel a hold placed by C<$patron_id> for the item identified by
C<$item_id> or C<$title_id>. The patron password C<$patron_pwd>
may be C<undef>, if it was not provided by the terminal.
=head2 C<$status = $ils-E<gt>alter_hold($patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack);>
The C<$status> object returned by C<$ils-E<gt>add_hold>,
C<$ils-E<gt>cancel_hold>, and C<$ils-E<gt>alter_hold> must all
support the same methods:
=over
=item C<expiration_date>
Returns the expiry date for the placed hold, in seconds since the
epoch.
=item C<queue_position>
Returns the new hold's place in the queue of outstanding holds.
=item C<pickup_location>
Returns the location code for the pickup location.
=back
=head2 C<$status = $ils-E<gt>renew($patron_id, $patron_pwd, $item_id, $title_id, $no_block, $nb_due_date, $third_party, $item_props, $fee_ack);>
Renew the item identified by C<$item_id> or C<$title_id>, as
requested by C<$patron_id> (with password C<$patron_pwd>). The
item has the properties C<$item_props> associated with it.
If the patron renewed the item while the terminal was
disconnected from the net, then it is a C<$no_block> transaction,
and the due date assigned by the terminal, and reported to the
patron was C<$nb_due_date> (so we have to honor it).
If there is a fee associated with renewing the item, and the
patron has agreed to pay the fee, then C<$fee_ack> will be
C<'Y'>.
If C<$third_party> is C<'Y'> and the book is not checked out to
C<$patron_id>, but to some other person, then this is a
third-party renewal; the item should be renewed for the person to
whom it is checked out, rather than checking it out to
C<$patron_id>, or the renewal should fail.
The C<$status> object returned by C<$ils-E<gt>renew> must support
the following methods:
=over
=item C<renewal_ok>
Boolean. If C<renewal_ok> is true, then the item was already
checked out to the patron, so it is being renewed. If
C<renewal_ok> is false, then the patron did not already have the
item checked out.
NOTE: HOW IS THIS USED IN PRACTICE?
=item C<desensitize>, C<security_inhibit>, C<fee_amount>, C<sip_currency>, C<sip_fee_type>, C<transaction_id>
See C<$ils-E<gt>checkout> for these methods.
=back
=head2 C<$status = $ils-E<gt>renew_all($patron_id, $patron_pwd, $fee_ack);>
Renew all items checked out by C<$patron_id> (with password
C<$patron_pwd>). If the patron has agreed to pay any fees
associated with this transaction, then C<$fee_ack> will be
C<'Y'>.
The C<$status> object must support the following methods:
=over
=item C<renewed>
Returns a list of the C<$item_id>s of the items that were renewed.
=item C<unrenewed>
Returns a list of the C<$item_id>s of the items that were not renewed.
=back

214
C4/SIP/ILS/Item.pm Normal file
View File

@ -0,0 +1,214 @@
#
# ILS::Item.pm
#
# A Class for hiding the ILS's concept of the item from the OpenSIP
# system
#
package ILS::Item;
use strict;
use warnings;
use Sys::Syslog qw(syslog);
use ILS::Transaction;
our %item_db = (
'1565921879' => {
title => "Perl 5 desktop reference",
id => '1565921879',
sip_media_type => '001',
magnetic_media => 0,
hold_queue => [],
},
'0440242746' => {
title => "The deep blue alibi",
id => '0440242746',
sip_media_type => '001',
magnetic_media => 0,
hold_queue => [],
},
'660' => {
title => "Harry Potter y el cáliz de fuego",
id => '660',
sip_media_type => '001',
magnetic_media => 0,
hold_queue => [],
},
);
sub new {
my ($class, $item_id) = @_;
my $type = ref($class) || $class;
my $self;
if (!exists($item_db{$item_id})) {
syslog("LOG_DEBUG", "new ILS::Item('%s'): not found", $item_id);
return undef;
}
$self = $item_db{$item_id};
bless $self, $type;
syslog("LOG_DEBUG", "new ILS::Item('%s'): found with title '%s'",
$item_id, $self->{title});
return $self;
}
sub magnetic {
my $self = shift;
return $self->{magnetic_media};
}
sub sip_media_type {
my $self = shift;
return $self->{sip_media_type};
}
sub sip_item_properties {
my $self = shift;
return $self->{sip_item_properties};
}
sub status_update {
my ($self, $props) = @_;
my $status = new ILS::Transaction;
$self->{sip_item_properties} = $props;
$status->{ok} = 1;
return $status;
}
sub id {
my $self = shift;
return $self->{id};
}
sub title_id {
my $self = shift;
return $self->{title};
}
sub permanent_location {
my $self = shift;
return $self->{permanent_location} || '';
}
sub current_location {
my $self = shift;
return $self->{current_location} || '';
}
sub sip_circulation_status {
my $self = shift;
if ($self->{patron}) {
return '04';
} elsif (scalar @{$self->{hold_queue}}) {
return '08';
} else {
return '03';
}
}
sub sip_security_marker {
return '02';
}
sub sip_fee_type {
return '01';
}
sub fee {
my $self = shift;
return $self->{fee} || 0;
}
sub fee_currency {
my $self = shift;
return $self->{currency} || 'CAD';
}
sub owner {
my $self = shift;
return 'UWOLS';
}
sub hold_queue {
my $self = shift;
return $self->{hold_queue};
}
sub hold_queue_position {
my ($self, $patron_id) = @_;
my $i;
for ($i = 0; $i < scalar @{$self->{hold_queue}}; $i += 1) {
if ($self->{hold_queue}[$i]->{patron_id} eq $patron_id) {
return $i + 1;
}
}
return 0;
}
sub due_date {
my $self = shift;
return $self->{due_date} || 0;
}
sub recall_date {
my $self = shift;
return $self->{recall_date} || 0;
}
sub hold_pickup_date {
my $self = shift;
return $self->{hold_pickup_date} || 0;
}
sub screen_msg {
my $self = shift;
return $self->{screen_msg} || '';
}
sub print_line {
my $self = shift;
return $self->{print_line} || '';
}
# An item is available for a patron if
# 1) It's not checked out and (there's no hold queue OR patron
# is at the front of the queue)
# OR
# 2) It's checked out to the patron and there's no hold queue
sub available {
my ($self, $for_patron) = @_;
return ((!defined($self->{patron_id}) && (!scalar @{$self->{hold_queue}}
|| ($self->{hold_queue}[0] eq $for_patron)))
|| ($self->{patron_id} && ($self->{patron_id} eq $for_patron)
&& !scalar @{$self->{hold_queue}}));
}
1;

231
C4/SIP/ILS/Item.pod Normal file
View File

@ -0,0 +1,231 @@
=head1 NAME
ILS::Item - Portable Item status object class for SIP
=head1 SYNOPSIS
use ILS;
use ILS::Item;
# Look up item based on item_id
my $item = new ILS::Item $item_id;
# Basic object access methods
$item_id = $item->id;
$title = $item->title_id;
$media_type = $item->sip_media_type;
$bool = $item->magnetic;
$locn = $item->permanent_location;
$locn = $item->current_location;
$props = $item->sip_item_props;
$owner = $item->owner;
$str = $item->sip_circulation_status;
$bool = $item->available;
@hold_queue = $item->hold_queue;
$pos = $item->hold_queue_position($patron_id);
$due = $item->due_date;
$pickup = $item->hold_pickup_date;
$recall = $item->recall_date;
$fee = $item->fee;
$currency = $item->fee_currency;
$type = $item->sip_fee_type;
$mark = $item->sip_security_marker;
$msg = $item->screen_msg;
$msg = $item->print_line;
# Operations on items
$status = $item->status_update($item_props);
=head1 DESCRIPTION
An C<ILS::Item> object holds the information necessary to
circulate an item in the library's collection. It does not need
to be a complete bibliographic description of the item; merely
basic human-appropriate identifying information is necessary
(that is, not the barcode, but just a title, and maybe author).
For the most part, C<ILS::Item>s are not operated on directly,
but are passed to C<ILS> methods as part of a transaction. That
is, rather than having an item check itself in:
$item->checkin;
the code tells the ILS that the item has returned:
$ils->checkin($item_id);
Similarly, patron's don't check things out (a la,
C<$patron-E<gt>checkout($item)>), but the ILS checks items out to
patrons. This means that the methods that are defined for items
are, almost exclusively, methods to retrieve information about
the state of the item.
=over
=item C<$item_id = $item-E<gt>id>
Return the item ID, or barcode, of C<$item>.
=item C<$title = $item-E<gt>title_id>
Return the title, or some other human-relevant description, of
the item.
=item C<$media_type = $item-E<gt>media_type>
Return the SIP-defined media type of the item. The specification
provides the following definitions:
000 Other
001 Book
002 Magazine
003 Bound journal
004 Audio tape
005 Video tape
006 CD/CDROM
007 Diskette
008 Book with diskette
009 Book with CD
010 Book with audio tape
The SIP server does not use the media type code to alter its
behavior at all; it merely passes it through to the self-service
terminal. In particular, it does not set indicators related to
whether an item is magnetic, or whether it should be
desensitized, based on this return type. The
C<$item-E<gt>magnetic> method will be used for that purpose.
=item C<magnetic>
Is the item some form of magnetic media (eg, a video or a book
with an accompanying floppy)? This method will not be called
unless
$ils->supports('magnetic media')
returns C<true>.
If this method is defined, it is assumed to return either C<true>
or C<false> for every item. If the magnetic media indication is
not supported by the ILS, then the SIP server will indicate that
all items are 'Unknown'.
=item C<$locn = $item-E<gt>permanent_location>
Where does this item normally reside? The protocol specification
is not clear on whether this is the item's "home branch", or a
location code within the branch, merely stating that it is, "The
location where an item is normally stored after being checked
in."
=item C<$locn = $item-E<gt>current_location>
According to the protocol, "[T]he current location of the item.
[A checkin terminal] could set this field to the ... system
terminal location on a Checkin message."
=item C<$props = $item-E<gt>sip_item_props>
Returns "item properties" associated with the item. This is an
(optional) opaque string that is passed between the self-service
terminals and the ILS. It can be set by the terminal, and should
be stored in the ILS if it is.
=item C<$owner = $item-E<gt>owner>
The spec says, "This field might contain the name of the
institution or library that owns the item."
=item C<$str = $item-E<gt>sip_circulation_status>
Returns a two-character string describing the circulation status
of the item, as defined in the specification:
01 Other
02 On order
03 Available
04 Charged
05 Charged; not to be recalled until earliest recall date
06 In process
07 Recalled
08 Waiting on hold shelf
09 Waiting to be re-shelved
10 In transit between library locations
11 Claimed returned
12 Lost
13 Missing
=item C<$bool = $item-E<gt>available>
Is the item available? That is, not checked out, and not on the
hold shelf?
=item C<@hold_queue = $item-E<gt>hold_queue>
Returns a list of the C<$patron_id>s of the patrons that have
outstanding holds on the item.
=item C<$pos = $item-E<gt>hold_queue_position($patron_id)>
Returns the location of C<$patron_id> in the hold queue for the
item, with '1' indicating the next person to receive the item. A
return status of '0' indicates that C<$patron_id> does not have a
hold on the item.
=item C<$date = $item-E<gt>recall_date>
=item C<$date = $item-E<gt>hold_pickup_date>
These functions all return the corresponding date as a standard
SIP-format timestamp:
YYYYMMDDZZZZHHMMSS
Where the C<'Z'> characters indicate spaces.
=item C<$date = $item-E<gt>due_date>
Returns the date the item is due. The format for this timestamp
is not defined by the specification, but it should be something
simple for a human reader to understand.
=item C<$fee = $item-E<gt>fee>
The amount of the fee associated with borrowing this item.
=item C<$currency = $item-E<gt>fee_currency>
The currency in which the fee type above is denominated. This
field is the ISO standard 4217 three-character currency code. It
is highly unlikely that many systems will denominate fees in more
than one currency, however.
=item C<$type = $item-E<gt>sip_fee_type>
The type of fee being charged, as defined by the SIP protocol
specification:
01 Other/unknown
02 Administrative
03 Damage
04 Overdue
05 Processing
06 Rental
07 Replacement
08 Computer access charge
09 Hold fee
=item C<$mark = $item-E<gt>sip_security_marker>
The type of security system with which the item is tagged:
00 Other
01 None
02 3M Tattle-tape
03 3M Whisper tape
=item C<$msg = $item-E<gt>screen_msg>
=item C<$msg = $item-E<gt>print_line>
The usual suspects.
=back

393
C4/SIP/ILS/Patron.pm Normal file
View File

@ -0,0 +1,393 @@
#
# ILS::Patron.pm
#
# A Class for hiding the ILS's concept of the patron from the OpenSIP
# system
#
package ILS::Patron;
use strict;
use warnings;
use Exporter;
use Sys::Syslog qw(syslog);
use Data::Dumper;
our (@ISA, @EXPORT_OK);
@ISA = qw(Exporter);
@EXPORT_OK = qw(invalid_patron);
our %patron_db = (
djfiander => {
name => "David J. Fiander",
id => 'djfiander',
password => '6789',
ptype => 'A', # 'A'dult. Whatever.
birthdate => '19640925',
address => '2 Meadowvale Dr. St Thomas, ON',
home_phone => '(519) 555 1234',
email_addr => 'djfiander@hotmail.com',
charge_ok => 1,
renew_ok => 1,
recall_ok => 0,
hold_ok => 1,
card_lost => 0,
claims_returned => 0,
fines => 100,
fees => 0,
recall_overdue => 0,
items_billed => 0,
screen_msg => '',
print_line => '',
items => [],
hold_items => [],
overdue_items => [],
fine_items => ['Computer Time'],
recall_items => [],
unavail_holds => [],
inet => 1,
},
miker => {
name => "Mike Rylander",
id => 'miker',
password => '6789',
ptype => 'A', # 'A'dult. Whatever.
birthdate => '19640925',
address => 'Somewhere in Atlanta',
home_phone => '(404) 555 1235',
email_addr => 'mrylander@gmail.com',
charge_ok => 1,
renew_ok => 1,
recall_ok => 0,
hold_ok => 1,
card_lost => 0,
claims_returned => 0,
fines => 0,
fees => 0,
recall_overdue => 0,
items_billed => 0,
screen_msg => '',
print_line => '',
items => [],
hold_items => [],
overdue_items => [],
fine_items => [],
recall_items => [],
unavail_holds => [],
inet => 0,
},
);
sub new {
my ($class, $patron_id) = @_;
my $type = ref($class) || $class;
my $self;
if (!exists($patron_db{$patron_id})) {
syslog("LOG_DEBUG", "new ILS::Patron(%s): no such patron", $patron_id);
return undef;
}
$self = $patron_db{$patron_id};
syslog("LOG_DEBUG", "new ILS::Patron(%s): found patron '%s'", $patron_id,
$self->{id});
bless $self, $type;
return $self;
}
sub id {
my $self = shift;
return $self->{id};
}
sub name {
my $self = shift;
return $self->{name};
}
sub address {
my $self = shift;
return $self->{address};
}
sub email_addr {
my $self = shift;
return $self->{email_addr};
}
sub home_phone {
my $self = shift;
return $self->{home_phone};
}
sub sip_birthdate {
my $self = shift;
return $self->{birthdate};
}
sub ptype {
my $self = shift;
return $self->{ptype};
}
sub language {
my $self = shift;
return $self->{language} || '000'; # Unspecified
}
sub charge_ok {
my $self = shift;
return $self->{charge_ok};
}
sub renew_ok {
my $self = shift;
return $self->{renew_ok};
}
sub recall_ok {
my $self = shift;
return $self->{recall_ok};
}
sub hold_ok {
my $self = shift;
return $self->{hold_ok};
}
sub card_lost {
my $self = shift;
return $self->{card_lost};
}
sub recall_overdue {
my $self = shift;
return $self->{recall_overdue};
}
sub check_password {
my ($self, $pwd) = @_;
# If the patron doesn't have a password,
# then we don't need to check
return (!$self->{password} || ($pwd && ($self->{password} eq $pwd)));
}
sub currency {
my $self = shift;
return $self->{currency};
}
sub fee_amount {
my $self = shift;
return $self->{fee_amount} || undef;
}
sub screen_msg {
my $self = shift;
return $self->{screen_msg};
}
sub print_line {
my $self = shift;
return $self->{print_line};
}
sub too_many_charged {
my $self = shift;
return $self->{too_many_charged};
}
sub too_many_overdue {
my $self = shift;
return $self->{too_many_overdue};
}
sub too_many_renewal {
my $self = shift;
return $self->{too_many_renewal};
}
sub too_many_claim_return {
my $self = shift;
return $self->{too_many_claim_return};
}
sub too_many_lost {
my $self = shift;
return $self->{too_many_lost};
}
sub excessive_fines {
my $self = shift;
return $self->{excessive_fines};
}
sub excessive_fees {
my $self = shift;
return $self->{excessive_fees};
}
sub too_many_billed {
my $self = shift;
return $self->{too_many_billed};
}
#
# List of outstanding holds placed
#
sub hold_items {
my ($self, $start, $end) = @_;
$start = 1 if !defined($start);
$end = scalar @{$self->{hold_items}} if !defined($end);
return [@{$self->{hold_items}}[$start-1 .. $end-1]];
}
#
# remove the hold on item item_id from my hold queue.
# return true if I was holding the item, false otherwise.
#
sub drop_hold {
my ($self, $item_id) = @_;
my $i;
for ($i = 0; $i < scalar @{$self->{hold_items}}; $i += 1) {
if ($self->{hold_items}[$i]->{item_id} eq $item_id) {
splice @{$self->{hold_items}}, $i, 1;
return 1;
}
}
return 0;
}
sub overdue_items {
my ($self, $start, $end) = @_;
$start = 1 if !defined($start);
$end = scalar @{$self->{overdue_items}} if !defined($end);
return [@{$self->{overdue_items}}[$start-1 .. $end-1]];
}
sub charged_items {
my ($self, $start, $end) = shift;
$start = 1 if !defined($start);
$end = scalar @{$self->{items}} if !defined($end);
syslog("LOG_DEBUG", "charged_items: start = %d, end = %d", $start, $end);
syslog("LOG_DEBUG", "charged_items: items = (%s)",
join(', ', @{$self->{items}}));
return [@{$self->{items}}[$start-1 .. $end-1]];
}
sub fine_items {
my ($self, $start, $end) = @_;
$start = 1 if !defined($start);
$end = scalar @{$self->{fine_items}} if !defined($end);
return [@{$self->{fine_items}}[$start-1 .. $end-1]];
}
sub recall_items {
my ($self, $start, $end) = @_;
$start = 1 if !defined($start);
$end = scalar @{$self->{recall_items}} if !defined($end);
return [@{$self->{recall_items}}[$start-1 .. $end-1]];
}
sub unavail_holds {
my ($self, $start, $end) = @_;
$start = 1 if !defined($start);
$end = scalar @{$self->{unavail_holds}} if !defined($end);
return [@{$self->{unavail_holds}}[$start-1 .. $end-1]];
}
sub block {
my ($self, $card_retained, $blocked_card_msg) = @_;
foreach my $field ('charge_ok', 'renew_ok', 'recall_ok', 'hold_ok') {
$self->{$field} = 0;
}
$self->{screen_msg} = $blocked_card_msg || "Card Blocked. Please contact library staff";
return $self;
}
sub enable {
my $self = shift;
foreach my $field ('charge_ok', 'renew_ok', 'recall_ok', 'hold_ok') {
$self->{$field} = 1;
}
syslog("LOG_DEBUG", "Patron(%s)->enable: charge: %s, renew:%s, recall:%s, hold:%s",
$self->{id}, $self->{charge_ok}, $self->{renew_ok},
$self->{recall_ok}, $self->{hold_ok});
$self->{screen_msg} = "All privileges restored.";
return $self;
}
sub inet_privileges {
my $self = shift;
return $self->{inet} ? 'Y' : 'N';
}
#
# Messages
#
sub invalid_patron {
return "Please contact library staff";
}
sub charge_denied {
return "Please contact library staff";
}
1;

210
C4/SIP/ILS/Patron.pod Normal file
View File

@ -0,0 +1,210 @@
=head1 NAME
ILS::Patron - Portable Patron status object class for SIP
=head1 DESCRIPTION
A C<ILS::Patron> object holds information about a patron that's
used by self service terminals to authenticate and authorize a patron,
and to display information about the patron's borrowing activity.
=head1 SYNOPSIS
use ILS;
use ILS::Patron;
# Look up patron based on patron_id
my $patron = new ILS::Patron $patron_id
# Basic object access methods
$patron_id = $patron->id;
$str = $patron->name;
$str = $patron->address;
$str = $patron->email_addr;
$str = $patron->home_phone;
$str = $patron->sip_birthdate;
$str = $patron->ptype;
$str = $patron->language;
$str = $patron->password;
$str = $patron->check_password($password);
$str = $patron->currency;
$str = $patron->screen_msg;
$str = $patron->print_line;
# Check patron permissions
$bool = $patron->charge_ok;
$bool = $patron->renew_ok;
$bool = $patron->recall_ok;
$bool = $patron->hold_ok;
$bool = $patron->card_lost;
$bool = $patron->too_many_charged;
$bool = $patron->too_many_overdue;
$bool = $patron->too_many_renewal;
$bool = $patron->too_many_claim_return;
$bool = $patron->too_many_lost;
$bool = $patron->excessive_fines;
$bool = $patron->excessive_fees;
$bool = $patron->too_many_billed;
# Patron borrowing activity
$num = $patron->recall_overdue;
$num = $patron->fee_amount;
$bool = $patron->drop_hold($item_id);
@holds = $patron->hold_items($start, $end);
@items = $patron->overdue_items($start, $end);
@items = $patron->charged_items($start, $end);
@items = $patron->fine_items($start, $end);
@items = $patron->recall_items($start, $end);
@items = $patron->unavail_holds($start, $end);
# Changing a patron's status
$patron->block($card_retained, $blocked_msg);
$patron->enable;
=head1 INITIALIZATION
A patron object is created by calling
$patron = new ILS::Patron $patron_id;
where C<$patron_id> is the patron's barcode as received from the
self service terminal. If the patron barcode is not registered,
then C<new> should return C<undef>.
=head1 BASIC OBJECT ACCESS METHODS
The following functions return the corresponding information
about the given patron, or C<undef> if the information is
unavailable.
$patron_id = $patron-E<gt>id;
$str = $patron-E<gt>name;
$str = $patron-E<gt>address;
$str = $patron-E<gt>email_addr;
$str = $patron-E<gt>home_phone;
$str = $patron-E<gt>screen_msg;
$str = $patron-E<gt>print_line;
If there are outstanding display messages associated with the
patron, then these return the screen message and print line,
respectively, as with the C<ILS> methods.
There are a few other object access methods that need a bit more
explication however.
=head2 C<$str = $patron-E<gt>sip_birthdate;>
Returns the patron's birthday formated according to the SIP
specification:
YYYYMMDD HHMMSS
=head2 C<$str = $patron-E<gt>ptype;>
Returns the "patron type" of the patron. This is not used by the
SIP server code, but is passed through to the self service
terminal (using the non-standard protocol field "PC"). Some self
service terminals use the patron type in determining what level
of service to provide (for example, Envisionware computer
management software can be configured to filter internet access
based on patron type).
=head2 C<$str = $patron-E<gt>language;>
A three-digit string encoding the patron's prefered language.
The full list is defined in the SIP specification, but some of
the important values are:
000 Unknown (default)
001 English
002 French
008 Spanish
011 Canadian French
016 Arabic
019 Chinese
021 North American Spanish
=head2 C<$bool = $patron-E<gt>check_password($password);>
Returns C<true> if C<$patron>'s password is C<$password>.
=head2 C<$str = $patron-E<gt>currency;>
Returns the three character ISO 4217 currency code for the
patron's preferred currency.
=head1 CHECKING PATRON PERMISSIONS
Most of the methods associated with Patrons are related to
checking if they're authorized to perform various actions:
$bool = $patron-E<gt>charge_ok;
$bool = $patron-E<gt>renew_ok;
$bool = $patron-E<gt>recall_ok;
$bool = $patron-E<gt>hold_ok;
$bool = $patron-E<gt>card_lost;
$bool = $patron-E<gt>recall_overdue;
$bool = $patron-E<gt>too_many_charged;
$bool = $patron-E<gt>too_many_overdue;
$bool = $patron-E<gt>too_many_renewal;
$bool = $patron-E<gt>too_many_claim_return;
$bool = $patron-E<gt>too_many_lost;
$bool = $patron-E<gt>excessive_fines;
$bool = $patron-E<gt>excessive_fees;
$bool = $patron-E<gt>too_many_billed;
=head1 LISTS OF ITEMS ASSOCIATED WITH THE USER
The C<$patron> object provides a set of methods to find out
information about various sets that are associated with the
user. All these methods take two optional parameters: C<$start>
and C<$end>, which define a subset of the list of items to be
returned (C<1> is the first item in the list). The following
methods all return a reference to a list of C<$item_id>s:
$items = $patron-E<gt>hold_items($start, $end);
$items = $patron-E<gt>overdue_items($start, $end);
$items = $patron-E<gt>charged_items($start, $end);
$items = $patron-E<gt>recall_items($start, $end);
$items = $patron-E<gt>unavail_holds($start, $end);
It is also possible to retrieve an itemized list of the fines
outstanding. This method returns a reference to an itemized list
of fines:
$fines = $patron-E<gt>fine_items($start, $end);
=head1 PATRON BORROWING ACTIVITY
=head2 C<$num = $patron-E<gt>fee_amount;>
The total amount of fees and fines owed by the patron.
=head2 C<$bool = $patron-E<gt>drop_hold($item_id);>
Drops the hold that C<$patron> has placed on the item
C<$item_id>. Returns C<false> if the patron did not have a hold
on the item, C<true> otherwise.
=head1 CHANGING A PATRON'S STATUS
=head2 C<$status = $ils-E<gt>block($card_retained, $blocked_card_msg);>
Block the account of the patron identified by C<$patron_id>. If
the self check unit captured the patron's card, then
C<$card_retained> will be C<true>. A message indicating why the
card was retained will be provided by the parameter
C<$blocked_card_msg>.
This function returns an C<ILS::Patron> object that has been
updated to indicate that the patron's privileges have been
blocked, or C<undef> if the patron ID is not valid.
=head2 C<$patron-E<gt>enable;>
Reenable the patron after she's been blocked. This is a test
function and will not normally be called by self-service
terminals in production.

59
C4/SIP/ILS/Transaction.pm Normal file
View File

@ -0,0 +1,59 @@
#
# Transaction: Superclass of all the transactional status objects
#
package ILS::Transaction;
use Carp;
use strict;
use warnings;
my %fields = (
ok => 0,
patron => undef,
item => undef,
desensitize => 0,
alert => '',
transation_id => undef,
sip_fee_type => '01', # Other/Unknown
fee_amount => undef,
sip_currency => 'CAD',
screen_msg => '',
print_line => '',
);
our $AUTOLOAD;
sub new {
my $class = shift;
my $self = {
_permitted => \%fields,
%fields,
};
return bless $self, $class;
}
sub DESTROY {
# be cool
}
sub AUTOLOAD {
my $self = shift;
my $class = ref($self) or croak "$self is not an object";
my $name = $AUTOLOAD;
$name =~ s/.*://;
unless (exists $self->{_permitted}->{$name}) {
croak "Can't access '$name' field of class '$class'";
}
if (@_) {
return $self->{$name} = shift;
} else {
return $self->{$name};
}
}
1;

View File

@ -0,0 +1,42 @@
#
# An object to handle checkin status
#
package ILS::Transaction::Checkin;
use warnings;
use strict;
use POSIX qw(strftime);
use ILS;
use ILS::Transaction;
our @ISA = qw(ILS::Transaction);
my %fields = (
magnetic => 0,
sort_bin => undef,
);
sub new {
my $class = shift;;
my $self = $class->SUPER::new();
my $element;
foreach $element (keys %fields) {
$self->{_permitted}->{$element} = $fields{$element};
}
@{$self}{keys %fields} = values %fields;
return bless $self, $class;
}
sub resensitize {
my $self = shift;
return !$self->{item}->magnetic;
}
1;

View File

@ -0,0 +1,39 @@
#
# An object to handle checkout status
#
package ILS::Transaction::Checkout;
use warnings;
use strict;
use POSIX qw(strftime);
use ILS;
use ILS::Transaction;
our @ISA = qw(ILS::Transaction);
# Most fields are handled by the Transaction superclass
my %fields = (
security_inhibit => 0,
due => undef,
renew_ok => 0,
);
sub new {
my $class = shift;;
my $self = $class->SUPER::new();
my $element;
foreach $element (keys %fields) {
$self->{_permitted}->{$element} = $fields{$element};
}
@{$self}{keys %fields} = values %fields;
$self->{'due'} = time() + (60*60*24*14); # two weeks hence
return bless $self, $class;
}
1;

View File

@ -0,0 +1,12 @@
package ILS::Transaction::FeePaid;
use Exporter;
use warnings;
use strict;
use ILS;
use ILS::Transaction;
our @ISA = qw(Exporter ILS::Transaction);
1;

View File

@ -0,0 +1,39 @@
#
# status of a Hold transaction
package ILS::Transaction::Hold;
use warnings;
use strict;
use ILS;
use ILS::Transaction;
our @ISA = qw(ILS::Transaction);
my %fields = (
expiration_date => 0,
pickup_location => undef,
);
sub new {
my $class = shift;;
my $self = $class->SUPER::new();
my $element;
foreach $element (keys %fields) {
$self->{_permitted}->{$element} = $fields{$element};
}
@{$self}{keys %fields} = values %fields;
return bless $self, $class;
}
sub queue_position {
my $self = shift;
return $self->item->hold_queue_position($self->patron->id);
}
1;

View File

@ -0,0 +1,33 @@
#
# Status of a Renew Transaction
#
package ILS::Transaction::Renew;
use warnings;
use strict;
use ILS;
use ILS::Transaction;
our @ISA = qw(ILS::Transaction);
my %fields = (
renewal_ok => 0,
);
sub new {
my $class = shift;;
my $self = $class->SUPER::new();
my $element;
foreach $element (keys %fields) {
$self->{_permitted}->{$element} = $fields{$element};
}
@{$self}{keys %fields} = values %fields;
return bless $self, $class;
}
1;

View File

@ -0,0 +1,29 @@
#
# RenewAll: class to manage status of "Renew All" transaction
package ILS::Transaction::RenewAll;
use strict;
use warnings;
our @ISA = qw(ILS::Transaction);
my %fields = (
renewed => [],
unrenewed => [],
);
sub new {
my $class = shift;;
my $self = $class->SUPER::new();
my $element;
foreach $element (keys %fields) {
$self->{_permitted}->{$element} = $fields{$element};
}
@{$self}{keys %fields} = values %fields;
return bless $self, $class;
}
1;

26
C4/SIP/Makefile Normal file
View File

@ -0,0 +1,26 @@
#
# There's not a lot to "make", but this simplifies the usual
# sorts of tasks
#
PODFLAGS = --htmlroot=. --podroot=.
.SUFFIXES: .pod .html
.pod.html:
pod2html $(PODFLAGS) --outfile=$@ --infile=$<
all:
@echo Nothing to make. The command '"make run"' will run the server.
# just run the server from the command line
run:
perl SIPServer.pm SIPconfig.xml
test:
cd t; $(MAKE) test
tags:
find . -name '*.pm' -print | etags -
html: ILS.html ILS/Item.html ILS/Patron.html

24
C4/SIP/README Executable file
View File

@ -0,0 +1,24 @@
README for Open NSIP 3M-SIP Server
DEPENDENCIES
SIPServer is written entirely in Perl, but it require these CPAN
perl modules to run:
Net::Server - The SIP server is a Net::Server::Prefork server
XML::LibXML
XML::Parser
XML::Simple - for parsing the config file
UNIVERSAL::require - for loading the correct ILS interface module
Clone - for running the test cases
LOGGING
SIPServer uses syslog() for status and debugging messages. All
syslog messages are logged using the syslog facility 'local6'.
If you need to change this, because something else on your system
is already using that facililty, just change the definition of
'LOG_SIP' at the top of the file SIPServer.pm
Make sure to update your syslog configuration to capture facility
'local6' and record it.

273
C4/SIP/SIPServer.pm Normal file
View File

@ -0,0 +1,273 @@
package SIPServer;
use strict;
use warnings;
use Exporter;
use Sys::Syslog qw(syslog);
use Net::Server::PreFork;
use IO::Socket::INET;
use Socket;
use Data::Dumper; # For debugging
require UNIVERSAL::require;
#use Sip qw(readline);
use Sip::Constants qw(:all);
use Sip::Configuration;
use Sip::Checksum qw(checksum verify_cksum);
use Sip::MsgType;
use constant LOG_SIP => "local6"; # Local alias for the logging facility
our @ISA = qw(Net::Server::PreFork);
#
# Main
#
my %transports = (
RAW => \&raw_transport,
telnet => \&telnet_transport,
http => \&http_transport,
);
# Read configuration
my $config = new Sip::Configuration $ARGV[0];
my @parms;
#
# Ports to bind
#
foreach my $svc (keys %{$config->{listeners}}) {
push @parms, "port=" . $svc;
}
#
# Logging
#
push @parms, "log_file=Sys::Syslog", "syslog_ident=acs-server",
"syslog_facility=" . LOG_SIP;
#
# Server Management: set parameters for the Net::Server::PreFork
# module. The module silently ignores parameters that it doesn't
# recognize, and complains about invalid values for parameters
# that it does.
#
if (defined($config->{'server-params'})) {
while (my ($key, $val) = each %{$config->{'server-params'}}) {
push @parms, $key . '=' . $val;
}
}
print Dumper(@parms);
#
# This is the main event.
SIPServer->run(@parms);
#
# Child
#
# process_request is the callback used by Net::Server to handle
# an incoming connection request.
sub process_request {
my $self = shift;
my $service;
my $sockname;
my ($sockaddr, $port, $proto);
my $transport;
$self->{config} = $config;
$sockname = getsockname(STDIN);
($port, $sockaddr) = sockaddr_in($sockname);
$sockaddr = inet_ntoa($sockaddr);
$proto = $self->{server}->{client}->NS_proto();
$self->{service} = $config->find_service($sockaddr, $port, $proto);
if (!defined($self->{service})) {
syslog("LOG_ERR", "process_request: Unknown recognized server connection: %s:%s/%s", $sockaddr, $port, $proto);
die "process_request: Bad server connection";
}
$transport = $transports{$self->{service}->{transport}};
if (!defined($transport)) {
syslog("LOG_WARN", "Unknown transport '%s', dropping", $service->{transport});
return;
} else {
&$transport($self);
}
}
#
# Transports
#
sub raw_transport {
my $self = shift;
my ($uid, $pwd);
my $input;
my $service = $self->{service};
my $strikes = 3;
my $expect;
my $inst;
eval {
local $SIG{ALRM} = sub { die "alarm\n"; };
syslog("LOG_DEBUG", "raw_transport: timeout is %d",
$service->{timeout});
while ($strikes--) {
alarm $service->{timeout};
$input = Sip::read_SIP_packet(*STDIN);
alarm 0;
if (!$input) {
# EOF on the socket
syslog("LOG_INFO", "raw_transport: shutting down: EOF during login");
return;
}
$input =~ s/[\r\n]+$//sm; # Strip off trailing line terminator
last if Sip::MsgType::handle($input, $self, LOGIN);
}
};
if ($@) {
syslog("LOG_ERR", "raw_transport: LOGIN ERROR: '$@'");
die "raw_transport: login error, exiting";
} elsif (!$self->{account}) {
syslog("LOG_ERR", "raw_transport: LOGIN FAILED");
die "raw_transport: Login failed, exiting";
}
syslog("LOG_DEBUG", "raw_transport: uname/inst: '%s/%s'",
$self->{account}->{id},
$self->{account}->{institution});
$self->sip_protocol_loop();
syslog("LOG_INFO", "raw_transport: shutting down");
}
sub telnet_transport {
my $self = shift;
my ($uid, $pwd);
my $strikes = 3;
my $account = undef;
my $input;
my $config = $self->{config};
# Until the terminal has logged in, we don't trust it
# so use a timeout to protect ourselves from hanging.
eval {
local $SIG{ALRM} = sub { die "alarm\n"; };
local $|;
my $timeout = 0;
$| = 1; # Unbuffered output
$timeout = $config->{timeout} if (exists($config->{timeout}));
while ($strikes--) {
print "login: ";
alarm $timeout;
$uid = <STDIN>;
alarm 0;
print "password: ";
alarm $timeout;
$pwd = <STDIN>;
alarm 0;
$uid =~ s/[\r\n]+$//;
$pwd =~ s/[\r\n]+$//;
if (exists($config->{accounts}->{$uid})
&& ($pwd eq $config->{accounts}->{$uid}->password())) {
$account = $config->{accounts}->{$uid};
last;
} else {
syslog("LOG_WARNING", "Invalid login attempt: '%s'", $uid);
print("Invalid login\n");
}
}
}; # End of eval
if ($@) {
syslog("LOG_ERR", "telnet_transport: Login timed out");
die "Telnet Login Timed out";
} elsif (!defined($account)) {
syslog("LOG_ERR", "telnet_transport: Login Failed");
die "Login Failure";
} else {
print "Login OK. Initiating SIP\n";
}
$self->{account} = $account;
$self->sip_protocol_loop();
syslog("LOG_INFO", "telnet_transport: shutting down");
}
sub http_transport {
}
#
# The terminal has logged in, using either the SIP login process
# over a raw socket, or via the pseudo-unix login provided by the
# telnet transport. From that point on, both the raw and the telnet
# processes are the same:
sub sip_protocol_loop {
my $self = shift;
my $expect;
my $service = $self->{service};
my $config = $self->{config};
my $input;
# Now that the terminal has logged in, the first message
# we recieve must be an SC_STATUS message. But it might be
# an SC_REQUEST_RESEND. So, as long as we keep receiving
# SC_REQUEST_RESEND, we keep waiting for an SC_STATUS
# Comprise reports that no other ILS actually enforces this
# constraint, so we'll relax about it too. As long as everybody
# uses the SIP "raw" login process, rather than telnet, this
# will be fine, becaues the LOGIN protocol exchange will force
# us into SIP 2.00 anyway. Machines that want to log in using
# telnet MUST send an SC Status message first, even though we're
# not enforcing it.
#
#$expect = SC_STATUS;
$expect = '';
while ($input = Sip::read_SIP_packet(*STDIN)) {
my $status;
$input =~ s/[\r\n]+$//sm; # Strip off any trailing line ends
$status = Sip::MsgType::handle($input, $self, $expect);
next if $status eq REQUEST_ACS_RESEND;
if (!$status) {
syslog("LOG_ERR", "raw_transport: failed to handle %s",
substr($input, 0, 2));
die "raw_transport: dying";
} elsif ($expect && ($status ne $expect)) {
# We received a non-"RESEND" that wasn't what we were
# expecting.
syslog("LOG_ERR",
"raw_transport: expected %s, received %s, exiting",
$expect, $input);
die "raw_transport: exiting: expected '$expect', received '$status'";
}
# We successfully received and processed what we were expecting
# to receive
$expect = '';
}
}

55
C4/SIP/SIPconfig.xml Normal file
View File

@ -0,0 +1,55 @@
<acsconfig xmlns="http://openncip.org/acs-config/1.0/">
<error-detect enabled="true" />
<!-- Set Net::Server::PreFork runtime parameters -->
<!-- <server-params
min_servers='1'
min_spare_servers='0' /> -->
<listeners>
<service
port="0:8080/tcp"
transport="http"
protocol="NCIP/1.0" />
<service
port="8023/tcp"
transport="telnet"
protocol="SIP/1.00"
timeout="60" />
<service
port="127.0.0.1:6001/tcp"
transport="RAW"
protocol="SIP/2.00"
timeout="60" />
</listeners>
<accounts>
<login id="koha" password="koha" institution="kohalibrary">
</login>
<login id="koha2" password="koha2" institution="kohalibrary" />
<login id="lpl-sc" password="1234" institution="LPL" />
<login id="lpl-sc-beacock" password="xyzzy"
delimiter="|" error-detect="enabled" institution="LPL" />
</accounts>
<!-- Institution tags will hold stuff used to interface to -->
<!-- the rest of the ILS: authentication parameters, etc. I -->
<!-- don't know what yet, so it'll just be blank. But there -->
<!-- needs to be one institution stanza for each institution -->
<!-- named in the accounts above. -->
<institutions>
<institution id="kohalibrary" implementation="ILS" parms="">
<policy checkin="true" renewal="false"
status_update="false" offline="false"
timeout="600"
retries="3" />
</institution>
<institution id="LPL" implementation="ILS">
</institution>
</institutions>
</acsconfig>

188
C4/SIP/Sip.pm Normal file
View File

@ -0,0 +1,188 @@
#
# Sip.pm: General Sip utility functions
#
package Sip;
use strict;
use warnings;
use English;
use Exporter;
use Sys::Syslog qw(syslog);
use POSIX qw(strftime);
use Sip::Constants qw(SIP_DATETIME);
use Sip::Checksum qw(checksum);
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(y_or_n timestamp add_field maybe_add add_count
denied sipbool boolspace write_msg read_SIP_packet
$error_detection $protocol_version $field_delimiter
$last_response);
our %EXPORT_TAGS = (
all => [qw(y_or_n timestamp add_field maybe_add
add_count denied sipbool boolspace write_msg
read_SIP_packet
$error_detection $protocol_version
$field_delimiter $last_response)]);
our $error_detection = 0;
our $protocol_version = 1;
our $field_delimiter = '|'; # Protocol Default
# We need to keep a copy of the last message we sent to the SC,
# in case there's a transmission error and the SC sends us a
# REQUEST_ACS_RESEND. If we receive a REQUEST_ACS_RESEND before
# we've ever sent anything, then we are to respond with a
# REQUEST_SC_RESEND (p.16)
our $last_response = '';
sub timestamp {
my $time = $_[0] || time();
return strftime(SIP_DATETIME, localtime($time));
}
#
# add_field(field_id, value)
# return constructed field value
#
sub add_field {
my ($field_id, $value) = @_;
my ($i, $ent);
if (!defined($value)) {
syslog("LOG_DEBUG", "add_field: Undefined value being added to '%s'",
$field_id);
$value = '';
}
# Replace any occurences of the field delimiter in the
# field value with the HTML character entity
$ent = sprintf("&#%d;", ord($field_delimiter));
while (($i = index($value, $field_delimiter)) != ($[-1)) {
substr($value, $i, 1) = $ent;
}
return $field_id . $value . $field_delimiter;
}
#
# maybe_add(field_id, value):
# If value is defined and non-empty, then return the
# constructed field value, otherwise return the empty string
#
sub maybe_add {
my ($fid, $value) = @_;
return (defined($value) && $value) ? add_field($fid, $value) : '';
}
#
# add_count() produce fixed four-character count field,
# or a string of four spaces if the count is invalid for some
# reason
#
sub add_count {
my ($label, $count) = @_;
# If the field is unsupported, it will be undef, return blanks
# as per the spec.
if (!defined($count)) {
return ' ' x 4;
}
$count = sprintf("%04d", $count);
if (length($count) != 4) {
syslog("LOG_WARNING", "handle_patron_info: %s wrong size: '%s'",
$label, $count);
$count = ' ' x 4;
}
return $count;
}
#
# denied($bool)
# if $bool is false, return true. This is because SIP statuses
# are inverted: we report that something has been denied, not that
# it's permitted. For example, 'renewal priv. denied' of 'Y' means
# that the user's not permitted to renew. I assume that the ILS has
# real positive tests.
#
sub denied {
my $bool = shift;
return boolspace(!$bool);
}
sub sipbool {
my $bool = shift;
return $bool ? 'Y' : 'N';
}
#
# boolspace: ' ' is false, 'Y' is true. (don't ask)
#
sub boolspace {
my $bool = shift;
return $bool ? 'Y' : ' ';
}
# read_SIP_packet($file)
#
# Read a packet from $file, using the correct record separator
#
sub read_SIP_packet {
my $file = shift;
my $record;
local $/ = "\r";
$record = readline($file);
syslog("LOG_INFO", "INPUT MSG: '$record'") if $record;
return $record;
}
#
# write_msg($msg, $file)
#
# Send $msg to the SC. If error detection is active, then
# add the sequence number (if $seqno is non-zero) and checksum
# to the message, and save the whole thing as $last_response
#
# If $file is set, then it's a file handle: write to it, otherwise
# just write to the default destination.
#
sub write_msg {
my ($self, $msg, $file) = @_;
my $cksum;
if ($error_detection) {
if (defined($self->{seqno})) {
$msg .= 'AY' . $self->{seqno};
}
$msg .= 'AZ';
$cksum = checksum($msg);
$msg .= sprintf('%04.4X', $cksum);
}
if ($file) {
print $file "$msg\r";
} else {
print "$msg\r";
syslog("LOG_INFO", "OUTPUT MSG: '$msg'");
}
$last_response = $msg;
}
1;

55
C4/SIP/Sip/Checksum.pm Normal file
View File

@ -0,0 +1,55 @@
package Sip::Checksum;
use Exporter;
use strict;
use warnings;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(checksum verify_cksum);
sub checksum {
my $pkt = shift;
return (-unpack('%16U*', $pkt) & 0xFFFF);
}
sub verify_cksum {
my $pkt = shift;
my $cksum;
my $shortsum;
return 0 if (substr($pkt, -6, 2) ne "AZ"); # No checksum at end
# Convert the checksum back to hex and calculate the sum of the
# pack without the checksum.
$cksum = hex(substr($pkt, -4));
$shortsum = unpack("%16U*", substr($pkt, 0, -4));
# The checksum is valid if the hex sum, plus the checksum of the
# base packet short when truncated to 16 bits.
return (($cksum + $shortsum) & 0xFFFF) == 0;
}
{
no warnings qw(once);
eval join('',<main::DATA>) || die $@ unless caller();
}
__END__
#
# Some simple test data
#
sub test {
my $testpkt = shift;
my $cksum = checksum($testpkt);
my $fullpkt = sprintf("%s%4X", $testpkt, $cksum);
print $fullpkt, "\n";
}
while (<>) {
chomp;
test($_);
}
1;

105
C4/SIP/Sip/Configuration.pm Normal file
View File

@ -0,0 +1,105 @@
#
# parse-config: Parse an XML-format
# ACS configuration file and build the configuration
# structure.
#
package Sip::Configuration;
use strict;
use English;
use warnings;
use XML::Simple qw(:strict);
use Sip::Configuration::Institution;
use Sip::Configuration::Account;
use Sip::Configuration::Service;
my $parser = new XML::Simple( KeyAttr => { login => '+id',
institution => '+id',
service => '+port' },
GroupTags => { listeners => 'service',
accounts => 'login',
institutions => 'institution', },
ForceArray=> [ 'service',
'login',
'institution' ],
ValueAttr => { 'error-detect' => 'enabled',
'min_servers' => 'value',
'max_servers' => 'value'} );
sub new {
my ($class, $config_file) = @_;
my $cfg = $parser->XMLin($config_file);
my %listeners;
foreach my $acct (values %{$cfg->{accounts}}) {
new Sip::Configuration::Account $acct;
}
# The key to the listeners hash is the 'port' component of the
# configuration, which is of the form '[host]:[port]/proto', and
# the 'proto' component could be upper-, lower-, or mixed-cased.
# Regularize it here to lower-case, and then do the same below in
# find_server() when building the keys to search the hash.
foreach my $service (values %{$cfg->{listeners}}) {
new Sip::Configuration::Service $service;
$listeners{lc $service->{port}} = $service;
}
$cfg->{listeners} = \%listeners;
foreach my $inst (values %{$cfg->{institutions}}) {
new Sip::Configuration::Institution $inst;
}
return bless $cfg, $class;
}
sub error_detect {
my $self = shift;
return $self->{'error-detect'};
}
sub accounts {
my $self = shift;
return values %{$self->{accounts}};
}
sub find_service {
my ($self, $sockaddr, $port, $proto) = @_;
my $portstr;
foreach my $addr ('', '*:', "$sockaddr:") {
$portstr = sprintf("%s%s/%s", $addr, $port, lc $proto);
Sys::Syslog::syslog("LOG_DEBUG", "Configuration::find_service: Trying $portstr");
last if (exists(($self->{listeners})->{$portstr}));
}
return $self->{listeners}->{$portstr};
}
#
# Testing
#
{
no warnings qw(once);
eval join('',<main::DATA>) || die $@ unless caller();
}
1;
__END__
my $config = new Sip::Configuration $ARGV[0];
foreach my $acct ($config->accounts) {
print "Found account '", $acct->name, "', part of '"
print $acct->institution, "'\n";
}
1;

View File

@ -0,0 +1,43 @@
#
#
#
#
package Sip::Configuration::Account;
use strict;
use warnings;
use English;
use Exporter;
sub new {
my ($class, $obj) = @_;
my $type = ref($class) || $class;
if (ref($obj) eq "HASH") {
# Just bless the object
return bless $obj, $type;
}
return bless {}, $type;
}
sub id {
my $self = shift;
return $self->{id};
}
sub institution {
my $self = shift;
return $self->{institution};
}
sub password {
my $self = shift;
return $self->{password};
}
1;

View File

@ -0,0 +1,31 @@
#
#
#
#
package Sip::Configuration::Institution;
use strict;
use warnings;
use English;
use Exporter;
sub new {
my ($class, $obj) = @_;
my $type = ref($class) || $class;
if (ref($obj) eq "HASH") {
# Just bless the object
return bless $obj, $type;
}
return bless {}, $type;
}
sub name {
my $self = shift;
return $self->{name};
}
1;

View File

@ -0,0 +1,25 @@
#
#
#
#
package Sip::Configuration::Service;
use strict;
use warnings;
use English;
use Exporter;
sub new {
my ($class, $obj) = @_;
my $type = ref($class) || $class;
if (ref($obj) eq "HASH") {
# Just bless the object
return bless $obj, $type;
}
return bless {}, $type;
}
1;

339
C4/SIP/Sip/Constants.pm Normal file
View File

@ -0,0 +1,339 @@
#
# Sip::Constants.pm
#
# Various protocol constant values for 3M's Standard Interchange
# Protocol for communication between a library's Automated
# Checkout System (ACS) and stand-alone Self-Check (SC) units
package Sip::Constants;
use strict;
use warnings;
use Exporter;
our (@ISA, @EXPORT_OK, %EXPORT_TAGS);
@ISA = qw(Exporter);
@EXPORT_OK = qw(PATRON_STATUS_REQ CHECKOUT CHECKIN BLOCK_PATRON
SC_STATUS REQUEST_ACS_RESEND LOGIN PATRON_INFO
END_PATRON_SESSION FEE_PAID ITEM_INFORMATION
ITEM_STATUS_UPDATE PATRON_ENABLE HOLD RENEW
RENEW_ALL PATRON_STATUS_RESP CHECKOUT_RESP
CHECKIN_RESP ACS_STATUS REQUEST_SC_RESEND
LOGIN_RESP PATRON_INFO_RESP END_SESSION_RESP
FEE_PAID_RESP ITEM_INFO_RESP
ITEM_STATUS_UPDATE_RESP PATRON_ENABLE_RESP
HOLD_RESP RENEW_RESP RENEW_ALL_RESP
REQUEST_ACS_RESEND_CKSUM REQUEST_SC_RESEND_CKSUM
FID_PATRON_ID FID_ITEM_ID FID_TERMINAL_PWD
FID_PATRON_PWD FID_PERSONAL_NAME FID_SCREEN_MSG
FID_PRINT_LINE FID_DUE_DATE FID_TITLE_ID
FID_BLOCKED_CARD_MSG FID_LIBRARY_NAME
FID_TERMINAL_LOCN FID_INST_ID FID_CURRENT_LOCN
FID_PERM_LOCN FID_HOLD_ITEMS FID_OVERDUE_ITEMS
FID_CHARGED_ITEMS FID_FINE_ITEMS FID_SEQNO
FID_CKSUM FID_HOME_ADDR FID_EMAIL FID_HOME_PHONE
FID_OWNER FID_CURRENCY FID_CANCEL
FID_TRANSACTION_ID FID_VALID_PATRON
FID_RENEWED_ITEMS FID_UNRENEWED_ITEMS FID_FEE_ACK
FID_START_ITEM FID_END_ITEM FID_QUEUE_POS
FID_PICKUP_LOCN FID_FEE_TYPE FID_RECALL_ITEMS
FID_FEE_AMT FID_EXPIRATION FID_SUPPORTED_MSGS
FID_HOLD_TYPE FID_HOLD_ITEMS_LMT
FID_OVERDUE_ITEMS_LMT FID_CHARGED_ITEMS_LMT
FID_FEE_LMT FID_UNAVAILABLE_HOLD_ITEMS
FID_HOLD_QUEUE_LEN FID_FEE_ID FID_ITEM_PROPS
FID_SECURITY_INHIBIT FID_RECALL_DATE
FID_MEDIA_TYPE FID_SORT_BIN FID_HOLD_PICKUP_DATE
FID_LOGIN_UID FID_LOGIN_PWD FID_LOCATION_CODE
FID_VALID_PATRON_PWD
FID_PATRON_BIRTHDATE FID_PATRON_CLASS FID_INET_PROFILE
SC_STATUS_OK SC_STATUS_PAPER SC_STATUS_SHUTDOWN
SIP_DATETIME);
%EXPORT_TAGS = (
SC_msgs => [qw(PATRON_STATUS_REQ CHECKOUT CHECKIN
BLOCK_PATRON SC_STATUS
REQUEST_ACS_RESEND LOGIN
PATRON_INFO
END_PATRON_SESSION FEE_PAID
ITEM_INFORMATION
ITEM_STATUS_UPDATE
PATRON_ENABLE HOLD RENEW
RENEW_ALL)],
ACS_msgs => [qw(PATRON_STATUS_RESP CHECKOUT_RESP
CHECKIN_RESP ACS_STATUS
REQUEST_SC_RESEND LOGIN_RESP
PATRON_INFO_RESP
END_SESSION_RESP
FEE_PAID_RESP ITEM_INFO_RESP
ITEM_STATUS_UPDATE_RESP
PATRON_ENABLE_RESP HOLD_RESP
RENEW_RESP RENEW_ALL_RESP)],
constant_msgs => [qw(REQUEST_ACS_RESEND_CKSUM
REQUEST_SC_RESEND_CKSUM)],
field_ids => [qw( FID_PATRON_ID FID_ITEM_ID
FID_TERMINAL_PWD
FID_PATRON_PWD
FID_PERSONAL_NAME
FID_SCREEN_MSG
FID_PRINT_LINE FID_DUE_DATE
FID_TITLE_ID
FID_BLOCKED_CARD_MSG
FID_LIBRARY_NAME
FID_TERMINAL_LOCN
FID_INST_ID
FID_CURRENT_LOCN
FID_PERM_LOCN
FID_HOLD_ITEMS
FID_OVERDUE_ITEMS
FID_CHARGED_ITEMS
FID_FINE_ITEMS FID_SEQNO
FID_CKSUM FID_HOME_ADDR
FID_EMAIL FID_HOME_PHONE
FID_OWNER FID_CURRENCY
FID_CANCEL
FID_TRANSACTION_ID
FID_VALID_PATRON
FID_RENEWED_ITEMS
FID_UNRENEWED_ITEMS
FID_FEE_ACK FID_START_ITEM
FID_END_ITEM FID_QUEUE_POS
FID_PICKUP_LOCN
FID_FEE_TYPE
FID_RECALL_ITEMS
FID_FEE_AMT FID_EXPIRATION
FID_SUPPORTED_MSGS
FID_HOLD_TYPE
FID_HOLD_ITEMS_LMT
FID_OVERDUE_ITEMS_LMT
FID_CHARGED_ITEMS_LMT
FID_FEE_LMT
FID_UNAVAILABLE_HOLD_ITEMS
FID_HOLD_QUEUE_LEN
FID_FEE_ID FID_ITEM_PROPS
FID_SECURITY_INHIBIT
FID_RECALL_DATE
FID_MEDIA_TYPE FID_SORT_BIN
FID_HOLD_PICKUP_DATE
FID_LOGIN_UID FID_LOGIN_PWD
FID_LOCATION_CODE
FID_VALID_PATRON_PWD
FID_PATRON_BIRTHDATE
FID_PATRON_CLASS
FID_INET_PROFILE)],
SC_status => [qw(SC_STATUS_OK SC_STATUS_PAPER
SC_STATUS_SHUTDOWN)],
formats => [qw(SIP_DATETIME)],
all => [qw(PATRON_STATUS_REQ CHECKOUT CHECKIN
BLOCK_PATRON SC_STATUS
REQUEST_ACS_RESEND LOGIN PATRON_INFO
END_PATRON_SESSION FEE_PAID
ITEM_INFORMATION ITEM_STATUS_UPDATE
PATRON_ENABLE HOLD RENEW RENEW_ALL
PATRON_STATUS_RESP CHECKOUT_RESP
CHECKIN_RESP ACS_STATUS
REQUEST_SC_RESEND LOGIN_RESP
PATRON_INFO_RESP END_SESSION_RESP
FEE_PAID_RESP ITEM_INFO_RESP
ITEM_STATUS_UPDATE_RESP
PATRON_ENABLE_RESP HOLD_RESP
RENEW_RESP RENEW_ALL_RESP
REQUEST_ACS_RESEND_CKSUM
REQUEST_SC_RESEND_CKSUM FID_PATRON_ID
FID_ITEM_ID FID_TERMINAL_PWD
FID_PATRON_PWD FID_PERSONAL_NAME
FID_SCREEN_MSG FID_PRINT_LINE
FID_DUE_DATE FID_TITLE_ID
FID_BLOCKED_CARD_MSG FID_LIBRARY_NAME
FID_TERMINAL_LOCN FID_INST_ID
FID_CURRENT_LOCN FID_PERM_LOCN
FID_HOLD_ITEMS FID_OVERDUE_ITEMS
FID_CHARGED_ITEMS FID_FINE_ITEMS
FID_SEQNO FID_CKSUM FID_HOME_ADDR
FID_EMAIL FID_HOME_PHONE FID_OWNER
FID_CURRENCY FID_CANCEL
FID_TRANSACTION_ID FID_VALID_PATRON
FID_RENEWED_ITEMS FID_UNRENEWED_ITEMS
FID_FEE_ACK FID_START_ITEM
FID_END_ITEM FID_QUEUE_POS
FID_PICKUP_LOCN FID_FEE_TYPE
FID_RECALL_ITEMS FID_FEE_AMT
FID_EXPIRATION FID_SUPPORTED_MSGS
FID_HOLD_TYPE FID_HOLD_ITEMS_LMT
FID_OVERDUE_ITEMS_LMT
FID_CHARGED_ITEMS_LMT FID_FEE_LMT
FID_UNAVAILABLE_HOLD_ITEMS
FID_HOLD_QUEUE_LEN FID_FEE_ID
FID_ITEM_PROPS FID_SECURITY_INHIBIT
FID_RECALL_DATE FID_MEDIA_TYPE
FID_SORT_BIN FID_HOLD_PICKUP_DATE
FID_LOGIN_UID FID_LOGIN_PWD
FID_LOCATION_CODE FID_VALID_PATRON_PWD
FID_PATRON_BIRTHDATE FID_PATRON_CLASS
FID_INET_PROFILE
SC_STATUS_OK SC_STATUS_PAPER SC_STATUS_SHUTDOWN
SIP_DATETIME
)]);
#
# Declare message types
#
# Messages from SC to ACS
use constant {
PATRON_STATUS_REQ => '23',
CHECKOUT => '11',
CHECKIN => '09',
BLOCK_PATRON => '01',
SC_STATUS => '99',
REQUEST_ACS_RESEND => '97',
LOGIN => '93',
PATRON_INFO => '63',
END_PATRON_SESSION => '35',
FEE_PAID => '37',
ITEM_INFORMATION => '17',
ITEM_STATUS_UPDATE => '19',
PATRON_ENABLE => '25',
HOLD => '15',
RENEW => '29',
RENEW_ALL => '65',
};
# Message responses from ACS to SC
use constant {
PATRON_STATUS_RESP => '24',
CHECKOUT_RESP => '12',
CHECKIN_RESP => '10',
ACS_STATUS => '98',
REQUEST_SC_RESEND => '96',
LOGIN_RESP => '94',
PATRON_INFO_RESP => '64',
END_SESSION_RESP => '36',
FEE_PAID_RESP => '38',
ITEM_INFO_RESP => '18',
ITEM_STATUS_UPDATE_RESP => '20',
PATRON_ENABLE_RESP => '26',
HOLD_RESP => '16',
RENEW_RESP => '30',
RENEW_ALL_RESP => '66',
};
#
# Some messages are short and invariant, so they're constant's too
#
use constant {
REQUEST_ACS_RESEND_CKSUM => '97AZFEF5',
REQUEST_SC_RESEND_CKSUM => '96AZFEF6',
};
#
# Field Identifiers
#
use constant {
FID_PATRON_ID => 'AA',
FID_ITEM_ID => 'AB',
FID_TERMINAL_PWD => 'AC',
FID_PATRON_PWD => 'AD',
FID_PERSONAL_NAME => 'AE',
FID_SCREEN_MSG => 'AF',
FID_PRINT_LINE => 'AG',
FID_DUE_DATE => 'AH',
# UNUSED AI
FID_TITLE_ID => 'AJ',
# UNUSED AK
FID_BLOCKED_CARD_MSG => 'AL',
FID_LIBRARY_NAME => 'AM',
FID_TERMINAL_LOCN => 'AN',
FID_INST_ID => 'AO',
FID_CURRENT_LOCN => 'AP',
FID_PERM_LOCN => 'AQ',
# UNUSED AR
FID_HOLD_ITEMS => 'AS', # SIP 2.0
FID_OVERDUE_ITEMS => 'AT', # SIP 2.0
FID_CHARGED_ITEMS => 'AU', # SIP 2.0
FID_FINE_ITEMS => 'AV', # SIP 2.0
# UNUSED AW
# UNUSED AX
FID_SEQNO => 'AY',
FID_CKSUM => 'AZ',
# SIP 2.0 Fields
# UNUSED BA
# UNUSED BB
# UNUSED BC
FID_HOME_ADDR => 'BD',
FID_EMAIL => 'BE',
FID_HOME_PHONE => 'BF',
FID_OWNER => 'BG',
FID_CURRENCY => 'BH',
FID_CANCEL => 'BI',
# UNUSED BJ
FID_TRANSACTION_ID => 'BK',
FID_VALID_PATRON => 'BL',
FID_RENEWED_ITEMS => 'BM',
FID_UNRENEWED_ITEMS => 'BN',
FID_FEE_ACK => 'BO',
FID_START_ITEM => 'BP',
FID_END_ITEM => 'BQ',
FID_QUEUE_POS => 'BR',
FID_PICKUP_LOCN => 'BS',
FID_FEE_TYPE => 'BT',
FID_RECALL_ITEMS => 'BU',
FID_FEE_AMT => 'BV',
FID_EXPIRATION => 'BW',
FID_SUPPORTED_MSGS => 'BX',
FID_HOLD_TYPE => 'BY',
FID_HOLD_ITEMS_LMT => 'BZ',
FID_OVERDUE_ITEMS_LMT => 'CA',
FID_CHARGED_ITEMS_LMT => 'CB',
FID_FEE_LMT => 'CC',
FID_UNAVAILABLE_HOLD_ITEMS => 'CD',
# UNUSED CE
FID_HOLD_QUEUE_LEN => 'CF',
FID_FEE_ID => 'CG',
FID_ITEM_PROPS => 'CH',
FID_SECURITY_INHIBIT => 'CI',
FID_RECALL_DATE => 'CJ',
FID_MEDIA_TYPE => 'CK',
FID_SORT_BIN => 'CL',
FID_HOLD_PICKUP_DATE => 'CM',
FID_LOGIN_UID => 'CN',
FID_LOGIN_PWD => 'CO',
FID_LOCATION_CODE => 'CP',
FID_VALID_PATRON_PWD => 'CQ',
# SIP Extensions used by Envisionware Terminals
FID_PATRON_BIRTHDATE => 'PB',
FID_PATRON_CLASS => 'PC',
# SIP Extension for reporting patron internet privileges
FID_INET_PROFILE => 'PI',
};
#
# SC Status Codes
#
use constant {
SC_STATUS_OK => '0',
SC_STATUS_PAPER => '1',
SC_STATUS_SHUTDOWN => '2',
};
#
# Various format strings
#
use constant {
SIP_DATETIME => "%Y%m%d %H%M%S",
};

1577
C4/SIP/Sip/MsgType.pm Normal file

File diff suppressed because it is too large Load Diff

42
C4/SIP/acstest.py Normal file
View File

@ -0,0 +1,42 @@
import operator
import socket
from time import strftime;
def SipSocket(host='localhost', port=5300):
so = socket.socket()
so.connect((host, port))
return so
def login(so, uname='scclient', passwd='clientpwd', locn='The basement',
seqno=0):
port = so.getpeername()[1]
if port == 5300:
resp = send(so, '9300CN%s|CO%s|CP%s|' % (uname, passwd, locn), seqno)
print "Received", repr(resp)
print "Verified: ", verify(resp)
else:
raise "Logging in is only support for the raw transport on port 5300"
def send(so, msg, seqno=0):
if seqno:
msg += 'AY' + str(seqno)[0] + 'AZ'
msg += ('%04X' % calculate_cksum(msg))
msg += '\r'
print 'Sending', repr(msg)
so.send(msg)
resp = so.recv(1000)
return resp, verify(resp)
def calculate_cksum(msg):
return (-reduce(operator.add, map(ord, msg)) & 0xFFFF)
def sipdate():
return(strftime("%Y%m%d %H%M%S"))
def verify(msg):
if msg[-1] == '\r': msg = msg[:-2]
if msg[-6:-4] == 'AZ':
cksum = calculate_cksum(msg[:-4])
return (msg[-4:] == ('%04X' % cksum))
# If there's no checksum, then the message is ok
return True

26
C4/SIP/t/00sc_status.t Normal file
View File

@ -0,0 +1,26 @@
#!/usr/bin/perl
#
# sc_status: test basic connection, login, and response
# to the SC Status message, which has to be sent before
# anything else
use strict;
use warnings;
use SIPtest qw($datepat $username $password $login_test $sc_status_test);
my $invalid_uname = { id => 'Invalid username',
msg => "9300CNinvalid$username|CO$password|CPThe floor|",
pat => qr/^940/,
fields => [], };
my $invalid_pwd = { id => 'Invalid username',
msg => "9300CN$username|COinvalid$password|CPThe floor|",
pat => qr/^940/,
fields => [], };
my @tests = ( $invalid_uname, $invalid_pwd, $login_test, $sc_status_test );
SIPtest::run_sip_tests(@tests);
1;

View File

@ -0,0 +1,80 @@
#!/usr/bin/perl
#
# patron_status: check status of valid patron and invalid patron
use strict;
use warnings;
use Sip::Constants qw(:all);
use SIPtest qw($datepat $instid $currency $user_barcode $user_pin
$user_fullname $user_homeaddr $user_email $user_phone
$user_birthday);
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
{ id => 'valid Patron Status',
msg => "2300120060101 084237AO$SIPtest::instid|AA$user_barcode|AD$user_pin|AC|",
pat => qr/^24 [ Y]{13}\d{3}$datepat/,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PERSONAL_NAME,
pat => qr/^$user_fullname$/o,
required => 1, },
{ field => FID_PATRON_ID,
pat => qr/^$user_barcode/o,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
required => 0, },
{ field => FID_VALID_PATRON_PWD,
pat => qr/^Y$/,
required => 0, },
{ field => FID_CURRENCY,
pat => qr/^$currency$/io,
required => 0, },
{ field => FID_FEE_AMT,
pat => qr/^[0-9.]+$/,
required => 0, },
], },
{ id => 'invalid password Patron Status',
msg => "2300120060101 084237AO$instid|AA$user_barcode|AC|ADbadw|",
pat => qr/^24[ Y]{14}\d{3}$datepat/,
fields => [
{ field => FID_PERSONAL_NAME,
pat => qr/^$user_fullname$/o,
required => 1, },
{ field => FID_PATRON_ID,
pat => qr/^$user_barcode$/o,
required => 1, },
{ field => FID_INST_ID,
pat => qr/^$instid$/o,
required => 1, },
{ field => FID_VALID_PATRON_PWD,
pat => qr/^N$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
required => 1, },
], },
{ id => 'invalid Patron Status',
msg => "2300120060101 084237AO$instid|AAwshakespeare|AC|",
pat => qr/^24Y[ Y]{13}\d{3}$datepat/,
fields => [
{ field => FID_PERSONAL_NAME,
pat => qr/^$/,
required => 1, },
{ field => FID_PATRON_ID,
pat => qr/^wshakespeare$/,
required => 1, },
{ field => FID_INST_ID,
pat => qr/^$instid$/o,
required => 1, },
], },
);
SIPtest::run_sip_tests(@tests);
1;

172
C4/SIP/t/02patron_info.t Normal file
View File

@ -0,0 +1,172 @@
#!/usr/bin/perl
# patron_info: test Patron Information Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat $instid $currency $user_barcode $user_pin
$user_fullname $user_homeaddr $user_email $user_phone
$user_birthday $user_ptype $user_inet);
# This is a template test case for the Patron Information
# message handling. Because of the large number of fields,
# this template forms the basis for all of the different
# situations: valid patron no details, valid patron with each
# individual detail requested, invalid patron, invalid patron
# password, etc.
my $patron_info_test_template = {
id => 'valid Patron Info no details',
msg => "6300020060329 201700 AO$instid|AA$user_barcode|",
pat => qr/^64 [ Y]{13}\d{3}$datepat(\d{4}){6}/,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^$user_barcode$/o,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^$user_fullname$/o,
required => 1, },
$SIPtest::field_specs{(FID_HOLD_ITEMS_LMT)},
$SIPtest::field_specs{(FID_OVERDUE_ITEMS_LMT)},
$SIPtest::field_specs{(FID_CHARGED_ITEMS_LMT)},
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
# Not required by the spec, but by the test
required => 1, },
$SIPtest::field_specs{(FID_CURRENCY)},
{ field => FID_FEE_AMT,
pat => $textpat,
required => 0, },
{ field => FID_FEE_LMT,
pat => $textpat,
required => 0, },
{ field => FID_HOME_ADDR,
pat => qr/^$user_homeaddr$/o,
required => 1, }, # required by this test case
{ field => FID_EMAIL,
pat => qr/^$user_email$/o,
required => 1, },
{ field => FID_HOME_PHONE,
pat => qr/^$user_phone$/o,
required => 1, },
{ field => FID_PATRON_BIRTHDATE,
pat => qr/^$user_birthday$/o,
required => 1, },
{ field => FID_PATRON_CLASS,
pat => qr/^$user_ptype$/o,
required => 1, },
{ field => FID_INET_PROFILE,
pat => qr/^$user_inet$/,
required => 1, },
], };
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
clone($patron_info_test_template),
);
# Create the test cases for the various summary detail fields
sub create_patron_summary_tests {
my $test;
my @patron_info_summary_tests = (
{ field => FID_HOLD_ITEMS,
pat => $textpat,
required => 0, },
{ field => FID_OVERDUE_ITEMS,
pat => $textpat,
required => 0, },
{ field => FID_CHARGED_ITEMS,
pat => $textpat,
required => 0, },
# The test user has no items of these types, so the tests seem to fail
# { field => FID_FINE_ITEMS,
# pat => $textpat,
# required => 1, },
# { field => FID_RECALL_ITEMS,
# pat => $textpat,
# required => 0, },
# { field => FID_UNAVAILABLE_HOLD_ITEMS,
# pat => $textpat,
# required => 0, },
);
foreach my $i (0 .. scalar @patron_info_summary_tests-1) {
# The tests for each of the summary fields are exactly the
# same as the basic one, except for the fact that there's
# an extra field to test
# Copy the hash, adjust it, add it to the end of the list
$test = clone($patron_info_test_template);
substr($test->{msg}, 23+$i, 1) = 'Y';
$test->{id} = "valid Patron Info details: "
. $patron_info_summary_tests[$i]->{field};
push @{$test->{fields}}, $patron_info_summary_tests[$i];
push @tests, $test;
}
}
sub create_invalid_patron_tests {
my $test;
$test = clone($patron_info_test_template);
$test->{id} = "invalid Patron Info id";
$test->{msg} =~ s/AA$user_barcode\|/AAberick|/o;
$test->{pat} = qr/^64Y[ Y]{13}\d{3}$datepat(\d{4}){6}/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^berick$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^N$/,
required => 1, },
];
push @tests, $test;
# Valid patron, invalid patron password
$test = clone($patron_info_test_template);
$test->{id} = "valid Patron Info, invalid password";
$test->{msg} .= (FID_PATRON_PWD) . 'badpwd|';
$test->{pat} = qr/^64[ Y]{14}\d{3}$datepat(\d{4}){6}/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^$user_barcode$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^$user_fullname$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
required => 1, },
{ field => FID_VALID_PATRON_PWD,
pat => qr/^N$/,
required => 1, },
];
push @tests, $test;
}
create_patron_summary_tests;
create_invalid_patron_tests;
SIPtest::run_sip_tests(@tests);
1;

209
C4/SIP/t/03checkout.t Normal file
View File

@ -0,0 +1,209 @@
#!/usr/bin/perl
# checkout: test Checkout Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat $instid $currency $user_barcode
$item_barcode $item_title
$item_diacritic_barcode $item_diacritic_title
$item_diacritic_owner);
my $patron_enable_template = {
id => 'Renew All: prep: enable patron permissions',
msg => "2520060102 084238AO$instid|AA$user_barcode|",
pat => qr/^26 {4}[ Y]{10}000$datepat/o,
fields => [],
};
my $patron_disable_template = {
id => 'Checkout: block patron (prep to test checkout denied)',
msg => "01N20060102 084238AO$instid|ALHe's a jerk|AA$user_barcode|",
# response to block patron is a patron status message
pat => qr/^24Y{4}[ Y]{10}000$datepat/o,
fields => [], };
my $checkin_template = {
id => 'Checkout: cleanup: check in item',
msg => "09N20050102 08423620060113 084235APUnder the bed|AO$instid|AB$item_barcode|ACterminal password|",
pat => qr/^101YNN$datepat/o,
fields => [],
};
my $checkout_test_template = {
id => 'Checkout: valid item, valid patron',
msg => "11YN20060329 203000 AO$instid|AA$user_barcode|AB$item_barcode|AC|",
pat => qr/^121NNY$datepat/,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^$user_barcode$/o,
required => 1, },
{ field => FID_ITEM_ID,
pat => qr/^$item_barcode$/o,
required => 1, },
{ field => FID_TITLE_ID,
pat => qr/^$item_title\s*$/o,
required => 1, },
{ field => FID_DUE_DATE,
pat => $textpat,
required => 1, },
{ field => FID_FEE_TYPE,
pat => qr/^\d{2}$/,
required => 0, },
{ field => FID_SECURITY_INHIBIT,
pat => qr/^[YN]$/,
required => 0, },
{ field => FID_CURRENCY,
pat => qr/^$currency$/o,
required => 0, },
{ field => FID_FEE_AMT,
pat => qr/^[.0-9]+$/,
required => 0, },
{ field => FID_MEDIA_TYPE,
pat => qr/^\d{3}$/,
required => 0, },
{ field => FID_ITEM_PROPS,
pat => $textpat,
required => 0, },
{ field => FID_TRANSACTION_ID,
pat => $textpat,
required => 0, },
], };
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
clone($checkout_test_template),
# Don't check the item in, because we're about to test renew
);
my $test;
## Renewal OK
## Test this by checking out exactly the same book a second time.
## The only difference should be that the "Renewal OK" flag should now
## be 'Y'.
#$test = clone($checkout_test_template);
#$test->{id} = 'Checkout: patron renewal';
#$test->{pat} = qr/^121YNY$datepat/;
#
#push @tests, $test;
# NOW check it in
push @tests, $checkin_template;
# Valid Patron, item with diacritical in the title
$test = clone($checkout_test_template);
$test->{id} = 'Checkout: valid patron, diacritical character in title';
$test->{msg} =~ s/AB$item_barcode/AB$item_diacritic_barcode/;
foreach my $i (0 .. (scalar @{$test->{fields}})-1) {
my $field = $test->{fields}[$i];
if ($field->{field} eq FID_ITEM_ID) {
$field->{pat} = qr/^$item_diacritic_barcode$/;
} elsif ($field->{field} eq FID_TITLE_ID) {
$field->{pat} = qr/^$item_diacritic_title\s*$/;
} elsif ($field->{field} eq FID_OWNER) {
$field->{pat} = qr/^$item_diacritic_owner$/;
}
}
push @tests, $test;
$test = clone($checkin_template);
$test->{msg} =~ s/AB$item_barcode/AB$item_diacritic_barcode/;
push @tests, $test;
# Valid Patron, Invalid Item_id
$test = clone($checkout_test_template);
$test->{id} = 'Checkout: valid patron, invalid item';
$test->{msg} =~ s/AB$item_barcode/ABno-barcode/o;
$test->{pat} = qr/^120NUN$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^$user_barcode$/o,
required => 1, },
{ field => FID_ITEM_ID,
pat => qr/^no-barcode$/,
required => 1, },
{ field => FID_TITLE_ID,
pat => qr/^$/,
required => 1, },
{ field => FID_DUE_DATE,
pat => qr/^$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
required => 1, },
];
push @tests, $test;
# Invalid patron, valid item
$test = clone($checkout_test_template);
$test->{id} = 'Checkout: invalid patron, valid item';
$test->{msg} =~ s/AA$user_barcode/AAberick/;
$test->{pat} = qr/^120NUN$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^berick$/,
required => 1, },
{ field => FID_ITEM_ID,
pat => qr/^$item_barcode$/o,
required => 1, },
{ field => FID_TITLE_ID,
pat => qr/^$item_title\s*$/o,
required => 1, },
{ field => FID_DUE_DATE,
pat => qr/^$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^N$/,
required => 1, },
];
push @tests, $test;
# Needed: tests for blocked patrons, patrons with excessive
# fines/fees, magnetic media, charging fees to borrow items.
## Blocked patron
#$test = clone($checkout_test_template);
#$test->{id} = 'Checkout: Blocked patron';
#$test->{pat} = qr/^120NUN$datepat/;
#delete $test->{fields};
#$test->{fields} = [
# $SIPtest::field_specs{(FID_INST_ID)},
# $SIPtest::field_specs{(FID_SCREEN_MSG)},
# $SIPtest::field_specs{(FID_PRINT_LINE)},
# { field => FID_PATRON_ID,
# pat => qr/^$user_barcode$/,
# required => 1, },
# { field => FID_VALID_PATRON,
# pat => qr/^Y$/,
# required => 1, },
# ];
#
#push @tests, $patron_disable_template, $test, $patron_enable_template;
#
SIPtest::run_sip_tests(@tests);
1;

100
C4/SIP/t/04patron_status.t Normal file
View File

@ -0,0 +1,100 @@
#!/usr/bin/perl
# patron_status: test Patron Status Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat);
my $patron_status_test_template = {
id => 'Patron Status: valid patron, no patron password',
msg => '2300120060101 084237AOUWOLS|AAdjfiander|ACterminal password|',
pat => qr/^24 [ Y]{13}001$datepat/,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^David J\. Fiander$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
# Not required by the spec, but by the test
required => 1, },
$SIPtest::field_specs{(FID_CURRENCY)},
{ field => FID_FEE_AMT,
pat => $textpat,
required => 0, },
], };
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
clone($patron_status_test_template),
);
# Invalid patron
my $test = clone($patron_status_test_template);
$test->{id} = 'Patron Status invalid id';
$test->{msg} =~ s/AAdjfiander\|/AAberick|/;
# The test assumes that the language sent by the terminal is
# just echoed back for invalid patrons.
$test->{pat} = qr/^24Y[ Y]{13}001$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^berick$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^N$/,
required => 1, },
];
push @tests, $test;
# Valid patron, invalid patron password
$test = clone($patron_status_test_template);
$test->{id} = 'Patron Status: Valid patron, invalid patron password';
$test->{msg} .= (FID_PATRON_PWD) . 'badpwd|';
$test->{pat} = qr/^24[ Y]{14}001$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^David J\. Fiander$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
required => 1, },
{ field => FID_VALID_PATRON_PWD,
pat => qr/^N$/,
required => 1, },
];
push @tests, $test;
# TODO: Need multiple patrons to test each individual
# status field
SIPtest::run_sip_tests(@tests);
1;

45
C4/SIP/t/05block_patron.t Normal file
View File

@ -0,0 +1,45 @@
#!/usr/bin/perl
# block_patron: test Block Patron Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat $instid $user_barcode $user_fullname);
my $block_patron_test_template = {
id => 'Block Patron: valid patron, card not retained',
msg => "01N20060102 084238AO$instid|ALHe's a jerk|AA$user_barcode|ACterminal password|",
# response to block patron is a patron status message
pat => qr/^24Y[ Y]{13}000$datepat/o,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^$user_barcode$/o,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^$user_fullname$/o,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
# Not required by the spec, but by the test
required => 1, },
$SIPtest::field_specs{(FID_CURRENCY)},
{ field => FID_FEE_AMT,
pat => $textpat,
required => 0, },
], };
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
clone($block_patron_test_template),
);
SIPtest::run_sip_tests(@tests);
1;

144
C4/SIP/t/06patron_enable.t Normal file
View File

@ -0,0 +1,144 @@
#!/usr/bin/perl
# patron_enable: test Patron Enable Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat);
my $patron_enable_test_template = {
id => 'Patron Enable: valid patron',
msg => "2520060102 084238AOUWOLS|AAdjfiander|",
pat => qr/^26 {4}[ Y]{10}000$datepat/,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^David J\. Fiander$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
# Not required by the spec, but by the test
required => 1, },
], };
# We need to disable the valid patron before we can
# ensure that he was properly enabled.
my $patron_disable_test_template = {
id => 'Patron Enable: block patron (prep to test enabling)',
msg => "01N20060102 084238AOUWOLS|ALHe's a jerk|AAdjfiander|",
# response to block patron is a patron status message
pat => qr/^24Y{4}[ Y]{10}000$datepat/,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^David J\. Fiander$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
# Not required by the spec, but by the test
required => 1, },
], };
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
$patron_disable_test_template,
clone($patron_enable_test_template),
);
my $test;
# Valid patron, valid password
$test = clone($patron_enable_test_template);
$test->{id} = "Patron Enable: valid patron, valid password";
$test->{msg} .= FID_PATRON_PWD . '6789|';
$test->{pat} = qr/^26 {4}[ Y]{10}000$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^David J\. Fiander$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
# Not required by the spec, but by the test
required => 1, },
{ field => FID_VALID_PATRON_PWD,
pat => qr/^Y$/,
required => 1, },
];
push @tests, $patron_disable_test_template, $test;
# Valid patron, invalid password
$test = clone($patron_enable_test_template);
$test->{id} = "Patron Enable: valid patron, invalid password";
$test->{msg} .= FID_PATRON_PWD . 'bad password|';
$test->{pat} = qr/^26[ Y]{14}000$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^David J\. Fiander$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^Y$/,
# Not required by the spec, but by the test
required => 1, },
{ field => FID_VALID_PATRON_PWD,
pat => qr/^N$/,
required => 1, },
];
push @tests, $patron_disable_test_template, $test;
# After this test, the patron is left disabled, so re-enable
push @tests, $patron_enable_test_template;
# Invalid patron
$test = clone($patron_enable_test_template);
$test->{id} =~ s/valid/invalid/;
$test->{msg} =~ s/AAdjfiander\|/AAberick|/;
$test->{pat} = qr/^26Y{4}[ Y]{10}000$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^berick$/,
required => 1, },
{ field => FID_PERSONAL_NAME,
pat => qr/^$/,
required => 1, },
{ field => FID_VALID_PATRON,
pat => qr/^N$/,
# Not required by the spec, but by the test
required => 1, },
];
push @tests, $test;
SIPtest::run_sip_tests(@tests);
1;

187
C4/SIP/t/07hold.t Normal file
View File

@ -0,0 +1,187 @@
#!/usr/bin/perl
# patron_enable: test Patron Enable Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat);
my $hold_test_template = {
id => 'Place Hold: valid item, valid patron',
msg => '15+20060415 110158BW20060815 110158|BSTaylor|BY2|AOUWOLS|AAdjfiander|AB1565921879|',
pat => qr/^161N$datepat/,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
{ field => FID_EXPIRATION,
pat => $datepat,
required => 0, },
{ field => FID_QUEUE_POS,
pat => qr/^1$/,
required => 1, },
{ field => FID_PICKUP_LOCN,
pat => qr/^Taylor$/,
required => 1, },
{ field => FID_TITLE_ID,
pat => qr/^Perl 5 desktop reference$/,
required => 1, },
{ field => FID_ITEM_ID,
pat => qr/^1565921879$/,
required => 1, },
],};
my $hold_count_test_template0 = {
id => 'Confirm patron has 0 holds',
msg => '6300020060329 201700 AOUWOLS|AAdjfiander|',
pat => qr/^64 [ Y]{13}\d{3}${datepat}0000(\d{4}){5}/,
fields => [],
};
my $hold_count_test_template1 = {
id => 'Confirm patron has 1 hold',
msg => '6300020060329 201700 AOUWOLS|AAdjfiander|',
pat => qr/^64 [ Y]{13}\d{3}${datepat}0001(\d{4}){5}/,
fields => [],
};
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
$hold_test_template, $hold_count_test_template1,
);
my $test;
# Hold Queue: second hold placed on item
$test = clone($hold_test_template);
$test->{id} = 'Place hold: second hold on item';
$test->{msg} =~ s/djfiander/miker/;
$test->{pat} = qr/^161N$datepat/;
foreach my $i (0 .. (scalar @{$test->{fields}})-1) {
my $field = $test->{fields}[$i];
if ($field->{field} eq FID_PATRON_ID) {
$field->{pat} = qr/^miker$/;
} elsif ($field->{field} eq FID_QUEUE_POS) {
$field->{pat} = qr/^2$/;
}
}
push @tests, $test;
# Cancel hold: valid hold
$test = clone($hold_test_template);
$test->{id} = 'Cancel hold: valid hold';
$test->{msg} =~ s/\+/-/;
$test->{pat} = qr/^161[NY]$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
];
push @tests, $test, $hold_count_test_template0;
# Cancel Hold: no hold on item
# $test is already set up to cancel a hold, just change
# the field tests
$test = clone($test);
$test->{id} = 'Cancel Hold: no hold on specified item';
$test->{pat} = qr/^160N$datepat/;
push @tests, $test, $hold_count_test_template0;
# Cleanup: cancel miker's hold too.
$test = clone($hold_test_template);
$test->{id} = "Cancel hold: cleanup second patron's hold";
$test->{msg} =~ s/\+/-/;
$test->{msg} =~ s/djfiander/miker/;
$test->{pat} = qr/^161[NY]$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^miker$/,
required => 1, },
];
push @tests, $test;
# Place hold: valid patron, item, invalid patron pwd
$test = clone($hold_test_template);
$test->{id} = 'Place hold: invalid patron password';
$test->{msg} .= FID_PATRON_PWD . 'bad password|';
$test->{pat} = qr/^160N$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
];
push @tests, $test, $hold_count_test_template0;
# Place hold: invalid patron
$test = clone($hold_test_template);
$test->{id} = 'Place hold: invalid patron';
$test->{msg} =~ s/AAdjfiander\|/AAberick|/;
$test->{pat} = qr/^160N$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^berick$/,
required => 1, },
];
# There's no patron to check the number of holds against
push @tests, $test;
# Place hold: invalid item
$test = clone($hold_test_template);
$test->{id} = 'Place hold: invalid item';
$test->{msg} =~ s/AB1565921879\|/ABnosuchitem|/;
$test->{pat} = qr/^160N$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^djfiander$/,
required => 1, },
{ field => FID_ITEM_ID,
pat => qr/^nosuchitem$/,
required => 0, },
];
push @tests, $test, $hold_count_test_template0;
# Still need tests for:
# - valid patron not permitted to place holds
# - valid item, not allowed to hold item
# - multiple holds on item: correct queue position management
# - setting and verifying hold expiry dates (requires ILS support)
SIPtest::run_sip_tests(@tests);
1;

67
C4/SIP/t/08checkin.t Normal file
View File

@ -0,0 +1,67 @@
#!/usr/bin/perl
# checkin: test Checkin Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat $instid $user_barcode
$item_barcode $item_title);
my $checkin_test_template = {
id => 'Checkin: Item is checked out',
msg => "09N20060102 08423620060113 084235APUnder the bed|AO$instid|AB$item_barcode|ACterminal password|",
pat => qr/^101YNN$datepat/o,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^$user_barcode$/o,
required => 1, },
{ field => FID_ITEM_ID,
pat => qr/^$item_barcode$/o,
required => 1, },
{ field => FID_PERM_LOCN,
pat => $textpat,
required => 1, },
{ field => FID_TITLE_ID,
pat => qr/^$item_title\s*$/o,
required => 1, }, # not required by the spec.
],};
my $checkout_template = {
id => 'Checkin: prep: check out item',
msg => "11YN20060329 203000 AO$instid|AA$user_barcode|AB$item_barcode|AC|",
pat => qr/^121NNY$datepat/o,
fields => [],
};
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
$checkout_template,
$checkin_test_template,
);
my $test;
# Checkin item that's not checked out. Basically, this
# is identical to the first case, except the header says that
# the ILS didn't check the item in, and there's no patron id.
$test = clone($checkin_test_template);
$test->{id} = 'Checkin: Item not checked out';
$test->{pat} = qr/^100YNN$datepat/o;
$test->{fields} = [grep $_->{field} ne FID_PATRON_ID, @{$test->{fields}}];
push @tests, $test;
#
# Still need tests for magnetic media
#
SIPtest::run_sip_tests(@tests);
1;

147
C4/SIP/t/09renew.t Normal file
View File

@ -0,0 +1,147 @@
#!/usr/bin/perl
# renew: test Renew Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat $instid $currency $user_barcode
$item_barcode $item_title);
my $checkout_template = {
id => 'Renew: prep: check out item',
msg => "11YN20060329 203000 AO$instid|AA$user_barcode|AB$item_barcode|AC|",
pat => qr/^121NNY$datepat/,
fields => [],
};
my $checkin_template = {
id => 'Renew: prep: check in item',
msg => "09N20060102 08423620060113 084235APUnder the bed|AO$instid|AB$item_barcode|ACterminal password|",
pat => qr/^101YNN$datepat/,
fields => [],
};
#my $hold_template = {
# id => 'Renew: prep: place hold on item',
# msg =>"15+20060415 110158BW20060815 110158|BSTaylor|BY2|AO$instid|AAmiker|AB$item_barcode|",
# pat => qr/^161N$datepat/,
# fields => [],
# };
#
#my $cancel_hold_template = {
# id => 'Renew: cleanup: cancel hold on item',
# msg =>"15-20060415 110158BW20060815 110158|BSTaylor|BY2|AO$instid|AAmiker|AB$item_barcode|",
# pat => qr/^161[NY]$datepat/,
# fields => [],
# };
#
my $renew_test_template = {
id => 'Renew: item id checked out to patron, renewal permitted, no 3rd party, no fees',
msg => "29NN20060102 084236 AO$instid|AA$user_barcode|AB$item_barcode|",
pat => qr/^301YNN$datepat/,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_PATRON_ID,
pat => qr/^$user_barcode$/,
required => 1, },
{ field => FID_ITEM_ID,
pat => qr/^$item_barcode$/,
required => 1, },
{ field => FID_TITLE_ID,
pat => qr/^$item_title\s*$/,
required => 1, },
{ field => FID_DUE_DATE,
pat => qr/^$datepat$/,
required => 1, },
{ field => FID_SECURITY_INHIBIT,
pat => qr/^[YN]$/,
required => 0, },
],};
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
$checkout_template,
$renew_test_template,
);
my $test;
# Renew: item checked out, identify by title
#$test = clone($renew_test_template);
#$test->{id} = 'Renew: identify item by title';
#$test->{msg} =~ s/AB$item_barcode\|/AJ$item_title|/;
## Everything else should be the same
#push @tests, $test;
#
## Renew: Item checked out, but another patron has placed a hold
#$test = clone($renew_test_template);
#$test->{id} = 'Renew: Item has outstanding hold';
#$test->{pat} = qr/^300NUN$datepat/;
#foreach my $field (@{$test->{fields}}) {
# if ($field->{field} eq FID_DUE_DATE || $field->{field} eq FID_TITLE_ID) {
# $field->{pat} = qr/^$/;
# }
#}
#
#push @tests, $hold_template, $test, $cancel_hold_template;
#
# Renew: item not checked out. Basically the same, except
# for the leader test.
$test = clone($renew_test_template);
$test->{id} = 'Renew: item not checked out at all';
$test->{pat} = qr/^300NUN$datepat/;
foreach my $field (@{$test->{fields}}) {
if ($field->{field} eq FID_DUE_DATE) {
$field->{pat} = qr/^$/;
} elsif ($field->{field} eq FID_TITLE_ID) {
$field->{pat} = qr/^($item_title\s*|)$/;
}
}
push @tests, $checkin_template, $test;
$test = clone($renew_test_template);
$test->{id} = 'Renew: Invalid item';
$test->{msg} =~ s/AB[^|]+/ABbad-item/;
$test->{pat} = qr/^300NUN$datepat/;
foreach my $field (@{$test->{fields}}) {
if ($field->{field} eq FID_TITLE_ID || $field->{field} eq FID_DUE_DATE) {
$field->{pat} = qr/^$/;
} elsif ($field->{field} eq FID_ITEM_ID) {
$field->{pat} = qr/^bad-item$/;
}
}
push @tests, $test;
$test = clone($renew_test_template);
$test->{id} = 'Renew: Invalid user';
$test->{msg} =~ s/AA$user_barcode/AAberick/;
$test->{pat} = qr/^300NUN$datepat/;
foreach my $field (@{$test->{fields}}) {
if ($field->{field} eq FID_DUE_DATE) {
$field->{pat} = qr/^$/;
} elsif ($field->{field} eq FID_PATRON_ID) {
$field->{pat} = qr/^berick$/;
} elsif ($field->{field} eq FID_TITLE_ID) {
$field->{pat} = qr/^($item_title\s*|)$/;
}
}
push @tests, $test;
# Still need tests for
# - renewing a for-fee item
# - patrons that are not permitted to renew
# - renewing item that has reached limit on number of renewals
SIPtest::run_sip_tests(@tests);
1;

107
C4/SIP/t/10renew_all.t Normal file
View File

@ -0,0 +1,107 @@
#!/usr/bin/perl
# renew_all: test Renew All Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat $user_barcode $item_barcode $item_owner
$item2_barcode $item2_owner $instid);
my $enable_template = {
id => 'Renew All: prep: enable patron permissions',
msg => "2520060102 084238AO$instid|AA$user_barcode|",
pat => qr/^26 {4}[ Y]{10}000$datepat/,
fields => [],
};
my @checkout_templates = (
{ id => "Renew All: prep: check out $item_barcode",
msg => "11YN20060329 203000 AO$instid|AA$user_barcode|AB$item_barcode|AC|",
pat => qr/^121NNY$datepat/,
fields => [],},
{ id => "Renew All: prep: check out $item2_barcode",
msg => "11YN20060329 203000 AO$instid|AA$user_barcode|AB$item2_barcode|AC|",
pat => qr/^121NNY$datepat/,
fields => [],}
);
my @checkin_templates = (
{ id => "Renew All: prep: check in $item_barcode",
msg => "09N20060102 08423620060113 084235APUnder the bed|AO$instid|AB$item_barcode|ACterminal password|",
pat => qr/^101YNN$datepat/,
fields => [],},
{ id => "Renew All: prep: check in $item2_barcode",
msg => "09N20060102 08423620060113 084235APUnder the bed|AO$instid|AB$item2_barcode|ACterminal password|",
pat => qr/^101YNN$datepat/,
fields => [],}
);
my $renew_all_test_template = {
id => 'Renew All: valid patron with one item checked out, no patron password',
msg => "6520060102 084236AO$instid|AA$user_barcode|",
pat => qr/^66100010000$datepat/,
fields => [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_RENEWED_ITEMS,
pat => qr/^$item_barcode$/,
required => 1, },
],};
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
# $enable_template,
$checkout_templates[0],
$renew_all_test_template,
$checkin_templates[0], # check the book in, when done testing
);
my $test;
#$test = clone($renew_all_test_template);
#$test->{id} = 'Renew All: Valid patron, two items checked out';
#$test->{pat} = qr/^66100020000$datepat/;
#foreach my $i (0 .. (scalar @{$test->{fields}})-1) {
# my $field = $test->{fields}[$i];
#
# if ($field->{field} eq FID_RENEWED_ITEMS) {
# $field->{pat} = qr/^$item_barcode\|$item2_barcode$/;
# }
#}
#
#push @tests, $checkout_templates[0], $checkout_templates[1],
# $renew_all_test_template, $checkin_templates[0], $checkin_templates[1];
$test = clone($renew_all_test_template);
$test->{id} = 'Renew All: valid patron, invalid patron password';
$test->{msg} .= (FID_PATRON_PWD) . 'badpwd|';
$test->{pat} = qr/^66000000000$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
];
push @tests, $checkout_templates[0], $test, $checkin_templates[0];
$test = clone($renew_all_test_template);
$test->{id} = 'Renew All: invalid patron';
$test->{msg} =~ s/AA$user_barcode/AAberick/;
$test->{pat} = qr/^66000000000$datepat/;
delete $test->{fields};
$test->{fields} = [
$SIPtest::field_specs{(FID_INST_ID)},
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
];
push @tests, $test;
SIPtest::run_sip_tests(@tests);
1;

42
C4/SIP/t/11item_info.t Normal file
View File

@ -0,0 +1,42 @@
#!/usr/bin/perl
# renew_all: test Renew All Response
use strict;
use warnings;
use Clone qw(clone);
use Sip::Constants qw(:all);
use SIPtest qw($datepat $textpat $instid $currency $user_barcode
$item_barcode $item_title $item_owner);
my $item_info_test_template = {
id => 'Item Information: check information for available item',
msg => "1720060110 215612AO$instid|AB$item_barcode|",
pat => qr/^180[13]0201$datepat/, # status of 'other' or 'available'
fields => [
$SIPtest::field_specs{(FID_SCREEN_MSG)},
$SIPtest::field_specs{(FID_PRINT_LINE)},
{ field => FID_ITEM_ID,
pat => qr/^$item_barcode$/,
required => 1, },
{ field => FID_TITLE_ID,
pat => qr/^$item_title\s*$/,
required => 1, },
{ field => FID_MEDIA_TYPE,
pat => qr/^\d{3}$/,
required => 0, },
{ field => FID_OWNER,
pat => qr/^$item_owner$/,
required => 0, },
], };
my @tests = (
$SIPtest::login_test,
$SIPtest::sc_status_test,
clone($item_info_test_template),
);
SIPtest::run_sip_tests(@tests);
1;

16
C4/SIP/t/Makefile Normal file
View File

@ -0,0 +1,16 @@
#
#
#
TESTS = 00sc_status.t 01patron_status.t 02patron_info.t 03checkout.t \
04patron_status.t 05block_patron.t 06patron_enable.t 07hold.t \
08checkin.t 09renew.t 10renew_all.t 11item_info.t
OILS_TESTS = 00sc_status.t 01patron_status.t 02patron_info.t 03checkout.t \
08checkin.t 09renew.t 11item_info.t 05block_patron.t
test-openils:
prove -I.. $(OILS_TESTS)
test:
prove -I.. $(TESTS)

50
C4/SIP/t/README Normal file
View File

@ -0,0 +1,50 @@
CONFIGURING THE TEST SUITE
Before you can run the test suite, you need to configure certain
information about the SIP server and the ILS data in the file
SIPtest.pm.
RUNNING THE TESTS
Every file tests a different protocol transaction.
Unfortunately, a lot of test cases are missing, but the basics
are tested, as are most of the simple error conditions (invalid
users, unknown items, checking in item that's not checked out).
To run a single test, just run
perl -I.. <file>
If the test fails, the output should be pretty clear about what
went wrong (assuming you can read raw SIP packets).
To run all the tests, just type
make test
Right now, that will run tests for functionality that isn't
supported in the Evergreen environment (the two main cases are
enable patron and hold management). To run just the Evergreen tests, use
make test-openils
which will run just the tests
00sc_status.t
01patron_status.t
02patron_info.t
03checkout.t
06patron_enable.t
08checkin.t
09renew.t
11item_info.t
05block_patron.t
NOTE: the Block Patron tests are run last because "Patron Enable"
isn't supported. Thus, after running the "Block Patron" test,
manual intervention is required to unblock the test patron.
The Renew All tests will fail when running the stub "ILS"
implementation unless there's only one ILS server running. This
won't be a problem for any real backend implementation that
properly manages the database of users and items.

225
C4/SIP/t/SIPtest.pm Normal file
View File

@ -0,0 +1,225 @@
package SIPtest;
use strict;
use warnings;
use Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(run_sip_tests no_tagged_fields
$datepat $textpat
$login_test $sc_status_test
%field_specs
$instid $currency $server $username $password
$user_barcode $user_pin $user_fullname $user_homeaddr
$user_email $user_phone $user_birthday $user_ptype
$user_inet
$item_barcode $item_title $item_owner
$item2_barcode $item2_title $item2_owner
$item_diacritic_barcode $item_diacritic_title
$item_diacritic_owner);
#use Data::Dumper;
# The number of tests is set in run_sip_tests() below, based
# on the size of the array of tests.
use Test::More;
use IO::Socket::INET;
use Sip qw(:all);
use Sip::Checksum qw(verify_cksum);
use Sip::Constants qw(:all);
#
# Configuration parameters to run the test suite
#
our $instid = 'UWOLS';
our $currency = 'CAD';
our $server = 'localhost:6001'; # Address of the SIP server
# SIP username and password to connect to the server. See the
# SIP config.xml for the correct values.
our $username = 'scclient';
our $password = 'clientpwd';
# ILS Information
# Valid user barcode and corresponding user password/pin and full name
our $user_barcode = 'djfiander';
our $user_pin = '6789';
our $user_fullname= 'David J\. Fiander';
our $user_homeaddr= '2 Meadowvale Dr\. St Thomas, ON';
our $user_email = 'djfiander\@hotmail\.com';
our $user_phone = '\(519\) 555 1234';
our $user_birthday= '19640925';
our $user_ptype = 'A';
our $user_inet = 'Y';
# Valid item barcode and corresponding title
our $item_barcode = '1565921879';
our $item_title = 'Perl 5 desktop reference';
our $item_owner = 'UWOLS';
# Another valid item
our $item2_barcode = '0440242746';
our $item2_title = 'The deep blue alibi';
our $item2_owner = 'UWOLS';
# An item with a diacritical in the title
our $item_diacritic_barcode = '660';
our $item_diacritic_title = 'Harry Potter y el cáliz de fuego';
our $item_diacritic_owner = 'UWOLS';
# End configuration
# Pattern for a SIP datestamp, to be used by individual tests to
# match timestamp fields (duh).
our $datepat = '\d{8} {4}\d{6}';
# Pattern for a random text field (may be empty)
our $textpat = qr/^[^|]*$/;
our %field_specs = (
(FID_SCREEN_MSG) => { field => FID_SCREEN_MSG,
pat => $textpat,
required => 0, },
(FID_PRINT_LINE) => { field => FID_PRINT_LINE,
pat => $textpat,
required => 0, },
(FID_INST_ID) => { field => FID_INST_ID,
pat => qr/^$instid$/o,
required => 1, },
(FID_HOLD_ITEMS_LMT)=> { field => FID_HOLD_ITEMS_LMT,
pat => qr/^\d{4}$/,
required => 0, },
(FID_OVERDUE_ITEMS_LMT)=> { field => FID_OVERDUE_ITEMS_LMT,
pat => qr/^\d{4}$/,
required => 0, },
(FID_CHARGED_ITEMS_LMT)=> { field => FID_CHARGED_ITEMS_LMT,
pat => qr/^\d{4}$/,
required => 0, },
(FID_VALID_PATRON) => { field => FID_VALID_PATRON,
pat => qr/^[NY]$/,
required => 0, },
(FID_VALID_PATRON_PWD)=> { field => FID_VALID_PATRON_PWD,
pat => qr/^[NY]$/,
required => 0, },
(FID_CURRENCY) => { field => FID_CURRENCY,
pat => qr/^$currency$/io,
required => 0, },
);
# Login and SC Status are always the first two messages that
# the terminal sends to the server, so just create the test
# cases here and reference them in the individual test files.
our $login_test = { id => 'login',
msg => "9300CN$username|CO$password|CPThe floor|",
pat => qr/^941/,
fields => [], };
our $sc_status_test = { id => 'SC status',
msg => '9910302.00',
pat => qr/^98[YN]{6}\d{3}\d{3}$datepat(2\.00|1\.00)/,
fields => [
$field_specs{(FID_SCREEN_MSG)},
$field_specs{(FID_PRINT_LINE)},
$field_specs{(FID_INST_ID)},
{ field => 'AM',
pat => $textpat,
required => 0, },
{ field => 'BX',
pat => qr/^[YN]{16}$/,
required => 1, },
{ field => 'AN',
pat => $textpat,
required => 0, },
],
};
sub one_msg {
my ($sock, $test, $seqno) = @_;
my $resp;
my %fields;
# If reading or writing fails, then the server's dead,
# so there's no point in continuing.
if (!write_msg({seqno => $seqno}, $test->{msg}, $sock)) {
BAIL_OUT("Write failure in $test->{id}");
} elsif (!($resp = <$sock>)) {
BAIL_OUT("Read failure in $test->{id}");
}
chomp($resp);
if (!verify_cksum($resp)) {
fail("checksum $test->{id}");
return;
}
if ($resp !~ $test->{pat}) {
fail("match leader $test->{id}");
diag("Response '$resp' doesn't match pattern '$test->{pat}'");
return;
}
# Split the tagged fields of the response into (name, value)
# pairs and stuff them into the hash.
$resp =~ $test->{pat};
%fields = substr($resp, $+[0]) =~ /(..)([^|]*)\|/go;
# print STDERR Dumper($test);
# print STDERR Dumper(\%fields);
if (!defined($test->{fields})) {
diag("TODO: $test->{id} field tests not written yet");
} else {
# If there are no tagged fields, then 'fields' should be an
# empty list which will automatically skip this loop
foreach my $ftest (@{$test->{fields}}) {
my $field = $ftest->{field};
if ($ftest->{required} && !exists($fields{$field})) {
fail("$test->{id} required field '$field' exists in '$resp'");
return;
}
if (exists($fields{$field}) && ($fields{$field} !~ $ftest->{pat})) {
fail("$test->{id} field test $field");
diag("Field pattern '$ftest->{pat}' for '$field' doesn't match in '$resp'");
return;
}
}
}
pass("$test->{id}");
return;
}
#
# _count_tests: Count the number of tests in a test array
sub _count_tests {
return scalar @_;
}
sub run_sip_tests {
my ($sock, $seqno);
$Sip::error_detection = 1;
$/ = "\r";
$sock = new IO::Socket::INET(PeerAddr => $server,
Type => SOCK_STREAM);
BAIL_OUT('failed to create connection to server') unless $sock;
$seqno = 1;
plan tests => _count_tests(@_);
foreach my $test (@_) {
one_msg($sock, $test, $seqno++);
$seqno %= 10; # sequence number is one digit
}
}
1;

17
C4/SIP/test.txt Normal file
View File

@ -0,0 +1,17 @@
97AZFEF5
2300120060101 084235AOUWOLS|AAdjfiander|ACterminal password|ADuser password|
2300120060101 084236AOUWOLS|AAmjandkilde|ACterminal password|ADuser password|
2300120060101 084237AOUWOLS|AAdjfiander|ACterminal password|ADuser password|
9300CNLoginUserID|COLoginPassword|CPLocationCode|
11YN20060329 203000 AOUWOLS|AAdjfiander|AB1565921879|AC|
09Y20060102 08423620060113 084235APUnder the bed|AOUWOLS|AB1565921879|ACterminal password|
01N20060102 084238AOUWOLS|ALHe's a jerk|AAdjfiander|ACterminal password|
2520060102 084238AOUWOLS|AAdjfiander|ACterminal password|AD6789|
9910302.00
3520060110 084237AOUWOLS|AAdjfiander|AD6789|
1720060110 215612AOUWOLS|AB1565921879|
6300020060329 201700Y AOUWOLS|AAdjfiander|
15+20060415 110158BW20060815 110158|BSTaylor|BY2|AOUWOLS|AAdjfiander|AB1565921879|
15-20060415 110158AOUWOLS|AAdjfiander|AB1565921879|
29NN20060415 110158 AOUWOLS|AAdjfiander|AD6789|AB1565921879|
6520060415 110158AOUWOLS|AAdjfiander|AD6789|

29
C4/SIP/xmlparse.pl Normal file
View File

@ -0,0 +1,29 @@
#
# This file reads a SIPServer xml-format configuration file and dumps it
# to stdout. Just to see what the structures look like.
#
# The 'new XML::Simple' option must agree exactly with the configuration
# in Sip::Configuration.pm
#
use strict;
use English;
use XML::Simple qw(:strict);
use Data::Dumper;
my $parser = new XML::Simple( KeyAttr => { login => '+id',
institution => '+id',
service => '+port', },
GroupTags => { listeners => 'service',
accounts => 'login',
institutions => 'institution', },
ForceArray=> [ 'service',
'login',
'institution' ],
ValueAttr => { 'error-detect' => 'enabled',
'min_servers' => 'value',
'max_servers' => 'value'} );
my $ref = $parser->XMLin($ARGV[0]);
print Dumper($ref);

617
C4/SIP_openils_pm Normal file
View File

@ -0,0 +1,617 @@
#
# ILS.pm: Test ILS interface module
#
package OpenILS::SIP;
use warnings; use strict;
use Sys::Syslog qw(syslog);
use OpenILS::SIP::Item;
use OpenILS::SIP::Patron;
use OpenILS::SIP::Transaction;
use OpenILS::SIP::Transaction::Checkout;
use OpenILS::SIP::Transaction::Checkin;
use OpenILS::SIP::Transaction::Renew;
use OpenSRF::System;
use OpenILS::Utils::Fieldmapper;
use OpenSRF::Utils::SettingsClient;
use OpenILS::Application::AppUtils;
use OpenSRF::Utils qw/:datetime/;
use DateTime::Format::ISO8601;
my $U = 'OpenILS::Application::AppUtils';
my $editor;
my $config;
use Digest::MD5 qw(md5_hex);
sub new {
my ($class, $institution, $login) = @_;
my $type = ref($class) || $class;
my $self = {};
$self->{login} = $login;
$config = $institution;
syslog("LOG_DEBUG", "OILS: new ILS '%s'", $institution->{id});
$self->{institution} = $institution;
my $bsconfig = $institution->{implementation_config}->{bootstrap};
syslog('LOG_DEBUG', "OILS: loading bootstrap config: $bsconfig");
local $/ = "\n";
OpenSRF::System->bootstrap_client(config_file => $bsconfig);
syslog('LOG_DEBUG', "OILS: bootstrap loaded..");
$self->{osrf_config} = OpenSRF::Utils::SettingsClient->new;
Fieldmapper->import($self->{osrf_config}->config_value('IDL'));
bless( $self, $type );
return undef unless
$self->login( $login->{id}, $login->{password} );
return $self;
}
sub verify_session {
my $self = shift;
my $ses = $U->simplereq(
'open-ils.auth',
'open-ils.auth.session.retrieve', $self->{authtoken} );
return 1 unless $U->event_code($ses);
syslog('LOG_INFO', "OILS: Logging back after session timeout as user ".$self->{login}->{id});
return $self->login( $self->{login}->{id}, $self->{login}->{password} );
}
sub to_bool {
my $val = shift;
return ($val and $val =~ /true/io);
}
sub editor {
return $editor
if $editor and $editor->{session}
and $editor->session->connected;
return $editor = make_editor();
}
sub reset_editor {
$editor = undef;
return editor();
}
sub config {
return $config;
}
# Creates the global editor object
sub make_editor {
require OpenILS::Utils::CStoreEditor;
my $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
# gnarly cstore hack to re-gen autogen methods after IDL is loaded
if(!UNIVERSAL::can($e, 'search_actor_card')) {
syslog("LOG_WARNING", "OILS: Reloading CStoreEditor...");
delete $INC{'OpenILS/Utils/CStoreEditor.pm'};
require OpenILS::Utils::CStoreEditor;
$e = OpenILS::Utils::CStoreEditor->new(xact =>1);
}
return $e;
}
sub format_date {
my $class = shift;
my $date = shift;
my $type = shift || 'dob';
return "" unless $date;
$date = DateTime::Format::ISO8601->new->
parse_datetime(OpenSRF::Utils::clense_ISO8601($date));
my @time = localtime($date->epoch);
my $year = $time[5]+1900;
my $mon = $time[4]+1;
my $day = $time[3];
$mon =~ s/^(\d)$/0$1/;
$day =~ s/^(\d)$/0$1/;
$date = "$year$mon$day";
$date = $year.'-'.$mon.'-'.$day .' 00:00:00' if $type eq 'due';
#$date = $year.'-'.$mon.'-'.$day if $type eq 'due';
syslog('LOG_DEBUG', "OILS: formatted date [type=$type]: $date");
return $date;
}
sub login {
my( $self, $username, $password ) = @_;
syslog('LOG_DEBUG', "OILS: Logging in with username $username");
my $seed = $U->simplereq(
'open-ils.auth',
'open-ils.auth.authenticate.init', $username );
my $response = $U->simplereq(
'open-ils.auth',
'open-ils.auth.authenticate.complete',
{
username => $username,
password => md5_hex($seed . md5_hex($password)),
type => 'opac',
}
);
if( my $code = $U->event_code($response) ) {
my $txt = $response->{textcode};
syslog('LOG_WARNING', "OILS: Login failed for $username. $txt:$code");
return undef;
}
my $key = $response->{payload}->{authtoken};
syslog('LOG_INFO', "OILS: Login succeeded for $username : authkey = $key");
return $self->{authtoken} = $key;
}
sub find_patron {
my $self = shift;
return OpenILS::SIP::Patron->new(@_);
}
sub find_item {
my $self = shift;
return OpenILS::SIP::Item->new(@_);
}
sub institution {
my $self = shift;
return $self->{institution}->{id};
}
sub supports {
my ($self, $op) = @_;
my ($i) = grep { $_->{name} eq $op }
@{$config->{implementation_config}->{supports}->{item}};
return to_bool($i->{value});
}
sub check_inst_id {
my ($self, $id, $whence) = @_;
if ($id ne $self->{institution}->{id}) {
syslog("LOG_WARNING",
"OILS: %s: received institution '%s', expected '%s'",
$whence, $id, $self->{institution}->{id});
}
}
sub checkout_ok {
return to_bool($config->{policy}->{checkout});
}
sub checkin_ok {
return to_bool($config->{policy}->{checkin});
return 0;
}
sub renew_ok {
return to_bool($config->{policy}->{renew});
}
sub status_update_ok {
return to_bool($config->{policy}->{status_update});
}
sub offline_ok {
return to_bool($config->{policy}->{offline});
}
##
## Checkout(patron_id, item_id, sc_renew):
## patron_id & item_id are the identifiers send by the terminal
## sc_renew is the renewal policy configured on the terminal
## returns a status opject that can be queried for the various bits
## of information that the protocol (SIP or NCIP) needs to generate
## the response.
##
sub checkout {
my ($self, $patron_id, $item_id, $sc_renew) = @_;
$self->verify_session;
syslog('LOG_DEBUG', "OILS: OpenILS::Checkout attempt: patron=$patron_id, item=$item_id");
my $xact = OpenILS::SIP::Transaction::Checkout->new( authtoken => $self->{authtoken} );
my $patron = $self->find_patron($patron_id);
my $item = $self->find_item($item_id);
$xact->patron($patron);
$xact->item($item);
if (!$patron) {
$xact->screen_msg("Invalid Patron");
return $xact;
}
if (!$patron->charge_ok) {
$xact->screen_msg("Patron Blocked");
return $xact;
}
if( !$item ) {
$xact->screen_msg("Invalid Item");
return $xact;
}
syslog('LOG_DEBUG', "OILS: OpenILS::Checkout data loaded OK, checking out...");
$xact->do_checkout();
if ($item->{patron} && ($item->{patron} ne $patron_id)) {
# I can't deal with this right now
# XXX check in then check out?
$xact->screen_msg("Item checked out to another patron");
$xact->ok(0);
}
$xact->desensitize(!$item->magnetic);
if( $xact->ok ) {
#editor()->commit;
syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
"patron %s checkout %s succeeded", $patron_id, $item_id);
} else {
#editor()->xact_rollback;
syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
"patron %s checkout %s FAILED, rolling back xact...", $patron_id, $item_id);
}
return $xact;
}
sub checkin {
my ($self, $item_id, $trans_date, $return_date,
$current_loc, $item_props, $cancel) = @_;
$self->verify_session;
syslog('LOG_DEBUG', "OILS: OpenILS::Checkin on item=$item_id");
my $patron;
my $xact = OpenILS::SIP::Transaction::Checkin->new(authtoken => $self->{authtoken});
my $item = $self->find_item($item_id);
$xact->item($item);
if(!$xact->item) {
$xact->screen_msg("Invalid item barcode: $item_id");
$xact->ok(0);
return $xact;
}
$xact->do_checkin( $trans_date, $return_date, $current_loc, $item_props );
if ($xact->ok) {
$xact->patron($patron = $self->find_patron($item->{patron}));
delete $item->{patron};
delete $item->{due_date};
syslog('LOG_INFO', "OILS: Checkin succeeded");
#editor()->commit;
} else {
#editor()->xact_rollback;
syslog('LOG_WARNING', "OILS: Checkin failed");
}
# END TRANSACTION
return $xact;
}
## If the ILS caches patron information, this lets it free it up
sub end_patron_session {
my ($self, $patron_id) = @_;
return (1, 'Thank you for using OpenILS!', '');
}
#sub pay_fee {
# my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
# $pay_type, $fee_id, $trans_id, $currency) = @_;
# my $trans;
# my $patron;
#
# $trans = new ILS::Transaction::FeePayment;
#
# $patron = new ILS::Patron $patron_id;
#
# $trans->transaction_id($trans_id);
# $trans->patron($patron);
# $trans->ok(1);
#
# return $trans;
#}
#
#sub add_hold {
# my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
# $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
# my ($patron, $item);
# my $hold;
# my $trans;
#
#
# $trans = new ILS::Transaction::Hold;
#
# # BEGIN TRANSACTION
# $patron = new ILS::Patron $patron_id;
# if (!$patron
# || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
# $trans->screen_msg("Invalid Patron.");
#
# return $trans;
# }
#
# $item = new ILS::Item ($item_id || $title_id);
# if (!$item) {
# $trans->screen_msg("No such item.");
#
# # END TRANSACTION (conditionally)
# return $trans;
# } elsif ($item->fee && ($fee_ack ne 'Y')) {
# $trans->screen_msg = "Fee required to place hold.";
#
# # END TRANSACTION (conditionally)
# return $trans;
# }
#
# $hold = {
# item_id => $item->id,
# patron_id => $patron->id,
# expiration_date => $expiry_date,
# pickup_location => $pickup_location,
# hold_type => $hold_type,
# };
#
# $trans->ok(1);
# $trans->patron($patron);
# $trans->item($item);
# $trans->pickup_location($pickup_location);
#
# push(@{$item->hold_queue}, $hold);
# push(@{$patron->{hold_items}}, $hold);
#
#
# # END TRANSACTION
# return $trans;
#}
#
#sub cancel_hold {
# my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
# my ($patron, $item, $hold);
# my $trans;
#
# $trans = new ILS::Transaction::Hold;
#
# # BEGIN TRANSACTION
# $patron = new ILS::Patron $patron_id;
# if (!$patron) {
# $trans->screen_msg("Invalid patron barcode.");
#
# return $trans;
# } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
# $trans->screen_msg('Invalid patron password.');
#
# return $trans;
# }
#
# $item = new ILS::Item ($item_id || $title_id);
# if (!$item) {
# $trans->screen_msg("No such item.");
#
# # END TRANSACTION (conditionally)
# return $trans;
# }
#
# # Remove the hold from the patron's record first
# $trans->ok($patron->drop_hold($item_id));
#
# if (!$trans->ok) {
# # We didn't find it on the patron record
# $trans->screen_msg("No such hold on patron record.");
#
# # END TRANSACTION (conditionally)
# return $trans;
# }
#
# # Now, remove it from the item record. If it was on the patron
# # record but not on the item record, we'll treat that as success.
# foreach my $i (0 .. scalar @{$item->hold_queue}) {
# $hold = $item->hold_queue->[$i];
#
# if ($hold->{patron_id} eq $patron->id) {
# # found it: delete it.
# splice @{$item->hold_queue}, $i, 1;
# last;
# }
# }
#
# $trans->screen_msg("Hold Cancelled.");
# $trans->patron($patron);
# $trans->item($item);
#
# return $trans;
#}
#
#
## The patron and item id's can't be altered, but the
## date, location, and type can.
#sub alter_hold {
# my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
# $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
# my ($patron, $item);
# my $hold;
# my $trans;
#
# $trans = new ILS::Transaction::Hold;
#
# # BEGIN TRANSACTION
# $patron = new ILS::Patron $patron_id;
# if (!$patron) {
# $trans->screen_msg("Invalid patron barcode.");
#
# return $trans;
# }
#
# foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
# $hold = $patron->{hold_items}[$i];
#
# if ($hold->{item_id} eq $item_id) {
# # Found it. So fix it.
# $hold->{expiration_date} = $expiry_date if $expiry_date;
# $hold->{pickup_location} = $pickup_location if $pickup_location;
# $hold->{hold_type} = $hold_type if $hold_type;
#
# $trans->ok(1);
# $trans->screen_msg("Hold updated.");
# $trans->patron($patron);
# $trans->item(new ILS::Item $hold->{item_id});
# last;
# }
# }
#
# # The same hold structure is linked into both the patron's
# # list of hold items and into the queue of outstanding holds
# # for the item, so we don't need to search the hold queue for
# # the item, since it's already been updated by the patron code.
#
# if (!$trans->ok) {
# $trans->screen_msg("No such outstanding hold.");
# }
#
# return $trans;
#}
sub renew {
my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
$no_block, $nb_due_date, $third_party, $item_props, $fee_ack) = @_;
$self->verify_session;
my $trans = OpenILS::SIP::Transaction::Renew->new( authtoken => $self->{authtoken} );
$trans->patron($self->find_patron($patron_id));
$trans->item($self->find_item($item_id));
if(!$trans->patron) {
$trans->screen_msg("Invalid patron barcode.");
$trans->ok(0);
return $trans;
}
if(!$trans->patron->renew_ok) {
$trans->screen_msg("Renewals not allowed.");
$trans->ok(0);
return $trans;
}
if(!$trans->item) {
if( $title_id ) {
$trans->screen_msg("Item Id renewal not supported.");
} else {
$trans->screen_msg("Invalid item barcode.");
}
$trans->ok(0);
return $trans;
}
if(!$trans->item->{patron} or
$trans->item->{patron} ne $patron_id) {
$trans->screen_msg("Item not checked out to " . $trans->patron->name);
$trans->ok(0);
return $trans;
}
# Perform the renewal
$trans->do_renew();
$trans->desensitize(0); # It's already checked out
$trans->item->{due_date} = $nb_due_date if $no_block eq 'Y';
$trans->item->{sip_item_properties} = $item_props if $item_props;
return $trans;
}
#
#sub renew_all {
# my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
# my ($patron, $item_id);
# my $trans;
#
# $trans = new ILS::Transaction::RenewAll;
#
# $trans->patron($patron = new ILS::Patron $patron_id);
# if (defined $patron) {
# syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
# $patron->name, $patron->renew_ok);
# } else {
# syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
# $patron_id);
# }
#
# if (!defined($patron)) {
# $trans->screen_msg("Invalid patron barcode.");
# return $trans;
# } elsif (!$patron->renew_ok) {
# $trans->screen_msg("Renewals not allowed.");
# return $trans;
# } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
# $trans->screen_msg("Invalid patron password.");
# return $trans;
# }
#
# foreach $item_id (@{$patron->{items}}) {
# my $item = new ILS::Item $item_id;
#
# if (!defined($item)) {
# syslog("LOG_WARNING",
# "renew_all: Invalid item id associated with patron '%s'",
# $patron->id);
# next;
# }
#
# if (@{$item->hold_queue}) {
# # Can't renew if there are outstanding holds
# push @{$trans->unrenewed}, $item_id;
# } else {
# $item->{due_date} = time + (14*24*60*60); # two weeks hence
# push @{$trans->renewed}, $item_id;
# }
# }
#
# $trans->ok(1);
#
# return $trans;
#}
1;