2 # Copyright (C) 2006-2008 Georgia Public Library Service
4 # Author: David J. Fiander
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of version 2 of the GNU General Public
8 # License as published by the Free Software Foundation.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public
16 # License along with this program; if not, write to the Free
17 # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
22 # A Class for handing SIP messages
30 use Sys::Syslog qw(syslog);
31 use UNIVERSAL qw(can);
34 use Sip::Constants qw(:all);
35 use Sip::Checksum qw(verify_cksum);
39 our (@ISA, @EXPORT_OK, $VERSION);
42 @EXPORT_OK = qw(handle);
45 # Predeclare handler subroutines
46 use subs qw(handle_patron_status handle_checkout handle_checkin
47 handle_block_patron handle_sc_status handle_request_acs_resend
48 handle_login handle_patron_info handle_end_patron_session
49 handle_fee_paid handle_item_information handle_item_status_update
50 handle_patron_enable handle_hold handle_renew handle_renew_all);
53 # For the most part, Version 2.00 of the protocol just adds new
54 # variable fields, but sometimes it changes the fixed header.
56 # In general, if there's no '2.00' protocol entry for a handler, that's
57 # because 2.00 didn't extend the 1.00 version of the protocol. This will
58 # be handled by the module initialization code following the declaration,
59 # which goes through the handlers table and creates a '2.00' entry that
60 # points to the same place as the '1.00' entry. If there's a 2.00 entry
61 # but no 1.00 entry, then that means that it's a completely new service
62 # in 2.00, so 1.00 shouldn't recognize it.
65 (PATRON_STATUS_REQ) => {
66 name => "Patron Status Request",
67 handler => \&handle_patron_status,
72 fields => [(FID_INST_ID), (FID_PATRON_ID),
73 (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
79 handler => \&handle_checkout,
82 template => "CCA18A18",
84 fields => [(FID_INST_ID), (FID_PATRON_ID),
85 (FID_ITEM_ID), (FID_TERMINAL_PWD)],
88 template => "CCA18A18",
90 fields => [(FID_INST_ID), (FID_PATRON_ID),
91 (FID_ITEM_ID), (FID_TERMINAL_PWD),
92 (FID_ITEM_PROPS), (FID_PATRON_PWD),
93 (FID_FEE_ACK), (FID_CANCEL)],
99 handler => \&handle_checkin,
102 template => "CA18A18",
104 fields => [(FID_CURRENT_LOCN), (FID_INST_ID),
105 (FID_ITEM_ID), (FID_TERMINAL_PWD)],
108 template => "CA18A18",
110 fields => [(FID_CURRENT_LOCN), (FID_INST_ID),
111 (FID_ITEM_ID), (FID_TERMINAL_PWD),
112 (FID_ITEM_PROPS), (FID_CANCEL)],
117 name => "Block Patron",
118 handler => \&handle_block_patron,
123 fields => [(FID_INST_ID), (FID_BLOCKED_CARD_MSG),
124 (FID_PATRON_ID), (FID_TERMINAL_PWD)],
130 handler => \&handle_sc_status,
139 (REQUEST_ACS_RESEND) => {
140 name => "Request ACS Resend",
141 handler => \&handle_request_acs_resend,
152 handler => \&handle_login,
157 fields => [(FID_LOGIN_UID), (FID_LOGIN_PWD),
158 (FID_LOCATION_CODE)],
163 name => "Patron Info",
164 handler => \&handle_patron_info,
167 template => "A3A18A10",
169 fields => [(FID_INST_ID), (FID_PATRON_ID),
170 (FID_TERMINAL_PWD), (FID_PATRON_PWD),
171 (FID_START_ITEM), (FID_END_ITEM)],
175 (END_PATRON_SESSION) => {
176 name => "End Patron Session",
177 handler => \&handle_end_patron_session,
182 fields => [(FID_INST_ID), (FID_PATRON_ID),
183 (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
189 handler => \&handle_fee_paid,
192 template => "A18A2A2A3",
194 fields => [(FID_FEE_AMT), (FID_INST_ID),
195 (FID_PATRON_ID), (FID_TERMINAL_PWD),
196 (FID_PATRON_PWD), (FID_FEE_ID),
197 (FID_TRANSACTION_ID)],
201 (ITEM_INFORMATION) => {
202 name => "Item Information",
203 handler => \&handle_item_information,
208 fields => [(FID_INST_ID), (FID_ITEM_ID),
213 (ITEM_STATUS_UPDATE) => {
214 name => "Item Status Update",
215 handler => \&handle_item_status_update,
220 fields => [(FID_INST_ID), (FID_PATRON_ID),
221 (FID_ITEM_ID), (FID_TERMINAL_PWD),
227 name => "Patron Enable",
228 handler => \&handle_patron_enable,
233 fields => [(FID_INST_ID), (FID_PATRON_ID),
234 (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
240 handler => \&handle_hold,
245 fields => [(FID_EXPIRATION), (FID_PICKUP_LOCN),
246 (FID_HOLD_TYPE), (FID_INST_ID),
247 (FID_PATRON_ID), (FID_PATRON_PWD),
248 (FID_ITEM_ID), (FID_TITLE_ID),
249 (FID_TERMINAL_PWD), (FID_FEE_ACK)],
255 handler => \&handle_renew,
258 template => "CCA18A18",
260 fields => [(FID_INST_ID), (FID_PATRON_ID),
261 (FID_PATRON_PWD), (FID_ITEM_ID),
262 (FID_TITLE_ID), (FID_TERMINAL_PWD),
263 (FID_ITEM_PROPS), (FID_FEE_ACK)],
269 handler => \&handle_renew_all,
274 fields => [(FID_INST_ID), (FID_PATRON_ID),
275 (FID_PATRON_PWD), (FID_TERMINAL_PWD),
283 # Now, initialize some of the missing bits of %handlers
285 foreach my $i (keys(%handlers)) {
286 if (!exists($handlers{$i}->{protocol}->{2})) {
288 $handlers{$i}->{protocol}->{2} = $handlers{$i}->{protocol}->{1};
293 my ($class, $msg, $seqno) = @_;
295 my $msgtag = substr($msg, 0, 2);
297 syslog("LOG_DEBUG", "Sip::MsgType::new('%s', '%s', '%s'): msgtag '%s'",
298 $class, substr($msg, 0, 10), $msgtag, $seqno);
299 if ($msgtag eq LOGIN) {
300 # If the client is using the 2.00-style "Login" message
301 # to authenticate to the server, then we get the Login message
302 # _before_ the client has indicated that it supports 2.00, but
303 # it's using the 2.00 login process, so it must support 2.00,
304 # so we'll just do it.
305 $protocol_version = 2;
307 if (!exists($handlers{$msgtag})) {
308 syslog("LOG_WARNING",
309 "new Sip::MsgType: Skipping message of unknown type '%s' in '%s'",
312 } elsif (!exists($handlers{$msgtag}->{protocol}->{$protocol_version})) {
313 syslog("LOG_WARNING", "new Sip::MsgType: Skipping message '%s' unsupported by protocol rev. '%d'",
314 $msgtag, $protocol_version);
320 $self->{seqno} = $seqno;
321 $self->_initialize(substr($msg,2), $handlers{$msgtag});
327 my ($self, $msg, $control_block) = @_;
329 my $proto = $control_block->{protocol}->{$protocol_version};
331 $self->{name} = $control_block->{name};
332 $self->{handler} = $control_block->{handler};
334 $self->{fields} = {};
335 $self->{fixed_fields} = [];
337 syslog("LOG_DEBUG", "Sip::MsgType::_initialize('%s', '%s...')", $self->{name}, substr($msg,0,20));
340 foreach my $field (@{$proto->{fields}}) {
341 $self->{fields}->{$field} = undef;
345 "Sip::MsgType::_initialize('%s', '%s', '%s', '%s', ...",
346 $self->{name}, $msg, $proto->{template},
347 $proto->{template_len});
349 $self->{fixed_fields} = [ unpack($proto->{template}, $msg) ];
351 # Skip over the fixed fields and the split the rest of
352 # the message into fields based on the delimiter and parse them
353 foreach my $field (split(quotemeta($field_delimiter), substr($msg, $proto->{template_len}))) {
354 $fn = substr($field, 0, 2);
356 if (!exists($self->{fields}->{$fn})) {
357 syslog("LOG_WARNING",
358 "Unsupported field '%s' in %s message '%s'",
359 $fn, $self->{name}, $msg);
360 } elsif (defined($self->{fields}->{$fn})) {
361 syslog("LOG_WARNING",
362 "Duplicate field '%s' (previous value '%s') in %s message '%s'",
363 $fn, $self->{fields}->{$fn}, $self->{name}, $msg);
365 $self->{fields}->{$fn} = substr($field, 2);
373 my ($msg, $server, $req) = @_;
374 my $config = $server->{config};
379 # What's the field delimiter for variable length fields?
380 # This can't be based on the account, since we need to know
381 # the field delimiter to parse a SIP login message
383 if (defined($server->{config}->{delimiter})) {
384 $field_delimiter = $server->{config}->{delimiter};
387 # error detection is active if this is a REQUEST_ACS_RESEND
388 # message with a checksum, or if the message is long enough
389 # and the last nine characters begin with a sequence number
391 if ($msg eq REQUEST_ACS_RESEND_CKSUM) {
394 $error_detection = 1;
395 $self = new Sip::MsgType ((REQUEST_ACS_RESEND), 0);
396 } elsif((length($msg) > 11) && (substr($msg, -9, 2) eq "AY")) {
397 $error_detection = 1;
399 if (!verify_cksum($msg)) {
400 syslog("LOG_WARNING", "Checksum failed on message '%s'", $msg);
401 # REQUEST_SC_RESEND with error detection
402 $last_response = REQUEST_SC_RESEND_CKSUM;
403 print("$last_response\r");
404 return REQUEST_ACS_RESEND;
406 # Save the sequence number, then strip off the
407 # error detection data to process the message
408 $self = new Sip::MsgType (substr($msg, 0, -9), substr($msg, -7, 1));
410 } elsif ($error_detection) {
411 # We've receive a non-ED message when ED is supposed
412 # to be active. Warn about this problem, then process
413 # the message anyway.
414 syslog("LOG_WARNING",
415 "Received message without error detection: '%s'", $msg);
416 $error_detection = 0;
417 $self = new Sip::MsgType ($msg, 0);
419 $self = new Sip::MsgType ($msg, 0);
422 if ((substr($msg, 0, 2) ne REQUEST_ACS_RESEND) &&
423 $req && (substr($msg, 0, 2) ne $req)) {
424 return substr($msg, 0, 2);
426 return($self->{handler}->($self, $server));
434 # Patron status messages are produced in response to both
435 # "Request Patron Status" and "Block Patron"
437 # Request Patron Status requires a patron password, but
438 # Block Patron doesn't (since the patron may never have
439 # provided one before attempting some illegal action).
441 # ASSUMPTION: If the patron password field is present in the
442 # message, then it must match, otherwise incomplete patron status
443 # information will be returned to the terminal.
445 sub build_patron_status {
446 my ($patron, $lang, $fields)= @_;
448 my $patron_pwd = $fields->{(FID_PATRON_PWD)};
449 my $resp = (PATRON_STATUS_RESP);
452 $resp .= patron_status_string($patron);
453 $resp .= $lang . Sip::timestamp();
454 $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
456 # while the patron ID we got from the SC is valid, let's
457 # use the one returned from the ILS, just in case...
458 $resp .= add_field(FID_PATRON_ID, $patron->id);
459 if ($protocol_version >= 2) {
460 $resp .= add_field(FID_VALID_PATRON, 'Y');
461 # Patron password is a required field.
462 $resp .= add_field(FID_VALID_PATRON_PWD, sipbool($patron->check_password($patron_pwd)));
463 $resp .= maybe_add(FID_CURRENCY, $patron->currency);
464 $resp .= maybe_add(FID_FEE_AMT, $patron->fee_amount);
467 $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
468 $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
470 # Invalid patron id. Report that the user has no privs.,
471 # no personal name, and is invalid (if we're using 2.00)
472 $resp .= 'YYYY' . (' ' x 10) . $lang . Sip::timestamp();
473 $resp .= add_field(FID_PERSONAL_NAME, '');
475 # the patron ID is invalid, but it's a required field, so
477 $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
479 if ($protocol_version >= 2) {
480 $resp .= add_field(FID_VALID_PATRON, 'N');
484 $resp .= add_field(FID_INST_ID, $fields->{(FID_INST_ID)});
489 sub handle_patron_status {
490 my ($self, $server) = @_;
491 my $ils = $server->{ils};
495 my $resp = (PATRON_STATUS_RESP);
496 my $account = $server->{account};
498 ($lang, $date) = @{$self->{fixed_fields}};
499 $fields = $self->{fields};
501 $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_patron_status");
503 $patron = $ils->find_patron($fields->{(FID_PATRON_ID)});
505 $resp = build_patron_status($patron, $lang, $fields);
507 $self->write_msg($resp);
509 return (PATRON_STATUS_REQ);
512 sub handle_checkout {
513 my ($self, $server) = @_;
514 my $account = $server->{account};
515 my $ils = $server->{ils};
516 my $inst = $ils->institution;
517 my ($sc_renewal_policy, $no_block, $trans_date, $nb_due_date);
519 my ($patron_id, $item_id, $status, $fee_ack);
523 ($sc_renewal_policy, $no_block, $trans_date, $nb_due_date) =
524 @{$self->{fixed_fields}};
525 $fields = $self->{fields};
527 $patron_id = $fields->{(FID_PATRON_ID)};
528 $item_id = $fields->{(FID_ITEM_ID)};
529 $fee_ack = $fields->{(FID_FEE_ACK)};
532 if ($no_block eq 'Y') {
533 # Off-line transactions need to be recorded, but there's
534 # not a lot we can do about it
535 syslog("LOG_WARNING", "received no-block checkout from terminal '%s'",
538 $status = $ils->checkout_no_block($patron_id, $item_id,
540 $trans_date, $nb_due_date);
542 # Does the transaction date really matter for items that are
543 # checkout out while the terminal is online? I'm guessing 'no'
544 $status = $ils->checkout($patron_id, $item_id, $sc_renewal_policy, $fee_ack);
548 $item = $status->item;
549 $patron = $status->patron;
552 # Item successfully checked out
554 $resp = CHECKOUT_RESP . '1';
555 $resp .= sipbool($status->renew_ok);
556 if ($ils->supports('magnetic media')) {
557 $resp .= sipbool($item->magnetic);
561 # We never return the obsolete 'U' value for 'desensitize'
562 $resp .= sipbool($status->desensitize);
563 $resp .= Sip::timestamp;
565 # Now for the variable fields
566 $resp .= add_field(FID_INST_ID, $inst);
567 $resp .= add_field(FID_PATRON_ID, $patron_id);
568 $resp .= add_field(FID_ITEM_ID, $item_id);
569 $resp .= add_field(FID_TITLE_ID, $item->title_id);
570 $resp .= add_field(FID_DUE_DATE, $item->due_date);
572 $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
573 $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
575 if ($protocol_version >= 2) {
576 if ($ils->supports('security inhibit')) {
577 $resp .= add_field(FID_SECURITY_INHIBIT, $status->security_inhibit);
579 $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type);
580 $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
583 if ($status->fee_amount) {
584 $resp .= add_field(FID_FEE_AMT, $status->fee_amount);
585 $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
586 $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
587 $resp .= maybe_add(FID_TRANSACTION_ID,
588 $status->transaction_id);
594 # Checkout Response: not ok, no renewal, don't know mag. media,
596 $resp = sprintf("120%sUN%s", sipbool($status->renew_ok), Sip::timestamp);
597 $resp .= add_field(FID_INST_ID, $inst);
598 $resp .= add_field(FID_PATRON_ID, $patron_id);
599 $resp .= add_field(FID_ITEM_ID, $item_id);
601 # If the item is valid, provide the title, otherwise
603 $resp .= add_field(FID_TITLE_ID, $item ? $item->title_id : '');
604 # Due date is required. Since it didn't get checked out,
605 # it's not due, so leave the date blank
606 $resp .= add_field(FID_DUE_DATE, '');
608 $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
609 $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
611 if ($protocol_version >= 2) {
612 # Is the patron ID valid?
613 $resp .= add_field(FID_VALID_PATRON, sipbool($patron));
615 if ($patron && exists($fields->{FID_PATRON_PWD})) {
616 # Password provided, so we can tell if it was valid or not
617 $resp .= add_field(FID_VALID_PATRON_PWD,
618 sipbool($patron->check_password($fields->{(FID_PATRON_PWD)})));
620 # For the patron to accept a fee in chargeable loans, we
621 # need to return fee information.
622 if ($status->fee_amount) {
623 $resp .= add_field(FID_FEE_AMT, $status->fee_amount);
624 $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
625 $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
630 $self->write_msg($resp);
635 my ($self, $server) = @_;
636 my $account = $server->{account};
637 my $ils = $server->{ils};
638 my ($current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel);
639 my ($patron, $item, $status);
640 my $resp = CHECKIN_RESP;
642 my ($no_block, $trans_date, $return_date) = @{$self->{fixed_fields}};
643 my $fields = $self->{fields};
645 $current_loc = $fields->{(FID_CURRENT_LOCN)};
646 $inst_id = $fields->{(FID_INST_ID) };
647 $item_id = $fields->{(FID_ITEM_ID) };
648 $item_props = $fields->{(FID_ITEM_PROPS) };
649 $cancel = $fields->{(FID_CANCEL) };
651 $ils->check_inst_id($inst_id, "handle_checkin");
653 if ($no_block eq 'Y') {
654 # Off-line transactions, ick.
655 syslog("LOG_WARNING", "received no-block checkin from terminal '%s'", $account->{id});
656 $status = $ils->checkin_no_block($item_id, $trans_date, $return_date, $item_props, $cancel);
658 $status = $ils->checkin($item_id, $inst_id, $trans_date, $return_date, $current_loc, $item_props, $cancel);
661 $patron = $status->patron;
662 $item = $status->item;
664 $resp .= $status->ok ? '1' : '0';
665 $resp .= $status->resensitize ? 'Y' : 'N';
666 if ($item && $ils->supports('magnetic media')) {
667 $resp .= sipbool($item->magnetic);
669 # The item barcode was invalid or the system doesn't support
670 # the 'magnetic media' indicator
673 $resp .= $status->alert ? 'Y' : 'N';
674 $resp .= Sip::timestamp;
675 $resp .= add_field(FID_INST_ID, $inst_id);
676 $resp .= add_field(FID_ITEM_ID, $item_id);
679 $resp .= add_field(FID_PERM_LOCN, $item->permanent_location);
680 $resp .= maybe_add(FID_TITLE_ID, $item->title_id);
683 if ($protocol_version >= 2) {
684 $resp .= maybe_add(FID_SORT_BIN, $status->sort_bin);
686 $resp .= add_field(FID_PATRON_ID, $patron->id);
689 $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type );
690 $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
691 $resp .= maybe_add(FID_COLLECTION_CODE, $item->collection_code );
692 $resp .= maybe_add(FID_CALL_NUMBER, $item->call_number );
693 $resp .= maybe_add(FID_DESTINATION_LOCATION, $item->destination_loc );
694 $resp .= maybe_add(FID_HOLD_PATRON_ID, $item->hold_patron_bcode );
695 $resp .= maybe_add(FID_HOLD_PATRON_NAME, $item->hold_patron_name );
699 $resp .= maybe_add(FID_ALERT_TYPE, $status->alert_type) if $status->alert;
700 $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
701 $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
703 $self->write_msg($resp);
708 sub handle_block_patron {
709 my ($self, $server) = @_;
710 my $account = $server->{account};
711 my $ils = $server->{ils};
712 my ($card_retained, $trans_date);
713 my ($inst_id, $blocked_card_msg, $patron_id, $terminal_pwd);
718 ($card_retained, $trans_date) = @{$self->{fixed_fields}};
719 $fields = $self->{fields};
720 $inst_id = $fields->{(FID_INST_ID)};
721 $blocked_card_msg = $fields->{(FID_BLOCKED_CARD_MSG)};
722 $patron_id = $fields->{(FID_PATRON_ID)};
723 $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
725 # Terminal passwords are different from account login
726 # passwords, but I have no idea what to do with them. So,
727 # I'll just ignore them for now.
729 $ils->check_inst_id($inst_id, "block_patron");
731 $patron = $ils->find_patron($patron_id);
733 # The correct response for a "Block Patron" message is a
734 # "Patron Status Response", so use that handler to generate
735 # the message, but then return the correct code from here.
737 # Normally, the language is provided by the "Patron Status"
738 # fixed field, but since we're not responding to one of those
739 # we'll just say, "Unspecified", as per the spec. Let the
740 # terminal default to something that, one hopes, will be
742 my $language = $patron ? $patron->language : '000';
745 $patron->block($card_retained, $blocked_card_msg);
748 $resp = build_patron_status($patron, $language, $fields);
750 $self->write_msg($resp);
751 return(BLOCK_PATRON);
754 sub handle_sc_status {
755 my ($self, $server) = @_;
756 my ($status, $print_width, $sc_protocol_version, $new_proto);
758 ($status, $print_width, $sc_protocol_version) = @{$self->{fixed_fields}};
760 if ($sc_protocol_version =~ /^1\./) {
762 } elsif ($sc_protocol_version =~ /^2\./) {
765 syslog("LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version);
769 if ($new_proto != $protocol_version) {
770 syslog("LOG_INFO", "Setting protocol level to $new_proto");
771 $protocol_version = $new_proto;
774 unless (exists $self->{account}) {
775 # If we haven't logged in yet, go ahead and
776 # return the SC status anyway, arbitrarily using the
777 # first account in Perl string sort order to specify
778 # the account, institution, and ILS. This supports
779 # raw clients such as Relais that insist on sending 99 first
781 syslog('LOG_INFO', 'sending SC status without logging in first');
783 $mock_server->{config} = $server->{config};
784 my $uid = (sort keys %{ $server->{config}->{accounts} })[0];
785 _load_ils_handler($mock_server, $uid);
786 return send_acs_status($self, $mock_server) ? SC_STATUS : '';
789 if ($status == SC_STATUS_PAPER) {
790 syslog("LOG_WARNING", "Self-Check unit '%s@%s' out of paper",
791 $self->{account}->{id}, $self->{account}->{institution});
792 } elsif ($status == SC_STATUS_SHUTDOWN) {
793 syslog("LOG_WARNING", "Self-Check unit '%s@%s' shutting down",
794 $self->{account}->{id}, $self->{account}->{institution});
797 $self->{account}->{print_width} = $print_width;
799 return send_acs_status($self, $server) ? SC_STATUS : '';
802 sub handle_request_acs_resend {
803 my ($self, $server) = @_;
805 if (!$last_response) {
806 # We haven't sent anything yet, so respond with a
807 # REQUEST_SC_RESEND msg (p. 16)
808 $self->write_msg(REQUEST_SC_RESEND);
809 } elsif ((length($last_response) < 9)
810 || substr($last_response, -9, 2) ne 'AY') {
811 # When resending a message, we aren't supposed to include
812 # a sequence number, even if the original had one (p. 4).
813 # If the last message didn't have a sequence number, then
814 # we can just send it.
815 print("$last_response\r");
817 # Cut out the sequence number and checksum, since the old
818 # checksum is wrong for the resent message.
819 $self->write_msg(substr($last_response, 0, -9));
821 return REQUEST_ACS_RESEND;
825 my ($self, $server) = @_;
826 my ($uid_algorithm, $pwd_algorithm);
830 my $status = 1; # Assume it all works
832 $fields = $self->{fields};
833 ($uid_algorithm, $pwd_algorithm) = @{$self->{fixed_fields}};
835 $uid = $fields->{(FID_LOGIN_UID)};
836 $pwd = $fields->{(FID_LOGIN_PWD)};
838 if ($uid_algorithm || $pwd_algorithm) {
839 syslog("LOG_ERR", "LOGIN: Can't cope with non-zero encryption methods: uid = $uid_algorithm, pwd = $pwd_algorithm");
843 if (!exists($server->{config}->{accounts}->{$uid})) {
844 syslog("LOG_WARNING", "MsgType::handle_login: Unknown login '$uid'");
846 } elsif ($server->{config}->{accounts}->{$uid}->{password} ne $pwd) {
847 syslog("LOG_WARNING", "MsgType::handle_login: Invalid password for login '$uid'");
850 _load_ils_handler($server, $uid);
853 $self->write_msg(LOGIN_RESP . $status);
855 return $status ? LOGIN : '';
858 sub _load_ils_handler {
859 my ($server, $uid) = @_;
861 # Store the active account someplace handy for everybody else to find.
862 $server->{account} = $server->{config}->{accounts}->{$uid};
863 my $inst = $server->{account}->{institution};
864 $server->{institution} = $server->{config}->{institutions}->{$inst};
865 $server->{policy} = $server->{institution}->{policy};
868 syslog("LOG_INFO", "Successful login for '%s' of '%s'", $server->{account}->{id}, $inst);
870 # initialize connection to ILS
872 my $module = $server->{config}->{institutions}->{$inst}->{implementation};
876 syslog("LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed",
877 $server->{service}, $module, $inst);
878 die("Failed to load ILS implementation '$module'");
881 $server->{ils} = $module->new($server->{institution}, $server->{account});
883 if (!$server->{ils}) {
884 syslog("LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst);
885 die("Unable to connect to ILS '$inst'");
890 # Build the detailed summary information for the Patron
891 # Information Response message based on the first 'Y' that appears
892 # in the 'summary' field of the Patron Information reqest. The
893 # specification says that only one 'Y' can appear in that field,
894 # and we're going to believe it.
897 my ($ils, $patron, $summary, $start, $end) = @_;
903 # Map from offsets in the "summary" field of the Patron Information
904 # message to the corresponding field and handler
907 { func => $patron->can("hold_items"), fid => FID_HOLD_ITEMS },
908 { func => $patron->can("overdue_items"), fid => FID_OVERDUE_ITEMS },
909 { func => $patron->can("charged_items"), fid => FID_CHARGED_ITEMS },
910 { func => $patron->can("fine_items"), fid => FID_FINE_ITEMS },
911 { func => $patron->can("recall_items"), fid => FID_RECALL_ITEMS },
912 { func => $patron->can("unavail_holds"), fid => FID_UNAVAILABLE_HOLD_ITEMS },
916 if (($summary_type = index($summary, 'Y')) == -1) {
917 # No detailed information required
921 syslog("LOG_DEBUG", "Summary_info: index == '%d', field '%s'",
922 $summary_type, $summary_map[$summary_type]->{fid});
924 $func = $summary_map[$summary_type]->{func};
925 $fid = $summary_map[$summary_type]->{fid};
926 $itemlist = &$func($patron, $start, $end);
928 syslog("LOG_DEBUG", "summary_info: list = (%s)", join(", ", @{$itemlist}));
929 foreach my $i (@{$itemlist}) {
930 $resp .= add_field($fid, $i);
936 sub handle_patron_info {
937 my ($self, $server) = @_;
938 my $ils = $server->{ils};
939 my ($lang, $trans_date, $summary) = @{$self->{fixed_fields}};
940 my $fields = $self->{fields};
941 my ($inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end);
942 my ($resp, $patron, $count);
943 $lang ||= '000'; # unspecified
945 $inst_id = $fields->{(FID_INST_ID)};
946 $patron_id = $fields->{(FID_PATRON_ID)};
947 $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
948 $patron_pwd = $fields->{(FID_PATRON_PWD)};
949 $start = $fields->{(FID_START_ITEM)};
950 $end = $fields->{(FID_END_ITEM)};
952 $patron = $ils->find_patron($patron_id);
954 $resp = (PATRON_INFO_RESP);
956 $resp .= patron_status_string($patron);
958 $lang = $patron->language if $patron->language;
959 $resp .= $lang . Sip::timestamp();
961 $resp .= add_count('patron_info/hold_items', scalar @{$patron->hold_items(undef,undef,1) });
962 $resp .= add_count('patron_info/overdue_items', scalar @{$patron->overdue_items(undef,undef,1)});
963 $resp .= add_count('patron_info/charged_items', scalar @{$patron->charged_items(undef,undef,1)});
964 $resp .= add_count('patron_info/fine_items', scalar @{$patron->fine_items(undef,undef,1) });
965 $resp .= add_count('patron_info/recall_items', scalar @{$patron->recall_items(undef,undef,1) });
966 $resp .= add_count('patron_info/unavail_holds', scalar @{$patron->unavail_holds(undef,undef,1)});
968 $resp .= add_field(FID_INST_ID, $server->{ils}->institution);
970 # while the patron ID we got from the SC is valid, let's
971 # use the one returned from the ILS, just in case...
972 $resp .= add_field(FID_PATRON_ID, $patron->id);
974 $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
976 # TODO: add code for the fields
978 # overdue items limit
979 # charged items limit
982 $resp .= maybe_add(FID_CURRENCY, $patron->currency );
983 $resp .= maybe_add(FID_FEE_AMT, $patron->fee_amount);
984 $resp .= maybe_add(FID_HOME_ADDR, $patron->address );
985 $resp .= maybe_add(FID_EMAIL, $patron->email_addr);
986 $resp .= maybe_add(FID_HOME_PHONE, $patron->home_phone);
988 # Extension requested by PINES. Report the home system for
989 # the patron in the 'AQ' field. This is normally the "permanent
990 # location" field for an ITEM, but it's not used in PATRON info.
991 # Apparently TLC systems do this.
992 $resp .= maybe_add(FID_HOME_LIBRARY, $patron->home_library);
994 $resp .= summary_info($ils, $patron, $summary, $start, $end);
996 $resp .= add_field(FID_VALID_PATRON, 'Y');
997 if (defined($patron_pwd)) {
998 # If the patron password was provided, report on if it was right.
999 $resp .= add_field(FID_VALID_PATRON_PWD,
1000 sipbool($patron->check_password($patron_pwd)));
1003 # SIP 2.0 extensions used by Envisionware
1004 # Other types of terminals will ignore the fields, if
1005 # they don't recognize the codes
1006 if ($patron->can('sip_expire')) {
1007 $resp .= maybe_add(FID_PATRON_EXPIRE, $patron->sip_expire);
1009 $resp .= maybe_add(FID_PATRON_BIRTHDATE, $patron->sip_birthdate);
1010 $resp .= maybe_add(FID_PATRON_CLASS, $patron->ptype);
1012 # Custom protocol extension to report patron internet privileges
1013 $resp .= maybe_add(FID_INET_PROFILE, $patron->inet_privileges);
1015 $resp .= maybe_add(FID_PATRON_INTERNAL_ID, $patron->internal_id); # another extension
1017 $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
1018 $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
1020 # Custom ILS-defined protocol extensions
1021 if ($patron->can('extra_fields')) {
1022 my $extra_fields = $patron->extra_fields();
1023 foreach my $field (keys %$extra_fields) {
1024 foreach my $value (@{$extra_fields->{ $field }}) {
1025 $resp .= maybe_add($field, $value);
1031 # He has no privileges, no items associated with him,
1032 # no personal name, and is invalid (if we're using 2.00)
1033 $resp .= 'YYYY' . (' ' x 10) . $lang . Sip::timestamp();
1034 $resp .= '0000' x 6;
1036 $resp .= add_field(FID_INST_ID, $server->{ils}->institution);
1037 # the patron ID is invalid, but it's a required field, so
1039 $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
1040 $resp .= add_field(FID_PERSONAL_NAME, '');
1042 if ($protocol_version >= 2) {
1043 $resp .= add_field(FID_VALID_PATRON, 'N');
1047 $self->write_msg($resp);
1049 return(PATRON_INFO);
1052 sub handle_end_patron_session {
1053 my ($self, $server) = @_;
1054 my $ils = $server->{ils};
1056 my $fields = $self->{fields};
1057 my $resp = END_SESSION_RESP;
1058 my ($status, $screen_msg, $print_line);
1060 ($trans_date) = @{$self->{fixed_fields}};
1062 $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_end_patron_session");
1064 ($status, $screen_msg, $print_line) = $ils->end_patron_session($fields->{(FID_PATRON_ID)});
1066 $resp .= $status ? 'Y' : 'N';
1067 $resp .= Sip::timestamp();
1069 $resp .= add_field(FID_INST_ID, $server->{ils}->institution);
1070 $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
1072 $resp .= maybe_add(FID_SCREEN_MSG, $screen_msg);
1073 $resp .= maybe_add(FID_PRINT_LINE, $print_line);
1075 $self->write_msg($resp);
1077 return(END_PATRON_SESSION);
1080 sub handle_fee_paid {
1081 my ($self, $server) = @_;
1082 my $ils = $server->{ils};
1083 my ($trans_date, $fee_type, $pay_type, $currency) = @{$self->{fixed_fields}};
1084 my $fields = $self->{fields};
1085 my ($fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd);
1086 my ($fee_id, $trans_id);
1088 my $resp = FEE_PAID_RESP;
1090 $fee_amt = $fields->{(FID_FEE_AMT)};
1091 $inst_id = $fields->{(FID_INST_ID)};
1092 $patron_id = $fields->{(FID_PATRON_ID)};
1093 $patron_pwd = $fields->{(FID_PATRON_PWD)};
1094 $fee_id = $fields->{(FID_FEE_ID)};
1095 $trans_id = $fields->{(FID_TRANSACTION_ID)};
1097 $ils->check_inst_id($inst_id, "handle_fee_paid");
1099 $status = $ils->pay_fee($patron_id, $patron_pwd, $fee_amt, $fee_type,
1100 $pay_type, $fee_id, $trans_id, $currency);
1102 $resp .= ($status->ok ? 'Y' : 'N') . Sip::timestamp;
1103 $resp .= add_field(FID_INST_ID, $inst_id);
1104 $resp .= add_field(FID_PATRON_ID, $patron_id);
1105 $resp .= maybe_add(FID_TRANSACTION_ID, $status->transaction_id);
1106 $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1107 $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1109 $self->write_msg($resp);
1114 sub handle_item_information {
1115 my ($self, $server) = @_;
1116 my $ils = $server->{ils};
1118 my $fields = $self->{fields};
1119 my $resp = ITEM_INFO_RESP;
1122 ($trans_date) = @{$self->{fixed_fields}};
1124 $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_item_information");
1126 $item = $ils->find_item($fields->{(FID_ITEM_ID)});
1128 if (!defined($item)) {
1130 # "Other" circ stat, "Other" security marker, "Unknown" fee type
1132 $resp .= Sip::timestamp;
1133 # Just echo back the invalid item id
1134 $resp .= add_field(FID_ITEM_ID, $fields->{(FID_ITEM_ID)});
1135 # title id is required, but we don't have one
1136 $resp .= add_field(FID_TITLE_ID, '');
1138 # Valid Item ID, send the good stuff
1139 $resp .= $item->sip_circulation_status;
1140 $resp .= $item->sip_security_marker;
1141 $resp .= $item->sip_fee_type;
1142 $resp .= Sip::timestamp;
1144 $resp .= add_field(FID_ITEM_ID, $item->id);
1145 $resp .= add_field(FID_TITLE_ID, $item->title_id);
1147 $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type);
1148 $resp .= maybe_add(FID_PERM_LOCN, $item->permanent_location);
1149 $resp .= maybe_add(FID_CURRENT_LOCN, $item->current_location);
1150 $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
1153 $resp .= add_field(FID_CURRENCY, $item->fee_currency);
1154 $resp .= add_field(FID_FEE_AMT, $item->fee);
1156 $resp .= maybe_add(FID_OWNER, $item->owner);
1157 $resp .= maybe_add(FID_HOLD_QUEUE_LEN, scalar @{$item->hold_queue});
1158 $resp .= maybe_add(FID_DUE_DATE, $item->due_date);
1159 $resp .= maybe_add(FID_RECALL_DATE, $item->recall_date);
1160 $resp .= maybe_add(FID_HOLD_PICKUP_DATE, $item->hold_pickup_date);
1161 $resp .= maybe_add(FID_DESTINATION_LOCATION, $item->destination_loc); # Extension for AMH sorting
1162 $resp .= maybe_add(FID_CALL_NUMBER, $item->call_number); # Extension for AMH sorting
1163 $resp .= maybe_add(FID_SCREEN_MSG, $item->screen_msg);
1164 $resp .= maybe_add(FID_PRINT_LINE, $item->print_line);
1166 # Custom ILS-defined protocol extensions
1167 if ($item->can('extra_fields')) {
1168 my $extra_fields = $item->extra_fields();
1169 foreach my $field (keys %$extra_fields) {
1170 foreach my $value (@{$extra_fields->{ $field }}) {
1171 $resp .= maybe_add($field, $value);
1177 $self->write_msg($resp);
1179 return(ITEM_INFORMATION);
1182 sub handle_item_status_update {
1183 my ($self, $server) = @_;
1184 my $ils = $server->{ils};
1185 my ($trans_date, $item_id, $terminal_pwd, $item_props);
1186 my $fields = $self->{fields};
1189 my $resp = ITEM_STATUS_UPDATE_RESP;
1191 ($trans_date) = @{$self->{fixed_fields}};
1193 $ils->check_inst_id($fields->{(FID_INST_ID)});
1195 $item_id = $fields->{(FID_ITEM_ID)};
1196 $item_props = $fields->{(FID_ITEM_PROPS)};
1198 if (!defined($item_id)) {
1199 syslog("LOG_WARNING", "handle_item_status: received message without Item ID field");
1201 $item = $ils->find_item($item_id);
1207 $resp .= Sip::timestamp;
1208 $resp .= add_field(FID_ITEM_ID, $item_id);
1211 $status = $item->status_update($item_props);
1213 $resp .= $status->ok ? '1' : '0';
1214 $resp .= Sip::timestamp;
1216 $resp .= add_field(FID_ITEM_ID, $item->id);
1217 $resp .= add_field(FID_TITLE_ID, $item->title_id);
1218 $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
1221 $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1222 $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1224 $self->write_msg($resp);
1226 return(ITEM_STATUS_UPDATE);
1229 sub handle_patron_enable {
1230 my ($self, $server) = @_;
1231 my $ils = $server->{ils};
1232 my $fields = $self->{fields};
1233 my ($trans_date, $patron_id, $terminal_pwd, $patron_pwd);
1234 my ($status, $patron);
1235 my $resp = PATRON_ENABLE_RESP;
1237 ($trans_date) = @{$self->{fixed_fields}};
1238 $patron_id = $fields->{(FID_PATRON_ID)};
1239 $patron_pwd = $fields->{(FID_PATRON_PWD)};
1241 syslog("LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'",
1242 $patron_id, $patron_pwd);
1244 $patron = $ils->find_patron($patron_id);
1246 if (!defined($patron)) {
1248 $resp .= 'YYYY' . (' ' x 10) . '000' . Sip::timestamp();
1249 $resp .= add_field(FID_PATRON_ID, $patron_id);
1250 $resp .= add_field(FID_PERSONAL_NAME, '' );
1251 $resp .= add_field(FID_VALID_PATRON, 'N');
1252 $resp .= add_field(FID_VALID_PATRON_PWD, 'N');
1255 if (!defined($patron_pwd) || $patron->check_password($patron_pwd)) {
1256 # Don't enable the patron if there was an invalid password
1257 $status = $patron->enable;
1259 $resp .= patron_status_string($patron);
1260 $resp .= $patron->language . Sip::timestamp();
1262 $resp .= add_field(FID_PATRON_ID, $patron->id);
1263 $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
1264 if (defined($patron_pwd)) {
1265 $resp .= add_field(FID_VALID_PATRON_PWD,
1266 sipbool($patron->check_password($patron_pwd)));
1268 $resp .= add_field(FID_VALID_PATRON, 'Y');
1269 $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
1270 $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
1273 $resp .= add_field(FID_INST_ID, $ils->institution);
1275 $self->write_msg($resp);
1277 return(PATRON_ENABLE);
1281 my ($self, $server) = @_;
1282 my $ils = $server->{ils};
1283 my ($hold_mode, $trans_date);
1284 my ($expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd);
1285 my ($item_id, $title_id, $fee_ack);
1286 my $fields = $self->{fields};
1288 my $resp = HOLD_RESP;
1290 ($hold_mode, $trans_date) = @{$self->{fixed_fields}};
1292 $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_hold");
1294 $patron_id = $fields->{(FID_PATRON_ID) };
1295 $expiry_date = $fields->{(FID_EXPIRATION) } || '';
1296 $pickup_locn = $fields->{(FID_PICKUP_LOCN)} || '';
1297 $hold_type = $fields->{(FID_HOLD_TYPE) } || '2'; # Any copy of title
1298 $patron_pwd = $fields->{(FID_PATRON_PWD) };
1299 $item_id = $fields->{(FID_ITEM_ID) } || '';
1300 $title_id = $fields->{(FID_TITLE_ID) } || '';
1301 $fee_ack = $fields->{(FID_FEE_ACK) } || 'N';
1303 if ($hold_mode eq '+') {
1304 $status = $ils->add_hold($patron_id, $patron_pwd,
1305 $item_id, $title_id,
1306 $expiry_date, $pickup_locn, $hold_type,
1308 } elsif ($hold_mode eq '-') {
1309 $status = $ils->cancel_hold($patron_id, $patron_pwd,
1310 $item_id, $title_id);
1311 } elsif ($hold_mode eq '*') {
1312 $status = $ils->alter_hold($patron_id, $patron_pwd,
1313 $item_id, $title_id,
1314 $expiry_date, $pickup_locn, $hold_type,
1317 syslog("LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'",
1318 $hold_mode, $server->{account}->{id});
1319 $status = $ils->Transaction::Hold;
1320 $status->screen_msg("System error. Please contact library status");
1323 $resp .= $status->ok;
1324 $resp .= sipbool($status->item && $status->item->available($patron_id));
1325 $resp .= Sip::timestamp;
1328 $resp .= add_field(FID_PATRON_ID, $status->patron->id);
1330 if ($status->expiration_date) {
1331 $resp .= maybe_add(FID_EXPIRATION,
1332 Sip::timestamp($status->expiration_date));
1334 $resp .= maybe_add(FID_QUEUE_POS, $status->queue_position);
1335 $resp .= maybe_add(FID_PICKUP_LOCN, $status->pickup_location);
1336 $resp .= maybe_add(FID_ITEM_ID, $status->item->id);
1337 $resp .= maybe_add(FID_TITLE_ID, $status->item->title_id);
1339 # Not ok. still need required fields
1340 $resp .= add_field(FID_PATRON_ID, $patron_id);
1343 $resp .= add_field(FID_INST_ID, $ils->institution);
1344 $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1345 $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1347 $self->write_msg($resp);
1353 my ($self, $server) = @_;
1354 my $ils = $server->{ils};
1355 my ($third_party, $no_block, $trans_date, $nb_due_date);
1356 my ($patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack);
1357 my $fields = $self->{fields};
1359 my ($patron, $item);
1360 my $resp = RENEW_RESP;
1362 ($third_party, $no_block, $trans_date, $nb_due_date) =
1363 @{$self->{fixed_fields}};
1365 $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_renew");
1367 if ($no_block eq 'Y') {
1368 syslog("LOG_WARNING",
1369 "handle_renew: recieved 'no block' renewal from terminal '%s'",
1370 $server->{account}->{id});
1373 $patron_id = $fields->{(FID_PATRON_ID)};
1374 $patron_pwd = $fields->{(FID_PATRON_PWD)};
1375 $item_id = $fields->{(FID_ITEM_ID)};
1376 $title_id = $fields->{(FID_TITLE_ID)};
1377 $item_props = $fields->{(FID_ITEM_PROPS)};
1378 $fee_ack = $fields->{(FID_FEE_ACK)};
1380 $status = $ils->renew($patron_id, $patron_pwd, $item_id, $title_id,
1381 $no_block, $nb_due_date, $third_party,
1382 $item_props, $fee_ack);
1384 $patron = $status->patron;
1385 $item = $status->item;
1389 $resp .= $status->renewal_ok ? 'Y' : 'N';
1390 if ($ils->supports('magnetic media')) {
1391 $resp .= sipbool($item->magnetic);
1395 $resp .= sipbool($status->desensitize);
1396 $resp .= Sip::timestamp;
1397 $resp .= add_field(FID_PATRON_ID, $patron->id);
1398 $resp .= add_field(FID_ITEM_ID, $item->id);
1399 $resp .= add_field(FID_TITLE_ID, $item->title_id);
1400 $resp .= add_field(FID_DUE_DATE, $item->due_date);
1401 if ($ils->supports('security inhibit')) {
1402 $resp .= add_field(FID_SECURITY_INHIBIT, $status->security_inhibit);
1404 $resp .= add_field(FID_MEDIA_TYPE, $item->sip_media_type);
1405 $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
1407 # renew failed for some reason
1408 # not OK, renewal not OK, Unknown media type (why bother checking?)
1410 $resp .= Sip::timestamp;
1411 # If we found the patron or the item, the return the ILS
1412 # information, otherwise echo back the infomation we received
1414 $resp .= add_field(FID_PATRON_ID, $patron ? $patron->id : $patron_id);
1415 $resp .= add_field(FID_ITEM_ID, $item ? $item->id : $item_id );
1416 $resp .= add_field(FID_TITLE_ID, $item ? $item->title_id : $title_id );
1417 $resp .= add_field(FID_DUE_DATE, '');
1420 if ($status->fee_amount) {
1421 $resp .= add_field(FID_FEE_AMT, $status->fee_amount);
1422 $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
1423 $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
1424 $resp .= maybe_add(FID_TRANSACTION_ID, $status->transaction_id);
1427 $resp .= add_field(FID_INST_ID, $ils->institution);
1428 $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1429 $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1431 $self->write_msg($resp);
1436 sub handle_renew_all {
1437 my ($self, $server) = @_;
1438 my $ils = $server->{ils};
1439 my ($trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack);
1440 my $fields = $self->{fields};
1441 my $resp = RENEW_ALL_RESP;
1443 my (@renewed, @unrenewed);
1445 $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_renew_all");
1447 ($trans_date) = @{$self->{fixed_fields}};
1449 $patron_id = $fields->{(FID_PATRON_ID)};
1450 $patron_pwd = $fields->{(FID_PATRON_PWD)};
1451 $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
1452 $fee_ack = $fields->{(FID_FEE_ACK)};
1454 $status = $ils->renew_all($patron_id, $patron_pwd, $fee_ack);
1456 $resp .= $status->ok ? '1' : '0';
1459 $resp .= add_count("renew_all/renewed_count", 0);
1460 $resp .= add_count("renew_all/unrenewed_count", 0);
1464 @renewed = @{$status->renewed};
1465 @unrenewed = @{$status->unrenewed};
1466 $resp .= add_count("renew_all/renewed_count", scalar @renewed);
1467 $resp .= add_count("renew_all/unrenewed_count", scalar @unrenewed);
1470 $resp .= Sip::timestamp;
1471 $resp .= add_field(FID_INST_ID, $ils->institution);
1473 $resp .= join('', map(add_field(FID_RENEWED_ITEMS, $_), @renewed));
1474 $resp .= join('', map(add_field(FID_UNRENEWED_ITEMS, $_), @unrenewed));
1476 $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1477 $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1479 $self->write_msg($resp);
1485 # send_acs_status($self, $server)
1487 # Send an ACS Status message, which is contains lots of little fields
1488 # of information gleaned from all sorts of places.
1491 my @message_type_names = (
1492 "patron status request",
1497 "request sc/acs resend",
1499 "patron information",
1500 "end patron session",
1503 "item status update",
1510 sub send_acs_status {
1511 my ($self, $server, $screen_msg, $print_line) = @_;
1512 my $msg = ACS_STATUS;
1513 my $account = $server->{account};
1514 my $policy = $server->{policy};
1515 my $ils = $server->{ils};
1516 my ($online_status, $checkin_ok, $checkout_ok, $ACS_renewal_policy);
1517 my ($status_update_ok, $offline_ok, $timeout, $retries);
1519 $online_status = 'Y';
1520 $checkout_ok = sipbool($ils->checkout_ok);
1521 $checkin_ok = sipbool($ils->checkin_ok);
1522 $ACS_renewal_policy = sipbool($policy->{renewal});
1523 $status_update_ok = sipbool($ils->status_update_ok);
1524 $offline_ok = sipbool($ils->offline_ok);
1525 $timeout = sprintf("%03d", $policy->{timeout});
1526 $retries = sprintf("%03d", $policy->{retries});
1528 if (length($timeout) != 3) {
1529 syslog("LOG_ERR", "handle_acs_status: timeout field wrong size: '%s'", $timeout);
1533 if (length($retries) != 3) {
1534 syslog("LOG_ERR", "handle_acs_status: retries field wrong size: '%s'", $retries);
1538 $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
1539 $msg .= "$status_update_ok$offline_ok$timeout$retries";
1540 $msg .= Sip::timestamp();
1542 if ($protocol_version == 1) {
1544 } elsif ($protocol_version == 2) {
1547 syslog("LOG_ERR", 'Bad setting for $protocol_version, "%s" in send_acs_status', $protocol_version);
1552 $msg .= add_field(FID_INST_ID, $account->{institution});
1554 if ($protocol_version >= 2) {
1555 # Supported messages: we do it all
1556 my $supported_msgs = '';
1558 foreach my $msg_name (@message_type_names) {
1559 if ( $msg_name eq 'request sc/acs resend' ) {
1560 $supported_msgs .= Sip::sipbool(1);
1562 $supported_msgs .= Sip::sipbool( $ils->supports($msg_name) );
1565 if (length($supported_msgs) < 16) {
1566 syslog("LOG_ERR", 'send_acs_status: supported messages "%s" too short', $supported_msgs);
1568 $msg .= add_field(FID_SUPPORTED_MSGS, $supported_msgs);
1571 $msg .= maybe_add(FID_SCREEN_MSG, $screen_msg);
1573 if (defined($account->{print_width}) && defined($print_line)
1574 && $account->{print_width} < length( $print_line)) {
1575 syslog("LOG_WARNING", "send_acs_status: print line '%s' too long. Truncating", $print_line);
1576 $print_line = substr($print_line, 0, $account->{print_width});
1579 $msg .= maybe_add(FID_PRINT_LINE, $print_line);
1581 # Do we want to tell the terminal its location?
1583 $self->write_msg($msg);
1588 # patron_status_string: create the 14-char patron status
1589 # string for the Patron Status message
1591 sub patron_status_string {
1593 syslog("LOG_DEBUG", "patron_status_string for %s charge_ok: %s", $patron->id, $patron->charge_ok);
1594 my $patron_status = sprintf('%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
1595 denied($patron->charge_ok),
1596 denied($patron->renew_ok),
1597 denied($patron->recall_ok),
1598 denied($patron->hold_ok),
1599 boolspace($patron->card_lost),
1600 boolspace($patron->too_many_charged),
1601 boolspace($patron->too_many_overdue),
1602 boolspace($patron->too_many_renewal),
1603 boolspace($patron->too_many_claim_return),
1604 boolspace($patron->too_many_lost),
1605 boolspace($patron->excessive_fines),
1606 boolspace($patron->excessive_fees),
1607 boolspace($patron->recall_overdue),
1608 boolspace($patron->too_many_billed)
1610 return $patron_status;