Bug 22014: Add ability to send "00" in SIP CV field on checkin success
[koha.git] / C4 / SIP / Sip / MsgType.pm
blobd28f1bd024cade92f201a38067c5edefdd3f8b3d
2 # Sip::MsgType.pm
4 # A Class for handing SIP messages
7 package C4::SIP::Sip::MsgType;
9 use strict;
10 use warnings;
11 use Exporter;
12 use Sys::Syslog qw(syslog);
14 use C4::SIP::Sip qw(:all);
15 use C4::SIP::Sip::Constants qw(:all);
16 use C4::SIP::Sip::Checksum qw(verify_cksum);
18 use Data::Dumper;
19 use CGI qw ( -utf8 );
20 use C4::Auth qw(&check_api_auth);
22 use Koha::Patron::Attributes;
24 use UNIVERSAL::can;
26 use vars qw(@ISA @EXPORT_OK);
28 use constant INVALID_CARD => 'Invalid cardnumber';
29 use constant INVALID_PW => 'Invalid password';
31 BEGIN {
32 @ISA = qw(Exporter);
33 @EXPORT_OK = qw(handle login_core);
36 # Predeclare handler subroutines
37 use subs qw(handle_patron_status handle_checkout handle_checkin
38 handle_block_patron handle_sc_status handle_request_acs_resend
39 handle_login handle_patron_info handle_end_patron_session
40 handle_fee_paid handle_item_information handle_item_status_update
41 handle_patron_enable handle_hold handle_renew handle_renew_all);
44 # For the most part, Version 2.00 of the protocol just adds new
45 # variable fields, but sometimes it changes the fixed header.
47 # In general, if there's no '2.00' protocol entry for a handler, that's
48 # because 2.00 didn't extend the 1.00 version of the protocol. This will
49 # be handled by the module initialization code following the declaration,
50 # which goes through the handlers table and creates a '2.00' entry that
51 # points to the same place as the '1.00' entry. If there's a 2.00 entry
52 # but no 1.00 entry, then that means that it's a completely new service
53 # in 2.00, so 1.00 shouldn't recognize it.
55 my %handlers = (
56 (PATRON_STATUS_REQ) => {
57 name => "Patron Status Request",
58 handler => \&handle_patron_status,
59 protocol => {
60 1 => {
61 template => "A3A18",
62 template_len => 21,
63 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
67 (CHECKOUT) => {
68 name => "Checkout",
69 handler => \&handle_checkout,
70 protocol => {
71 1 => {
72 template => "CCA18A18",
73 template_len => 38,
74 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
76 2 => {
77 template => "CCA18A18",
78 template_len => 38,
79 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS), (FID_PATRON_PWD), (FID_FEE_ACK), (FID_CANCEL) ],
83 (CHECKIN) => {
84 name => "Checkin",
85 handler => \&handle_checkin,
86 protocol => {
87 1 => {
88 template => "CA18A18",
89 template_len => 37,
90 fields => [ (FID_CURRENT_LOCN), (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
92 2 => {
93 template => "CA18A18",
94 template_len => 37,
95 fields => [ (FID_CURRENT_LOCN), (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS), (FID_CANCEL) ],
99 (BLOCK_PATRON) => {
100 name => "Block Patron",
101 handler => \&handle_block_patron,
102 protocol => {
103 1 => {
104 template => "CA18",
105 template_len => 19,
106 fields => [ (FID_INST_ID), (FID_BLOCKED_CARD_MSG), (FID_PATRON_ID), (FID_TERMINAL_PWD) ],
110 (SC_STATUS) => {
111 name => "SC Status",
112 handler => \&handle_sc_status,
113 protocol => {
114 1 => {
115 template => "CA3A4",
116 template_len => 8,
117 fields => [],
121 (REQUEST_ACS_RESEND) => {
122 name => "Request ACS Resend",
123 handler => \&handle_request_acs_resend,
124 protocol => {
125 1 => {
126 template => "",
127 template_len => 0,
128 fields => [],
132 (LOGIN) => {
133 name => "Login",
134 handler => \&handle_login,
135 protocol => {
136 2 => {
137 template => "A1A1",
138 template_len => 2,
139 fields => [ (FID_LOGIN_UID), (FID_LOGIN_PWD), (FID_LOCATION_CODE) ],
143 (PATRON_INFO) => {
144 name => "Patron Info",
145 handler => \&handle_patron_info,
146 protocol => {
147 2 => {
148 template => "A3A18A10",
149 template_len => 31,
150 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD), (FID_START_ITEM), (FID_END_ITEM) ],
154 (END_PATRON_SESSION) => {
155 name => "End Patron Session",
156 handler => \&handle_end_patron_session,
157 protocol => {
158 2 => {
159 template => "A18",
160 template_len => 18,
161 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
165 (FEE_PAID) => {
166 name => "Fee Paid",
167 handler => \&handle_fee_paid,
168 protocol => {
169 2 => {
170 template => "A18A2A2A3",
171 template_len => 25,
172 fields => [ (FID_FEE_AMT), (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD), (FID_FEE_ID), (FID_TRANSACTION_ID) ],
176 (ITEM_INFORMATION) => {
177 name => "Item Information",
178 handler => \&handle_item_information,
179 protocol => {
180 2 => {
181 template => "A18",
182 template_len => 18,
183 fields => [ (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
187 (ITEM_STATUS_UPDATE) => {
188 name => "Item Status Update",
189 handler => \&handle_item_status_update,
190 protocol => {
191 2 => {
192 template => "A18",
193 template_len => 18,
194 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS) ],
198 (PATRON_ENABLE) => {
199 name => "Patron Enable",
200 handler => \&handle_patron_enable,
201 protocol => {
202 2 => {
203 template => "A18",
204 template_len => 18,
205 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
209 (HOLD) => {
210 name => "Hold",
211 handler => \&handle_hold,
212 protocol => {
213 2 => {
214 template => "AA18",
215 template_len => 19,
216 fields => [
217 (FID_EXPIRATION), (FID_PICKUP_LOCN), (FID_HOLD_TYPE), (FID_INST_ID), (FID_PATRON_ID), (FID_PATRON_PWD),
218 (FID_ITEM_ID), (FID_TITLE_ID), (FID_TERMINAL_PWD), (FID_FEE_ACK)
223 (RENEW) => {
224 name => "Renew",
225 handler => \&handle_renew,
226 protocol => {
227 2 => {
228 template => "CCA18A18",
229 template_len => 38,
230 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) ],
234 (RENEW_ALL) => {
235 name => "Renew All",
236 handler => \&handle_renew_all,
237 protocol => {
238 2 => {
239 template => "A18",
240 template_len => 18,
241 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_PATRON_PWD), (FID_TERMINAL_PWD), (FID_FEE_ACK) ],
248 # Now, initialize some of the missing bits of %handlers
250 foreach my $i ( keys(%handlers) ) {
251 if ( !exists( $handlers{$i}->{protocol}->{2} ) ) {
252 $handlers{$i}->{protocol}->{2} = $handlers{$i}->{protocol}->{1};
256 sub new {
257 my ( $class, $msg, $seqno ) = @_;
258 my $self = {};
259 my $msgtag = substr( $msg, 0, 2 );
261 if ( $msgtag eq LOGIN ) {
263 # If the client is using the 2.00-style "Login" message
264 # to authenticate to the server, then we get the Login message
265 # _before_ the client has indicated that it supports 2.00, but
266 # it's using the 2.00 login process, so it must support 2.00.
267 $protocol_version = 2;
269 syslog( "LOG_DEBUG", "Sip::MsgType::new('%s', '%s...', '%s'): seq.no '%s', protocol %s", $class, substr( $msg, 0, 10 ), $msgtag, $seqno, $protocol_version );
271 # warn "SIP PROTOCOL: $protocol_version";
272 if ( !exists( $handlers{$msgtag} ) ) {
273 syslog( "LOG_WARNING", "new Sip::MsgType: Skipping message of unknown type '%s' in '%s'", $msgtag, $msg );
274 return;
275 } elsif ( !exists( $handlers{$msgtag}->{protocol}->{$protocol_version} ) ) {
276 syslog( "LOG_WARNING", "new Sip::MsgType: Skipping message '%s' unsupported by protocol rev. '%d'", $msgtag, $protocol_version );
277 return;
280 bless $self, $class;
282 $self->{seqno} = $seqno;
283 $self->_initialize( substr( $msg, 2 ), $handlers{$msgtag} );
285 return ($self);
288 sub _initialize {
289 my ( $self, $msg, $control_block ) = @_;
290 my $fn;
291 my $proto = $control_block->{protocol}->{$protocol_version};
293 $self->{name} = $control_block->{name};
294 $self->{handler} = $control_block->{handler};
296 $self->{fields} = {};
297 $self->{fixed_fields} = [];
299 chomp($msg); # These four are probably unnecessary now.
300 $msg =~ tr/\cM//d;
301 $msg =~ s/\^M$//;
302 chomp($msg);
304 foreach my $field ( @{ $proto->{fields} } ) {
305 $self->{fields}->{$field} = undef;
308 syslog( "LOG_DEBUG", "Sip::MsgType::_initialize('%s', '%s', '%s', '%s', ...)", $self->{name}, $msg, $proto->{template}, $proto->{template_len} );
310 $self->{fixed_fields} = [ unpack( $proto->{template}, $msg ) ]; # see http://perldoc.perl.org/5.8.8/functions/unpack.html
312 # Skip over the fixed fields and the split the rest of
313 # the message into fields based on the delimiter and parse them
314 foreach my $field ( split( quotemeta($field_delimiter), substr( $msg, $proto->{template_len} ) ) ) {
315 $fn = substr( $field, 0, 2 );
317 if ( !exists( $self->{fields}->{$fn} ) ) {
318 syslog( "LOG_WARNING", "Unsupported field '%s' in %s message '%s'", $fn, $self->{name}, $msg );
319 } elsif ( defined( $self->{fields}->{$fn} ) ) {
320 syslog( "LOG_WARNING", "Duplicate field '%s' (previous value '%s') in %s message '%s'", $fn, $self->{fields}->{$fn}, $self->{name}, $msg );
321 } else {
322 $self->{fields}->{$fn} = substr( $field, 2 );
326 return ($self);
329 sub handle {
330 my ( $msg, $server, $req ) = @_;
331 my $config = $server->{config};
332 my $self;
335 # What's the field delimiter for variable length fields?
336 # This can't be based on the account, since we need to know
337 # the field delimiter to parse a SIP login message
339 if ( defined( $server->{config}->{delimiter} ) ) {
340 $field_delimiter = $server->{config}->{delimiter};
343 # error detection is active if this is a REQUEST_ACS_RESEND
344 # message with a checksum, or if the message is long enough
345 # and the last nine characters begin with a sequence number
346 # field
347 if ( $msg eq REQUEST_ACS_RESEND_CKSUM ) {
349 # Special case
350 $error_detection = 1;
351 $self = C4::SIP::Sip::MsgType->new( (REQUEST_ACS_RESEND), 0 );
352 } elsif ( ( length($msg) > 11 ) && ( substr( $msg, -9, 2 ) eq "AY" ) ) {
353 $error_detection = 1;
355 if ( !verify_cksum($msg) ) {
356 syslog( "LOG_WARNING", "Checksum failed on message '%s'", $msg );
358 # REQUEST_SC_RESEND with error detection
359 $last_response = REQUEST_SC_RESEND_CKSUM;
360 print("$last_response\r");
361 return REQUEST_ACS_RESEND;
362 } else {
364 # Save the sequence number, then strip off the
365 # error detection data to process the message
366 $self = C4::SIP::Sip::MsgType->new( substr( $msg, 0, -9 ), substr( $msg, -7, 1 ) );
368 } elsif ($error_detection) {
370 # We received a non-ED message when ED is supposed to be active.
371 # Warn about this problem, then process the message anyway.
372 syslog( "LOG_WARNING", "Received message without error detection: '%s'", $msg );
373 $error_detection = 0;
374 $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
375 } else {
376 $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
379 if ( ( substr( $msg, 0, 2 ) ne REQUEST_ACS_RESEND )
380 && $req
381 && ( substr( $msg, 0, 2 ) ne $req ) ) {
382 return substr( $msg, 0, 2 );
384 unless ( $self->{handler} ) {
385 syslog( "LOG_WARNING", "No handler defined for '%s'", $msg );
386 $last_response = REQUEST_SC_RESEND;
387 print("$last_response\r");
388 return REQUEST_ACS_RESEND;
390 return ( $self->{handler}->( $self, $server ) ); # FIXME
391 # FIXME: Use of uninitialized value in subroutine entry
392 # Can't use string ("") as a subroutine ref while "strict refs" in use
396 ## Message Handlers
400 # Patron status messages are produced in response to both
401 # "Request Patron Status" and "Block Patron"
403 # Request Patron Status requires a patron password, but
404 # Block Patron doesn't (since the patron may never have
405 # provided one before attempting some illegal action).
407 # ASSUMPTION: If the patron password field is present in the
408 # message, then it must match, otherwise incomplete patron status
409 # information will be returned to the terminal.
411 sub build_patron_status {
412 my ( $patron, $lang, $fields, $server ) = @_;
414 my $patron_pwd = $fields->{ (FID_PATRON_PWD) };
415 my $resp = (PATRON_STATUS_RESP);
416 my $password_rc;
418 if ( $patron ) {
419 if ($patron_pwd) {
420 $password_rc = $patron->check_password($patron_pwd);
423 $resp .= patron_status_string($patron);
424 $resp .= $lang . timestamp();
425 $resp .= add_field( FID_PERSONAL_NAME, $patron->name( $server->{account}->{ae_field_template} ) );
427 # while the patron ID we got from the SC is valid, let's
428 # use the one returned from the ILS, just in case...
429 $resp .= add_field( FID_PATRON_ID, $patron->id );
431 if ( $protocol_version >= 2 ) {
432 $resp .= add_field( FID_VALID_PATRON, 'Y' );
434 # Patron password is a required field.
435 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool($password_rc) );
436 $resp .= maybe_add( FID_CURRENCY, $patron->currency );
437 $resp .= maybe_add( FID_FEE_AMT, $patron->fee_amount );
440 my $msg = $patron->screen_msg;
441 $msg .= ' -- '. INVALID_PW if $patron_pwd && !$password_rc;
442 $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
444 $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server )
445 if ( $server->{account}->{send_patron_home_library_in_af} );
446 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line );
448 $resp .= $patron->build_patron_attributes_string( $server );
450 } else {
451 # Invalid patron (cardnumber)
452 # Report that the user has no privs.
454 # no personal name, and is invalid (if we're using 2.00)
455 $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
456 $resp .= add_field( FID_PERSONAL_NAME, '' );
458 # the patron ID is invalid, but it's a required field, so
459 # just echo it back
460 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) } );
462 ( $protocol_version >= 2 )
463 and $resp .= add_field( FID_VALID_PATRON, 'N' );
465 $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
468 $resp .= add_field( FID_INST_ID, $fields->{ (FID_INST_ID) } );
469 return $resp;
472 sub handle_patron_status {
473 my ( $self, $server ) = @_;
474 my $ils = $server->{ils};
475 my $patron;
476 my $resp = (PATRON_STATUS_RESP);
477 my $account = $server->{account};
478 my ( $lang, $date ) = @{ $self->{fixed_fields} };
479 my $fields = $self->{fields};
481 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_patron_status" );
482 $patron = $ils->find_patron( $fields->{ (FID_PATRON_ID) } );
483 $resp = build_patron_status( $patron, $lang, $fields, $server );
484 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
485 return (PATRON_STATUS_REQ);
488 sub handle_checkout {
489 my ( $self, $server ) = @_;
490 my $account = $server->{account};
491 my $ils = $server->{ils};
492 my $inst = $ils->institution;
493 my ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date );
494 my $fields;
495 my ( $patron_id, $item_id, $status );
496 my ( $item, $patron );
497 my $resp;
499 ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
500 $fields = $self->{fields};
502 $patron_id = $fields->{ (FID_PATRON_ID) };
503 $item_id = $fields->{ (FID_ITEM_ID) };
504 my $fee_ack = $fields->{ (FID_FEE_ACK) };
506 if ( $no_block eq 'Y' ) {
508 # Off-line transactions need to be recorded, but there's
509 # not a lot we can do about it
510 syslog( "LOG_WARNING", "received no-block checkout from terminal '%s'", $account->{id} );
512 $status = $ils->checkout_no_block( $patron_id, $item_id, $sc_renewal_policy, $trans_date, $nb_due_date );
513 } else {
515 # Does the transaction date really matter for items that are
516 # checkout out while the terminal is online? I'm guessing 'no'
517 $status = $ils->checkout( $patron_id, $item_id, $sc_renewal_policy, $fee_ack );
520 $item = $status->item;
521 $patron = $status->patron;
523 if ( $status->ok ) {
525 # Item successfully checked out
526 # Fixed fields
527 $resp = CHECKOUT_RESP . '1';
528 $resp .= sipbool( $status->renew_ok );
529 if ( $ils->supports('magnetic media') ) {
530 $resp .= sipbool( $item->magnetic_media );
531 } else {
532 $resp .= 'U';
535 # We never return the obsolete 'U' value for 'desensitize'
536 $resp .= sipbool( $status->desensitize );
537 $resp .= timestamp;
539 # Now for the variable fields
540 $resp .= add_field( FID_INST_ID, $inst );
541 $resp .= add_field( FID_PATRON_ID, $patron_id );
542 $resp .= add_field( FID_ITEM_ID, $item_id );
543 $resp .= add_field( FID_TITLE_ID, $item->title_id );
544 if ( $item->due_date ) {
545 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ) );
546 } else {
547 $resp .= add_field( FID_DUE_DATE, q{} );
550 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
551 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
553 if ( $protocol_version >= 2 ) {
554 if ( $ils->supports('security inhibit') ) {
555 $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit );
557 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type );
558 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
563 else {
565 # Checkout failed
566 # Checkout Response: not ok, no renewal, don't know mag. media,
567 # no desensitize
568 $resp = sprintf( "120NUN%s", timestamp );
569 $resp .= add_field( FID_INST_ID, $inst );
570 $resp .= add_field( FID_PATRON_ID, $patron_id );
571 $resp .= add_field( FID_ITEM_ID, $item_id );
573 # If the item is valid, provide the title, otherwise
574 # leave it blank
575 $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : '' );
577 # Due date is required. Since it didn't get checked out,
578 # it's not due, so leave the date blank
579 $resp .= add_field( FID_DUE_DATE, '' );
581 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
582 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
584 if ( $protocol_version >= 2 ) {
586 # Is the patron ID valid?
587 $resp .= add_field( FID_VALID_PATRON, sipbool($patron) );
589 if ( $patron && exists( $fields->{FID_PATRON_PWD} ) ) {
591 # Password provided, so we can tell if it was valid or not
592 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password( $fields->{ (FID_PATRON_PWD) } ) ) );
597 if ( $protocol_version >= 2 ) {
599 # Financials : return irrespective of ok status
600 if ( $status->fee_amount ) {
601 $resp .= add_field( FID_FEE_AMT, $status->fee_amount );
602 $resp .= maybe_add( FID_CURRENCY, $status->sip_currency );
603 $resp .= maybe_add( FID_FEE_TYPE, $status->sip_fee_type );
604 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id );
608 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
609 return (CHECKOUT);
612 sub handle_checkin {
613 my ( $self, $server ) = @_;
614 my $account = $server->{account};
615 my $ils = $server->{ils};
616 my $my_branch = $ils->institution;
617 my ( $current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel );
618 my ( $patron, $item, $status );
619 my $resp = CHECKIN_RESP;
620 my ( $no_block, $trans_date, $return_date ) = @{ $self->{fixed_fields} };
621 my $fields = $self->{fields};
623 $current_loc = $fields->{ (FID_CURRENT_LOCN) };
624 $inst_id = $fields->{ (FID_INST_ID) };
625 $item_id = $fields->{ (FID_ITEM_ID) };
626 $item_props = $fields->{ (FID_ITEM_PROPS) };
627 $cancel = $fields->{ (FID_CANCEL) };
628 if ($current_loc) {
629 $my_branch = $current_loc; # most scm do not set $current_loc
632 $ils->check_inst_id( $inst_id, "handle_checkin" );
634 if ( $no_block eq 'Y' ) {
636 # Off-line transactions, ick.
637 syslog( "LOG_WARNING", "received no-block checkin from terminal '%s'", $account->{id} );
638 $status = $ils->checkin_no_block( $item_id, $trans_date, $return_date, $item_props, $cancel );
639 } else {
640 $status = $ils->checkin( $item_id, $trans_date, $return_date, $my_branch, $item_props, $cancel, $account->{checked_in_ok} );
643 $patron = $status->patron;
644 $item = $status->item;
646 $resp .= $status->ok ? '1' : '0';
647 $resp .= $status->resensitize ? 'Y' : 'N';
648 if ( $item && $ils->supports('magnetic media') ) {
649 $resp .= sipbool( $item->magnetic_media );
650 } else {
652 # item barcode is invalid or system doesn't support 'magnetic media' indicator
653 $resp .= 'U';
656 $resp .= $status->alert ? 'Y' : 'N';
657 $resp .= timestamp;
658 $resp .= add_field( FID_INST_ID, $inst_id );
659 $resp .= add_field( FID_ITEM_ID, $item_id );
661 if ($item) {
662 $resp .= add_field( FID_PERM_LOCN, $item->permanent_location );
663 $resp .= maybe_add( FID_TITLE_ID, $item->title_id );
666 if ( $protocol_version >= 2 ) {
667 $resp .= maybe_add( FID_SORT_BIN, $status->sort_bin );
668 if ($patron) {
669 $resp .= add_field( FID_PATRON_ID, $patron->id );
671 if ($item) {
672 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type );
673 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
674 $resp .= maybe_add( FID_COLLECTION_CODE, $item->collection_code );
675 $resp .= maybe_add( FID_CALL_NUMBER, $item->call_number );
676 $resp .= maybe_add( FID_DESTINATION_LOCATION, $item->destination_loc );
677 $resp .= maybe_add( FID_HOLD_PATRON_ID, $item->hold_patron_bcode );
678 $resp .= maybe_add( FID_HOLD_PATRON_NAME, $item->hold_patron_name( $server->{account}->{da_field_template} ) );
679 if ( $status->hold and $status->hold->{branchcode} ne $item->destination_loc ) {
680 warn 'SIP hold mismatch: $status->hold->{branchcode}=' . $status->hold->{branchcode} . '; $item->destination_loc=' . $item->destination_loc;
682 # just me being paranoid.
687 if ( $status->alert && $status->alert_type ) {
688 $resp .= maybe_add( FID_ALERT_TYPE, $status->alert_type );
689 } elsif ( $server->{account}->{cv_send_00_on_success} ) {
690 $resp .= add_field( FID_ALERT_TYPE, '00' );
692 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
693 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
695 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
697 return (CHECKIN);
700 sub handle_block_patron {
701 my ( $self, $server ) = @_;
702 my $account = $server->{account};
703 my $ils = $server->{ils};
704 my ( $card_retained, $trans_date );
705 my ( $inst_id, $blocked_card_msg, $patron_id, $terminal_pwd );
706 my ( $fields, $resp, $patron );
708 ( $card_retained, $trans_date ) = @{ $self->{fixed_fields} };
709 $fields = $self->{fields};
710 $inst_id = $fields->{ (FID_INST_ID) };
711 $blocked_card_msg = $fields->{ (FID_BLOCKED_CARD_MSG) };
712 $patron_id = $fields->{ (FID_PATRON_ID) };
713 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
715 # Terminal passwords are different from account login
716 # passwords, but I have no idea what to do with them. So,
717 # I'll just ignore them for now.
719 # FIXME ???
721 $ils->check_inst_id( $inst_id, "block_patron" );
722 $patron = $ils->find_patron($patron_id);
724 # The correct response for a "Block Patron" message is a
725 # "Patron Status Response", so use that handler to generate
726 # the message, but then return the correct code from here.
728 # Normally, the language is provided by the "Patron Status"
729 # fixed field, but since we're not responding to one of those
730 # we'll just say, "Unspecified", as per the spec. Let the
731 # terminal default to something that, one hopes, will be
732 # intelligible
733 if ($patron) {
735 # Valid patron id
736 $patron->block( $card_retained, $blocked_card_msg );
739 $resp = build_patron_status( $patron, $patron->language, $fields, $server );
740 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
741 return (BLOCK_PATRON);
744 sub handle_sc_status {
745 my ( $self, $server ) = @_;
746 ($server) or warn "handle_sc_status error: no \$server argument received.";
747 my ( $status, $print_width, $sc_protocol_version ) = @{ $self->{fixed_fields} };
748 my ($new_proto);
750 if ( $sc_protocol_version =~ /^1\./ ) {
751 $new_proto = 1;
752 } elsif ( $sc_protocol_version =~ /^2\./ ) {
753 $new_proto = 2;
754 } else {
755 syslog( "LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version );
756 $new_proto = 1;
759 if ( $new_proto != $protocol_version ) {
760 syslog( "LOG_INFO", "Setting protocol level to $new_proto" );
761 $protocol_version = $new_proto;
764 if ( $status == SC_STATUS_PAPER ) {
765 syslog( "LOG_WARNING", "Self-Check unit '%s@%s' out of paper", $self->{account}->{id}, $self->{account}->{institution} );
766 } elsif ( $status == SC_STATUS_SHUTDOWN ) {
767 syslog( "LOG_WARNING", "Self-Check unit '%s@%s' shutting down", $self->{account}->{id}, $self->{account}->{institution} );
770 $self->{account}->{print_width} = $print_width;
771 return ( send_acs_status( $self, $server ) ? SC_STATUS : '' );
774 sub handle_request_acs_resend {
775 my ( $self, $server ) = @_;
777 if ( !$last_response ) {
779 # We haven't sent anything yet, so respond with a
780 # REQUEST_SC_RESEND msg (p. 16)
781 $self->write_msg( REQUEST_SC_RESEND, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
782 } elsif ( ( length($last_response) < 9 )
783 || substr( $last_response, -9, 2 ) ne 'AY' ) {
785 # When resending a message, we aren't supposed to include
786 # a sequence number, even if the original had one (p. 4).
787 # If the last message didn't have a sequence number, then
788 # we can just send it.
789 print("$last_response\r"); # not write_msg?
790 } else {
792 # Cut out the sequence number and checksum, since the old
793 # checksum is wrong for the resent message.
794 my $rebuilt = substr( $last_response, 0, -9 );
795 $self->write_msg( $rebuilt, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
798 return REQUEST_ACS_RESEND;
801 sub login_core {
802 my $server = shift or return;
803 my $uid = shift;
804 my $pwd = shift;
805 my $status = 1; # Assume it all works
806 if ( !exists( $server->{config}->{accounts}->{$uid} ) ) {
807 syslog( "LOG_WARNING", "MsgType::login_core: Unknown login '$uid'" );
808 $status = 0;
809 } elsif ( $server->{config}->{accounts}->{$uid}->{password} ne $pwd ) {
810 syslog( "LOG_WARNING", "MsgType::login_core: Invalid password for login '$uid'" );
811 $status = 0;
812 } else {
814 # Store the active account someplace handy for everybody else to find.
815 $server->{account} = $server->{config}->{accounts}->{$uid};
816 my $inst = $server->{account}->{institution};
817 $server->{institution} = $server->{config}->{institutions}->{$inst};
818 $server->{policy} = $server->{institution}->{policy};
819 $server->{sip_username} = $uid;
820 $server->{sip_password} = $pwd;
822 my $auth_status = api_auth( $uid, $pwd, $inst );
823 if ( !$auth_status or $auth_status !~ /^ok$/i ) {
824 syslog( "LOG_WARNING", "api_auth failed for SIP terminal '%s' of '%s': %s", $uid, $inst, ( $auth_status || 'unknown' ) );
825 $status = 0;
826 } else {
827 syslog( "LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst );
830 # initialize connection to ILS
832 my $module = $server->{config}->{institutions}->{$inst}->{implementation};
833 syslog( "LOG_DEBUG", 'login_core: ' . Dumper($module) );
835 # Suspect this is always ILS but so we don't break any eccentic install (for now)
836 if ( $module eq 'ILS' ) {
837 $module = 'C4::SIP::ILS';
839 $module->use;
840 if ($@) {
841 syslog( "LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed", $server->{service}, $module, $inst );
842 die("Failed to load ILS implementation '$module' for $inst");
845 # like ILS->new(), I think.
846 $server->{ils} = $module->new( $server->{institution}, $server->{account} );
847 if ( !$server->{ils} ) {
848 syslog( "LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst );
849 die("Unable to connect to ILS '$inst'");
853 return $status;
856 sub handle_login {
857 my ( $self, $server ) = @_;
858 my ( $uid_algorithm, $pwd_algorithm );
859 my ( $uid, $pwd );
860 my $inst;
861 my $fields;
862 my $status = 1; # Assume it all works
864 $fields = $self->{fields};
865 ( $uid_algorithm, $pwd_algorithm ) = @{ $self->{fixed_fields} };
867 $uid = $fields->{ (FID_LOGIN_UID) }; # Terminal ID, not patron ID.
868 $pwd = $fields->{ (FID_LOGIN_PWD) }; # Terminal PWD, not patron PWD.
870 if ( $uid_algorithm || $pwd_algorithm ) {
871 syslog( "LOG_ERR", "LOGIN: Unsupported non-zero encryption method(s): uid = $uid_algorithm, pwd = $pwd_algorithm" );
872 $status = 0;
873 } else {
874 $status = login_core( $server, $uid, $pwd );
877 $self->write_msg( LOGIN_RESP . $status, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
878 return $status ? LOGIN : '';
882 # Build the detailed summary information for the Patron
883 # Information Response message based on the first 'Y' that appears
884 # in the 'summary' field of the Patron Information request. The
885 # specification says that only one 'Y' can appear in that field,
886 # and we're going to believe it.
888 sub summary_info {
889 my ( $ils, $patron, $summary, $start, $end, $server ) = @_;
890 my $resp = '';
891 my $summary_type;
894 # Map from offsets in the "summary" field of the Patron Information
895 # message to the corresponding field and handler
897 my @summary_map = (
898 { func => $patron->can("hold_items"), fid => FID_HOLD_ITEMS },
899 { func => $patron->can("overdue_items"), fid => FID_OVERDUE_ITEMS },
900 { func => $patron->can("charged_items"), fid => FID_CHARGED_ITEMS },
901 { func => $patron->can("fine_items"), fid => FID_FINE_ITEMS },
902 { func => $patron->can("recall_items"), fid => FID_RECALL_ITEMS },
903 { func => $patron->can("unavail_holds"), fid => FID_UNAVAILABLE_HOLD_ITEMS },
906 if ( ( $summary_type = index( $summary, 'Y' ) ) == -1 ) {
907 return ''; # No detailed information required
910 syslog( "LOG_DEBUG", "Summary_info: index == '%d', field '%s'", $summary_type, $summary_map[$summary_type]->{fid} );
912 my $func = $summary_map[$summary_type]->{func};
913 my $fid = $summary_map[$summary_type]->{fid};
914 my $itemlist = &$func( $patron, $start, $end, $server );
916 syslog( "LOG_DEBUG", "summary_info: list = (%s)", join( ", ", @{$itemlist} ) );
917 foreach my $i ( @{$itemlist} ) {
918 $resp .= add_field( $fid, $i->{barcode} );
921 return $resp;
924 sub handle_patron_info {
925 my ( $self, $server ) = @_;
926 my $ils = $server->{ils};
927 my ( $lang, $trans_date, $summary ) = @{ $self->{fixed_fields} };
928 my $fields = $self->{fields};
929 my ( $inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end );
930 my ( $resp, $patron );
932 $inst_id = $fields->{ (FID_INST_ID) };
933 $patron_id = $fields->{ (FID_PATRON_ID) };
934 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
935 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
936 $start = $fields->{ (FID_START_ITEM) };
937 $end = $fields->{ (FID_END_ITEM) };
939 $patron = $ils->find_patron($patron_id);
941 $resp = (PATRON_INFO_RESP);
942 if ($patron) {
943 $patron->update_lastseen();
944 $resp .= patron_status_string($patron);
945 $resp .= ( defined($lang) and length($lang) == 3 ) ? $lang : $patron->language;
946 $resp .= timestamp();
948 $resp .= add_count( 'patron_info/hold_items', scalar @{ $patron->hold_items } );
949 $resp .= add_count( 'patron_info/overdue_items', scalar @{ $patron->overdue_items } );
950 $resp .= add_count( 'patron_info/charged_items', scalar @{ $patron->charged_items } );
951 $resp .= add_count( 'patron_info/fine_items', scalar @{ $patron->fine_items } );
952 $resp .= add_count( 'patron_info/recall_items', scalar @{ $patron->recall_items } );
953 $resp .= add_count( 'patron_info/unavail_holds', scalar @{ $patron->unavail_holds } );
955 $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ) );
957 # while the patron ID we got from the SC is valid, let's
958 # use the one returned from the ILS, just in case...
959 $resp .= add_field( FID_PATRON_ID, $patron->id );
960 $resp .= add_field( FID_PERSONAL_NAME, $patron->name( $server->{account}->{ae_field_template} ) );
962 # TODO: add code for the fields
963 # hold items limit
964 # overdue items limit
965 # charged items limit
967 $resp .= add_field( FID_VALID_PATRON, 'Y' );
968 my $password_rc;
969 if ( defined($patron_pwd) ) {
971 # If patron password was provided, report whether it was right or not.
972 if ( $patron_pwd eq q{} && $server->{account}->{allow_empty_passwords} ) {
973 $password_rc = 1;
974 } else {
975 $password_rc = $patron->check_password($patron_pwd);
977 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $password_rc ) );
980 $resp .= maybe_add( FID_CURRENCY, $patron->currency );
981 $resp .= maybe_add( FID_FEE_AMT, $patron->fee_amount );
982 $resp .= add_field( FID_FEE_LMT, $patron->fee_limit );
984 # TODO: zero or more item details for 2.0 can go here:
985 # hold_items
986 # overdue_items
987 # charged_items
988 # fine_items
989 # recall_items
991 $resp .= summary_info( $ils, $patron, $summary, $start, $end, $server );
993 $resp .= maybe_add( FID_HOME_ADDR, $patron->address );
994 $resp .= maybe_add( FID_EMAIL, $patron->email_addr );
995 $resp .= maybe_add( FID_HOME_PHONE, $patron->home_phone );
997 # SIP 2.0 extensions used by Envisionware
998 # Other terminals will ignore unrecognized fields (unrecognized field identifiers)
999 $resp .= maybe_add( FID_PATRON_BIRTHDATE, $patron->birthdate );
1000 $resp .= maybe_add( FID_PATRON_CLASS, $patron->ptype );
1002 # Custom protocol extension to report patron internet privileges
1003 $resp .= maybe_add( FID_INET_PROFILE, $patron->inet_privileges );
1005 my $msg = $patron->screen_msg;
1006 if( defined( $patron_pwd ) && !$password_rc ) {
1007 $msg .= ' -- ' . INVALID_PW;
1009 $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
1010 if ( $server->{account}->{send_patron_home_library_in_af} ) {
1011 $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server);
1013 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line );
1015 $resp .= $patron->build_patron_attributes_string( $server );
1016 } else {
1018 # Invalid patron ID:
1019 # no privileges, no items associated,
1020 # no personal name, and is invalid (if we're using 2.00)
1021 $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
1022 $resp .= '0000' x 6;
1024 $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ) );
1026 # patron ID is invalid, but field is required, so just echo it back
1027 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) } );
1028 $resp .= add_field( FID_PERSONAL_NAME, '' );
1030 if ( $protocol_version >= 2 ) {
1031 $resp .= add_field( FID_VALID_PATRON, 'N' );
1033 $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
1036 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1037 return (PATRON_INFO);
1040 sub handle_end_patron_session {
1041 my ( $self, $server ) = @_;
1042 my $ils = $server->{ils};
1043 my $trans_date;
1044 my $fields = $self->{fields};
1045 my $resp = END_SESSION_RESP;
1046 my ( $status, $screen_msg, $print_line );
1048 ($trans_date) = @{ $self->{fixed_fields} };
1050 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, 'handle_end_patron_session' );
1052 ( $status, $screen_msg, $print_line ) = $ils->end_patron_session( $fields->{ (FID_PATRON_ID) } );
1054 $resp .= $status ? 'Y' : 'N';
1055 $resp .= timestamp();
1057 $resp .= add_field( FID_INST_ID, $server->{ils}->institution );
1058 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) } );
1060 $resp .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1061 $resp .= maybe_add( FID_PRINT_LINE, $print_line );
1063 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1065 return (END_PATRON_SESSION);
1068 sub handle_fee_paid {
1069 my ( $self, $server ) = @_;
1070 my $ils = $server->{ils};
1071 my ( $trans_date, $fee_type, $pay_type, $currency ) = @{ $self->{fixed_fields} };
1072 my $fields = $self->{fields};
1073 my ( $fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd );
1074 my ( $fee_id, $trans_id );
1075 my $status;
1076 my $resp = FEE_PAID_RESP;
1078 my $disallow_overpayment = $server->{account}->{disallow_overpayment};
1079 my $payment_type_writeoff = $server->{account}->{payment_type_writeoff} || q{};
1081 my $is_writeoff = $pay_type eq $payment_type_writeoff;
1083 $fee_amt = $fields->{ (FID_FEE_AMT) };
1084 $inst_id = $fields->{ (FID_INST_ID) };
1085 $patron_id = $fields->{ (FID_PATRON_ID) };
1086 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1087 $fee_id = $fields->{ (FID_FEE_ID) };
1088 $trans_id = $fields->{ (FID_TRANSACTION_ID) };
1090 $ils->check_inst_id( $inst_id, "handle_fee_paid" );
1092 $status = $ils->pay_fee( $patron_id, $patron_pwd, $fee_amt, $fee_type, $pay_type, $fee_id, $trans_id, $currency, $is_writeoff, $disallow_overpayment );
1094 $resp .= ( $status->ok ? 'Y' : 'N' ) . timestamp;
1095 $resp .= add_field( FID_INST_ID, $inst_id );
1096 $resp .= add_field( FID_PATRON_ID, $patron_id );
1097 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id );
1098 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1099 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1101 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1103 return (FEE_PAID);
1106 sub handle_item_information {
1107 my ( $self, $server ) = @_;
1108 my $ils = $server->{ils};
1109 my $trans_date;
1110 my $fields = $self->{fields};
1111 my $resp = ITEM_INFO_RESP;
1112 my $item;
1113 my $i;
1115 ($trans_date) = @{ $self->{fixed_fields} };
1117 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_item_information" );
1119 $item = $ils->find_item( $fields->{ (FID_ITEM_ID) } );
1121 if ( !defined($item) ) {
1123 # Invalid Item ID
1124 # "Other" circ stat, "Other" security marker, "Unknown" fee type
1125 $resp .= "010101";
1126 $resp .= timestamp;
1128 # Just echo back the invalid item id
1129 $resp .= add_field( FID_ITEM_ID, $fields->{ (FID_ITEM_ID) } );
1131 # title id is required, but we don't have one
1132 $resp .= add_field( FID_TITLE_ID, '' );
1133 } else {
1135 # Valid Item ID, send the good stuff
1136 $resp .= $item->sip_circulation_status;
1137 $resp .= $item->sip_security_marker;
1138 $resp .= $item->sip_fee_type;
1139 $resp .= timestamp;
1141 $resp .= add_field( FID_ITEM_ID, $item->id );
1142 $resp .= add_field( FID_TITLE_ID, $item->title_id );
1144 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type );
1145 $resp .= maybe_add( FID_PERM_LOCN, $item->permanent_location );
1146 $resp .= maybe_add( FID_CURRENT_LOCN, $item->current_location );
1147 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
1149 if ( ( $i = $item->fee ) != 0 ) {
1150 $resp .= add_field( FID_CURRENCY, $item->fee_currency );
1151 $resp .= add_field( FID_FEE_AMT, $i );
1153 $resp .= maybe_add( FID_OWNER, $item->owner );
1155 if ( ( $i = scalar @{ $item->hold_queue } ) > 0 ) {
1156 $resp .= add_field( FID_HOLD_QUEUE_LEN, $i );
1158 if ( $item->due_date ) {
1159 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ) );
1161 if ( ( $i = $item->recall_date ) != 0 ) {
1162 $resp .= add_field( FID_RECALL_DATE, timestamp($i) );
1164 if ( ( $i = $item->hold_pickup_date ) != 0 ) {
1165 $resp .= add_field( FID_HOLD_PICKUP_DATE, timestamp($i) );
1168 $resp .= maybe_add( FID_SCREEN_MSG, $item->screen_msg, $server );
1169 $resp .= maybe_add( FID_PRINT_LINE, $item->print_line );
1172 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1174 return (ITEM_INFORMATION);
1177 sub handle_item_status_update {
1178 my ( $self, $server ) = @_;
1179 my $ils = $server->{ils};
1180 my ( $trans_date, $item_id, $terminal_pwd, $item_props );
1181 my $fields = $self->{fields};
1182 my $status;
1183 my $item;
1184 my $resp = ITEM_STATUS_UPDATE_RESP;
1186 ($trans_date) = @{ $self->{fixed_fields} };
1188 $ils->check_inst_id( $fields->{ (FID_INST_ID) } );
1190 $item_id = $fields->{ (FID_ITEM_ID) };
1191 $item_props = $fields->{ (FID_ITEM_PROPS) };
1193 if ( !defined($item_id) ) {
1194 syslog( "LOG_WARNING", "handle_item_status: received message without Item ID field" );
1195 } else {
1196 $item = $ils->find_item($item_id);
1199 if ( !$item ) {
1201 # Invalid Item ID
1202 $resp .= '0';
1203 $resp .= timestamp;
1204 $resp .= add_field( FID_ITEM_ID, $item_id );
1205 } else {
1207 # Valid Item ID
1209 $status = $item->status_update($item_props);
1211 $resp .= $status->ok ? '1' : '0';
1212 $resp .= timestamp;
1214 $resp .= add_field( FID_ITEM_ID, $item->id );
1215 $resp .= add_field( FID_TITLE_ID, $item->title_id );
1216 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
1219 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1220 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1222 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1224 return (ITEM_STATUS_UPDATE);
1227 sub handle_patron_enable {
1228 my ( $self, $server ) = @_;
1229 my $ils = $server->{ils};
1230 my $fields = $self->{fields};
1231 my ( $trans_date, $patron_id, $terminal_pwd, $patron_pwd );
1232 my ( $status, $patron );
1233 my $resp = PATRON_ENABLE_RESP;
1235 ($trans_date) = @{ $self->{fixed_fields} };
1236 $patron_id = $fields->{ (FID_PATRON_ID) };
1237 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1239 syslog( "LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'", $patron_id, $patron_pwd );
1241 $patron = $ils->find_patron($patron_id);
1243 if ( !defined($patron) ) {
1245 # Invalid patron ID
1246 $resp .= 'YYYY' . ( ' ' x 10 ) . '000' . timestamp();
1247 $resp .= add_field( FID_PATRON_ID, $patron_id );
1248 $resp .= add_field( FID_PERSONAL_NAME, '' );
1249 $resp .= add_field( FID_VALID_PATRON, 'N' );
1250 $resp .= add_field( FID_VALID_PATRON_PWD, 'N' );
1251 } else {
1253 # valid patron
1254 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 . timestamp();
1262 $resp .= add_field( FID_PATRON_ID, $patron->id );
1263 $resp .= add_field( FID_PERSONAL_NAME, $patron->name( $server->{account}->{ae_field_template} ) );
1264 if ( defined($patron_pwd) ) {
1265 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password($patron_pwd) ) );
1267 $resp .= add_field( FID_VALID_PATRON, 'Y' );
1268 $resp .= maybe_add( FID_SCREEN_MSG, $patron->screen_msg, $server );
1269 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line );
1272 $resp .= add_field( FID_INST_ID, $ils->institution );
1274 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1276 return (PATRON_ENABLE);
1279 sub handle_hold {
1280 my ( $self, $server ) = @_;
1281 my $ils = $server->{ils};
1282 my ( $hold_mode, $trans_date );
1283 my ( $expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd );
1284 my ( $item_id, $title_id, $fee_ack );
1285 my $fields = $self->{fields};
1286 my $status;
1287 my $resp = HOLD_RESP;
1289 ( $hold_mode, $trans_date ) = @{ $self->{fixed_fields} };
1291 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_hold" );
1293 $patron_id = $fields->{ (FID_PATRON_ID) };
1294 $expiry_date = $fields->{ (FID_EXPIRATION) } || '';
1295 $pickup_locn = $fields->{ (FID_PICKUP_LOCN) } || '';
1296 $hold_type = $fields->{ (FID_HOLD_TYPE) } || '2'; # Any copy of title
1297 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1298 $item_id = $fields->{ (FID_ITEM_ID) } || '';
1299 $title_id = $fields->{ (FID_TITLE_ID) } || '';
1300 $fee_ack = $fields->{ (FID_FEE_ACK) } || 'N';
1302 if ( $hold_mode eq '+' ) {
1303 $status = $ils->add_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1304 } elsif ( $hold_mode eq '-' ) {
1305 $status = $ils->cancel_hold( $patron_id, $patron_pwd, $item_id, $title_id );
1306 } elsif ( $hold_mode eq '*' ) {
1307 $status = $ils->alter_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1308 } else {
1309 syslog( "LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'", $hold_mode, $server->{account}->{id} );
1310 $status = $ils->Transaction::Hold; # new?
1311 $status->screen_msg("System error. Please contact library staff.");
1314 $resp .= $status->ok;
1315 $resp .= sipbool( $status->item && $status->item->available($patron_id) );
1316 $resp .= timestamp;
1318 if ( $status->ok ) {
1319 $resp .= add_field( FID_PATRON_ID, $status->patron->id );
1321 ( $status->expiration_date )
1322 and $resp .= maybe_add( FID_EXPIRATION, timestamp( $status->expiration_date ) );
1323 $resp .= maybe_add( FID_QUEUE_POS, $status->queue_position );
1324 $resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location );
1325 $resp .= maybe_add( FID_ITEM_ID, $status->item->id );
1326 $resp .= maybe_add( FID_TITLE_ID, $status->item->title_id );
1327 } else {
1329 # Not ok. still need required fields
1330 $resp .= add_field( FID_PATRON_ID, $patron_id );
1333 $resp .= add_field( FID_INST_ID, $ils->institution );
1334 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1335 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1337 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1339 return (HOLD);
1342 sub handle_renew {
1343 my ( $self, $server ) = @_;
1344 my $ils = $server->{ils};
1345 my ( $third_party, $no_block, $trans_date, $nb_due_date );
1346 my ( $patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack );
1347 my $fields = $self->{fields};
1348 my $status;
1349 my ( $patron, $item );
1350 my $resp = RENEW_RESP;
1352 ( $third_party, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
1354 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew" );
1356 if ( $no_block eq 'Y' ) {
1357 syslog( "LOG_WARNING", "handle_renew: received 'no block' renewal from terminal '%s'", $server->{account}->{id} );
1360 $patron_id = $fields->{ (FID_PATRON_ID) };
1361 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1362 $item_id = $fields->{ (FID_ITEM_ID) };
1363 $title_id = $fields->{ (FID_TITLE_ID) };
1364 $item_props = $fields->{ (FID_ITEM_PROPS) };
1365 $fee_ack = $fields->{ (FID_FEE_ACK) };
1367 $status = $ils->renew( $patron_id, $patron_pwd, $item_id, $title_id, $no_block, $nb_due_date, $third_party, $item_props, $fee_ack );
1369 $patron = $status->patron;
1370 $item = $status->item;
1372 if ( $status->renewal_ok ) {
1373 $resp .= '1';
1374 $resp .= $status->renewal_ok ? 'Y' : 'N';
1375 if ( $ils->supports('magnetic media') ) {
1376 $resp .= sipbool( $item->magnetic_media );
1377 } else {
1378 $resp .= 'U';
1380 $resp .= sipbool( $status->desensitize );
1381 $resp .= timestamp;
1382 $resp .= add_field( FID_PATRON_ID, $patron->id );
1383 $resp .= add_field( FID_ITEM_ID, $item->id );
1384 $resp .= add_field( FID_TITLE_ID, $item->title_id );
1385 if ( $item->due_date ) {
1386 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ) );
1387 } else {
1388 $resp .= add_field( FID_DUE_DATE, q{} );
1390 if ( $ils->supports('security inhibit') ) {
1391 $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit );
1393 $resp .= add_field( FID_MEDIA_TYPE, $item->sip_media_type );
1394 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
1395 } else {
1397 # renew failed for some reason
1398 # not OK, renewal not OK, Unknown media type (why bother checking?)
1399 $resp .= '0NUN';
1400 $resp .= timestamp;
1402 # If we found the patron or the item, the return the ILS
1403 # information, otherwise echo back the information we received
1404 # from the terminal
1405 $resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id );
1406 $resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id );
1407 $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : $title_id );
1408 $resp .= add_field( FID_DUE_DATE, '' );
1411 if ( $status->fee_amount ) {
1412 $resp .= add_field( FID_FEE_AMT, $status->fee_amount );
1413 $resp .= maybe_add( FID_CURRENCY, $status->sip_currency );
1414 $resp .= maybe_add( FID_FEE_TYPE, $status->sip_fee_type );
1415 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id );
1418 $resp .= add_field( FID_INST_ID, $ils->institution );
1419 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1420 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1422 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1424 return (RENEW);
1427 sub handle_renew_all {
1429 # my ($third_party, $no_block, $nb_due_date, $fee_ack, $patron);
1431 my ( $self, $server ) = @_;
1432 my $ils = $server->{ils};
1433 my ( $trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack );
1434 my $fields = $self->{fields};
1435 my $resp = RENEW_ALL_RESP;
1436 my $status;
1437 my ( @renewed, @unrenewed );
1439 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew_all" );
1441 ($trans_date) = @{ $self->{fixed_fields} };
1443 $patron_id = $fields->{ (FID_PATRON_ID) };
1444 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1445 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
1446 $fee_ack = $fields->{ (FID_FEE_ACK) };
1448 $status = $ils->renew_all( $patron_id, $patron_pwd, $fee_ack );
1450 $resp .= $status->ok ? '1' : '0';
1452 if ( !$status->ok ) {
1453 $resp .= add_count( "renew_all/renewed_count", 0 );
1454 $resp .= add_count( "renew_all/unrenewed_count", 0 );
1455 @renewed = ();
1456 @unrenewed = ();
1457 } else {
1458 @renewed = ( @{ $status->renewed } );
1459 @unrenewed = ( @{ $status->unrenewed } );
1460 $resp .= add_count( "renew_all/renewed_count", scalar @renewed );
1461 $resp .= add_count( "renew_all/unrenewed_count", scalar @unrenewed );
1464 $resp .= timestamp;
1465 $resp .= add_field( FID_INST_ID, $ils->institution );
1467 $resp .= join( '', map( add_field( FID_RENEWED_ITEMS, $_ ), @renewed ) );
1468 $resp .= join( '', map( add_field( FID_UNRENEWED_ITEMS, $_ ), @unrenewed ) );
1470 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1471 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1473 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1475 return (RENEW_ALL);
1479 # send_acs_status($self, $server)
1481 # Send an ACS Status message, which is contains lots of little fields
1482 # of information gleaned from all sorts of places.
1485 my @message_type_names = (
1486 "patron status request",
1487 "checkout",
1488 "checkin",
1489 "block patron",
1490 "acs status",
1491 "request sc/acs resend",
1492 "login",
1493 "patron information",
1494 "end patron session",
1495 "fee paid",
1496 "item information",
1497 "item status update",
1498 "patron enable",
1499 "hold",
1500 "renew",
1501 "renew all",
1504 sub send_acs_status {
1505 my ( $self, $server, $screen_msg, $print_line ) = @_;
1506 my $msg = ACS_STATUS;
1507 ($server) or die "send_acs_status error: no \$server argument received";
1508 my $account = $server->{account} or die "send_acs_status error: no 'account' in \$server object:\n" . Dumper($server);
1509 my $policy = $server->{policy} or die "send_acs_status error: no 'policy' in \$server object:\n" . Dumper($server);
1510 my $ils = $server->{ils} or die "send_acs_status error: no 'ils' in \$server object:\n" . Dumper($server);
1511 my ( $online_status, $checkin_ok, $checkout_ok, $ACS_renewal_policy );
1512 my ( $status_update_ok, $offline_ok, $timeout, $retries );
1514 $online_status = 'Y';
1515 $checkout_ok = sipbool( $ils->checkout_ok );
1516 $checkin_ok = sipbool( $ils->checkin_ok );
1517 $ACS_renewal_policy = sipbool( $policy->{renewal} );
1518 $status_update_ok = sipbool( $ils->status_update_ok );
1519 $offline_ok = sipbool( $ils->offline_ok );
1520 $timeout = $server->get_timeout({ policy => 1 });
1521 $retries = sprintf( "%03d", $policy->{retries} );
1523 if ( length($retries) != 3 ) {
1524 syslog( "LOG_ERR", "handle_acs_status: retries field wrong size: '%s'", $retries );
1525 $retries = '000';
1528 $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
1529 $msg .= "$status_update_ok$offline_ok$timeout$retries";
1530 $msg .= timestamp();
1532 if ( $protocol_version == 1 ) {
1533 $msg .= '1.00';
1534 } elsif ( $protocol_version == 2 ) {
1535 $msg .= '2.00';
1536 } else {
1537 syslog( "LOG_ERR", 'Bad setting for $protocol_version, "%s" in send_acs_status', $protocol_version );
1538 $msg .= '1.00';
1541 # Institution ID
1542 $msg .= add_field( FID_INST_ID, $account->{institution} );
1544 if ( $protocol_version >= 2 ) {
1546 # Supported messages: we do it all
1547 my $supported_msgs = '';
1549 foreach my $msg_name (@message_type_names) {
1550 if ( $msg_name eq 'request sc/acs resend' ) {
1551 $supported_msgs .= sipbool(1);
1552 } else {
1553 $supported_msgs .= sipbool( $ils->supports($msg_name) );
1556 if ( length($supported_msgs) < 16 ) {
1557 syslog( "LOG_ERR", 'send_acs_status: supported messages "%s" too short', $supported_msgs );
1559 $msg .= add_field( FID_SUPPORTED_MSGS, $supported_msgs );
1562 $msg .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1564 if ( defined( $account->{print_width} )
1565 && defined($print_line)
1566 && $account->{print_width} < length($print_line) ) {
1567 syslog( "LOG_WARNING", "send_acs_status: print line '%s' too long. Truncating", $print_line );
1568 $print_line = substr( $print_line, 0, $account->{print_width} );
1571 $msg .= maybe_add( FID_PRINT_LINE, $print_line );
1573 # Do we want to tell the terminal its location?
1575 $self->write_msg( $msg, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1576 return 1;
1580 # build_patron_status: create the 14-char patron status
1581 # string for the Patron Status message
1583 sub patron_status_string {
1584 my $patron = shift;
1585 my $patron_status;
1587 syslog( "LOG_DEBUG", "patron_status_string: %s charge_ok: %s", $patron->id, $patron->charge_ok );
1588 $patron_status = sprintf(
1589 '%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
1590 denied( $patron->charge_ok ),
1591 denied( $patron->renew_ok ),
1592 denied( $patron->recall_ok ),
1593 denied( $patron->hold_ok ),
1594 boolspace( $patron->card_lost ),
1595 boolspace( $patron->too_many_charged ),
1596 boolspace( $patron->too_many_overdue ),
1597 boolspace( $patron->too_many_renewal ),
1598 boolspace( $patron->too_many_claim_return ),
1599 boolspace( $patron->too_many_lost ),
1600 boolspace( $patron->excessive_fines ),
1601 boolspace( $patron->excessive_fees ),
1602 boolspace( $patron->recall_overdue ),
1603 boolspace( $patron->too_many_billed )
1605 return $patron_status;
1608 sub api_auth {
1609 my ( $username, $password, $branch ) = @_;
1610 $ENV{REMOTE_USER} = $username;
1611 my $query = CGI->new();
1612 $query->param( userid => $username );
1613 $query->param( password => $password );
1614 if ($branch) {
1615 $query->param( branch => $branch );
1617 my ( $status, $cookie, $sessionID ) = check_api_auth( $query, { circulate => 1 }, 'intranet' );
1618 return $status;
1622 __END__