Bug 16895: [QA Follow-up] Resolve warning on $payment_type_writeoff
[koha.git] / C4 / SIP / Sip / MsgType.pm
blob192239ef790d312dd1a3953447eb1f7897e91803
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 UNIVERSAL::can;
24 use vars qw(@ISA @EXPORT_OK);
26 use constant INVALID_CARD => 'Invalid cardnumber';
27 use constant INVALID_PW => 'Invalid password';
29 BEGIN {
30 @ISA = qw(Exporter);
31 @EXPORT_OK = qw(handle login_core);
34 # Predeclare handler subroutines
35 use subs qw(handle_patron_status handle_checkout handle_checkin
36 handle_block_patron handle_sc_status handle_request_acs_resend
37 handle_login handle_patron_info handle_end_patron_session
38 handle_fee_paid handle_item_information handle_item_status_update
39 handle_patron_enable handle_hold handle_renew handle_renew_all);
42 # For the most part, Version 2.00 of the protocol just adds new
43 # variable fields, but sometimes it changes the fixed header.
45 # In general, if there's no '2.00' protocol entry for a handler, that's
46 # because 2.00 didn't extend the 1.00 version of the protocol. This will
47 # be handled by the module initialization code following the declaration,
48 # which goes through the handlers table and creates a '2.00' entry that
49 # points to the same place as the '1.00' entry. If there's a 2.00 entry
50 # but no 1.00 entry, then that means that it's a completely new service
51 # in 2.00, so 1.00 shouldn't recognize it.
53 my %handlers = (
54 (PATRON_STATUS_REQ) => {
55 name => "Patron Status Request",
56 handler => \&handle_patron_status,
57 protocol => {
58 1 => {
59 template => "A3A18",
60 template_len => 21,
61 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
65 (CHECKOUT) => {
66 name => "Checkout",
67 handler => \&handle_checkout,
68 protocol => {
69 1 => {
70 template => "CCA18A18",
71 template_len => 38,
72 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
74 2 => {
75 template => "CCA18A18",
76 template_len => 38,
77 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS), (FID_PATRON_PWD), (FID_FEE_ACK), (FID_CANCEL) ],
81 (CHECKIN) => {
82 name => "Checkin",
83 handler => \&handle_checkin,
84 protocol => {
85 1 => {
86 template => "CA18A18",
87 template_len => 37,
88 fields => [ (FID_CURRENT_LOCN), (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
90 2 => {
91 template => "CA18A18",
92 template_len => 37,
93 fields => [ (FID_CURRENT_LOCN), (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS), (FID_CANCEL) ],
97 (BLOCK_PATRON) => {
98 name => "Block Patron",
99 handler => \&handle_block_patron,
100 protocol => {
101 1 => {
102 template => "CA18",
103 template_len => 19,
104 fields => [ (FID_INST_ID), (FID_BLOCKED_CARD_MSG), (FID_PATRON_ID), (FID_TERMINAL_PWD) ],
108 (SC_STATUS) => {
109 name => "SC Status",
110 handler => \&handle_sc_status,
111 protocol => {
112 1 => {
113 template => "CA3A4",
114 template_len => 8,
115 fields => [],
119 (REQUEST_ACS_RESEND) => {
120 name => "Request ACS Resend",
121 handler => \&handle_request_acs_resend,
122 protocol => {
123 1 => {
124 template => "",
125 template_len => 0,
126 fields => [],
130 (LOGIN) => {
131 name => "Login",
132 handler => \&handle_login,
133 protocol => {
134 2 => {
135 template => "A1A1",
136 template_len => 2,
137 fields => [ (FID_LOGIN_UID), (FID_LOGIN_PWD), (FID_LOCATION_CODE) ],
141 (PATRON_INFO) => {
142 name => "Patron Info",
143 handler => \&handle_patron_info,
144 protocol => {
145 2 => {
146 template => "A3A18A10",
147 template_len => 31,
148 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD), (FID_START_ITEM), (FID_END_ITEM) ],
152 (END_PATRON_SESSION) => {
153 name => "End Patron Session",
154 handler => \&handle_end_patron_session,
155 protocol => {
156 2 => {
157 template => "A18",
158 template_len => 18,
159 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
163 (FEE_PAID) => {
164 name => "Fee Paid",
165 handler => \&handle_fee_paid,
166 protocol => {
167 2 => {
168 template => "A18A2A2A3",
169 template_len => 25,
170 fields => [ (FID_FEE_AMT), (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD), (FID_FEE_ID), (FID_TRANSACTION_ID) ],
174 (ITEM_INFORMATION) => {
175 name => "Item Information",
176 handler => \&handle_item_information,
177 protocol => {
178 2 => {
179 template => "A18",
180 template_len => 18,
181 fields => [ (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
185 (ITEM_STATUS_UPDATE) => {
186 name => "Item Status Update",
187 handler => \&handle_item_status_update,
188 protocol => {
189 2 => {
190 template => "A18",
191 template_len => 18,
192 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS) ],
196 (PATRON_ENABLE) => {
197 name => "Patron Enable",
198 handler => \&handle_patron_enable,
199 protocol => {
200 2 => {
201 template => "A18",
202 template_len => 18,
203 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
207 (HOLD) => {
208 name => "Hold",
209 handler => \&handle_hold,
210 protocol => {
211 2 => {
212 template => "AA18",
213 template_len => 19,
214 fields => [
215 (FID_EXPIRATION), (FID_PICKUP_LOCN), (FID_HOLD_TYPE), (FID_INST_ID), (FID_PATRON_ID), (FID_PATRON_PWD),
216 (FID_ITEM_ID), (FID_TITLE_ID), (FID_TERMINAL_PWD), (FID_FEE_ACK)
221 (RENEW) => {
222 name => "Renew",
223 handler => \&handle_renew,
224 protocol => {
225 2 => {
226 template => "CCA18A18",
227 template_len => 38,
228 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) ],
232 (RENEW_ALL) => {
233 name => "Renew All",
234 handler => \&handle_renew_all,
235 protocol => {
236 2 => {
237 template => "A18",
238 template_len => 18,
239 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_PATRON_PWD), (FID_TERMINAL_PWD), (FID_FEE_ACK) ],
246 # Now, initialize some of the missing bits of %handlers
248 foreach my $i ( keys(%handlers) ) {
249 if ( !exists( $handlers{$i}->{protocol}->{2} ) ) {
250 $handlers{$i}->{protocol}->{2} = $handlers{$i}->{protocol}->{1};
254 sub new {
255 my ( $class, $msg, $seqno ) = @_;
256 my $self = {};
257 my $msgtag = substr( $msg, 0, 2 );
259 if ( $msgtag eq LOGIN ) {
261 # If the client is using the 2.00-style "Login" message
262 # to authenticate to the server, then we get the Login message
263 # _before_ the client has indicated that it supports 2.00, but
264 # it's using the 2.00 login process, so it must support 2.00.
265 $protocol_version = 2;
267 syslog( "LOG_DEBUG", "Sip::MsgType::new('%s', '%s...', '%s'): seq.no '%s', protocol %s", $class, substr( $msg, 0, 10 ), $msgtag, $seqno, $protocol_version );
269 # warn "SIP PROTOCOL: $protocol_version";
270 if ( !exists( $handlers{$msgtag} ) ) {
271 syslog( "LOG_WARNING", "new Sip::MsgType: Skipping message of unknown type '%s' in '%s'", $msgtag, $msg );
272 return;
273 } elsif ( !exists( $handlers{$msgtag}->{protocol}->{$protocol_version} ) ) {
274 syslog( "LOG_WARNING", "new Sip::MsgType: Skipping message '%s' unsupported by protocol rev. '%d'", $msgtag, $protocol_version );
275 return;
278 bless $self, $class;
280 $self->{seqno} = $seqno;
281 $self->_initialize( substr( $msg, 2 ), $handlers{$msgtag} );
283 return ($self);
286 sub _initialize {
287 my ( $self, $msg, $control_block ) = @_;
288 my $fn;
289 my $proto = $control_block->{protocol}->{$protocol_version};
291 $self->{name} = $control_block->{name};
292 $self->{handler} = $control_block->{handler};
294 $self->{fields} = {};
295 $self->{fixed_fields} = [];
297 chomp($msg); # These four are probably unnecessary now.
298 $msg =~ tr/\cM//d;
299 $msg =~ s/\^M$//;
300 chomp($msg);
302 foreach my $field ( @{ $proto->{fields} } ) {
303 $self->{fields}->{$field} = undef;
306 syslog( "LOG_DEBUG", "Sip::MsgType::_initialize('%s', '%s', '%s', '%s', ...)", $self->{name}, $msg, $proto->{template}, $proto->{template_len} );
308 $self->{fixed_fields} = [ unpack( $proto->{template}, $msg ) ]; # see http://perldoc.perl.org/5.8.8/functions/unpack.html
310 # Skip over the fixed fields and the split the rest of
311 # the message into fields based on the delimiter and parse them
312 foreach my $field ( split( quotemeta($field_delimiter), substr( $msg, $proto->{template_len} ) ) ) {
313 $fn = substr( $field, 0, 2 );
315 if ( !exists( $self->{fields}->{$fn} ) ) {
316 syslog( "LOG_WARNING", "Unsupported field '%s' in %s message '%s'", $fn, $self->{name}, $msg );
317 } elsif ( defined( $self->{fields}->{$fn} ) ) {
318 syslog( "LOG_WARNING", "Duplicate field '%s' (previous value '%s') in %s message '%s'", $fn, $self->{fields}->{$fn}, $self->{name}, $msg );
319 } else {
320 $self->{fields}->{$fn} = substr( $field, 2 );
324 return ($self);
327 sub handle {
328 my ( $msg, $server, $req ) = @_;
329 my $config = $server->{config};
330 my $self;
333 # What's the field delimiter for variable length fields?
334 # This can't be based on the account, since we need to know
335 # the field delimiter to parse a SIP login message
337 if ( defined( $server->{config}->{delimiter} ) ) {
338 $field_delimiter = $server->{config}->{delimiter};
341 # error detection is active if this is a REQUEST_ACS_RESEND
342 # message with a checksum, or if the message is long enough
343 # and the last nine characters begin with a sequence number
344 # field
345 if ( $msg eq REQUEST_ACS_RESEND_CKSUM ) {
347 # Special case
348 $error_detection = 1;
349 $self = C4::SIP::Sip::MsgType->new( (REQUEST_ACS_RESEND), 0 );
350 } elsif ( ( length($msg) > 11 ) && ( substr( $msg, -9, 2 ) eq "AY" ) ) {
351 $error_detection = 1;
353 if ( !verify_cksum($msg) ) {
354 syslog( "LOG_WARNING", "Checksum failed on message '%s'", $msg );
356 # REQUEST_SC_RESEND with error detection
357 $last_response = REQUEST_SC_RESEND_CKSUM;
358 print("$last_response\r");
359 return REQUEST_ACS_RESEND;
360 } else {
362 # Save the sequence number, then strip off the
363 # error detection data to process the message
364 $self = C4::SIP::Sip::MsgType->new( substr( $msg, 0, -9 ), substr( $msg, -7, 1 ) );
366 } elsif ($error_detection) {
368 # We received a non-ED message when ED is supposed to be active.
369 # Warn about this problem, then process the message anyway.
370 syslog( "LOG_WARNING", "Received message without error detection: '%s'", $msg );
371 $error_detection = 0;
372 $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
373 } else {
374 $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
377 if ( ( substr( $msg, 0, 2 ) ne REQUEST_ACS_RESEND )
378 && $req
379 && ( substr( $msg, 0, 2 ) ne $req ) ) {
380 return substr( $msg, 0, 2 );
382 unless ( $self->{handler} ) {
383 syslog( "LOG_WARNING", "No handler defined for '%s'", $msg );
384 $last_response = REQUEST_SC_RESEND;
385 print("$last_response\r");
386 return REQUEST_ACS_RESEND;
388 return ( $self->{handler}->( $self, $server ) ); # FIXME
389 # FIXME: Use of uninitialized value in subroutine entry
390 # Can't use string ("") as a subroutine ref while "strict refs" in use
394 ## Message Handlers
398 # Patron status messages are produced in response to both
399 # "Request Patron Status" and "Block Patron"
401 # Request Patron Status requires a patron password, but
402 # Block Patron doesn't (since the patron may never have
403 # provided one before attempting some illegal action).
405 # ASSUMPTION: If the patron password field is present in the
406 # message, then it must match, otherwise incomplete patron status
407 # information will be returned to the terminal.
409 sub build_patron_status {
410 my ( $patron, $lang, $fields, $server ) = @_;
412 my $patron_pwd = $fields->{ (FID_PATRON_PWD) };
413 my $resp = (PATRON_STATUS_RESP);
414 my $password_rc;
416 if ( $patron ) {
417 if ($patron_pwd) {
418 $password_rc = $patron->check_password($patron_pwd);
421 $resp .= patron_status_string($patron);
422 $resp .= $lang . timestamp();
423 $resp .= add_field( FID_PERSONAL_NAME, $patron->name );
425 # while the patron ID we got from the SC is valid, let's
426 # use the one returned from the ILS, just in case...
427 $resp .= add_field( FID_PATRON_ID, $patron->id );
429 if ( $protocol_version >= 2 ) {
430 $resp .= add_field( FID_VALID_PATRON, 'Y' );
432 # Patron password is a required field.
433 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool($password_rc) );
434 $resp .= maybe_add( FID_CURRENCY, $patron->currency );
435 $resp .= maybe_add( FID_FEE_AMT, $patron->fee_amount );
438 my $msg = $patron->screen_msg;
439 $msg .= ' -- '. INVALID_PW if $patron_pwd && !$password_rc;
440 $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
442 $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server )
443 if ( $server->{account}->{send_patron_home_library_in_af} );
444 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line );
446 } else {
447 # Invalid patron (cardnumber)
448 # Report that the user has no privs.
450 # no personal name, and is invalid (if we're using 2.00)
451 $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
452 $resp .= add_field( FID_PERSONAL_NAME, '' );
454 # the patron ID is invalid, but it's a required field, so
455 # just echo it back
456 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) } );
458 ( $protocol_version >= 2 )
459 and $resp .= add_field( FID_VALID_PATRON, 'N' );
461 $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
464 $resp .= add_field( FID_INST_ID, $fields->{ (FID_INST_ID) } );
465 return $resp;
468 sub handle_patron_status {
469 my ( $self, $server ) = @_;
470 my $ils = $server->{ils};
471 my $patron;
472 my $resp = (PATRON_STATUS_RESP);
473 my $account = $server->{account};
474 my ( $lang, $date ) = @{ $self->{fixed_fields} };
475 my $fields = $self->{fields};
477 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_patron_status" );
478 $patron = $ils->find_patron( $fields->{ (FID_PATRON_ID) } );
479 $resp = build_patron_status( $patron, $lang, $fields, $server );
480 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
481 return (PATRON_STATUS_REQ);
484 sub handle_checkout {
485 my ( $self, $server ) = @_;
486 my $account = $server->{account};
487 my $ils = $server->{ils};
488 my $inst = $ils->institution;
489 my ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date );
490 my $fields;
491 my ( $patron_id, $item_id, $status );
492 my ( $item, $patron );
493 my $resp;
495 ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
496 $fields = $self->{fields};
498 $patron_id = $fields->{ (FID_PATRON_ID) };
499 $item_id = $fields->{ (FID_ITEM_ID) };
500 my $fee_ack = $fields->{ (FID_FEE_ACK) };
502 if ( $no_block eq 'Y' ) {
504 # Off-line transactions need to be recorded, but there's
505 # not a lot we can do about it
506 syslog( "LOG_WARNING", "received no-block checkout from terminal '%s'", $account->{id} );
508 $status = $ils->checkout_no_block( $patron_id, $item_id, $sc_renewal_policy, $trans_date, $nb_due_date );
509 } else {
511 # Does the transaction date really matter for items that are
512 # checkout out while the terminal is online? I'm guessing 'no'
513 $status = $ils->checkout( $patron_id, $item_id, $sc_renewal_policy, $fee_ack );
516 $item = $status->item;
517 $patron = $status->patron;
519 if ( $status->ok ) {
521 # Item successfully checked out
522 # Fixed fields
523 $resp = CHECKOUT_RESP . '1';
524 $resp .= sipbool( $status->renew_ok );
525 if ( $ils->supports('magnetic media') ) {
526 $resp .= sipbool( $item->magnetic_media );
527 } else {
528 $resp .= 'U';
531 # We never return the obsolete 'U' value for 'desensitize'
532 $resp .= sipbool( $status->desensitize );
533 $resp .= timestamp;
535 # Now for the variable fields
536 $resp .= add_field( FID_INST_ID, $inst );
537 $resp .= add_field( FID_PATRON_ID, $patron_id );
538 $resp .= add_field( FID_ITEM_ID, $item_id );
539 $resp .= add_field( FID_TITLE_ID, $item->title_id );
540 if ( $item->due_date ) {
541 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ) );
542 } else {
543 $resp .= add_field( FID_DUE_DATE, q{} );
546 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
547 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
549 if ( $protocol_version >= 2 ) {
550 if ( $ils->supports('security inhibit') ) {
551 $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit );
553 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type );
554 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
559 else {
561 # Checkout failed
562 # Checkout Response: not ok, no renewal, don't know mag. media,
563 # no desensitize
564 $resp = sprintf( "120NUN%s", timestamp );
565 $resp .= add_field( FID_INST_ID, $inst );
566 $resp .= add_field( FID_PATRON_ID, $patron_id );
567 $resp .= add_field( FID_ITEM_ID, $item_id );
569 # If the item is valid, provide the title, otherwise
570 # leave it blank
571 $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : '' );
573 # Due date is required. Since it didn't get checked out,
574 # it's not due, so leave the date blank
575 $resp .= add_field( FID_DUE_DATE, '' );
577 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
578 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
580 if ( $protocol_version >= 2 ) {
582 # Is the patron ID valid?
583 $resp .= add_field( FID_VALID_PATRON, sipbool($patron) );
585 if ( $patron && exists( $fields->{FID_PATRON_PWD} ) ) {
587 # Password provided, so we can tell if it was valid or not
588 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password( $fields->{ (FID_PATRON_PWD) } ) ) );
593 if ( $protocol_version >= 2 ) {
595 # Financials : return irrespective of ok status
596 if ( $status->fee_amount ) {
597 $resp .= add_field( FID_FEE_AMT, $status->fee_amount );
598 $resp .= maybe_add( FID_CURRENCY, $status->sip_currency );
599 $resp .= maybe_add( FID_FEE_TYPE, $status->sip_fee_type );
600 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id );
604 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
605 return (CHECKOUT);
608 sub handle_checkin {
609 my ( $self, $server ) = @_;
610 my $account = $server->{account};
611 my $ils = $server->{ils};
612 my $my_branch = $ils->institution;
613 my ( $current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel );
614 my ( $patron, $item, $status );
615 my $resp = CHECKIN_RESP;
616 my ( $no_block, $trans_date, $return_date ) = @{ $self->{fixed_fields} };
617 my $fields = $self->{fields};
619 $current_loc = $fields->{ (FID_CURRENT_LOCN) };
620 $inst_id = $fields->{ (FID_INST_ID) };
621 $item_id = $fields->{ (FID_ITEM_ID) };
622 $item_props = $fields->{ (FID_ITEM_PROPS) };
623 $cancel = $fields->{ (FID_CANCEL) };
624 if ($current_loc) {
625 $my_branch = $current_loc; # most scm do not set $current_loc
628 $ils->check_inst_id( $inst_id, "handle_checkin" );
630 if ( $no_block eq 'Y' ) {
632 # Off-line transactions, ick.
633 syslog( "LOG_WARNING", "received no-block checkin from terminal '%s'", $account->{id} );
634 $status = $ils->checkin_no_block( $item_id, $trans_date, $return_date, $item_props, $cancel );
635 } else {
636 $status = $ils->checkin( $item_id, $trans_date, $return_date, $my_branch, $item_props, $cancel, $account->{checked_in_ok} );
639 $patron = $status->patron;
640 $item = $status->item;
642 $resp .= $status->ok ? '1' : '0';
643 $resp .= $status->resensitize ? 'Y' : 'N';
644 if ( $item && $ils->supports('magnetic media') ) {
645 $resp .= sipbool( $item->magnetic_media );
646 } else {
648 # item barcode is invalid or system doesn't support 'magnetic media' indicator
649 $resp .= 'U';
652 $resp .= $status->alert ? 'Y' : 'N';
653 $resp .= timestamp;
654 $resp .= add_field( FID_INST_ID, $inst_id );
655 $resp .= add_field( FID_ITEM_ID, $item_id );
657 if ($item) {
658 $resp .= add_field( FID_PERM_LOCN, $item->permanent_location );
659 $resp .= maybe_add( FID_TITLE_ID, $item->title_id );
662 if ( $protocol_version >= 2 ) {
663 $resp .= maybe_add( FID_SORT_BIN, $status->sort_bin );
664 if ($patron) {
665 $resp .= add_field( FID_PATRON_ID, $patron->id );
667 if ($item) {
668 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type );
669 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
670 $resp .= maybe_add( FID_COLLECTION_CODE, $item->collection_code );
671 $resp .= maybe_add( FID_CALL_NUMBER, $item->call_number );
672 $resp .= maybe_add( FID_DESTINATION_LOCATION, $item->destination_loc );
673 $resp .= maybe_add( FID_HOLD_PATRON_ID, $item->hold_patron_bcode );
674 $resp .= maybe_add( FID_HOLD_PATRON_NAME, $item->hold_patron_name );
675 if ( $status->hold and $status->hold->{branchcode} ne $item->destination_loc ) {
676 warn 'SIP hold mismatch: $status->hold->{branchcode}=' . $status->hold->{branchcode} . '; $item->destination_loc=' . $item->destination_loc;
678 # just me being paranoid.
683 $resp .= maybe_add( FID_ALERT_TYPE, $status->alert_type ) if $status->alert;
684 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
685 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
687 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
689 return (CHECKIN);
692 sub handle_block_patron {
693 my ( $self, $server ) = @_;
694 my $account = $server->{account};
695 my $ils = $server->{ils};
696 my ( $card_retained, $trans_date );
697 my ( $inst_id, $blocked_card_msg, $patron_id, $terminal_pwd );
698 my ( $fields, $resp, $patron );
700 ( $card_retained, $trans_date ) = @{ $self->{fixed_fields} };
701 $fields = $self->{fields};
702 $inst_id = $fields->{ (FID_INST_ID) };
703 $blocked_card_msg = $fields->{ (FID_BLOCKED_CARD_MSG) };
704 $patron_id = $fields->{ (FID_PATRON_ID) };
705 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
707 # Terminal passwords are different from account login
708 # passwords, but I have no idea what to do with them. So,
709 # I'll just ignore them for now.
711 # FIXME ???
713 $ils->check_inst_id( $inst_id, "block_patron" );
714 $patron = $ils->find_patron($patron_id);
716 # The correct response for a "Block Patron" message is a
717 # "Patron Status Response", so use that handler to generate
718 # the message, but then return the correct code from here.
720 # Normally, the language is provided by the "Patron Status"
721 # fixed field, but since we're not responding to one of those
722 # we'll just say, "Unspecified", as per the spec. Let the
723 # terminal default to something that, one hopes, will be
724 # intelligible
725 if ($patron) {
727 # Valid patron id
728 $patron->block( $card_retained, $blocked_card_msg );
731 $resp = build_patron_status( $patron, $patron->language, $fields, $server );
732 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
733 return (BLOCK_PATRON);
736 sub handle_sc_status {
737 my ( $self, $server ) = @_;
738 ($server) or warn "handle_sc_status error: no \$server argument received.";
739 my ( $status, $print_width, $sc_protocol_version ) = @{ $self->{fixed_fields} };
740 my ($new_proto);
742 if ( $sc_protocol_version =~ /^1\./ ) {
743 $new_proto = 1;
744 } elsif ( $sc_protocol_version =~ /^2\./ ) {
745 $new_proto = 2;
746 } else {
747 syslog( "LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version );
748 $new_proto = 1;
751 if ( $new_proto != $protocol_version ) {
752 syslog( "LOG_INFO", "Setting protocol level to $new_proto" );
753 $protocol_version = $new_proto;
756 if ( $status == SC_STATUS_PAPER ) {
757 syslog( "LOG_WARNING", "Self-Check unit '%s@%s' out of paper", $self->{account}->{id}, $self->{account}->{institution} );
758 } elsif ( $status == SC_STATUS_SHUTDOWN ) {
759 syslog( "LOG_WARNING", "Self-Check unit '%s@%s' shutting down", $self->{account}->{id}, $self->{account}->{institution} );
762 $self->{account}->{print_width} = $print_width;
763 return ( send_acs_status( $self, $server ) ? SC_STATUS : '' );
766 sub handle_request_acs_resend {
767 my ( $self, $server ) = @_;
769 if ( !$last_response ) {
771 # We haven't sent anything yet, so respond with a
772 # REQUEST_SC_RESEND msg (p. 16)
773 $self->write_msg( REQUEST_SC_RESEND, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
774 } elsif ( ( length($last_response) < 9 )
775 || substr( $last_response, -9, 2 ) ne 'AY' ) {
777 # When resending a message, we aren't supposed to include
778 # a sequence number, even if the original had one (p. 4).
779 # If the last message didn't have a sequence number, then
780 # we can just send it.
781 print("$last_response\r"); # not write_msg?
782 } else {
784 # Cut out the sequence number and checksum, since the old
785 # checksum is wrong for the resent message.
786 my $rebuilt = substr( $last_response, 0, -9 );
787 $self->write_msg( $rebuilt, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
790 return REQUEST_ACS_RESEND;
793 sub login_core {
794 my $server = shift or return;
795 my $uid = shift;
796 my $pwd = shift;
797 my $status = 1; # Assume it all works
798 if ( !exists( $server->{config}->{accounts}->{$uid} ) ) {
799 syslog( "LOG_WARNING", "MsgType::login_core: Unknown login '$uid'" );
800 $status = 0;
801 } elsif ( $server->{config}->{accounts}->{$uid}->{password} ne $pwd ) {
802 syslog( "LOG_WARNING", "MsgType::login_core: Invalid password for login '$uid'" );
803 $status = 0;
804 } else {
806 # Store the active account someplace handy for everybody else to find.
807 $server->{account} = $server->{config}->{accounts}->{$uid};
808 my $inst = $server->{account}->{institution};
809 $server->{institution} = $server->{config}->{institutions}->{$inst};
810 $server->{policy} = $server->{institution}->{policy};
811 $server->{sip_username} = $uid;
812 $server->{sip_password} = $pwd;
814 my $auth_status = api_auth( $uid, $pwd, $inst );
815 if ( !$auth_status or $auth_status !~ /^ok$/i ) {
816 syslog( "LOG_WARNING", "api_auth failed for SIP terminal '%s' of '%s': %s", $uid, $inst, ( $auth_status || 'unknown' ) );
817 $status = 0;
818 } else {
819 syslog( "LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst );
822 # initialize connection to ILS
824 my $module = $server->{config}->{institutions}->{$inst}->{implementation};
825 syslog( "LOG_DEBUG", 'login_core: ' . Dumper($module) );
827 # Suspect this is always ILS but so we don't break any eccentic install (for now)
828 if ( $module eq 'ILS' ) {
829 $module = 'C4::SIP::ILS';
831 $module->use;
832 if ($@) {
833 syslog( "LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed", $server->{service}, $module, $inst );
834 die("Failed to load ILS implementation '$module' for $inst");
837 # like ILS->new(), I think.
838 $server->{ils} = $module->new( $server->{institution}, $server->{account} );
839 if ( !$server->{ils} ) {
840 syslog( "LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst );
841 die("Unable to connect to ILS '$inst'");
845 return $status;
848 sub handle_login {
849 my ( $self, $server ) = @_;
850 my ( $uid_algorithm, $pwd_algorithm );
851 my ( $uid, $pwd );
852 my $inst;
853 my $fields;
854 my $status = 1; # Assume it all works
856 $fields = $self->{fields};
857 ( $uid_algorithm, $pwd_algorithm ) = @{ $self->{fixed_fields} };
859 $uid = $fields->{ (FID_LOGIN_UID) }; # Terminal ID, not patron ID.
860 $pwd = $fields->{ (FID_LOGIN_PWD) }; # Terminal PWD, not patron PWD.
862 if ( $uid_algorithm || $pwd_algorithm ) {
863 syslog( "LOG_ERR", "LOGIN: Unsupported non-zero encryption method(s): uid = $uid_algorithm, pwd = $pwd_algorithm" );
864 $status = 0;
865 } else {
866 $status = login_core( $server, $uid, $pwd );
869 $self->write_msg( LOGIN_RESP . $status, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
870 return $status ? LOGIN : '';
874 # Build the detailed summary information for the Patron
875 # Information Response message based on the first 'Y' that appears
876 # in the 'summary' field of the Patron Information request. The
877 # specification says that only one 'Y' can appear in that field,
878 # and we're going to believe it.
880 sub summary_info {
881 my ( $ils, $patron, $summary, $start, $end, $server ) = @_;
882 my $resp = '';
883 my $summary_type;
886 # Map from offsets in the "summary" field of the Patron Information
887 # message to the corresponding field and handler
889 my @summary_map = (
890 { func => $patron->can("hold_items"), fid => FID_HOLD_ITEMS },
891 { func => $patron->can("overdue_items"), fid => FID_OVERDUE_ITEMS },
892 { func => $patron->can("charged_items"), fid => FID_CHARGED_ITEMS },
893 { func => $patron->can("fine_items"), fid => FID_FINE_ITEMS },
894 { func => $patron->can("recall_items"), fid => FID_RECALL_ITEMS },
895 { func => $patron->can("unavail_holds"), fid => FID_UNAVAILABLE_HOLD_ITEMS },
898 if ( ( $summary_type = index( $summary, 'Y' ) ) == -1 ) {
899 return ''; # No detailed information required
902 syslog( "LOG_DEBUG", "Summary_info: index == '%d', field '%s'", $summary_type, $summary_map[$summary_type]->{fid} );
904 my $func = $summary_map[$summary_type]->{func};
905 my $fid = $summary_map[$summary_type]->{fid};
906 my $itemlist = &$func( $patron, $start, $end, $server );
908 syslog( "LOG_DEBUG", "summary_info: list = (%s)", join( ", ", @{$itemlist} ) );
909 foreach my $i ( @{$itemlist} ) {
910 $resp .= add_field( $fid, $i->{barcode} );
913 return $resp;
916 sub handle_patron_info {
917 my ( $self, $server ) = @_;
918 my $ils = $server->{ils};
919 my ( $lang, $trans_date, $summary ) = @{ $self->{fixed_fields} };
920 my $fields = $self->{fields};
921 my ( $inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end );
922 my ( $resp, $patron );
924 $inst_id = $fields->{ (FID_INST_ID) };
925 $patron_id = $fields->{ (FID_PATRON_ID) };
926 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
927 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
928 $start = $fields->{ (FID_START_ITEM) };
929 $end = $fields->{ (FID_END_ITEM) };
931 $patron = $ils->find_patron($patron_id);
933 $resp = (PATRON_INFO_RESP);
934 if ($patron) {
935 $resp .= patron_status_string($patron);
936 $resp .= ( defined($lang) and length($lang) == 3 ) ? $lang : $patron->language;
937 $resp .= timestamp();
939 $resp .= add_count( 'patron_info/hold_items', scalar @{ $patron->hold_items } );
940 $resp .= add_count( 'patron_info/overdue_items', scalar @{ $patron->overdue_items } );
941 $resp .= add_count( 'patron_info/charged_items', scalar @{ $patron->charged_items } );
942 $resp .= add_count( 'patron_info/fine_items', scalar @{ $patron->fine_items } );
943 $resp .= add_count( 'patron_info/recall_items', scalar @{ $patron->recall_items } );
944 $resp .= add_count( 'patron_info/unavail_holds', scalar @{ $patron->unavail_holds } );
946 $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ) );
948 # while the patron ID we got from the SC is valid, let's
949 # use the one returned from the ILS, just in case...
950 $resp .= add_field( FID_PATRON_ID, $patron->id );
951 $resp .= add_field( FID_PERSONAL_NAME, $patron->name );
953 # TODO: add code for the fields
954 # hold items limit
955 # overdue items limit
956 # charged items limit
958 $resp .= add_field( FID_VALID_PATRON, 'Y' );
959 my $password_rc;
960 if ( defined($patron_pwd) ) {
962 # If patron password was provided, report whether it was right or not.
963 $password_rc = $patron->check_password($patron_pwd);
964 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $password_rc ) );
967 $resp .= maybe_add( FID_CURRENCY, $patron->currency );
968 $resp .= maybe_add( FID_FEE_AMT, $patron->fee_amount );
969 $resp .= add_field( FID_FEE_LMT, $patron->fee_limit );
971 # TODO: zero or more item details for 2.0 can go here:
972 # hold_items
973 # overdue_items
974 # charged_items
975 # fine_items
976 # recall_items
978 $resp .= summary_info( $ils, $patron, $summary, $start, $end, $server );
980 $resp .= maybe_add( FID_HOME_ADDR, $patron->address );
981 $resp .= maybe_add( FID_EMAIL, $patron->email_addr );
982 $resp .= maybe_add( FID_HOME_PHONE, $patron->home_phone );
984 # SIP 2.0 extensions used by Envisionware
985 # Other terminals will ignore unrecognized fields (unrecognized field identifiers)
986 $resp .= maybe_add( FID_PATRON_BIRTHDATE, $patron->birthdate );
987 $resp .= maybe_add( FID_PATRON_CLASS, $patron->ptype );
989 # Custom protocol extension to report patron internet privileges
990 $resp .= maybe_add( FID_INET_PROFILE, $patron->inet_privileges );
992 my $msg = $patron->screen_msg;
993 if( defined( $patron_pwd ) && !$password_rc ) {
994 $msg .= ' -- ' . INVALID_PW;
996 $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
997 if ( $server->{account}->{send_patron_home_library_in_af} ) {
998 $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server);
1000 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line );
1001 } else {
1003 # Invalid patron ID:
1004 # no privileges, no items associated,
1005 # no personal name, and is invalid (if we're using 2.00)
1006 $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
1007 $resp .= '0000' x 6;
1009 $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ) );
1011 # patron ID is invalid, but field is required, so just echo it back
1012 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) } );
1013 $resp .= add_field( FID_PERSONAL_NAME, '' );
1015 if ( $protocol_version >= 2 ) {
1016 $resp .= add_field( FID_VALID_PATRON, 'N' );
1018 $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
1021 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1022 return (PATRON_INFO);
1025 sub handle_end_patron_session {
1026 my ( $self, $server ) = @_;
1027 my $ils = $server->{ils};
1028 my $trans_date;
1029 my $fields = $self->{fields};
1030 my $resp = END_SESSION_RESP;
1031 my ( $status, $screen_msg, $print_line );
1033 ($trans_date) = @{ $self->{fixed_fields} };
1035 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, 'handle_end_patron_session' );
1037 ( $status, $screen_msg, $print_line ) = $ils->end_patron_session( $fields->{ (FID_PATRON_ID) } );
1039 $resp .= $status ? 'Y' : 'N';
1040 $resp .= timestamp();
1042 $resp .= add_field( FID_INST_ID, $server->{ils}->institution );
1043 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) } );
1045 $resp .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1046 $resp .= maybe_add( FID_PRINT_LINE, $print_line );
1048 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1050 return (END_PATRON_SESSION);
1053 sub handle_fee_paid {
1054 my ( $self, $server ) = @_;
1055 my $ils = $server->{ils};
1056 my ( $trans_date, $fee_type, $pay_type, $currency ) = @{ $self->{fixed_fields} };
1057 my $fields = $self->{fields};
1058 my ( $fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd );
1059 my ( $fee_id, $trans_id );
1060 my $status;
1061 my $resp = FEE_PAID_RESP;
1063 my $payment_type_writeoff = $server->{account}->{payment_type_writeoff} || '';
1064 my $is_writeoff = $pay_type eq $payment_type_writeoff;
1066 $fee_amt = $fields->{ (FID_FEE_AMT) };
1067 $inst_id = $fields->{ (FID_INST_ID) };
1068 $patron_id = $fields->{ (FID_PATRON_ID) };
1069 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1070 $fee_id = $fields->{ (FID_FEE_ID) };
1071 $trans_id = $fields->{ (FID_TRANSACTION_ID) };
1073 $ils->check_inst_id( $inst_id, "handle_fee_paid" );
1075 $status = $ils->pay_fee( $patron_id, $patron_pwd, $fee_amt, $fee_type, $pay_type, $fee_id, $trans_id, $currency, $is_writeoff );
1077 $resp .= ( $status->ok ? 'Y' : 'N' ) . timestamp;
1078 $resp .= add_field( FID_INST_ID, $inst_id );
1079 $resp .= add_field( FID_PATRON_ID, $patron_id );
1080 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id );
1081 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1082 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1084 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1086 return (FEE_PAID);
1089 sub handle_item_information {
1090 my ( $self, $server ) = @_;
1091 my $ils = $server->{ils};
1092 my $trans_date;
1093 my $fields = $self->{fields};
1094 my $resp = ITEM_INFO_RESP;
1095 my $item;
1096 my $i;
1098 ($trans_date) = @{ $self->{fixed_fields} };
1100 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_item_information" );
1102 $item = $ils->find_item( $fields->{ (FID_ITEM_ID) } );
1104 if ( !defined($item) ) {
1106 # Invalid Item ID
1107 # "Other" circ stat, "Other" security marker, "Unknown" fee type
1108 $resp .= "010101";
1109 $resp .= timestamp;
1111 # Just echo back the invalid item id
1112 $resp .= add_field( FID_ITEM_ID, $fields->{ (FID_ITEM_ID) } );
1114 # title id is required, but we don't have one
1115 $resp .= add_field( FID_TITLE_ID, '' );
1116 } else {
1118 # Valid Item ID, send the good stuff
1119 $resp .= $item->sip_circulation_status;
1120 $resp .= $item->sip_security_marker;
1121 $resp .= $item->sip_fee_type;
1122 $resp .= timestamp;
1124 $resp .= add_field( FID_ITEM_ID, $item->id );
1125 $resp .= add_field( FID_TITLE_ID, $item->title_id );
1127 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type );
1128 $resp .= maybe_add( FID_PERM_LOCN, $item->permanent_location );
1129 $resp .= maybe_add( FID_CURRENT_LOCN, $item->current_location );
1130 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
1132 if ( ( $i = $item->fee ) != 0 ) {
1133 $resp .= add_field( FID_CURRENCY, $item->fee_currency );
1134 $resp .= add_field( FID_FEE_AMT, $i );
1136 $resp .= maybe_add( FID_OWNER, $item->owner );
1138 if ( ( $i = scalar @{ $item->hold_queue } ) > 0 ) {
1139 $resp .= add_field( FID_HOLD_QUEUE_LEN, $i );
1141 if ( $item->due_date ) {
1142 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ) );
1144 if ( ( $i = $item->recall_date ) != 0 ) {
1145 $resp .= add_field( FID_RECALL_DATE, timestamp($i) );
1147 if ( ( $i = $item->hold_pickup_date ) != 0 ) {
1148 $resp .= add_field( FID_HOLD_PICKUP_DATE, timestamp($i) );
1151 $resp .= maybe_add( FID_SCREEN_MSG, $item->screen_msg, $server );
1152 $resp .= maybe_add( FID_PRINT_LINE, $item->print_line );
1155 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1157 return (ITEM_INFORMATION);
1160 sub handle_item_status_update {
1161 my ( $self, $server ) = @_;
1162 my $ils = $server->{ils};
1163 my ( $trans_date, $item_id, $terminal_pwd, $item_props );
1164 my $fields = $self->{fields};
1165 my $status;
1166 my $item;
1167 my $resp = ITEM_STATUS_UPDATE_RESP;
1169 ($trans_date) = @{ $self->{fixed_fields} };
1171 $ils->check_inst_id( $fields->{ (FID_INST_ID) } );
1173 $item_id = $fields->{ (FID_ITEM_ID) };
1174 $item_props = $fields->{ (FID_ITEM_PROPS) };
1176 if ( !defined($item_id) ) {
1177 syslog( "LOG_WARNING", "handle_item_status: received message without Item ID field" );
1178 } else {
1179 $item = $ils->find_item($item_id);
1182 if ( !$item ) {
1184 # Invalid Item ID
1185 $resp .= '0';
1186 $resp .= timestamp;
1187 $resp .= add_field( FID_ITEM_ID, $item_id );
1188 } else {
1190 # Valid Item ID
1192 $status = $item->status_update($item_props);
1194 $resp .= $status->ok ? '1' : '0';
1195 $resp .= timestamp;
1197 $resp .= add_field( FID_ITEM_ID, $item->id );
1198 $resp .= add_field( FID_TITLE_ID, $item->title_id );
1199 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
1202 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1203 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1205 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1207 return (ITEM_STATUS_UPDATE);
1210 sub handle_patron_enable {
1211 my ( $self, $server ) = @_;
1212 my $ils = $server->{ils};
1213 my $fields = $self->{fields};
1214 my ( $trans_date, $patron_id, $terminal_pwd, $patron_pwd );
1215 my ( $status, $patron );
1216 my $resp = PATRON_ENABLE_RESP;
1218 ($trans_date) = @{ $self->{fixed_fields} };
1219 $patron_id = $fields->{ (FID_PATRON_ID) };
1220 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1222 syslog( "LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'", $patron_id, $patron_pwd );
1224 $patron = $ils->find_patron($patron_id);
1226 if ( !defined($patron) ) {
1228 # Invalid patron ID
1229 $resp .= 'YYYY' . ( ' ' x 10 ) . '000' . timestamp();
1230 $resp .= add_field( FID_PATRON_ID, $patron_id );
1231 $resp .= add_field( FID_PERSONAL_NAME, '' );
1232 $resp .= add_field( FID_VALID_PATRON, 'N' );
1233 $resp .= add_field( FID_VALID_PATRON_PWD, 'N' );
1234 } else {
1236 # valid patron
1237 if ( !defined($patron_pwd) || $patron->check_password($patron_pwd) ) {
1239 # Don't enable the patron if there was an invalid password
1240 $status = $patron->enable;
1242 $resp .= patron_status_string($patron);
1243 $resp .= $patron->language . timestamp();
1245 $resp .= add_field( FID_PATRON_ID, $patron->id );
1246 $resp .= add_field( FID_PERSONAL_NAME, $patron->name );
1247 if ( defined($patron_pwd) ) {
1248 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password($patron_pwd) ) );
1250 $resp .= add_field( FID_VALID_PATRON, 'Y' );
1251 $resp .= maybe_add( FID_SCREEN_MSG, $patron->screen_msg, $server );
1252 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line );
1255 $resp .= add_field( FID_INST_ID, $ils->institution );
1257 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1259 return (PATRON_ENABLE);
1262 sub handle_hold {
1263 my ( $self, $server ) = @_;
1264 my $ils = $server->{ils};
1265 my ( $hold_mode, $trans_date );
1266 my ( $expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd );
1267 my ( $item_id, $title_id, $fee_ack );
1268 my $fields = $self->{fields};
1269 my $status;
1270 my $resp = HOLD_RESP;
1272 ( $hold_mode, $trans_date ) = @{ $self->{fixed_fields} };
1274 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_hold" );
1276 $patron_id = $fields->{ (FID_PATRON_ID) };
1277 $expiry_date = $fields->{ (FID_EXPIRATION) } || '';
1278 $pickup_locn = $fields->{ (FID_PICKUP_LOCN) } || '';
1279 $hold_type = $fields->{ (FID_HOLD_TYPE) } || '2'; # Any copy of title
1280 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1281 $item_id = $fields->{ (FID_ITEM_ID) } || '';
1282 $title_id = $fields->{ (FID_TITLE_ID) } || '';
1283 $fee_ack = $fields->{ (FID_FEE_ACK) } || 'N';
1285 if ( $hold_mode eq '+' ) {
1286 $status = $ils->add_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1287 } elsif ( $hold_mode eq '-' ) {
1288 $status = $ils->cancel_hold( $patron_id, $patron_pwd, $item_id, $title_id );
1289 } elsif ( $hold_mode eq '*' ) {
1290 $status = $ils->alter_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1291 } else {
1292 syslog( "LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'", $hold_mode, $server->{account}->{id} );
1293 $status = $ils->Transaction::Hold; # new?
1294 $status->screen_msg("System error. Please contact library staff.");
1297 $resp .= $status->ok;
1298 $resp .= sipbool( $status->item && $status->item->available($patron_id) );
1299 $resp .= timestamp;
1301 if ( $status->ok ) {
1302 $resp .= add_field( FID_PATRON_ID, $status->patron->id );
1304 ( $status->expiration_date )
1305 and $resp .= maybe_add( FID_EXPIRATION, timestamp( $status->expiration_date ) );
1306 $resp .= maybe_add( FID_QUEUE_POS, $status->queue_position );
1307 $resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location );
1308 $resp .= maybe_add( FID_ITEM_ID, $status->item->id );
1309 $resp .= maybe_add( FID_TITLE_ID, $status->item->title_id );
1310 } else {
1312 # Not ok. still need required fields
1313 $resp .= add_field( FID_PATRON_ID, $patron_id );
1316 $resp .= add_field( FID_INST_ID, $ils->institution );
1317 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1318 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1320 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1322 return (HOLD);
1325 sub handle_renew {
1326 my ( $self, $server ) = @_;
1327 my $ils = $server->{ils};
1328 my ( $third_party, $no_block, $trans_date, $nb_due_date );
1329 my ( $patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack );
1330 my $fields = $self->{fields};
1331 my $status;
1332 my ( $patron, $item );
1333 my $resp = RENEW_RESP;
1335 ( $third_party, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
1337 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew" );
1339 if ( $no_block eq 'Y' ) {
1340 syslog( "LOG_WARNING", "handle_renew: received 'no block' renewal from terminal '%s'", $server->{account}->{id} );
1343 $patron_id = $fields->{ (FID_PATRON_ID) };
1344 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1345 $item_id = $fields->{ (FID_ITEM_ID) };
1346 $title_id = $fields->{ (FID_TITLE_ID) };
1347 $item_props = $fields->{ (FID_ITEM_PROPS) };
1348 $fee_ack = $fields->{ (FID_FEE_ACK) };
1350 $status = $ils->renew( $patron_id, $patron_pwd, $item_id, $title_id, $no_block, $nb_due_date, $third_party, $item_props, $fee_ack );
1352 $patron = $status->patron;
1353 $item = $status->item;
1355 if ( $status->renewal_ok ) {
1356 $resp .= '1';
1357 $resp .= $status->renewal_ok ? 'Y' : 'N';
1358 if ( $ils->supports('magnetic media') ) {
1359 $resp .= sipbool( $item->magnetic_media );
1360 } else {
1361 $resp .= 'U';
1363 $resp .= sipbool( $status->desensitize );
1364 $resp .= timestamp;
1365 $resp .= add_field( FID_PATRON_ID, $patron->id );
1366 $resp .= add_field( FID_ITEM_ID, $item->id );
1367 $resp .= add_field( FID_TITLE_ID, $item->title_id );
1368 if ( $item->due_date ) {
1369 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ) );
1370 } else {
1371 $resp .= add_field( FID_DUE_DATE, q{} );
1373 if ( $ils->supports('security inhibit') ) {
1374 $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit );
1376 $resp .= add_field( FID_MEDIA_TYPE, $item->sip_media_type );
1377 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
1378 } else {
1380 # renew failed for some reason
1381 # not OK, renewal not OK, Unknown media type (why bother checking?)
1382 $resp .= '0NUN';
1383 $resp .= timestamp;
1385 # If we found the patron or the item, the return the ILS
1386 # information, otherwise echo back the information we received
1387 # from the terminal
1388 $resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id );
1389 $resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id );
1390 $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : $title_id );
1391 $resp .= add_field( FID_DUE_DATE, '' );
1394 if ( $status->fee_amount ) {
1395 $resp .= add_field( FID_FEE_AMT, $status->fee_amount );
1396 $resp .= maybe_add( FID_CURRENCY, $status->sip_currency );
1397 $resp .= maybe_add( FID_FEE_TYPE, $status->sip_fee_type );
1398 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id );
1401 $resp .= add_field( FID_INST_ID, $ils->institution );
1402 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1403 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1405 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1407 return (RENEW);
1410 sub handle_renew_all {
1412 # my ($third_party, $no_block, $nb_due_date, $fee_ack, $patron);
1414 my ( $self, $server ) = @_;
1415 my $ils = $server->{ils};
1416 my ( $trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack );
1417 my $fields = $self->{fields};
1418 my $resp = RENEW_ALL_RESP;
1419 my $status;
1420 my ( @renewed, @unrenewed );
1422 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew_all" );
1424 ($trans_date) = @{ $self->{fixed_fields} };
1426 $patron_id = $fields->{ (FID_PATRON_ID) };
1427 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1428 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
1429 $fee_ack = $fields->{ (FID_FEE_ACK) };
1431 $status = $ils->renew_all( $patron_id, $patron_pwd, $fee_ack );
1433 $resp .= $status->ok ? '1' : '0';
1435 if ( !$status->ok ) {
1436 $resp .= add_count( "renew_all/renewed_count", 0 );
1437 $resp .= add_count( "renew_all/unrenewed_count", 0 );
1438 @renewed = ();
1439 @unrenewed = ();
1440 } else {
1441 @renewed = ( @{ $status->renewed } );
1442 @unrenewed = ( @{ $status->unrenewed } );
1443 $resp .= add_count( "renew_all/renewed_count", scalar @renewed );
1444 $resp .= add_count( "renew_all/unrenewed_count", scalar @unrenewed );
1447 $resp .= timestamp;
1448 $resp .= add_field( FID_INST_ID, $ils->institution );
1450 $resp .= join( '', map( add_field( FID_RENEWED_ITEMS, $_ ), @renewed ) );
1451 $resp .= join( '', map( add_field( FID_UNRENEWED_ITEMS, $_ ), @unrenewed ) );
1453 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1454 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1456 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1458 return (RENEW_ALL);
1462 # send_acs_status($self, $server)
1464 # Send an ACS Status message, which is contains lots of little fields
1465 # of information gleaned from all sorts of places.
1468 my @message_type_names = (
1469 "patron status request",
1470 "checkout",
1471 "checkin",
1472 "block patron",
1473 "acs status",
1474 "request sc/acs resend",
1475 "login",
1476 "patron information",
1477 "end patron session",
1478 "fee paid",
1479 "item information",
1480 "item status update",
1481 "patron enable",
1482 "hold",
1483 "renew",
1484 "renew all",
1487 sub send_acs_status {
1488 my ( $self, $server, $screen_msg, $print_line ) = @_;
1489 my $msg = ACS_STATUS;
1490 ($server) or die "send_acs_status error: no \$server argument received";
1491 my $account = $server->{account} or die "send_acs_status error: no 'account' in \$server object:\n" . Dumper($server);
1492 my $policy = $server->{policy} or die "send_acs_status error: no 'policy' in \$server object:\n" . Dumper($server);
1493 my $ils = $server->{ils} or die "send_acs_status error: no 'ils' in \$server object:\n" . Dumper($server);
1494 my ( $online_status, $checkin_ok, $checkout_ok, $ACS_renewal_policy );
1495 my ( $status_update_ok, $offline_ok, $timeout, $retries );
1497 $online_status = 'Y';
1498 $checkout_ok = sipbool( $ils->checkout_ok );
1499 $checkin_ok = sipbool( $ils->checkin_ok );
1500 $ACS_renewal_policy = sipbool( $policy->{renewal} );
1501 $status_update_ok = sipbool( $ils->status_update_ok );
1502 $offline_ok = sipbool( $ils->offline_ok );
1503 $timeout = $server->get_timeout({ policy => 1 });
1504 $retries = sprintf( "%03d", $policy->{retries} );
1506 if ( length($retries) != 3 ) {
1507 syslog( "LOG_ERR", "handle_acs_status: retries field wrong size: '%s'", $retries );
1508 $retries = '000';
1511 $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
1512 $msg .= "$status_update_ok$offline_ok$timeout$retries";
1513 $msg .= timestamp();
1515 if ( $protocol_version == 1 ) {
1516 $msg .= '1.00';
1517 } elsif ( $protocol_version == 2 ) {
1518 $msg .= '2.00';
1519 } else {
1520 syslog( "LOG_ERR", 'Bad setting for $protocol_version, "%s" in send_acs_status', $protocol_version );
1521 $msg .= '1.00';
1524 # Institution ID
1525 $msg .= add_field( FID_INST_ID, $account->{institution} );
1527 if ( $protocol_version >= 2 ) {
1529 # Supported messages: we do it all
1530 my $supported_msgs = '';
1532 foreach my $msg_name (@message_type_names) {
1533 if ( $msg_name eq 'request sc/acs resend' ) {
1534 $supported_msgs .= sipbool(1);
1535 } else {
1536 $supported_msgs .= sipbool( $ils->supports($msg_name) );
1539 if ( length($supported_msgs) < 16 ) {
1540 syslog( "LOG_ERR", 'send_acs_status: supported messages "%s" too short', $supported_msgs );
1542 $msg .= add_field( FID_SUPPORTED_MSGS, $supported_msgs );
1545 $msg .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1547 if ( defined( $account->{print_width} )
1548 && defined($print_line)
1549 && $account->{print_width} < length($print_line) ) {
1550 syslog( "LOG_WARNING", "send_acs_status: print line '%s' too long. Truncating", $print_line );
1551 $print_line = substr( $print_line, 0, $account->{print_width} );
1554 $msg .= maybe_add( FID_PRINT_LINE, $print_line );
1556 # Do we want to tell the terminal its location?
1558 $self->write_msg( $msg, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1559 return 1;
1563 # build_patron_status: create the 14-char patron status
1564 # string for the Patron Status message
1566 sub patron_status_string {
1567 my $patron = shift;
1568 my $patron_status;
1570 syslog( "LOG_DEBUG", "patron_status_string: %s charge_ok: %s", $patron->id, $patron->charge_ok );
1571 $patron_status = sprintf(
1572 '%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
1573 denied( $patron->charge_ok ),
1574 denied( $patron->renew_ok ),
1575 denied( $patron->recall_ok ),
1576 denied( $patron->hold_ok ),
1577 boolspace( $patron->card_lost ),
1578 boolspace( $patron->too_many_charged ),
1579 boolspace( $patron->too_many_overdue ),
1580 boolspace( $patron->too_many_renewal ),
1581 boolspace( $patron->too_many_claim_return ),
1582 boolspace( $patron->too_many_lost ),
1583 boolspace( $patron->excessive_fines ),
1584 boolspace( $patron->excessive_fees ),
1585 boolspace( $patron->recall_overdue ),
1586 boolspace( $patron->too_many_billed )
1588 return $patron_status;
1591 sub api_auth {
1592 my ( $username, $password, $branch ) = @_;
1593 $ENV{REMOTE_USER} = $username;
1594 my $query = CGI->new();
1595 $query->param( userid => $username );
1596 $query->param( password => $password );
1597 if ($branch) {
1598 $query->param( branch => $branch );
1600 my ( $status, $cookie, $sessionID ) = check_api_auth( $query, { circulate => 1 }, 'intranet' );
1601 return $status;
1605 __END__