Browse Source

adding openncip / opensip SIP2 service

Signed-off-by: Chris Cormack <crc@liblime.com>
Signed-off-by: Joshua Ferraro <jmf@liblime.com>
3.0.x
Ryan Higgins 15 years ago
committed by Joshua Ferraro
parent
commit
7b9b36bd2e
  1. 498
      C4/SIP/ILS.pm
  2. 486
      C4/SIP/ILS.pod
  3. 214
      C4/SIP/ILS/Item.pm
  4. 231
      C4/SIP/ILS/Item.pod
  5. 393
      C4/SIP/ILS/Patron.pm
  6. 210
      C4/SIP/ILS/Patron.pod
  7. 59
      C4/SIP/ILS/Transaction.pm
  8. 42
      C4/SIP/ILS/Transaction/Checkin.pm
  9. 39
      C4/SIP/ILS/Transaction/Checkout.pm
  10. 12
      C4/SIP/ILS/Transaction/FeePayment.pm
  11. 39
      C4/SIP/ILS/Transaction/Hold.pm
  12. 33
      C4/SIP/ILS/Transaction/Renew.pm
  13. 29
      C4/SIP/ILS/Transaction/RenewAll.pm
  14. 26
      C4/SIP/Makefile
  15. 24
      C4/SIP/README
  16. 273
      C4/SIP/SIPServer.pm
  17. 55
      C4/SIP/SIPconfig.xml
  18. 188
      C4/SIP/Sip.pm
  19. 55
      C4/SIP/Sip/Checksum.pm
  20. 105
      C4/SIP/Sip/Configuration.pm
  21. 43
      C4/SIP/Sip/Configuration/Account.pm
  22. 31
      C4/SIP/Sip/Configuration/Institution.pm
  23. 25
      C4/SIP/Sip/Configuration/Service.pm
  24. 339
      C4/SIP/Sip/Constants.pm
  25. 1577
      C4/SIP/Sip/MsgType.pm
  26. 42
      C4/SIP/acstest.py
  27. 26
      C4/SIP/t/00sc_status.t
  28. 80
      C4/SIP/t/01patron_status.t
  29. 172
      C4/SIP/t/02patron_info.t
  30. 209
      C4/SIP/t/03checkout.t
  31. 100
      C4/SIP/t/04patron_status.t
  32. 45
      C4/SIP/t/05block_patron.t
  33. 144
      C4/SIP/t/06patron_enable.t
  34. 187
      C4/SIP/t/07hold.t
  35. 67
      C4/SIP/t/08checkin.t
  36. 147
      C4/SIP/t/09renew.t
  37. 107
      C4/SIP/t/10renew_all.t
  38. 42
      C4/SIP/t/11item_info.t
  39. 16
      C4/SIP/t/Makefile
  40. 50
      C4/SIP/t/README
  41. 225
      C4/SIP/t/SIPtest.pm
  42. 17
      C4/SIP/test.txt
  43. 29
      C4/SIP/xmlparse.pl
  44. 617
      C4/SIP_openils_pm

498
C4/SIP/ILS.pm

@ -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

@ -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

@ -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

@ -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

@ -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;
}