--- /dev/null
+CVS
+.svn
+.bzr
--- /dev/null
+Copyright (C) 2006-2008 Georgia Public Library Service
+
+Author: David J. Fiander
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of version 2 of the GNU General Public
+License as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public
+License along with this program; if not, write to the Free
+Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+MA 02111-1307 USA
--- /dev/null
+#
+# ILS.pm: Test ILS interface module
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+package ILS;
+
+use warnings;
+use strict;
+use Sys::Syslog qw(syslog);
+use Encode;
+
+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' => 1,
+ '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" => 1,
+ );
+
+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(', ', encode_utf8(@{$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;
--- /dev/null
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+=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
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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 Encode;
+
+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 => decode_utf8('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, encode_utf8($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;
+
+ if ($self->{due_date}) {
+ return Sip::timestamp($self->{due_date});
+ } else {
+ return 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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+=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
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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',
+ home_library => 'Beacock',
+ 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';
+}
+
+# Extension requested by PINES. Report the home system for
+# the patron in the 'AQ' field. This is normally the "permanent
+# location" field for an ITEM, but it's not used in PATRON info.
+# Apparently TLC systems do this.
+sub home_library {
+ my $self = shift;
+
+ return $self->{home_library}
+}
+
+#
+# Messages
+#
+
+sub invalid_patron {
+ return "Please contact library staff";
+}
+
+sub charge_denied {
+ return "Please contact library staff";
+}
+
+1;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+=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.
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# An object to handle checkout status
+#
+
+package ILS::Transaction::Checkout;
+
+use warnings;
+use strict;
+
+use POSIX qw(strftime);
+use Sip::Constants qw(SIP_DATETIME);
+
+
+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'} = strftime(SIP_DATETIME,
+ localtime(time() + (60*60*24*14))); # two weeks hence
+
+ return bless $self, $class;
+}
+
+1;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+package ILS::Transaction::FeePaid;
+
+use Exporter;
+use warnings;
+use strict;
+
+use ILS;
+use ILS::Transaction;
+
+our @ISA = qw(Exporter ILS::Transaction);
+
+1;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
--- /dev/null
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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
--- /dev/null
+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::LibXML depends on the C library libxml2
+ 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.
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+package SIPServer;
+
+use strict;
+use warnings;
+use Exporter;
+use Sys::Syslog qw(syslog);
+use Net::Server::PreFork;
+use Net::Server::Proto;
+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;
+
+ $sockaddr = $self->{server}->{sockaddr};
+ $port = $self->{server}->{sockport};
+ $proto = $self->{server}->{client}->NS_proto();
+ syslog('LOG_INFO', "Inbound connection from $sockaddr on port $port and proto $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 = '';
+ }
+}
--- /dev/null
+<!--
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+-->
+<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="scclient" password="clientpwd" institution="UWOLS">
+ </login>
+ <login id="scclient-2" password="clientpwd-2"
+ institution="UWOLS" />
+ <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="UWOLS" 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>
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# Sip.pm: General Sip utility functions
+#
+
+package Sip;
+
+use strict;
+use warnings;
+use English;
+use Exporter;
+use Encode;
+
+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);
+
+ #
+ # Cen-Tec self-check terminals transmit '\r\n' line terminators.
+ # This is actually very hard to deal with in perl in a reasonable
+ # since every OTHER piece of hardware out there gets the protocol
+ # right.
+ #
+ # The incorrect line terminator presents as a \r at the end of the
+ # first record, and then a \n at the BEGINNING of the next record.
+ # So, the simplest thing to do is just throw away a leading newline
+ # on the input.
+ #
+ $record =~ s/^\012// if $record;
+ syslog("LOG_INFO", encode_utf8("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;
+
+ $msg = encode_utf8($msg);
+ 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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+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('%16C*', $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("%16C*", 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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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',
+ 'timeout' => 'value',
+ '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 timeout {
+ my $self = shift;
+
+ return $self->{'timeout'}
+}
+
+sub accounts {
+ my $self = shift;
+
+ return values %{$self->{accounts}};
+}
+
+sub find_service {
+ my ($self, $sockaddr, $port, $proto) = @_;
+ my $portstr;
+
+ $proto = lc($proto);
+ 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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+#
+#
+#
+
+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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+#
+#
+#
+
+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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+#
+#
+#
+
+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;
+}
+
+sub timeout {
+ my $self = shift;
+
+ return $self->{timeout} || $self->SUPER::timeout();
+}
+
+1;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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_HOME_LIBRARY 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_HOME_LIBRARY
+ 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_HOME_LIBRARY
+ 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',
+ FID_HOME_LIBRARY => 'AQ', # Extension: AQ in patron info
+ # 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",
+};
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# Sip::MsgType.pm
+#
+# A Class for handing SIP messages
+#
+
+package Sip::MsgType;
+
+use strict;
+use warnings;
+use Exporter;
+use Sys::Syslog qw(syslog);
+use UNIVERSAL qw(can);
+
+use Sip qw(:all);
+use Sip::Constants qw(:all);
+use Sip::Checksum qw(verify_cksum);
+
+use Data::Dumper;
+
+our (@ISA, @EXPORT_OK);
+
+@ISA = qw(Exporter);
+@EXPORT_OK = qw(handle);
+
+# Predeclare handler subroutines
+use subs qw(handle_patron_status handle_checkout handle_checkin
+ handle_block_patron handle_sc_status handle_request_acs_resend
+ handle_login handle_patron_info handle_end_patron_session
+ handle_fee_paid handle_item_information handle_item_status_update
+ handle_patron_enable handle_hold handle_renew handle_renew_all);
+
+#
+# For the most part, Version 2.00 of the protocol just adds new
+# variable fields, but sometimes it changes the fixed header.
+#
+# In general, if there's no '2.00' protocol entry for a handler, that's
+# because 2.00 didn't extend the 1.00 version of the protocol. This will
+# be handled by the module initialization code following the declaration,
+# which goes through the handlers table and creates a '2.00' entry that
+# points to the same place as the '1.00' entry. If there's a 2.00 entry
+# but no 1.00 entry, then that means that it's a completely new service
+# in 2.00, so 1.00 shouldn't recognize it.
+
+my %handlers = (
+ (PATRON_STATUS_REQ) => {
+ name => "Patron Status Request",
+ handler => \&handle_patron_status,
+ protocol => {
+ 1 => {
+ template => "A3A18",
+ template_len => 21,
+ fields => [(FID_INST_ID), (FID_PATRON_ID),
+ (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
+ }
+ }
+ },
+ (CHECKOUT) => {
+ name => "Checkout",
+ handler => \&handle_checkout,
+ protocol => {
+ 1 => {
+ template => "CCA18A18",
+ template_len => 38,
+ fields => [(FID_INST_ID), (FID_PATRON_ID),
+ (FID_ITEM_ID), (FID_TERMINAL_PWD)],
+ },
+ 2 => {
+ template => "CCA18A18",
+ template_len => 38,
+ fields => [(FID_INST_ID), (FID_PATRON_ID),
+ (FID_ITEM_ID), (FID_TERMINAL_PWD),
+ (FID_ITEM_PROPS), (FID_PATRON_PWD),
+ (FID_FEE_ACK), (FID_CANCEL)],
+ },
+ }
+ },
+ (CHECKIN) => {
+ name => "Checkin",
+ handler => \&handle_checkin,
+ protocol => {
+ 1 => {
+ template => "CA18A18",
+ template_len => 37,
+ fields => [(FID_CURRENT_LOCN), (FID_INST_ID),
+ (FID_ITEM_ID), (FID_TERMINAL_PWD)],
+ },
+ 2 => {
+ template => "CA18A18",
+ template_len => 37,
+ fields => [(FID_CURRENT_LOCN), (FID_INST_ID),
+ (FID_ITEM_ID), (FID_TERMINAL_PWD),
+ (FID_ITEM_PROPS), (FID_CANCEL)],
+ }
+ }
+ },
+ (BLOCK_PATRON) => {
+ name => "Block Patron",
+ handler => \&handle_block_patron,
+ protocol => {
+ 1 => {
+ template => "CA18",
+ template_len => 19,
+ fields => [(FID_INST_ID), (FID_BLOCKED_CARD_MSG),
+ (FID_PATRON_ID), (FID_TERMINAL_PWD)],
+ },
+ }
+ },
+ (SC_STATUS) => {
+ name => "SC Status",
+ handler => \&handle_sc_status,
+ protocol => {
+ 1 => {
+ template =>"CA3A4",
+ template_len => 8,
+ fields => [],
+ }
+ }
+ },
+ (REQUEST_ACS_RESEND) => {
+ name => "Request ACS Resend",
+ handler => \&handle_request_acs_resend,
+ protocol => {
+ 1 => {
+ template => "",
+ template_len => 0,
+ fields => [],
+ }
+ }
+ },
+ (LOGIN) => {
+ name => "Login",
+ handler => \&handle_login,
+ protocol => {
+ 2 => {
+ template => "A1A1",
+ template_len => 2,
+ fields => [(FID_LOGIN_UID), (FID_LOGIN_PWD),
+ (FID_LOCATION_CODE)],
+ }
+ }
+ },
+ (PATRON_INFO) => {
+ name => "Patron Info",
+ handler => \&handle_patron_info,
+ protocol => {
+ 2 => {
+ template => "A3A18A10",
+ template_len => 31,
+ fields => [(FID_INST_ID), (FID_PATRON_ID),
+ (FID_TERMINAL_PWD), (FID_PATRON_PWD),
+ (FID_START_ITEM), (FID_END_ITEM)],
+ }
+ }
+ },
+ (END_PATRON_SESSION) => {
+ name => "End Patron Session",
+ handler => \&handle_end_patron_session,
+ protocol => {
+ 2 => {
+ template => "A18",
+ template_len => 18,
+ fields => [(FID_INST_ID), (FID_PATRON_ID),
+ (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
+ }
+ }
+ },
+ (FEE_PAID) => {
+ name => "Fee Paid",
+ handler => \&handle_fee_paid,
+ protocol => {
+ 2 => {
+ template => "A18A2A3",
+ template_len => 0,
+ fields => [(FID_FEE_AMT), (FID_INST_ID),
+ (FID_PATRON_ID), (FID_TERMINAL_PWD),
+ (FID_PATRON_PWD), (FID_FEE_ID),
+ (FID_TRANSACTION_ID)],
+ }
+ }
+ },
+ (ITEM_INFORMATION) => {
+ name => "Item Information",
+ handler => \&handle_item_information,
+ protocol => {
+ 2 => {
+ template => "A18",
+ template_len => 18,
+ fields => [(FID_INST_ID), (FID_ITEM_ID),
+ (FID_TERMINAL_PWD)],
+ }
+ }
+ },
+ (ITEM_STATUS_UPDATE) => {
+ name => "Item Status Update",
+ handler => \&handle_item_status_update,
+ protocol => {
+ 2 => {
+ template => "A18",
+ template_len => 18,
+ fields => [(FID_INST_ID), (FID_PATRON_ID),
+ (FID_ITEM_ID), (FID_TERMINAL_PWD),
+ (FID_ITEM_PROPS)],
+ }
+ }
+ },
+ (PATRON_ENABLE) => {
+ name => "Patron Enable",
+ handler => \&handle_patron_enable,
+ protocol => {
+ 2 => {
+ template => "A18",
+ template_len => 18,
+ fields => [(FID_INST_ID), (FID_PATRON_ID),
+ (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
+ }
+ }
+ },
+ (HOLD) => {
+ name => "Hold",
+ handler => \&handle_hold,
+ protocol => {
+ 2 => {
+ template => "AA18",
+ template_len => 19,
+ fields => [(FID_EXPIRATION), (FID_PICKUP_LOCN),
+ (FID_HOLD_TYPE), (FID_INST_ID),
+ (FID_PATRON_ID), (FID_PATRON_PWD),
+ (FID_ITEM_ID), (FID_TITLE_ID),
+ (FID_TERMINAL_PWD), (FID_FEE_ACK)],
+ }
+ }
+ },
+ (RENEW) => {
+ name => "Renew",
+ handler => \&handle_renew,
+ protocol => {
+ 2 => {
+ template => "CCA18A18",
+ template_len => 38,
+ fields => [(FID_INST_ID), (FID_PATRON_ID),
+ (FID_PATRON_PWD), (FID_ITEM_ID),
+ (FID_TITLE_ID), (FID_TERMINAL_PWD),
+ (FID_ITEM_PROPS), (FID_FEE_ACK)],
+ }
+ }
+ },
+ (RENEW_ALL) => {
+ name => "Renew All",
+ handler => \&handle_renew_all,
+ protocol => {
+ 2 => {
+ template => "A18",
+ template_len => 18,
+ fields => [(FID_INST_ID), (FID_PATRON_ID),
+ (FID_PATRON_PWD), (FID_TERMINAL_PWD),
+ (FID_FEE_ACK)],
+ }
+ }
+ }
+ );
+
+#
+# Now, initialize some of the missing bits of %handlers
+#
+foreach my $i (keys(%handlers)) {
+ if (!exists($handlers{$i}->{protocol}->{2})) {
+
+ $handlers{$i}->{protocol}->{2} = $handlers{$i}->{protocol}->{1};
+ }
+}
+
+sub new {
+ my ($class, $msg, $seqno) = @_;
+ my $self = {};
+ my $msgtag = substr($msg, 0, 2);
+
+ syslog("LOG_DEBUG", "Sip::MsgType::new('%s', '%s', '%s'): msgtag '%s'",
+ $class, substr($msg, 0, 10), $msgtag, $seqno);
+ if ($msgtag eq LOGIN) {
+ # If the client is using the 2.00-style "Login" message
+ # to authenticate to the server, then we get the Login message
+ # _before_ the client has indicated that it supports 2.00, but
+ # it's using the 2.00 login process, so it must support 2.00,
+ # so we'll just do it.
+ $protocol_version = 2;
+ }
+ if (!exists($handlers{$msgtag})) {
+ syslog("LOG_WARNING",
+ "new Sip::MsgType: Skipping message of unknown type '%s' in '%s'",
+ $msgtag, $msg);
+ return(undef);
+ } elsif (!exists($handlers{$msgtag}->{protocol}->{$protocol_version})) {
+ syslog("LOG_WARNING", "new Sip::MsgType: Skipping message '%s' unsupported by protocol rev. '%d'",
+ $msgtag, $protocol_version);
+ return(undef);
+ }
+
+ bless $self, $class;
+
+ $self->{seqno} = $seqno;
+ $self->_initialize(substr($msg,2), $handlers{$msgtag});
+
+ return($self);
+}
+
+sub _initialize {
+ my ($self, $msg, $control_block) = @_;
+ my ($fs, $fn, $fe);
+ my $proto = $control_block->{protocol}->{$protocol_version};
+
+ $self->{name} = $control_block->{name};
+ $self->{handler} = $control_block->{handler};
+
+ $self->{fields} = {};
+ $self->{fixed_fields} = [];
+
+ syslog("LOG_DEBUG", "Sip::MsgType:_initialize('%s', '%s...')",
+ $self->{name}, substr($msg, 0, 20));
+
+ foreach my $field (@{$proto->{fields}}) {
+ $self->{fields}->{$field} = undef;
+ }
+
+ syslog("LOG_DEBUG",
+ "Sip::MsgType::_initialize('%s', '%s', '%s', '%s', ...",
+ $self->{name}, $msg, $proto->{template},
+ $proto->{template_len});
+
+ $self->{fixed_fields} = [ unpack($proto->{template}, $msg) ];
+
+ # Skip over the fixed fields and the split the rest of
+ # the message into fields based on the delimiter and parse them
+ foreach my $field (split(quotemeta($field_delimiter), substr($msg, $proto->{template_len}))) {
+ $fn = substr($field, 0, 2);
+
+ if (!exists($self->{fields}->{$fn})) {
+ syslog("LOG_WARNING",
+ "Unsupported field '%s' in %s message '%s'",
+ $fn, $self->{name}, $msg);
+ } elsif (defined($self->{fields}->{$fn})) {
+ syslog("LOG_WARNING",
+ "Duplicate field '%s' (previous value '%s') in %s message '%s'",
+ $fn, $self->{fields}->{$fn}, $self->{name}, $msg);
+ } else {
+ $self->{fields}->{$fn} = substr($field, 2);
+ }
+ }
+
+ return($self);
+}
+
+sub handle {
+ my ($msg, $server, $req) = @_;
+ my $config = $server->{config};
+ my $self;
+
+
+ #
+ # What's the field delimiter for variable length fields?
+ # This can't be based on the account, since we need to know
+ # the field delimiter to parse a SIP login message
+ #
+ if (defined($server->{config}->{delimiter})) {
+ $field_delimiter = $server->{config}->{delimiter};
+ }
+
+ # error detection is active if this is a REQUEST_ACS_RESEND
+ # message with a checksum, or if the message is long enough
+ # and the last nine characters begin with a sequence number
+ # field
+ if ($msg eq REQUEST_ACS_RESEND_CKSUM) {
+ # Special case
+
+ $error_detection = 1;
+ $self = new Sip::MsgType ((REQUEST_ACS_RESEND), 0);
+ } elsif((length($msg) > 11) && (substr($msg, -9, 2) eq "AY")) {
+ $error_detection = 1;
+
+ if (!verify_cksum($msg)) {
+ syslog("LOG_WARNING", "Checksum failed on message '%s'", $msg);
+ # REQUEST_SC_RESEND with error detection
+ $last_response = REQUEST_SC_RESEND_CKSUM;
+ print("$last_response\r");
+ return REQUEST_ACS_RESEND;
+ } else {
+ # Save the sequence number, then strip off the
+ # error detection data to process the message
+ $self = new Sip::MsgType (substr($msg, 0, -9), substr($msg, -7, 1));
+ }
+ } elsif ($error_detection) {
+ # We've receive a non-ED message when ED is supposed
+ # to be active. Warn about this problem, then process
+ # the message anyway.
+ syslog("LOG_WARNING",
+ "Received message without error detection: '%s'", $msg);
+ $error_detection = 0;
+ $self = new Sip::MsgType ($msg, 0);
+ } else {
+ $self = new Sip::MsgType ($msg, 0);
+ }
+
+ if ((substr($msg, 0, 2) ne REQUEST_ACS_RESEND) &&
+ $req && (substr($msg, 0, 2) ne $req)) {
+ return substr($msg, 0, 2);
+ }
+ return($self->{handler}->($self, $server));
+}
+
+##
+## Message Handlers
+##
+
+#
+# Patron status messages are produced in response to both
+# "Request Patron Status" and "Block Patron"
+#
+# Request Patron Status requires a patron password, but
+# Block Patron doesn't (since the patron may never have
+# provided one before attempting some illegal action).
+#
+# ASSUMPTION: If the patron password field is present in the
+# message, then it must match, otherwise incomplete patron status
+# information will be returned to the terminal.
+#
+sub build_patron_status {
+ my ($patron, $lang, $fields)= @_;
+ my $patron_pwd = $fields->{(FID_PATRON_PWD)};
+ my $resp = (PATRON_STATUS_RESP);
+
+ if ($patron) {
+ $resp .= patron_status_string($patron);
+ $resp .= $lang . Sip::timestamp();
+ $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
+
+ # while the patron ID we got from the SC is valid, let's
+ # use the one returned from the ILS, just in case...
+ $resp .= add_field(FID_PATRON_ID, $patron->id);
+ if ($protocol_version >= 2) {
+ $resp .= add_field(FID_VALID_PATRON, 'Y');
+ # If the patron password field doesn't exist, then
+ # we can't report that the password was valid, now can
+ # we? But if it does exist, then we know it's valid.
+ if (defined($patron_pwd)) {
+ $resp .= add_field(FID_VALID_PATRON_PWD,
+ sipbool($patron->check_password($patron_pwd)));
+ }
+ $resp .= maybe_add(FID_CURRENCY, $patron->currency);
+ $resp .= maybe_add(FID_FEE_AMT, $patron->fee_amount);
+ }
+
+ $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
+ } else {
+ # Invalid patron id. Report that the user has no privs.,
+ # no personal name, and is invalid (if we're using 2.00)
+ $resp .= 'YYYY' . (' ' x 10) . $lang . Sip::timestamp();
+ $resp .= add_field(FID_PERSONAL_NAME, '');
+
+ # the patron ID is invalid, but it's a required field, so
+ # just echo it back
+ $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
+
+ if ($protocol_version >= 2) {
+ $resp .= add_field(FID_VALID_PATRON, 'N');
+ }
+ }
+
+ $resp .= add_field(FID_INST_ID, $fields->{(FID_INST_ID)});
+
+ return $resp;
+}
+
+sub handle_patron_status {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my ($lang, $date);
+ my $fields;
+ my $patron;
+ my $resp = (PATRON_STATUS_RESP);
+ my $account = $server->{account};
+
+ ($lang, $date) = @{$self->{fixed_fields}};
+ $fields = $self->{fields};
+
+ $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_patron_status");
+
+ $patron = $ils->find_patron($fields->{(FID_PATRON_ID)});
+
+ $resp = build_patron_status($patron, $lang, $fields);
+
+ $self->write_msg($resp);
+
+ return (PATRON_STATUS_REQ);
+}
+
+sub handle_checkout {
+ my ($self, $server) = @_;
+ my $account = $server->{account};
+ my $ils = $server->{ils};
+ my $inst = $ils->institution;
+ my ($sc_renewal_policy, $no_block, $trans_date, $nb_due_date);
+ my $fields;
+ my ($patron_id, $item_id, $status);
+ my ($item, $patron);
+ my $resp;
+
+ ($sc_renewal_policy, $no_block, $trans_date, $nb_due_date) =
+ @{$self->{fixed_fields}};
+ $fields = $self->{fields};
+
+ $patron_id = $fields->{(FID_PATRON_ID)};
+ $item_id = $fields->{(FID_ITEM_ID)};
+
+
+ if ($no_block eq 'Y') {
+ # Off-line transactions need to be recorded, but there's
+ # not a lot we can do about it
+ syslog("LOG_WARN", "received no-block checkout from terminal '%s'",
+ $account->{id});
+
+ $status = $ils->checkout_no_block($patron_id, $item_id,
+ $sc_renewal_policy,
+ $trans_date, $nb_due_date);
+ } else {
+ # Does the transaction date really matter for items that are
+ # checkout out while the terminal is online? I'm guessing 'no'
+ $status = $ils->checkout($patron_id, $item_id, $sc_renewal_policy);
+ }
+
+
+ $item = $status->item;
+ $patron = $status->patron;
+
+ if ($status->ok) {
+ # Item successfully checked out
+ # Fixed fields
+ $resp = CHECKOUT_RESP . '1';
+ $resp .= sipbool($status->renew_ok);
+ if ($ils->supports('magnetic media')) {
+ $resp .= sipbool($item->magnetic);
+ } else {
+ $resp .= 'U';
+ }
+ # We never return the obsolete 'U' value for 'desensitize'
+ $resp .= sipbool($status->desensitize);
+ $resp .= Sip::timestamp;
+
+ # Now for the variable fields
+ $resp .= add_field(FID_INST_ID, $inst);
+ $resp .= add_field(FID_PATRON_ID, $patron_id);
+ $resp .= add_field(FID_ITEM_ID, $item_id);
+ $resp .= add_field(FID_TITLE_ID, $item->title_id);
+ $resp .= add_field(FID_DUE_DATE, $item->due_date);
+
+ $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
+
+ if ($protocol_version >= 2) {
+ if ($ils->supports('security inhibit')) {
+ $resp .= add_field(FID_SECURITY_INHIBIT,
+ $status->security_inhibit);
+ }
+ $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type);
+ $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
+
+ # Financials
+ if ($status->fee_amount) {
+ $resp .= add_field(FID_FEE_AMT, $status->fee_amount);
+ $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
+ $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
+ $resp .= maybe_add(FID_TRANSACTION_ID,
+ $status->transaction_id);
+ }
+ }
+
+ } else {
+ # Checkout failed
+ # Checkout Response: not ok, no renewal, don't know mag. media,
+ # no desensitize
+ $resp = sprintf("120NUN%s", Sip::timestamp);
+ $resp .= add_field(FID_INST_ID, $inst);
+ $resp .= add_field(FID_PATRON_ID, $patron_id);
+ $resp .= add_field(FID_ITEM_ID, $item_id);
+
+ # If the item is valid, provide the title, otherwise
+ # leave it blank
+ $resp .= add_field(FID_TITLE_ID, $item ? $item->title_id : '');
+ # Due date is required. Since it didn't get checked out,
+ # it's not due, so leave the date blank
+ $resp .= add_field(FID_DUE_DATE, '');
+
+ $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
+
+ if ($protocol_version >= 2) {
+ # Is the patron ID valid?
+ $resp .= add_field(FID_VALID_PATRON, sipbool($patron));
+
+ if ($patron && exists($fields->{FID_PATRON_PWD})) {
+ # Password provided, so we can tell if it was valid or not
+ $resp .= add_field(FID_VALID_PATRON_PWD,
+ sipbool($patron->check_password($fields->{(FID_PATRON_PWD)})));
+ }
+ }
+ }
+
+ $self->write_msg($resp);
+ return(CHECKOUT);
+}
+
+sub handle_checkin {
+ my ($self, $server) = @_;
+ my $account = $server->{account};
+ my $ils = $server->{ils};
+ my ($no_block, $trans_date, $return_date);
+ my $fields;
+ my ($current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel);
+ my $resp = CHECKIN_RESP;
+ my ($patron, $item);
+ my $status;
+
+ ($no_block, $trans_date, $return_date) = @{$self->{fixed_fields}};
+ $fields = $self->{fields};
+
+ $current_loc = $fields->{(FID_CURRENT_LOCN)};
+ $inst_id = $fields->{(FID_INST_ID)};
+ $item_id = $fields->{(FID_ITEM_ID)};
+ $item_props = $fields->{(FID_ITEM_PROPS)};
+ $cancel = $fields->{(FID_CANCEL)};
+
+ $ils->check_inst_id($inst_id, "handle_checkin");
+
+ if ($no_block eq 'Y') {
+ # Off-line transactions, ick.
+ syslog("LOG_WARN", "received no-block checkin from terminal '%s'",
+ $account->{id});
+ $status = $ils->checkin_no_block($item_id, $trans_date,
+ $return_date, $item_props, $cancel);
+ } else {
+ $status = $ils->checkin($item_id, $trans_date, $return_date,
+ $current_loc, $item_props, $cancel);
+ }
+
+ $patron = $status->patron;
+ $item = $status->item;
+
+ $resp .= $status->ok ? '1' : '0';
+ $resp .= $status->resensitize ? 'Y' : 'N';
+ if ($item && $ils->supports('magnetic media')) {
+ $resp .= sipbool($item->magnetic);
+ } else {
+ # The item barcode was invalid or the system doesn't support
+ # the 'magnetic media' indicator
+ $resp .= 'U';
+ }
+ $resp .= $status->alert ? 'Y' : 'N';
+ $resp .= Sip::timestamp;
+ $resp .= add_field(FID_INST_ID, $inst_id);
+ $resp .= add_field(FID_ITEM_ID, $item_id);
+
+ if ($item) {
+ $resp .= add_field(FID_PERM_LOCN, $item->permanent_location);
+ $resp .= maybe_add(FID_TITLE_ID, $item->title_id);
+ }
+
+ if ($protocol_version >= 2) {
+ $resp .= maybe_add(FID_SORT_BIN, $status->sort_bin);
+ if ($patron) {
+ $resp .= add_field(FID_PATRON_ID, $patron->id);
+ }
+ if ($item) {
+ $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type);
+ $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
+ }
+ }
+
+ $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
+
+ $self->write_msg($resp);
+
+ return(CHECKIN);
+}
+
+sub handle_block_patron {
+ my ($self, $server) = @_;
+ my $account = $server->{account};
+ my $ils = $server->{ils};
+ my ($card_retained, $trans_date);
+ my ($inst_id, $blocked_card_msg, $patron_id, $terminal_pwd);
+ my $fields;
+ my $resp;
+ my $patron;
+
+ ($card_retained, $trans_date) = @{$self->{fixed_fields}};
+ $fields = $self->{fields};
+ $inst_id = $fields->{(FID_INST_ID)};
+ $blocked_card_msg = $fields->{(FID_BLOCKED_CARD_MSG)};
+ $patron_id = $fields->{(FID_PATRON_ID)};
+ $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
+
+ # Terminal passwords are different from account login
+ # passwords, but I have no idea what to do with them. So,
+ # I'll just ignore them for now.
+
+ $ils->check_inst_id($inst_id, "block_patron");
+
+ $patron = $ils->find_patron($patron_id);
+
+ # The correct response for a "Block Patron" message is a
+ # "Patron Status Response", so use that handler to generate
+ # the message, but then return the correct code from here.
+ #
+ # Normally, the language is provided by the "Patron Status"
+ # fixed field, but since we're not responding to one of those
+ # we'll just say, "Unspecified", as per the spec. Let the
+ # terminal default to something that, one hopes, will be
+ # intelligible
+ if ($patron) {
+ # Valid patron id
+ $patron->block($card_retained, $blocked_card_msg);
+ }
+
+ $resp = build_patron_status($patron, $patron->language, $fields);
+
+ $self->write_msg($resp);
+ return(BLOCK_PATRON);
+}
+
+sub handle_sc_status {
+ my ($self, $server) = @_;
+ my ($status, $print_width, $sc_protocol_version, $new_proto);
+
+ ($status, $print_width, $sc_protocol_version) = @{$self->{fixed_fields}};
+
+ if ($sc_protocol_version =~ /^1\./) {
+ $new_proto = 1;
+ } elsif ($sc_protocol_version =~ /^2\./) {
+ $new_proto = 2;
+ } else {
+ syslog("LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version);
+ $new_proto = 1;
+ }
+
+ if ($new_proto != $protocol_version) {
+ syslog("LOG_INFO", "Setting protocol level to $new_proto");
+ $protocol_version = $new_proto;
+ }
+
+ if ($status == SC_STATUS_PAPER) {
+ syslog("LOG_WARN", "Self-Check unit '%s@%s' out of paper",
+ $self->{account}->{id}, $self->{account}->{institution});
+ } elsif ($status == SC_STATUS_SHUTDOWN) {
+ syslog("LOG_WARN", "Self-Check unit '%s@%s' shutting down",
+ $self->{account}->{id}, $self->{account}->{institution});
+ }
+
+ $self->{account}->{print_width} = $print_width;
+
+ return send_acs_status($self, $server) ? SC_STATUS : '';
+}
+
+sub handle_request_acs_resend {
+ my ($self, $server) = @_;
+
+ if (!$last_response) {
+ # We haven't sent anything yet, so respond with a
+ # REQUEST_SC_RESEND msg (p. 16)
+ $self->write_msg(REQUEST_SC_RESEND);
+ } elsif ((length($last_response) < 9)
+ || substr($last_response, -9, 2) ne 'AY') {
+ # When resending a message, we aren't supposed to include
+ # a sequence number, even if the original had one (p. 4).
+ # If the last message didn't have a sequence number, then
+ # we can just send it.
+ print("$last_response\r");
+ } else {
+ my $rebuilt;
+
+ # Cut out the sequence number and checksum, since the old
+ # checksum is wrong for the resent message.
+ $rebuilt = substr($last_response, 0, -9);
+ $self->write_msg($rebuilt);
+ }
+
+ return REQUEST_ACS_RESEND;
+}
+
+sub handle_login {
+ my ($self, $server) = @_;
+ my ($uid_algorithm, $pwd_algorithm);
+ my ($uid, $pwd);
+ my $inst;
+ my $fields;
+ my $status = 1; # Assume it all works
+
+ $fields = $self->{fields};
+ ($uid_algorithm, $pwd_algorithm) = @{$self->{fixed_fields}};
+
+ $uid = $fields->{(FID_LOGIN_UID)};
+ $pwd = $fields->{(FID_LOGIN_PWD)};
+
+ if ($uid_algorithm || $pwd_algorithm) {
+ syslog("LOG_ERR", "LOGIN: Can't cope with non-zero encryption methods: uid = $uid_algorithm, pwd = $pwd_algorithm");
+ $status = 0;
+ }
+
+ if (!exists($server->{config}->{accounts}->{$uid})) {
+ syslog("LOG_WARNING", "MsgType::handle_login: Unknown login '$uid'");
+ $status = 0;
+ } elsif ($server->{config}->{accounts}->{$uid}->{password} ne $pwd) {
+ syslog("LOG_WARNING",
+ "MsgType::handle_login: Invalid password for login '$uid'");
+ $status = 0;
+ } else {
+ # Store the active account someplace handy for everybody else to find.
+ $server->{account} = $server->{config}->{accounts}->{$uid};
+ $inst = $server->{account}->{institution};
+ $server->{institution} = $server->{config}->{institutions}->{$inst};
+ $server->{policy} = $server->{institution}->{policy};
+
+
+ syslog("LOG_INFO", "Successful login for '%s' of '%s'",
+ $server->{account}->{id}, $inst);
+ #
+ # initialize connection to ILS
+ #
+ my $module = $server->{config}
+ ->{institutions}
+ ->{ $inst }
+ ->{implementation};
+ $module->use;
+
+ if ($@) {
+ syslog("LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed",
+ $server->{service}, $module, $inst);
+ die("Failed to load ILS implementation '$module'");
+ }
+
+ $server->{ils} = $module->new($server->{institution}, $server->{account});
+
+ if (!$server->{ils}) {
+ syslog("LOG_ERR", "%s: ILS connection to '%s' failed",
+ $server->{service}, $inst);
+ die("Unable to connect to ILS '$inst'");
+ }
+ }
+
+ $self->write_msg(LOGIN_RESP . $status);
+
+ return $status ? LOGIN : '';
+}
+
+#
+# Build the detailed summary information for the Patron
+# Information Response message based on the first 'Y' that appears
+# in the 'summary' field of the Patron Information reqest. The
+# specification says that only one 'Y' can appear in that field,
+# and we're going to believe it.
+#
+sub summary_info {
+ my ($ils, $patron, $summary, $start, $end) = @_;
+ my $resp = '';
+ my $itemlist;
+ my $summary_type;
+ my ($func, $fid);
+ #
+ # Map from offsets in the "summary" field of the Patron Information
+ # message to the corresponding field and handler
+ #
+ my @summary_map = (
+ { func => $patron->can("hold_items"),
+ fid => FID_HOLD_ITEMS },
+ { func => $patron->can("overdue_items"),
+ fid => FID_OVERDUE_ITEMS },
+ { func => $patron->can("charged_items"),
+ fid => FID_CHARGED_ITEMS },
+ { func => $patron->can("fine_items"),
+ fid => FID_FINE_ITEMS },
+ { func => $patron->can("recall_items"),
+ fid => FID_RECALL_ITEMS },
+ { func => $patron->can("unavail_holds"),
+ fid => FID_UNAVAILABLE_HOLD_ITEMS },
+ );
+
+
+ if (($summary_type = index($summary, 'Y')) == -1) {
+ # No detailed information required
+ return '';
+ }
+
+ syslog("LOG_DEBUG", "Summary_info: index == '%d', field '%s'",
+ $summary_type, $summary_map[$summary_type]->{fid});
+
+ $func = $summary_map[$summary_type]->{func};
+ $fid = $summary_map[$summary_type]->{fid};
+ $itemlist = &$func($patron, $start, $end);
+
+ syslog("LOG_DEBUG", "summary_info: list = (%s)", join(", ", @{$itemlist}));
+ foreach my $i (@{$itemlist}) {
+ $resp .= add_field($fid, $i);
+ }
+
+ return $resp;
+}
+
+sub handle_patron_info {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my ($lang, $trans_date, $summary) = @{$self->{fixed_fields}};
+ my $fields = $self->{fields};
+ my ($inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end);
+ my ($resp, $patron, $count);
+
+ $inst_id = $fields->{(FID_INST_ID)};
+ $patron_id = $fields->{(FID_PATRON_ID)};
+ $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
+ $patron_pwd = $fields->{(FID_PATRON_PWD)};
+ $start = $fields->{(FID_START_ITEM)};
+ $end = $fields->{(FID_END_ITEM)};
+
+ $patron = $ils->find_patron($patron_id);
+
+ $resp = (PATRON_INFO_RESP);
+ if ($patron) {
+ $resp .= patron_status_string($patron);
+ $resp .= $lang . Sip::timestamp();
+
+ $resp .= add_count('patron_info/hold_items',
+ scalar @{$patron->hold_items});
+ $resp .= add_count('patron_info/overdue_items',
+ scalar @{$patron->overdue_items});
+ $resp .= add_count('patron_info/charged_items',
+ scalar @{$patron->charged_items});
+ $resp .= add_count('patron_info/fine_items',
+ scalar @{$patron->fine_items});
+ $resp .= add_count('patron_info/recall_items',
+ scalar @{$patron->recall_items});
+ $resp .= add_count('patron_info/unavail_holds',
+ scalar @{$patron->unavail_holds});
+
+ # while the patron ID we got from the SC is valid, let's
+ # use the one returned from the ILS, just in case...
+ $resp .= add_field(FID_PATRON_ID, $patron->id);
+
+ $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
+
+ # TODO: add code for the fields
+ # hold items limit
+ # overdue items limit
+ # charged items limit
+ # fee limit
+
+ $resp .= maybe_add(FID_CURRENCY, $patron->currency);
+ $resp .= maybe_add(FID_FEE_AMT, $patron->fee_amount);
+
+ $resp .= maybe_add(FID_HOME_ADDR,$patron->address);
+ $resp .= maybe_add(FID_EMAIL, $patron->email_addr);
+ $resp .= maybe_add(FID_HOME_PHONE, $patron->home_phone);
+
+ # Extension requested by PINES. Report the home system for
+ # the patron in the 'AQ' field. This is normally the "permanent
+ # location" field for an ITEM, but it's not used in PATRON info.
+ # Apparently TLC systems do this.
+ $resp .= maybe_add(FID_HOME_LIBRARY, $patron->home_library);
+
+ $resp .= summary_info($ils, $patron, $summary, $start, $end);
+
+ $resp .= add_field(FID_VALID_PATRON, 'Y');
+ if (defined($patron_pwd)) {
+ # If the patron password was provided, report on if
+ # it was right.
+ $resp .= add_field(FID_VALID_PATRON_PWD,
+ sipbool($patron->check_password($patron_pwd)));
+ }
+
+ # SIP 2.0 extensions used by Envisionware
+ # Other types of terminals will ignore the fields, if
+ # they don't recognize the codes
+ $resp .= maybe_add(FID_PATRON_BIRTHDATE, $patron->sip_birthdate);
+ $resp .= maybe_add(FID_PATRON_CLASS, $patron->ptype);
+
+ # Custom protocol extension to report patron internet privileges
+ $resp .= maybe_add(FID_INET_PROFILE, $patron->inet_privileges);
+
+ $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
+ } else {
+ # Invalid patron ID
+ # He has no privileges, no items associated with him,
+ # no personal name, and is invalid (if we're using 2.00)
+ $resp .= 'YYYY' . (' ' x 10) . $lang . Sip::timestamp();
+ $resp .= '0000' x 6;
+ $resp .= add_field(FID_PERSONAL_NAME, '');
+
+ # the patron ID is invalid, but it's a required field, so
+ # just echo it back
+ $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
+
+ if ($protocol_version >= 2) {
+ $resp .= add_field(FID_VALID_PATRON, 'N');
+ }
+ }
+
+ $resp .= add_field(FID_INST_ID, $server->{ils}->institution);
+
+ $self->write_msg($resp);
+
+ return(PATRON_INFO);
+}
+
+sub handle_end_patron_session {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my $trans_date;
+ my $fields = $self->{fields};
+ my $resp = END_SESSION_RESP;
+ my ($status, $screen_msg, $print_line);
+
+ ($trans_date) = @{$self->{fixed_fields}};
+
+ $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_end_patron_session");
+
+ ($status, $screen_msg, $print_line) = $ils->end_patron_session($fields->{(FID_PATRON_ID)});
+
+ $resp .= $status ? 'Y' : 'N';
+ $resp .= Sip::timestamp();
+
+ $resp .= add_field(FID_INST_ID, $server->{ils}->institution);
+ $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
+
+ $resp .= maybe_add(FID_SCREEN_MSG, $screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $print_line);
+
+ $self->write_msg($resp);
+
+ return(END_PATRON_SESSION);
+}
+
+sub handle_fee_paid {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my ($trans_date, $fee_type, $pay_type, $currency) = $self->{fixed_fields};
+ my $fields = $self->{fields};
+ my ($fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd);
+ my ($fee_id, $trans_id);
+ my $status;
+ my $resp = FEE_PAID_RESP;
+
+ $fee_amt = $fields->{(FID_FEE_AMT)};
+ $inst_id = $fields->{(FID_INST_ID)};
+ $patron_id = $fields->{(FID_PATRON_ID)};
+ $patron_pwd = $fields->{(FID_PATRON_PWD)};
+ $fee_id = $fields->{(FID_FEE_ID)};
+ $trans_id = $fields->{(FID_TRANSACTION_ID)};
+
+ $ils->check_inst_id($inst_id, "handle_fee_paid");
+
+ $status = $ils->pay_fee($patron_id, $patron_pwd, $fee_amt, $fee_type,
+ $pay_type, $fee_id, $trans_id, $currency);
+
+ $resp .= ($status->ok ? 'Y' : 'N') . Sip::timestamp;
+ $resp .= add_field(FID_INST_ID, $inst_id);
+ $resp .= add_field(FID_PATRON_ID, $patron_id);
+ $resp .= maybe_add(FID_TRANSACTION_ID, $status->transaction_id);
+ $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
+
+ $self->write_msg($resp);
+
+ return(FEE_PAID);
+}
+
+sub handle_item_information {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my $trans_date;
+ my $fields = $self->{fields};
+ my $resp = ITEM_INFO_RESP;
+ my $item;
+ my $i;
+
+ ($trans_date) = @{$self->{fixed_fields}};
+
+ $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_item_information");
+
+ $item = $ils->find_item($fields->{(FID_ITEM_ID)});
+
+ if (!defined($item)) {
+ # Invalid Item ID
+ # "Other" circ stat, "Other" security marker, "Unknown" fee type
+ $resp .= "010101";
+ $resp .= Sip::timestamp;
+ # Just echo back the invalid item id
+ $resp .= add_field(FID_ITEM_ID, $fields->{(FID_ITEM_ID)});
+ # title id is required, but we don't have one
+ $resp .= add_field(FID_TITLE_ID, '');
+ } else {
+ # Valid Item ID, send the good stuff
+ $resp .= $item->sip_circulation_status;
+ $resp .= $item->sip_security_marker;
+ $resp .= $item->sip_fee_type;
+ $resp .= Sip::timestamp;
+
+ $resp .= add_field(FID_ITEM_ID, $item->id);
+ $resp .= add_field(FID_TITLE_ID, $item->title_id);
+
+ $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type);
+ $resp .= maybe_add(FID_PERM_LOCN, $item->permanent_location);
+ $resp .= maybe_add(FID_CURRENT_LOCN, $item->current_location);
+ $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
+
+ $i = $item->fee;
+ if ($i != 0) {
+ $resp .= add_field(FID_CURRENCY, $item->fee_currency);
+ $resp .= add_field(FID_FEE_AMT, $i);
+ }
+ $resp .= maybe_add(FID_OWNER, $item->owner);
+
+ $i = scalar @{$item->hold_queue};
+ if ($i > 0) {
+ $resp .= add_field(FID_HOLD_QUEUE_LEN, $i);
+ }
+ $i = $item->due_date;
+ if ($i) {
+ $resp .= add_field(FID_DUE_DATE, $i);
+ }
+ $i = $item->recall_date;
+ if ($i) {
+ $resp .= add_field(FID_RECALL_DATE, $i);
+ }
+ $i = $item->hold_pickup_date;
+ if ($i) {
+ $resp .= add_field(FID_HOLD_PICKUP_DATE, $i);
+ }
+
+ $resp .= maybe_add(FID_SCREEN_MSG, $item->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $item->print_line);
+ }
+
+ $self->write_msg($resp);
+
+ return(ITEM_INFORMATION);
+}
+
+sub handle_item_status_update {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my ($trans_date, $item_id, $terminal_pwd, $item_props);
+ my $fields = $self->{fields};
+ my $status;
+ my $item;
+ my $resp = ITEM_STATUS_UPDATE_RESP;
+
+ ($trans_date) = @{$self->{fixed_fields}};
+
+ $ils->check_inst_id($fields->{(FID_INST_ID)});
+
+ $item_id = $fields->{(FID_ITEM_ID)};
+ $item_props = $fields->{(FID_ITEM_PROPS)};
+
+ if (!defined($item_id)) {
+ syslog("LOG_WARNING",
+ "handle_item_status: received message without Item ID field");
+ } else {
+ $item = $ils->find_item($item_id);
+ }
+
+ if (!$item) {
+ # Invalid Item ID
+ $resp .= '0';
+ $resp .= Sip::timestamp;
+ $resp .= add_field(FID_ITEM_ID, $item_id);
+ } else {
+ # Valid Item ID
+
+ $status = $item->status_update($item_props);
+
+ $resp .= $status->ok ? '1' : '0';
+ $resp .= Sip::timestamp;
+
+ $resp .= add_field(FID_ITEM_ID, $item->id);
+ $resp .= add_field(FID_TITLE_ID, $item->title_id);
+ $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
+ }
+
+ $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
+
+ $self->write_msg($resp);
+
+ return(ITEM_STATUS_UPDATE);
+}
+
+sub handle_patron_enable {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my $fields = $self->{fields};
+ my ($trans_date, $patron_id, $terminal_pwd, $patron_pwd);
+ my ($status, $patron);
+ my $resp = PATRON_ENABLE_RESP;
+
+ ($trans_date) = @{$self->{fixed_fields}};
+ $patron_id = $fields->{(FID_PATRON_ID)};
+ $patron_pwd = $fields->{(FID_PATRON_PWD)};
+
+ syslog("LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'",
+ $patron_id, $patron_pwd);
+
+ $patron = $ils->find_patron($patron_id);
+
+ if (!defined($patron)) {
+ # Invalid patron ID
+ $resp .= 'YYYY' . (' ' x 10) . '000' . Sip::timestamp();
+ $resp .= add_field(FID_PATRON_ID, $patron_id);
+ $resp .= add_field(FID_PERSONAL_NAME, '');
+ $resp .= add_field(FID_VALID_PATRON, 'N');
+ $resp .= add_field(FID_VALID_PATRON_PWD, 'N');
+ } else {
+ # valid patron
+ if (!defined($patron_pwd) || $patron->check_password($patron_pwd)) {
+ # Don't enable the patron if there was an invalid password
+ $status = $patron->enable;
+ }
+ $resp .= patron_status_string($patron);
+ $resp .= $patron->language . Sip::timestamp();
+
+ $resp .= add_field(FID_PATRON_ID, $patron->id);
+ $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
+ if (defined($patron_pwd)) {
+ $resp .= add_field(FID_VALID_PATRON_PWD,
+ sipbool($patron->check_password($patron_pwd)));
+ }
+ $resp .= add_field(FID_VALID_PATRON, 'Y');
+ $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
+ }
+
+ $resp .= add_field(FID_INST_ID, $ils->institution);
+
+ $self->write_msg($resp);
+
+ return(PATRON_ENABLE);
+}
+
+sub handle_hold {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my ($hold_mode, $trans_date);
+ my ($expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd);
+ my ($item_id, $title_id, $fee_ack);
+ my $fields = $self->{fields};
+ my $status;
+ my $resp = HOLD_RESP;
+
+ ($hold_mode, $trans_date) = @{$self->{fixed_fields}};
+
+ $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_hold");
+
+ $patron_id = $fields->{(FID_PATRON_ID)};
+ $expiry_date = $fields->{(FID_EXPIRATION)} || '';
+ $pickup_locn = $fields->{(FID_PICKUP_LOCN)} || '';
+ $hold_type = $fields->{(FID_HOLD_TYPE)} || '2'; # Any copy of title
+ $patron_pwd = $fields->{(FID_PATRON_PWD)};
+ $item_id = $fields->{(FID_ITEM_ID)} || '';
+ $title_id = $fields->{(FID_TITLE_ID)} || '';
+ $fee_ack = $fields->{(FID_FEE_ACK)} || 'N';
+
+ if ($hold_mode eq '+') {
+ $status = $ils->add_hold($patron_id, $patron_pwd,
+ $item_id, $title_id,
+ $expiry_date, $pickup_locn, $hold_type,
+ $fee_ack);
+ } elsif ($hold_mode eq '-') {
+ $status = $ils->cancel_hold($patron_id, $patron_pwd,
+ $item_id, $title_id);
+ } elsif ($hold_mode eq '*') {
+ $status = $ils->alter_hold($patron_id, $patron_pwd,
+ $item_id, $title_id,
+ $expiry_date, $pickup_locn, $hold_type,
+ $fee_ack);
+ } else {
+ syslog("LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'",
+ $hold_mode, $server->{account}->{id});
+ $status = $ils->Transaction::Hold;
+ $status->screen_msg("System error. Please contact library status");
+ }
+
+ $resp .= $status->ok;
+ $resp .= sipbool($status->item && $status->item->available($patron_id));
+ $resp .= Sip::timestamp;
+
+ if ($status->ok) {
+ $resp .= add_field(FID_PATRON_ID, $status->patron->id);
+
+ if ($status->expiration_date) {
+ $resp .= maybe_add(FID_EXPIRATION,
+ Sip::timestamp($status->expiration_date));
+ }
+ $resp .= maybe_add(FID_QUEUE_POS, $status->queue_position);
+ $resp .= maybe_add(FID_PICKUP_LOCN, $status->pickup_location);
+ $resp .= maybe_add(FID_ITEM_ID, $status->item->id);
+ $resp .= maybe_add(FID_TITLE_ID, $status->item->title_id);
+ } else {
+ # Not ok. still need required fields
+ $resp .= add_field(FID_PATRON_ID, $patron_id);
+ }
+
+ $resp .= add_field(FID_INST_ID, $ils->institution);
+ $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
+
+ $self->write_msg($resp);
+
+ return(HOLD);
+}
+
+sub handle_renew {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my ($third_party, $no_block, $trans_date, $nb_due_date);
+ my ($patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack);
+ my $fields = $self->{fields};
+ my $status;
+ my ($patron, $item);
+ my $resp = RENEW_RESP;
+
+ ($third_party, $no_block, $trans_date, $nb_due_date) =
+ @{$self->{fixed_fields}};
+
+ $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_renew");
+
+ if ($no_block eq 'Y') {
+ syslog("LOG_WARNING",
+ "handle_renew: recieved 'no block' renewal from terminal '%s'",
+ $server->{account}->{id});
+ }
+
+ $patron_id = $fields->{(FID_PATRON_ID)};
+ $patron_pwd = $fields->{(FID_PATRON_PWD)};
+ $item_id = $fields->{(FID_ITEM_ID)};
+ $title_id = $fields->{(FID_TITLE_ID)};
+ $item_props = $fields->{(FID_ITEM_PROPS)};
+ $fee_ack = $fields->{(FID_FEE_ACK)};
+
+ $status = $ils->renew($patron_id, $patron_pwd, $item_id, $title_id,
+ $no_block, $nb_due_date, $third_party,
+ $item_props, $fee_ack);
+
+ $patron = $status->patron;
+ $item = $status->item;
+
+ if ($status->ok) {
+ $resp .= '1';
+ $resp .= $status->renewal_ok ? 'Y' : 'N';
+ if ($ils->supports('magnetic media')) {
+ $resp .= sipbool($item->magnetic);
+ } else {
+ $resp .= 'U';
+ }
+ $resp .= sipbool($status->desensitize);
+ $resp .= Sip::timestamp;
+ $resp .= add_field(FID_PATRON_ID, $patron->id);
+ $resp .= add_field(FID_ITEM_ID, $item->id);
+ $resp .= add_field(FID_TITLE_ID, $item->title_id);
+ $resp .= add_field(FID_DUE_DATE, $item->due_date);
+ if ($ils->supports('security inhibit')) {
+ $resp .= add_field(FID_SECURITY_INHIBIT,
+ $status->security_inhibit);
+ }
+ $resp .= add_field(FID_MEDIA_TYPE, $item->sip_media_type);
+ $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
+ } else {
+ # renew failed for some reason
+ # not OK, renewal not OK, Unknown media type (why bother checking?)
+ $resp .= '0NUN';
+ $resp .= Sip::timestamp;
+ # If we found the patron or the item, the return the ILS
+ # information, otherwise echo back the infomation we received
+ # from the terminal
+ $resp .= add_field(FID_PATRON_ID, $patron ? $patron->id : $patron_id);
+ $resp .= add_field(FID_ITEM_ID, $item ? $item->id : $item_id);
+ $resp .= add_field(FID_TITLE_ID, $item ? $item->title_id : $title_id);
+ $resp .= add_field(FID_DUE_DATE, '');
+ }
+
+ if ($status->fee_amount) {
+ $resp .= add_field(FID_FEE_AMT, $status->fee_amount);
+ $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
+ $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
+ $resp .= maybe_add(FID_TRANSACTION_ID, $status->transaction_id);
+ }
+
+ $resp .= add_field(FID_INST_ID, $ils->institution);
+ $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
+
+ $self->write_msg($resp);
+
+ return(RENEW);
+}
+
+sub handle_renew_all {
+ my ($self, $server) = @_;
+ my $ils = $server->{ils};
+ my ($trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack);
+ my $fields = $self->{fields};
+ my $resp = RENEW_ALL_RESP;
+ my $status;
+ my (@renewed, @unrenewed);
+
+ $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_renew_all");
+
+ ($trans_date) = @{$self->{fixed_fields}};
+
+ $patron_id = $fields->{(FID_PATRON_ID)};
+ $patron_pwd = $fields->{(FID_PATRON_PWD)};
+ $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
+ $fee_ack = $fields->{(FID_FEE_ACK)};
+
+ $status = $ils->renew_all($patron_id, $patron_pwd, $fee_ack);
+
+ $resp .= $status->ok ? '1' : '0';
+
+ if (!$status->ok) {
+ $resp .= add_count("renew_all/renewed_count", 0);
+ $resp .= add_count("renew_all/unrenewed_count", 0);
+ @renewed = [];
+ @unrenewed = [];
+ } else {
+ @renewed = @{$status->renewed};
+ @unrenewed = @{$status->unrenewed};
+ $resp .= add_count("renew_all/renewed_count", scalar @renewed);
+ $resp .= add_count("renew_all/unrenewed_count", scalar @unrenewed);
+ }
+
+ $resp .= Sip::timestamp;
+ $resp .= add_field(FID_INST_ID, $ils->institution);
+
+ $resp .= join('', map(add_field(FID_RENEWED_ITEMS, $_), @renewed));
+ $resp .= join('', map(add_field(FID_UNRENEWED_ITEMS, $_), @unrenewed));
+
+ $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
+ $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
+
+ $self->write_msg($resp);
+
+ return(RENEW_ALL);
+}
+
+#
+# send_acs_status($self, $server)
+#
+# Send an ACS Status message, which is contains lots of little fields
+# of information gleaned from all sorts of places.
+#
+
+my @message_type_names = (
+ "patron status request",
+ "checkout",
+ "checkin",
+ "block patron",
+ "acs status",
+ "request sc/acs resend",
+ "login",
+ "patron information",
+ "end patron session",
+ "fee paid",
+ "item information",
+ "item status update",
+ "patron enable",
+ "hold",
+ "renew",
+ "renew all",
+ );
+
+sub send_acs_status {
+ my ($self, $server, $screen_msg, $print_line) = @_;
+ my $msg = ACS_STATUS;
+ my $account = $server->{account};
+ my $policy = $server->{policy};
+ my $ils = $server->{ils};
+ my ($online_status, $checkin_ok, $checkout_ok, $ACS_renewal_policy);
+ my ($status_update_ok, $offline_ok, $timeout, $retries);
+
+ $online_status = 'Y';
+ $checkout_ok = sipbool($ils->checkout_ok);
+ $checkin_ok = sipbool($ils->checkin_ok);
+ $ACS_renewal_policy = sipbool($policy->{renewal});
+ $status_update_ok = sipbool($ils->status_update_ok);
+ $offline_ok = sipbool($ils->offline_ok);
+ $timeout = sprintf("%03d", $policy->{timeout});
+ $retries = sprintf("%03d", $policy->{retries});
+
+ if (length($timeout) != 3) {
+ syslog("LOG_ERR", "handle_acs_status: timeout field wrong size: '%s'",
+ $timeout);
+ $timeout = '000';
+ }
+
+ if (length($retries) != 3) {
+ syslog("LOG_ERR", "handle_acs_status: retries field wrong size: '%s'",
+ $retries);
+ $retries = '000';
+ }
+
+ $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
+ $msg .= "$status_update_ok$offline_ok$timeout$retries";
+ $msg .= Sip::timestamp();
+
+ if ($protocol_version == 1) {
+ $msg .= '1.00';
+ } elsif ($protocol_version == 2) {
+ $msg .= '2.00';
+ } else {
+ syslog("LOG_ERROR",
+ 'Bad setting for $protocol_version, "%s" in send_acs_status',
+ $protocol_version);
+ $msg .= '1.00';
+ }
+
+ # Institution ID
+ $msg .= add_field(FID_INST_ID, $account->{institution});
+
+ if ($protocol_version >= 2) {
+ # Supported messages: we do it all
+ my $supported_msgs = '';
+
+ foreach my $msg_name (@message_type_names) {
+ if ($msg_name eq 'request sc/acs resend') {
+ $supported_msgs .= Sip::sipbool(1);
+ } else {
+ $supported_msgs .= Sip::sipbool($ils->supports($msg_name));
+ }
+ }
+ if (length($supported_msgs) < 16) {
+ syslog("LOG_ERROR", 'send_acs_status: supported messages "%s" too short', $supported_msgs);
+ }
+ $msg .= add_field(FID_SUPPORTED_MSGS, $supported_msgs);
+ }
+
+ $msg .= maybe_add(FID_SCREEN_MSG, $screen_msg);
+
+ if (defined($account->{print_width}) && defined($print_line)
+ && $account->{print_width} < length($print_line)) {
+ syslog("LOG_WARNING", "send_acs_status: print line '%s' too long. Truncating",
+ $print_line);
+ $print_line = substr($print_line, 0, $account->{print_width});
+ }
+
+ $msg .= maybe_add(FID_PRINT_LINE, $print_line);
+
+ # Do we want to tell the terminal its location?
+
+ $self->write_msg($msg);
+ return 1;
+}
+
+#
+# build_patron_status: create the 14-char patron status
+# string for the Patron Status message
+#
+sub patron_status_string {
+ my $patron = shift;
+ my $patron_status;
+
+ syslog("LOG_DEBUG", "patron_status_string: %s charge_ok: %s", $patron->id,
+ $patron->charge_ok);
+ $patron_status = sprintf('%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
+ denied($patron->charge_ok),
+ denied($patron->renew_ok),
+ denied($patron->recall_ok),
+ denied($patron->hold_ok),
+ boolspace($patron->card_lost),
+ boolspace($patron->too_many_charged),
+ boolspace($patron->too_many_overdue),
+ boolspace($patron->too_many_renewal),
+ boolspace($patron->too_many_claim_return),
+ boolspace($patron->too_many_lost),
+ boolspace($patron->excessive_fines),
+ boolspace($patron->excessive_fees),
+ boolspace($patron->recall_overdue),
+ boolspace($patron->too_many_billed));
+ return $patron_status;
+}
+
+1;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+
+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
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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 $user_homelib);
+
+# 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, },
+ { field => FID_HOME_LIBRARY,
+ pat => qr/^$user_homelib$/,
+ required => 1, }, # Required for this test
+ ], };
+
+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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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;
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+#
+#
+
+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)
--- /dev/null
+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.
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+
+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 $user_homelib
+ $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 Encode;
+
+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';
+our $user_homelib = 'Beacock';
+
+# 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 = decode_utf8('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}) && (decode_utf8($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;
--- /dev/null
+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|
--- /dev/null
+#
+# Copyright (C) 2006-2008 Georgia Public Library Service
+#
+# Author: David J. Fiander
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA 02111-1307 USA
+#
+# 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);