Bug 25541: Add ability to prevent checkin via SIP of items with holds
[koha.git] / C4 / SIP / Sip / MsgType.pm
blobd9f8901c06e4ebfa2c039e2a4eb5fba7b18b1af6
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;
13 use C4::SIP::Sip qw(:all);
14 use C4::SIP::Sip::Constants qw(:all);
15 use C4::SIP::Sip::Checksum qw(verify_cksum);
17 use Data::Dumper;
18 use CGI qw ( -utf8 );
19 use C4::Auth qw(&check_api_auth);
21 use Koha::Patron::Attributes;
22 use Koha::Items;
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 siplog( "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 siplog( "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 siplog( "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 siplog( "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 siplog( "LOG_WARNING", "Unsupported field '%s' in %s message '%s'", $fn, $self->{name}, $msg );
319 } elsif ( defined( $self->{fields}->{$fn} ) ) {
320 siplog( "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;
334 # Set system preference overrides, first global, then account level
335 # Clear overrides from previous message handling first
336 foreach my $key ( %ENV ) {
337 delete $ENV{$key} if index($key, 'OVERRIDE_SYSPREF_') > 0;
339 foreach my $key ( keys %{ $config->{'syspref_overrides'} } ) {
340 $ENV{"OVERRIDE_SYSPREF_$key"} = $config->{'syspref_overrides'}->{$key};
342 foreach my $key ( keys %{ $server->{account}->{'syspref_overrides'} } ) {
343 $ENV{"OVERRIDE_SYSPREF_$key"} =
344 $server->{account}->{'syspref_overrides'}->{$key};
348 # What's the field delimiter for variable length fields?
349 # This can't be based on the account, since we need to know
350 # the field delimiter to parse a SIP login message
352 if ( defined( $server->{config}->{delimiter} ) ) {
353 $field_delimiter = $server->{config}->{delimiter};
356 # error detection is active if this is a REQUEST_ACS_RESEND
357 # message with a checksum, or if the message is long enough
358 # and the last nine characters begin with a sequence number
359 # field
360 if ( $msg eq REQUEST_ACS_RESEND_CKSUM ) {
362 # Special case
363 $error_detection = 1;
364 $self = C4::SIP::Sip::MsgType->new( (REQUEST_ACS_RESEND), 0 );
365 } elsif ( ( length($msg) > 11 ) && ( substr( $msg, -9, 2 ) eq "AY" ) ) {
366 $error_detection = 1;
368 if ( !verify_cksum($msg) ) {
369 siplog( "LOG_WARNING", "Checksum failed on message '%s'", $msg );
371 # REQUEST_SC_RESEND with error detection
372 $last_response = REQUEST_SC_RESEND_CKSUM;
373 print("$last_response\r");
374 return REQUEST_ACS_RESEND;
375 } else {
377 # Save the sequence number, then strip off the
378 # error detection data to process the message
379 $self = C4::SIP::Sip::MsgType->new( substr( $msg, 0, -9 ), substr( $msg, -7, 1 ) );
381 } elsif ($error_detection) {
383 # We received a non-ED message when ED is supposed to be active.
384 # Warn about this problem, then process the message anyway.
385 siplog( "LOG_WARNING", "Received message without error detection: '%s'", $msg );
386 $error_detection = 0;
387 $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
388 } else {
389 $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
392 if ( ( substr( $msg, 0, 2 ) ne REQUEST_ACS_RESEND )
393 && $req
394 && ( substr( $msg, 0, 2 ) ne $req ) ) {
395 return substr( $msg, 0, 2 );
397 unless ( $self->{handler} ) {
398 siplog( "LOG_WARNING", "No handler defined for '%s'", $msg );
399 $last_response = REQUEST_SC_RESEND;
400 print("$last_response\r");
401 return REQUEST_ACS_RESEND;
403 return ( $self->{handler}->( $self, $server ) ); # FIXME
404 # FIXME: Use of uninitialized value in subroutine entry
405 # Can't use string ("") as a subroutine ref while "strict refs" in use
409 ## Message Handlers
413 # Patron status messages are produced in response to both
414 # "Request Patron Status" and "Block Patron"
416 # Request Patron Status requires a patron password, but
417 # Block Patron doesn't (since the patron may never have
418 # provided one before attempting some illegal action).
420 # ASSUMPTION: If the patron password field is present in the
421 # message, then it must match, otherwise incomplete patron status
422 # information will be returned to the terminal.
424 sub build_patron_status {
425 my ( $patron, $lang, $fields, $server ) = @_;
427 my $patron_pwd = $fields->{ (FID_PATRON_PWD) };
428 my $resp = (PATRON_STATUS_RESP);
429 my $password_rc;
431 if ( $patron ) {
432 if ($patron_pwd) {
433 $password_rc = $patron->check_password($patron_pwd);
436 $resp .= patron_status_string($patron);
437 $resp .= $lang . timestamp();
438 if ( defined $server->{account}->{ae_field_template} ) {
439 $resp .= add_field( FID_PERSONAL_NAME, $patron->format( $server->{account}->{ae_field_template}, $server ) );
440 } else {
441 $resp .= add_field( FID_PERSONAL_NAME, $patron->name, $server );
445 # while the patron ID we got from the SC is valid, let's
446 # use the one returned from the ILS, just in case...
447 $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
449 if ( $protocol_version >= 2 ) {
450 $resp .= add_field( FID_VALID_PATRON, 'Y', $server );
452 # Patron password is a required field.
453 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool($password_rc), $server );
454 $resp .= maybe_add( FID_CURRENCY, $patron->currency, $server );
455 $resp .= maybe_add( FID_FEE_AMT, $patron->fee_amount, $server );
458 my $msg = $patron->screen_msg;
459 $msg .= ' -- '. INVALID_PW if $patron_pwd && !$password_rc;
460 $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
462 $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server )
463 if ( $server->{account}->{send_patron_home_library_in_af} );
464 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line, $server );
466 $resp .= $patron->build_custom_field_string( $server );
467 $resp .= $patron->build_patron_attributes_string( $server );
469 } else {
470 # Invalid patron (cardnumber)
471 # Report that the user has no privs.
473 # no personal name, and is invalid (if we're using 2.00)
474 $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
475 $resp .= add_field( FID_PERSONAL_NAME, '', $server );
477 # the patron ID is invalid, but it's a required field, so
478 # just echo it back
479 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) }, $server );
481 ( $protocol_version >= 2 )
482 and $resp .= add_field( FID_VALID_PATRON, 'N', $server );
484 $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
487 $resp .= add_field( FID_INST_ID, $fields->{ (FID_INST_ID) }, $server );
488 return $resp;
491 sub handle_patron_status {
492 my ( $self, $server ) = @_;
493 my $ils = $server->{ils};
494 my $patron;
495 my $resp = (PATRON_STATUS_RESP);
496 my $account = $server->{account};
497 my ( $lang, $date ) = @{ $self->{fixed_fields} };
498 my $fields = $self->{fields};
500 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_patron_status" );
501 $patron = $ils->find_patron( $fields->{ (FID_PATRON_ID) } );
502 $resp = build_patron_status( $patron, $lang, $fields, $server );
503 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
504 return (PATRON_STATUS_REQ);
507 sub handle_checkout {
508 my ( $self, $server ) = @_;
509 my $account = $server->{account};
510 my $ils = $server->{ils};
511 my $inst = $ils->institution;
512 my ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date );
513 my $fields;
514 my ( $patron_id, $item_id, $status );
515 my ( $item, $patron );
516 my $resp;
518 ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
519 $fields = $self->{fields};
521 $patron_id = $fields->{ (FID_PATRON_ID) };
522 $item_id = $fields->{ (FID_ITEM_ID) };
523 my $fee_ack = $fields->{ (FID_FEE_ACK) };
525 if ( $no_block eq 'Y' ) {
527 # Off-line transactions need to be recorded, but there's
528 # not a lot we can do about it
529 siplog( "LOG_WARNING", "received no-block checkout from terminal '%s'", $account->{id} );
531 $status = $ils->checkout_no_block( $patron_id, $item_id, $sc_renewal_policy, $trans_date, $nb_due_date );
532 } else {
534 # Does the transaction date really matter for items that are
535 # checkout out while the terminal is online? I'm guessing 'no'
536 $status = $ils->checkout( $patron_id, $item_id, $sc_renewal_policy, $fee_ack );
539 $item = $status->item;
540 $patron = $status->patron;
542 if ( $status->ok ) {
544 # Item successfully checked out
545 # Fixed fields
546 $resp = CHECKOUT_RESP . '1';
547 $resp .= sipbool( $status->renew_ok );
548 if ( $ils->supports('magnetic media') ) {
549 $resp .= sipbool( $item->magnetic_media );
550 } else {
551 $resp .= 'U';
554 # We never return the obsolete 'U' value for 'desensitize'
555 $resp .= sipbool( $status->desensitize );
556 $resp .= timestamp;
558 # Now for the variable fields
559 $resp .= add_field( FID_INST_ID, $inst, $server );
560 $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
561 $resp .= add_field( FID_ITEM_ID, $item_id, $server );
562 $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
563 if ( $item->due_date ) {
564 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ), $server );
565 } else {
566 $resp .= add_field( FID_DUE_DATE, q{}, $server );
569 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
570 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
572 if ( $protocol_version >= 2 ) {
573 if ( $ils->supports('security inhibit') ) {
574 $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit, $server );
576 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type, $server );
577 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
582 else {
584 # Checkout failed
585 # Checkout Response: not ok, no renewal, don't know mag. media,
586 # no desensitize
587 $resp = sprintf( "120NUN%s", timestamp );
588 $resp .= add_field( FID_INST_ID, $inst, $server );
589 $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
590 $resp .= add_field( FID_ITEM_ID, $item_id, $server );
592 # If the item is valid, provide the title, otherwise
593 # leave it blank
594 $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : '', $server );
596 # Due date is required. Since it didn't get checked out,
597 # it's not due, so leave the date blank
598 $resp .= add_field( FID_DUE_DATE, '', $server );
600 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
601 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
603 if ( $protocol_version >= 2 ) {
605 # Is the patron ID valid?
606 $resp .= add_field( FID_VALID_PATRON, sipbool($patron), $server );
608 if ( $patron && exists( $fields->{FID_PATRON_PWD} ) ) {
610 # Password provided, so we can tell if it was valid or not
611 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password( $fields->{ (FID_PATRON_PWD) } ) ), $server );
616 $resp .= $item->build_additional_item_fields_string( $server ) if $item;
618 if ( $protocol_version >= 2 ) {
620 # Financials : return irrespective of ok status
621 if ( $status->fee_amount ) {
622 $resp .= add_field( FID_FEE_AMT, $status->fee_amount, $server );
623 $resp .= maybe_add( FID_CURRENCY, $status->sip_currency, $server );
624 $resp .= maybe_add( FID_FEE_TYPE, $status->sip_fee_type, $server );
625 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id, $server );
629 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
630 return (CHECKOUT);
633 sub handle_checkin {
634 my ( $self, $server ) = @_;
635 my $account = $server->{account};
636 my $ils = $server->{ils};
637 my $my_branch = $ils->institution;
638 my ( $current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel );
639 my ( $patron, $item, $status );
640 my $resp = CHECKIN_RESP;
641 my ( $no_block, $trans_date, $return_date ) = @{ $self->{fixed_fields} };
642 my $fields = $self->{fields};
644 $current_loc = $fields->{ (FID_CURRENT_LOCN) };
645 $inst_id = $fields->{ (FID_INST_ID) };
646 $item_id = $fields->{ (FID_ITEM_ID) };
647 $item_props = $fields->{ (FID_ITEM_PROPS) };
648 $cancel = $fields->{ (FID_CANCEL) };
649 if ($current_loc) {
650 $my_branch = $current_loc; # most scm do not set $current_loc
653 $ils->check_inst_id( $inst_id, "handle_checkin" );
655 if ( $no_block eq 'Y' ) {
657 # Off-line transactions, ick.
658 siplog( "LOG_WARNING", "received no-block checkin from terminal '%s'", $account->{id} );
659 $status = $ils->checkin_no_block( $item_id, $trans_date, $return_date, $item_props, $cancel );
660 } else {
661 $status = $ils->checkin( $item_id, $trans_date, $return_date, $my_branch, $item_props, $cancel, $account );
664 $patron = $status->patron;
665 $item = $status->item;
667 $resp .= $status->ok ? '1' : '0';
668 $resp .= $status->resensitize ? 'Y' : 'N';
669 if ( $item && $ils->supports('magnetic media') ) {
670 $resp .= sipbool( $item->magnetic_media );
671 } else {
673 # item barcode is invalid or system doesn't support 'magnetic media' indicator
674 $resp .= 'U';
677 $resp .= $status->alert ? 'Y' : 'N';
678 $resp .= timestamp;
679 $resp .= add_field( FID_INST_ID, $inst_id, $server );
680 $resp .= add_field( FID_ITEM_ID, $item_id, $server );
682 if ($item) {
683 $resp .= add_field( FID_PERM_LOCN, $item->permanent_location, $server );
684 $resp .= maybe_add( FID_TITLE_ID, $item->title_id, $server );
685 $resp .= $item->build_additional_item_fields_string( $server );
688 if ( $protocol_version >= 2 ) {
689 $resp .= maybe_add( FID_SORT_BIN, $status->sort_bin, $server );
690 if ($patron) {
691 $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
693 if ($item) {
694 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type, $server );
695 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
696 $resp .= maybe_add( FID_COLLECTION_CODE, $item->collection_code, $server );
697 $resp .= maybe_add( FID_CALL_NUMBER, $item->call_number, $server );
698 $resp .= maybe_add( FID_HOLD_PATRON_ID, $item->hold_patron_bcode, $server );
699 $resp .= add_field( FID_DESTINATION_LOCATION, $item->destination_loc, $server ) if ( $item->destination_loc || $server->{account}->{ct_always_send} );
700 $resp .= maybe_add( FID_HOLD_PATRON_NAME, $item->hold_patron_name( $server->{account}->{da_field_template} ), $server );
702 if ( $status->hold and $status->hold->{branchcode} ne $item->destination_loc ) {
703 warn 'SIP hold mismatch: $status->hold->{branchcode}=' . $status->hold->{branchcode} . '; $item->destination_loc=' . $item->destination_loc;
705 # just me being paranoid.
710 if ( $status->alert && $status->alert_type ) {
711 $resp .= maybe_add( FID_ALERT_TYPE, $status->alert_type, $server );
712 } elsif ( $server->{account}->{cv_send_00_on_success} ) {
713 $resp .= add_field( FID_ALERT_TYPE, '00', $server );
715 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
716 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
718 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
720 return (CHECKIN);
723 sub handle_block_patron {
724 my ( $self, $server ) = @_;
725 my $account = $server->{account};
726 my $ils = $server->{ils};
727 my ( $card_retained, $trans_date );
728 my ( $inst_id, $blocked_card_msg, $patron_id, $terminal_pwd );
729 my ( $fields, $resp, $patron );
731 ( $card_retained, $trans_date ) = @{ $self->{fixed_fields} };
732 $fields = $self->{fields};
733 $inst_id = $fields->{ (FID_INST_ID) };
734 $blocked_card_msg = $fields->{ (FID_BLOCKED_CARD_MSG) };
735 $patron_id = $fields->{ (FID_PATRON_ID) };
736 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
738 # Terminal passwords are different from account login
739 # passwords, but I have no idea what to do with them. So,
740 # I'll just ignore them for now.
742 # FIXME ???
744 $ils->check_inst_id( $inst_id, "block_patron" );
745 $patron = $ils->find_patron($patron_id);
747 # The correct response for a "Block Patron" message is a
748 # "Patron Status Response", so use that handler to generate
749 # the message, but then return the correct code from here.
751 # Normally, the language is provided by the "Patron Status"
752 # fixed field, but since we're not responding to one of those
753 # we'll just say, "Unspecified", as per the spec. Let the
754 # terminal default to something that, one hopes, will be
755 # intelligible
756 if ($patron) {
758 # Valid patron id
759 $patron->block( $card_retained, $blocked_card_msg );
762 $resp = build_patron_status( $patron, $patron->language, $fields, $server );
763 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
764 return (BLOCK_PATRON);
767 sub handle_sc_status {
768 my ( $self, $server ) = @_;
769 ($server) or warn "handle_sc_status error: no \$server argument received.";
770 my ( $status, $print_width, $sc_protocol_version ) = @{ $self->{fixed_fields} };
771 my ($new_proto);
773 if ( $sc_protocol_version =~ /^1\./ ) {
774 $new_proto = 1;
775 } elsif ( $sc_protocol_version =~ /^2\./ ) {
776 $new_proto = 2;
777 } else {
778 siplog( "LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version );
779 $new_proto = 1;
782 if ( $new_proto != $protocol_version ) {
783 siplog( "LOG_INFO", "Setting protocol level to $new_proto" );
784 $protocol_version = $new_proto;
787 if ( $status == SC_STATUS_PAPER ) {
788 siplog( "LOG_WARNING", "Self-Check unit '%s@%s' out of paper", $self->{account}->{id}, $self->{account}->{institution} );
789 } elsif ( $status == SC_STATUS_SHUTDOWN ) {
790 siplog( "LOG_WARNING", "Self-Check unit '%s@%s' shutting down", $self->{account}->{id}, $self->{account}->{institution} );
793 $self->{account}->{print_width} = $print_width;
794 return ( send_acs_status( $self, $server ) ? SC_STATUS : '' );
797 sub handle_request_acs_resend {
798 my ( $self, $server ) = @_;
800 if ( !$last_response ) {
802 # We haven't sent anything yet, so respond with a
803 # REQUEST_SC_RESEND msg (p. 16)
804 $self->write_msg( REQUEST_SC_RESEND, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
805 } elsif ( ( length($last_response) < 9 )
806 || substr( $last_response, -9, 2 ) ne 'AY' ) {
808 # When resending a message, we aren't supposed to include
809 # a sequence number, even if the original had one (p. 4).
810 # If the last message didn't have a sequence number, then
811 # we can just send it.
812 print("$last_response\r"); # not write_msg?
813 } else {
815 # Cut out the sequence number and checksum, since the old
816 # checksum is wrong for the resent message.
817 my $rebuilt = substr( $last_response, 0, -9 );
818 $self->write_msg( $rebuilt, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
821 return REQUEST_ACS_RESEND;
824 sub login_core {
825 my $server = shift or return;
826 my $uid = shift;
827 my $pwd = shift;
828 my $status = 1; # Assume it all works
829 if ( !exists( $server->{config}->{accounts}->{$uid} ) ) {
830 siplog( "LOG_WARNING", "MsgType::login_core: Unknown login '$uid'" );
831 $status = 0;
832 } elsif ( $server->{config}->{accounts}->{$uid}->{password} ne $pwd ) {
833 siplog( "LOG_WARNING", "MsgType::login_core: Invalid password for login '$uid'" );
834 $status = 0;
835 } else {
837 # Store the active account someplace handy for everybody else to find.
838 $server->{account} = $server->{config}->{accounts}->{$uid};
839 my $inst = $server->{account}->{institution};
840 $server->{institution} = $server->{config}->{institutions}->{$inst};
841 $server->{policy} = $server->{institution}->{policy};
842 $server->{sip_username} = $uid;
843 $server->{sip_password} = $pwd;
845 my $auth_status = api_auth( $uid, $pwd, $inst );
846 if ( !$auth_status or $auth_status !~ /^ok$/i ) {
847 siplog( "LOG_WARNING", "api_auth failed for SIP terminal '%s' of '%s': %s", $uid, $inst, ( $auth_status || 'unknown' ) );
848 $status = 0;
849 } else {
850 siplog( "LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst );
853 # initialize connection to ILS
855 my $module = $server->{config}->{institutions}->{$inst}->{implementation};
856 siplog( "LOG_DEBUG", 'login_core: ' . Dumper($module) );
858 # Suspect this is always ILS but so we don't break any eccentic install (for now)
859 if ( $module eq 'ILS' ) {
860 $module = 'C4::SIP::ILS';
862 $module->use;
863 if ($@) {
864 siplog( "LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed", $server->{service}, $module, $inst );
865 die("Failed to load ILS implementation '$module' for $inst");
868 # like ILS->new(), I think.
869 $server->{ils} = $module->new( $server->{institution}, $server->{account} );
870 if ( !$server->{ils} ) {
871 siplog( "LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst );
872 die("Unable to connect to ILS '$inst'");
876 return $status;
879 sub handle_login {
880 my ( $self, $server ) = @_;
881 my ( $uid_algorithm, $pwd_algorithm );
882 my ( $uid, $pwd );
883 my $inst;
884 my $fields;
885 my $status = 1; # Assume it all works
887 $fields = $self->{fields};
888 ( $uid_algorithm, $pwd_algorithm ) = @{ $self->{fixed_fields} };
890 $uid = $fields->{ (FID_LOGIN_UID) }; # Terminal ID, not patron ID.
891 $pwd = $fields->{ (FID_LOGIN_PWD) }; # Terminal PWD, not patron PWD.
893 if ( $uid_algorithm || $pwd_algorithm ) {
894 siplog( "LOG_ERR", "LOGIN: Unsupported non-zero encryption method(s): uid = $uid_algorithm, pwd = $pwd_algorithm" );
895 $status = 0;
896 } else {
897 $status = login_core( $server, $uid, $pwd );
900 $self->write_msg( LOGIN_RESP . $status, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
901 return $status ? LOGIN : '';
905 # Build the detailed summary information for the Patron
906 # Information Response message based on the first 'Y' that appears
907 # in the 'summary' field of the Patron Information request. The
908 # specification says that only one 'Y' can appear in that field,
909 # and we're going to believe it.
911 sub summary_info {
912 my ( $ils, $patron, $summary, $start, $end, $server ) = @_;
913 my $resp = '';
916 # Map from offsets in the "summary" field of the Patron Information
917 # message to the corresponding field and handler
919 my @summary_map = (
920 { func => $patron->can("hold_items"), fid => FID_HOLD_ITEMS },
921 { func => $patron->can("overdue_items"), fid => FID_OVERDUE_ITEMS },
922 { func => $patron->can("charged_items"), fid => FID_CHARGED_ITEMS },
923 { func => $patron->can("fine_items"), fid => FID_FINE_ITEMS },
924 { func => $patron->can("recall_items"), fid => FID_RECALL_ITEMS },
925 { func => $patron->can("unavail_holds"), fid => FID_UNAVAILABLE_HOLD_ITEMS },
928 my $summary_type = index( $summary, 'Y' );
929 return q{} if $summary_type == -1; # No detailed information required.
930 return q{} if $summary_type > 5; # Positions 6-9 are not defined in the sip spec,
931 # and we have no extensions to handle them.
933 siplog( "LOG_DEBUG", "Summary_info: index == '%d', field '%s'", $summary_type, $summary_map[$summary_type]->{fid} );
935 my $func = $summary_map[$summary_type]->{func};
936 my $fid = $summary_map[$summary_type]->{fid};
937 my $itemlist = &$func( $patron, $start, $end, $server );
939 siplog( "LOG_DEBUG", "summary_info: list = (%s)", join( ", ", map{ $_->{barcode} } @{$itemlist} ) );
940 foreach my $i ( @{$itemlist} ) {
941 $resp .= add_field( $fid, $i->{barcode}, $server );
944 return $resp;
947 sub handle_patron_info {
948 my ( $self, $server ) = @_;
949 my $ils = $server->{ils};
950 my ( $lang, $trans_date, $summary ) = @{ $self->{fixed_fields} };
951 my $fields = $self->{fields};
952 my ( $inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end );
953 my ( $resp, $patron );
955 $inst_id = $fields->{ (FID_INST_ID) };
956 $patron_id = $fields->{ (FID_PATRON_ID) };
957 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
958 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
959 $start = $fields->{ (FID_START_ITEM) };
960 $end = $fields->{ (FID_END_ITEM) };
962 $patron = $ils->find_patron($patron_id);
964 $resp = (PATRON_INFO_RESP);
965 if ($patron) {
966 $patron->update_lastseen();
967 $resp .= patron_status_string($patron);
968 $resp .= ( defined($lang) and length($lang) == 3 ) ? $lang : $patron->language;
969 $resp .= timestamp();
971 $resp .= add_count( 'patron_info/hold_items', scalar @{ $patron->hold_items } );
972 $resp .= add_count( 'patron_info/overdue_items', scalar @{ $patron->overdue_items } );
973 $resp .= add_count( 'patron_info/charged_items', scalar @{ $patron->charged_items } );
974 $resp .= add_count( 'patron_info/fine_items', scalar @{ $patron->fine_items } );
975 $resp .= add_count( 'patron_info/recall_items', scalar @{ $patron->recall_items } );
976 $resp .= add_count( 'patron_info/unavail_holds', scalar @{ $patron->unavail_holds } );
978 $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ), $server );
980 # while the patron ID we got from the SC is valid, let's
981 # use the one returned from the ILS, just in case...
982 $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
983 if ( defined $server->{account}->{ae_field_template} ) {
984 $resp .= add_field( FID_PERSONAL_NAME, $patron->format( $server->{account}->{ae_field_template} ), $server );
985 } else {
986 $resp .= add_field( FID_PERSONAL_NAME, $patron->name, $server );
989 # TODO: add code for the fields
990 # hold items limit
991 # overdue items limit
992 # charged items limit
994 $resp .= add_field( FID_VALID_PATRON, 'Y', $server );
995 my $password_rc;
996 if ( defined($patron_pwd) ) {
998 # If patron password was provided, report whether it was right or not.
999 if ( $patron_pwd eq q{} && $server->{account}->{allow_empty_passwords} ) {
1000 $password_rc = 1;
1001 } else {
1002 $password_rc = $patron->check_password($patron_pwd);
1004 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $password_rc ), $server );
1007 $resp .= maybe_add( FID_CURRENCY, $patron->currency, $server );
1008 $resp .= maybe_add( FID_FEE_AMT, $patron->fee_amount, $server );
1009 $resp .= add_field( FID_FEE_LMT, $patron->fee_limit, $server );
1011 # TODO: zero or more item details for 2.0 can go here:
1012 # hold_items
1013 # overdue_items
1014 # charged_items
1015 # fine_items
1016 # recall_items
1018 $resp .= summary_info( $ils, $patron, $summary, $start, $end, $server );
1020 $resp .= maybe_add( FID_HOME_ADDR, $patron->address, $server );
1021 $resp .= maybe_add( FID_EMAIL, $patron->email_addr, $server );
1022 $resp .= maybe_add( FID_HOME_PHONE, $patron->home_phone, $server );
1024 # SIP 2.0 extensions used by Envisionware
1025 # Other terminals will ignore unrecognized fields (unrecognized field identifiers)
1026 $resp .= maybe_add( FID_PATRON_BIRTHDATE, $patron->birthdate, $server );
1027 $resp .= maybe_add( FID_PATRON_CLASS, $patron->ptype, $server );
1029 # Custom protocol extension to report patron internet privileges
1030 $resp .= maybe_add( FID_INET_PROFILE, $patron->inet_privileges, $server );
1032 my $msg = $patron->screen_msg;
1033 if( defined( $patron_pwd ) && !$password_rc ) {
1034 $msg .= ' -- ' . INVALID_PW;
1036 $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
1037 if ( $server->{account}->{send_patron_home_library_in_af} ) {
1038 $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server);
1040 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line, $server );
1042 $resp .= $patron->build_custom_field_string( $server );
1043 $resp .= $patron->build_patron_attributes_string( $server );
1044 } else {
1046 # Invalid patron ID:
1047 # no privileges, no items associated,
1048 # no personal name, and is invalid (if we're using 2.00)
1049 $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
1050 $resp .= '0000' x 6;
1052 $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ), $server );
1054 # patron ID is invalid, but field is required, so just echo it back
1055 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) }, $server );
1056 $resp .= add_field( FID_PERSONAL_NAME, '', $server );
1058 if ( $protocol_version >= 2 ) {
1059 $resp .= add_field( FID_VALID_PATRON, 'N', $server );
1061 $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
1064 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1065 return (PATRON_INFO);
1068 sub handle_end_patron_session {
1069 my ( $self, $server ) = @_;
1070 my $ils = $server->{ils};
1071 my $trans_date;
1072 my $fields = $self->{fields};
1073 my $resp = END_SESSION_RESP;
1074 my ( $status, $screen_msg, $print_line );
1076 ($trans_date) = @{ $self->{fixed_fields} };
1078 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, 'handle_end_patron_session' );
1080 ( $status, $screen_msg, $print_line ) = $ils->end_patron_session( $fields->{ (FID_PATRON_ID) } );
1082 $resp .= $status ? 'Y' : 'N';
1083 $resp .= timestamp();
1085 $resp .= add_field( FID_INST_ID, $server->{ils}->institution, $server );
1086 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) }, $server );
1088 $resp .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1089 $resp .= maybe_add( FID_PRINT_LINE, $print_line, $server );
1091 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1093 return (END_PATRON_SESSION);
1096 sub handle_fee_paid {
1097 my ( $self, $server ) = @_;
1098 my $ils = $server->{ils};
1099 my ( $trans_date, $fee_type, $pay_type, $currency ) = @{ $self->{fixed_fields} };
1100 my $fields = $self->{fields};
1101 my ( $fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd );
1102 my ( $fee_id, $trans_id );
1103 my $status;
1104 my $resp = FEE_PAID_RESP;
1106 my $disallow_overpayment = $server->{account}->{disallow_overpayment};
1107 my $payment_type_writeoff = $server->{account}->{payment_type_writeoff} || q{};
1108 my $register_id = $server->{account}->{register_id};
1110 my $is_writeoff = $pay_type eq $payment_type_writeoff;
1112 $fee_amt = $fields->{ (FID_FEE_AMT) };
1113 $inst_id = $fields->{ (FID_INST_ID) };
1114 $patron_id = $fields->{ (FID_PATRON_ID) };
1115 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1116 $fee_id = $fields->{ (FID_FEE_ID) };
1117 $trans_id = $fields->{ (FID_TRANSACTION_ID) };
1119 $ils->check_inst_id( $inst_id, "handle_fee_paid" );
1121 my $pay_result = $ils->pay_fee( $patron_id, $patron_pwd, $fee_amt, $fee_type, $pay_type, $fee_id, $trans_id, $currency, $is_writeoff, $disallow_overpayment, $register_id );
1122 $status = $pay_result->{status};
1123 my $pay_response = $pay_result->{pay_response};
1125 my $failmap = {
1126 "no_item" => "No matching item could be found",
1127 "no_checkout" => "Item is not checked out",
1128 "too_soon" => "Cannot yet be renewed",
1129 "too_many" => "Renewed the maximum number of times",
1130 "auto_too_soon" => "Scheduled for automatic renewal and cannot yet be renewed",
1131 "auto_too_late" => "Scheduled for automatic renewal and cannot yet be any more",
1132 "auto_account_expired" => "Scheduled for automatic renewal and cannot be renewed because the patron's account has expired",
1133 "auto_renew" => "Scheduled for automatic renewal",
1134 "auto_too_much_oweing" => "Scheduled for automatic renewal",
1135 "on_reserve" => "On hold for another patron",
1136 "patron_restricted" => "Patron is currently restricted",
1137 "item_denied_renewal" => "Item is not allowed renewal",
1138 "onsite_checkout" => "Item is an onsite checkout"
1140 my @success = ();
1141 my @fail = ();
1142 foreach my $result( @{$pay_response->{renew_result}} ) {
1143 my $item = Koha::Items->find({ itemnumber => $result->{itemnumber} });
1144 if ($result->{success}) {
1145 push @success, '"' . $item->biblio->title . '"';
1146 } else {
1147 push @fail, '"' . $item->biblio->title . '" : ' . $failmap->{$result->{error}};
1151 my $msg = "";
1152 if (scalar @success > 0) {
1153 $msg.="The following items were renewed: " . join(", ", @success) . ". ";
1155 if (scalar @fail > 0) {
1156 $msg.="The following items were not renewed: " . join(", ", @fail) . ".";
1158 if (length $msg > 0) {
1159 $status->screen_msg($status->screen_msg . " $msg");
1162 $resp .= ( $status->ok ? 'Y' : 'N' ) . timestamp;
1163 $resp .= add_field( FID_INST_ID, $inst_id, $server );
1164 $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
1165 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id, $server );
1166 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1167 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1169 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1171 return (FEE_PAID);
1174 sub handle_item_information {
1175 my ( $self, $server ) = @_;
1176 my $ils = $server->{ils};
1177 my $trans_date;
1178 my $fields = $self->{fields};
1179 my $resp = ITEM_INFO_RESP;
1180 my $item;
1181 my $i;
1183 ($trans_date) = @{ $self->{fixed_fields} };
1185 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_item_information" );
1187 $item = $ils->find_item( $fields->{ (FID_ITEM_ID) } );
1189 if ( !defined($item) ) {
1191 # Invalid Item ID
1192 # "Other" circ stat, "Other" security marker, "Unknown" fee type
1193 $resp .= "010101";
1194 $resp .= timestamp;
1196 # Just echo back the invalid item id
1197 $resp .= add_field( FID_ITEM_ID, $fields->{ (FID_ITEM_ID) }, $server );
1199 # title id is required, but we don't have one
1200 $resp .= add_field( FID_TITLE_ID, '', $server );
1201 } else {
1203 # Valid Item ID, send the good stuff
1204 $resp .= $item->sip_circulation_status;
1205 $resp .= $item->sip_security_marker;
1206 $resp .= $item->sip_fee_type;
1207 $resp .= timestamp;
1209 $resp .= add_field( FID_ITEM_ID, $item->id, $server );
1210 $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
1212 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type, $server );
1213 $resp .= maybe_add( FID_PERM_LOCN, $item->permanent_location, $server );
1214 $resp .= maybe_add( FID_CURRENT_LOCN, $item->current_location, $server );
1215 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
1217 if ( ( $i = $item->fee ) != 0 ) {
1218 $resp .= add_field( FID_CURRENCY, $item->fee_currency, $server );
1219 $resp .= add_field( FID_FEE_AMT, $i, $server );
1221 $resp .= maybe_add( FID_OWNER, $item->owner, $server );
1223 if ( ( $i = scalar @{ $item->hold_queue } ) > 0 ) {
1224 $resp .= add_field( FID_HOLD_QUEUE_LEN, $i, $server );
1226 if ( $item->due_date ) {
1227 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ), $server );
1229 if ( ( $i = $item->recall_date ) != 0 ) {
1230 $resp .= add_field( FID_RECALL_DATE, timestamp($i), $server );
1232 if ( ( $i = $item->hold_pickup_date ) != 0 ) {
1233 $resp .= add_field( FID_HOLD_PICKUP_DATE, timestamp($i), $server );
1236 $resp .= maybe_add( FID_SCREEN_MSG, $item->screen_msg, $server );
1237 $resp .= maybe_add( FID_PRINT_LINE, $item->print_line, $server );
1239 $resp .= $item->build_additional_item_fields_string( $server );
1242 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1244 return (ITEM_INFORMATION);
1247 sub handle_item_status_update {
1248 my ( $self, $server ) = @_;
1249 my $ils = $server->{ils};
1250 my ( $trans_date, $item_id, $terminal_pwd, $item_props );
1251 my $fields = $self->{fields};
1252 my $status;
1253 my $item;
1254 my $resp = ITEM_STATUS_UPDATE_RESP;
1256 ($trans_date) = @{ $self->{fixed_fields} };
1258 $ils->check_inst_id( $fields->{ (FID_INST_ID) } );
1260 $item_id = $fields->{ (FID_ITEM_ID) };
1261 $item_props = $fields->{ (FID_ITEM_PROPS) };
1263 if ( !defined($item_id) ) {
1264 siplog( "LOG_WARNING", "handle_item_status: received message without Item ID field" );
1265 } else {
1266 $item = $ils->find_item($item_id);
1269 if ( !$item ) {
1271 # Invalid Item ID
1272 $resp .= '0';
1273 $resp .= timestamp;
1274 $resp .= add_field( FID_ITEM_ID, $item_id, $server );
1275 } else {
1277 # Valid Item ID
1279 $status = $item->status_update($item_props);
1281 $resp .= $status->ok ? '1' : '0';
1282 $resp .= timestamp;
1284 $resp .= add_field( FID_ITEM_ID, $item->id, $server );
1285 $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
1286 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
1289 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1290 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1292 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1294 return (ITEM_STATUS_UPDATE);
1297 sub handle_patron_enable {
1298 my ( $self, $server ) = @_;
1299 my $ils = $server->{ils};
1300 my $fields = $self->{fields};
1301 my ( $trans_date, $patron_id, $terminal_pwd, $patron_pwd );
1302 my ( $status, $patron );
1303 my $resp = PATRON_ENABLE_RESP;
1305 ($trans_date) = @{ $self->{fixed_fields} };
1306 $patron_id = $fields->{ (FID_PATRON_ID) };
1307 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1309 siplog( "LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'", $patron_id, $patron_pwd );
1311 $patron = $ils->find_patron($patron_id);
1313 if ( !defined($patron) ) {
1315 # Invalid patron ID
1316 $resp .= 'YYYY' . ( ' ' x 10 ) . '000' . timestamp();
1317 $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
1318 $resp .= add_field( FID_PERSONAL_NAME, '', $server );
1319 $resp .= add_field( FID_VALID_PATRON, 'N', $server );
1320 $resp .= add_field( FID_VALID_PATRON_PWD, 'N', $server );
1321 } else {
1323 # valid patron
1324 if ( !defined($patron_pwd) || $patron->check_password($patron_pwd) ) {
1326 # Don't enable the patron if there was an invalid password
1327 $status = $patron->enable;
1329 $resp .= patron_status_string($patron);
1330 $resp .= $patron->language . timestamp();
1332 $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
1333 $resp .= add_field( FID_PERSONAL_NAME, $patron->format( $server->{account}->{ae_field_template} ), $server );
1334 if ( defined($patron_pwd) ) {
1335 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password($patron_pwd) ), $server );
1337 $resp .= add_field( FID_VALID_PATRON, 'Y', $server );
1338 $resp .= maybe_add( FID_SCREEN_MSG, $patron->screen_msg, $server );
1339 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line, $server );
1342 $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1344 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1346 return (PATRON_ENABLE);
1349 sub handle_hold {
1350 my ( $self, $server ) = @_;
1351 my $ils = $server->{ils};
1352 my ( $hold_mode, $trans_date );
1353 my ( $expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd );
1354 my ( $item_id, $title_id, $fee_ack );
1355 my $fields = $self->{fields};
1356 my $status;
1357 my $resp = HOLD_RESP;
1359 ( $hold_mode, $trans_date ) = @{ $self->{fixed_fields} };
1361 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_hold" );
1363 $patron_id = $fields->{ (FID_PATRON_ID) };
1364 $expiry_date = $fields->{ (FID_EXPIRATION) } || '';
1365 $pickup_locn = $fields->{ (FID_PICKUP_LOCN) } || '';
1366 $hold_type = $fields->{ (FID_HOLD_TYPE) } || '2'; # Any copy of title
1367 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1368 $item_id = $fields->{ (FID_ITEM_ID) } || '';
1369 $title_id = $fields->{ (FID_TITLE_ID) } || '';
1370 $fee_ack = $fields->{ (FID_FEE_ACK) } || 'N';
1372 if ( $hold_mode eq '+' ) {
1373 $status = $ils->add_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1374 } elsif ( $hold_mode eq '-' ) {
1375 $status = $ils->cancel_hold( $patron_id, $patron_pwd, $item_id, $title_id );
1376 } elsif ( $hold_mode eq '*' ) {
1377 $status = $ils->alter_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1378 } else {
1379 siplog( "LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'", $hold_mode, $server->{account}->{id} );
1380 $status = $ils->Transaction::Hold; # new?
1381 $status->screen_msg("System error. Please contact library staff.");
1384 $resp .= $status->ok;
1385 $resp .= sipbool( $status->item && $status->item->available($patron_id) );
1386 $resp .= timestamp;
1388 if ( $status->ok ) {
1389 $resp .= add_field( FID_PATRON_ID, $status->patron->id, $server );
1391 ( $status->expiration_date )
1392 and $resp .= maybe_add( FID_EXPIRATION, timestamp( $status->expiration_date ), $server );
1393 $resp .= maybe_add( FID_QUEUE_POS, $status->queue_position, $server );
1394 $resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location, $server );
1395 $resp .= maybe_add( FID_ITEM_ID, $status->item->id, $server );
1396 $resp .= maybe_add( FID_TITLE_ID, $status->item->title_id, $server );
1397 } else {
1399 # Not ok. still need required fields
1400 $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
1403 $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1404 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1405 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1407 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1409 return (HOLD);
1412 sub handle_renew {
1413 my ( $self, $server ) = @_;
1414 my $ils = $server->{ils};
1415 my ( $third_party, $no_block, $trans_date, $nb_due_date );
1416 my ( $patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack );
1417 my $fields = $self->{fields};
1418 my $status;
1419 my ( $patron, $item );
1420 my $resp = RENEW_RESP;
1422 ( $third_party, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
1424 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew" );
1426 if ( $no_block eq 'Y' ) {
1427 siplog( "LOG_WARNING", "handle_renew: received 'no block' renewal from terminal '%s'", $server->{account}->{id} );
1430 $patron_id = $fields->{ (FID_PATRON_ID) };
1431 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1432 $item_id = $fields->{ (FID_ITEM_ID) };
1433 $title_id = $fields->{ (FID_TITLE_ID) };
1434 $item_props = $fields->{ (FID_ITEM_PROPS) };
1435 $fee_ack = $fields->{ (FID_FEE_ACK) };
1437 $status = $ils->renew( $patron_id, $patron_pwd, $item_id, $title_id, $no_block, $nb_due_date, $third_party, $item_props, $fee_ack );
1439 $patron = $status->patron;
1440 $item = $status->item;
1442 if ( $status->renewal_ok ) {
1443 $resp .= '1';
1444 $resp .= $status->renewal_ok ? 'Y' : 'N';
1445 if ( $ils->supports('magnetic media') ) {
1446 $resp .= sipbool( $item->magnetic_media );
1447 } else {
1448 $resp .= 'U';
1450 $resp .= sipbool( $status->desensitize );
1451 $resp .= timestamp;
1452 $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
1453 $resp .= add_field( FID_ITEM_ID, $item->id, $server );
1454 $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
1455 if ( $item->due_date ) {
1456 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ), $server );
1457 } else {
1458 $resp .= add_field( FID_DUE_DATE, q{}, $server );
1460 if ( $ils->supports('security inhibit') ) {
1461 $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit, $server );
1463 $resp .= add_field( FID_MEDIA_TYPE, $item->sip_media_type, $server );
1464 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
1465 } else {
1467 # renew failed for some reason
1468 # not OK, renewal not OK, Unknown media type (why bother checking?)
1469 $resp .= '0NUN';
1470 $resp .= timestamp;
1472 # If we found the patron or the item, the return the ILS
1473 # information, otherwise echo back the information we received
1474 # from the terminal
1475 $resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id, $server );
1476 $resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id, $server );
1477 $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : $title_id, $server );
1478 $resp .= add_field( FID_DUE_DATE, '', $server );
1481 if ( $status->fee_amount ) {
1482 $resp .= add_field( FID_FEE_AMT, $status->fee_amount, $server );
1483 $resp .= maybe_add( FID_CURRENCY, $status->sip_currency, $server );
1484 $resp .= maybe_add( FID_FEE_TYPE, $status->sip_fee_type, $server );
1485 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id, $server );
1488 $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1489 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1490 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1492 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1494 return (RENEW);
1497 sub handle_renew_all {
1499 # my ($third_party, $no_block, $nb_due_date, $fee_ack, $patron);
1501 my ( $self, $server ) = @_;
1502 my $ils = $server->{ils};
1503 my ( $trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack );
1504 my $fields = $self->{fields};
1505 my $resp = RENEW_ALL_RESP;
1506 my $status;
1507 my ( @renewed, @unrenewed );
1509 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew_all" );
1511 ($trans_date) = @{ $self->{fixed_fields} };
1513 $patron_id = $fields->{ (FID_PATRON_ID) };
1514 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1515 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
1516 $fee_ack = $fields->{ (FID_FEE_ACK) };
1518 $status = $ils->renew_all( $patron_id, $patron_pwd, $fee_ack );
1520 $resp .= $status->ok ? '1' : '0';
1522 if ( !$status->ok ) {
1523 $resp .= add_count( "renew_all/renewed_count", 0 );
1524 $resp .= add_count( "renew_all/unrenewed_count", 0 );
1525 @renewed = ();
1526 @unrenewed = ();
1527 } else {
1528 @renewed = ( @{ $status->renewed } );
1529 @unrenewed = ( @{ $status->unrenewed } );
1530 $resp .= add_count( "renew_all/renewed_count", scalar @renewed );
1531 $resp .= add_count( "renew_all/unrenewed_count", scalar @unrenewed );
1534 $resp .= timestamp;
1535 $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1537 $resp .= join( '', map( add_field( FID_RENEWED_ITEMS, $_ ), @renewed ), $server );
1538 $resp .= join( '', map( add_field( FID_UNRENEWED_ITEMS, $_ ), @unrenewed ), $server );
1540 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1541 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1543 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1545 return (RENEW_ALL);
1549 # send_acs_status($self, $server)
1551 # Send an ACS Status message, which is contains lots of little fields
1552 # of information gleaned from all sorts of places.
1555 my @message_type_names = (
1556 "patron status request",
1557 "checkout",
1558 "checkin",
1559 "block patron",
1560 "acs status",
1561 "request sc/acs resend",
1562 "login",
1563 "patron information",
1564 "end patron session",
1565 "fee paid",
1566 "item information",
1567 "item status update",
1568 "patron enable",
1569 "hold",
1570 "renew",
1571 "renew all",
1574 sub send_acs_status {
1575 my ( $self, $server, $screen_msg, $print_line ) = @_;
1576 my $msg = ACS_STATUS;
1577 ($server) or die "send_acs_status error: no \$server argument received";
1578 my $account = $server->{account} or die "send_acs_status error: no 'account' in \$server object:\n" . Dumper($server);
1579 my $policy = $server->{policy} or die "send_acs_status error: no 'policy' in \$server object:\n" . Dumper($server);
1580 my $ils = $server->{ils} or die "send_acs_status error: no 'ils' in \$server object:\n" . Dumper($server);
1581 my ( $online_status, $checkin_ok, $checkout_ok, $ACS_renewal_policy );
1582 my ( $status_update_ok, $offline_ok, $timeout, $retries );
1584 $online_status = 'Y';
1585 $checkout_ok = sipbool( $ils->checkout_ok );
1586 $checkin_ok = sipbool( $ils->checkin_ok );
1587 $ACS_renewal_policy = sipbool( $policy->{renewal} );
1588 $status_update_ok = sipbool( $ils->status_update_ok );
1589 $offline_ok = sipbool( $ils->offline_ok );
1590 $timeout = $server->get_timeout({ policy => 1 });
1591 $retries = sprintf( "%03d", $policy->{retries} );
1593 if ( length($retries) != 3 ) {
1594 siplog( "LOG_ERR", "handle_acs_status: retries field wrong size: '%s'", $retries );
1595 $retries = '000';
1598 $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
1599 $msg .= "$status_update_ok$offline_ok$timeout$retries";
1600 $msg .= timestamp();
1602 if ( $protocol_version == 1 ) {
1603 $msg .= '1.00';
1604 } elsif ( $protocol_version == 2 ) {
1605 $msg .= '2.00';
1606 } else {
1607 siplog( "LOG_ERR", 'Bad setting for $protocol_version, "%s" in send_acs_status', $protocol_version );
1608 $msg .= '1.00';
1611 # Institution ID
1612 $msg .= add_field( FID_INST_ID, $account->{institution}, $server );
1614 if ( $protocol_version >= 2 ) {
1616 # Supported messages: we do it all
1617 my $supported_msgs = '';
1619 foreach my $msg_name (@message_type_names) {
1620 if ( $msg_name eq 'request sc/acs resend' ) {
1621 $supported_msgs .= sipbool(1);
1622 } else {
1623 $supported_msgs .= sipbool( $ils->supports($msg_name) );
1626 if ( length($supported_msgs) < 16 ) {
1627 siplog( "LOG_ERR", 'send_acs_status: supported messages "%s" too short', $supported_msgs );
1629 $msg .= add_field( FID_SUPPORTED_MSGS, $supported_msgs, $server );
1632 $msg .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1634 if ( defined( $account->{print_width} )
1635 && defined($print_line)
1636 && $account->{print_width} < length($print_line) ) {
1637 siplog( "LOG_WARNING", "send_acs_status: print line '%s' too long. Truncating", $print_line );
1638 $print_line = substr( $print_line, 0, $account->{print_width} );
1641 $msg .= maybe_add( FID_PRINT_LINE, $print_line, $server );
1643 # Do we want to tell the terminal its location?
1645 $self->write_msg( $msg, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1646 return 1;
1650 # build_patron_status: create the 14-char patron status
1651 # string for the Patron Status message
1653 sub patron_status_string {
1654 my $patron = shift;
1655 my $patron_status;
1657 siplog( "LOG_DEBUG", "patron_status_string: %s charge_ok: %s", $patron->id, $patron->charge_ok );
1658 $patron_status = sprintf(
1659 '%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
1660 denied( $patron->charge_ok ),
1661 denied( $patron->renew_ok ),
1662 denied( $patron->recall_ok ),
1663 denied( $patron->hold_ok ),
1664 boolspace( $patron->card_lost ),
1665 boolspace( $patron->too_many_charged ),
1666 boolspace( $patron->too_many_overdue ),
1667 boolspace( $patron->too_many_renewal ),
1668 boolspace( $patron->too_many_claim_return ),
1669 boolspace( $patron->too_many_lost ),
1670 boolspace( $patron->excessive_fines ),
1671 boolspace( $patron->excessive_fees ),
1672 boolspace( $patron->recall_overdue ),
1673 boolspace( $patron->too_many_billed )
1675 return $patron_status;
1678 sub api_auth {
1679 my ( $username, $password, $branch ) = @_;
1680 $ENV{REMOTE_USER} = $username;
1681 my $query = CGI->new();
1682 $query->param( userid => $username );
1683 $query->param( password => $password );
1684 if ($branch) {
1685 $query->param( branch => $branch );
1687 my ( $status, $cookie, $sessionID ) = check_api_auth( $query, { circulate => 1 }, 'intranet' );
1688 return $status;
1692 __END__