Bug 31552: SIP2 option format_due_date not honored for AH field in item information...
[koha.git] / C4 / SIP / Sip / MsgType.pm
1 #
2 # Sip::MsgType.pm
3 #
4 # A Class for handing SIP messages
5 #
6
7 package C4::SIP::Sip::MsgType;
8
9 use strict;
10 use warnings;
11 use Exporter;
12
13 use C4::SIP::Sip qw(:all);
14 use C4::SIP::Sip::Constants qw(:all);
15 use C4::SIP::Sip::Checksum qw(verify_cksum);
16
17 use Data::Dumper;
18 use CGI qw ( -utf8 );
19 use C4::Auth qw(&check_api_auth);
20
21 use Koha::Patrons;
22 use Koha::Patron::Attributes;
23 use Koha::Plugins;
24 use Koha::Items;
25 use Koha::DateUtils qw( output_pref );
26
27 use UNIVERSAL::can;
28
29 use vars qw(@ISA @EXPORT_OK);
30
31 use constant INVALID_CARD => 'Invalid cardnumber';
32 use constant INVALID_PW   => 'Invalid password';
33
34 BEGIN {
35     @ISA       = qw(Exporter);
36     @EXPORT_OK = qw(handle login_core);
37 }
38
39 # Predeclare handler subroutines
40 use subs qw(handle_patron_status handle_checkout handle_checkin
41   handle_block_patron handle_sc_status handle_request_acs_resend
42   handle_login handle_patron_info handle_end_patron_session
43   handle_fee_paid handle_item_information handle_item_status_update
44   handle_patron_enable handle_hold handle_renew handle_renew_all);
45
46 #
47 # For the most part, Version 2.00 of the protocol just adds new
48 # variable fields, but sometimes it changes the fixed header.
49 #
50 # In general, if there's no '2.00' protocol entry for a handler, that's
51 # because 2.00 didn't extend the 1.00 version of the protocol.  This will
52 # be handled by the module initialization code following the declaration,
53 # which goes through the handlers table and creates a '2.00' entry that
54 # points to the same place as the '1.00' entry.  If there's a 2.00 entry
55 # but no 1.00 entry, then that means that it's a completely new service
56 # in 2.00, so 1.00 shouldn't recognize it.
57
58 my %handlers = (
59     (PATRON_STATUS_REQ) => {
60         name     => "Patron Status Request",
61         handler  => \&handle_patron_status,
62         protocol => {
63             1 => {
64                 template     => "A3A18",
65                 template_len => 21,
66                 fields       => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
67             }
68         }
69     },
70     (CHECKOUT) => {
71         name     => "Checkout",
72         handler  => \&handle_checkout,
73         protocol => {
74             1 => {
75                 template     => "A1A1A18A18",
76                 template_len => 38,
77                 fields       => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
78             },
79             2 => {
80                 template     => "A1A1A18A18",
81                 template_len => 38,
82                 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             },
84         }
85     },
86     (CHECKIN) => {
87         name     => "Checkin",
88         handler  => \&handle_checkin,
89         protocol => {
90             1 => {
91                 template     => "A1A18A18",
92                 template_len => 37,
93                 fields       => [ (FID_CURRENT_LOCN), (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
94             },
95             2 => {
96                 template     => "A1A18A18",
97                 template_len => 37,
98                 fields       => [ (FID_CURRENT_LOCN), (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS), (FID_CANCEL) ],
99             }
100         }
101     },
102     (BLOCK_PATRON) => {
103         name     => "Block Patron",
104         handler  => \&handle_block_patron,
105         protocol => {
106             1 => {
107                 template     => "A1A18",
108                 template_len => 19,
109                 fields       => [ (FID_INST_ID), (FID_BLOCKED_CARD_MSG), (FID_PATRON_ID), (FID_TERMINAL_PWD) ],
110             },
111         }
112     },
113     (SC_STATUS) => {
114         name     => "SC Status",
115         handler  => \&handle_sc_status,
116         protocol => {
117             1 => {
118                 template     => "A1A3A4",
119                 template_len => 8,
120                 fields       => [],
121             }
122         }
123     },
124     (REQUEST_ACS_RESEND) => {
125         name     => "Request ACS Resend",
126         handler  => \&handle_request_acs_resend,
127         protocol => {
128             1 => {
129                 template     => q{},
130                 template_len => 0,
131                 fields       => [],
132             }
133         }
134     },
135     (LOGIN) => {
136         name     => "Login",
137         handler  => \&handle_login,
138         protocol => {
139             2 => {
140                 template     => "A1A1",
141                 template_len => 2,
142                 fields       => [ (FID_LOGIN_UID), (FID_LOGIN_PWD), (FID_LOCATION_CODE) ],
143             }
144         }
145     },
146     (PATRON_INFO) => {
147         name     => "Patron Info",
148         handler  => \&handle_patron_info,
149         protocol => {
150             2 => {
151                 template     => "A3A18A10",
152                 template_len => 31,
153                 fields       => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD), (FID_START_ITEM), (FID_END_ITEM) ],
154             }
155         }
156     },
157     (END_PATRON_SESSION) => {
158         name     => "End Patron Session",
159         handler  => \&handle_end_patron_session,
160         protocol => {
161             2 => {
162                 template     => "A18",
163                 template_len => 18,
164                 fields       => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
165             }
166         }
167     },
168     (FEE_PAID) => {
169         name     => "Fee Paid",
170         handler  => \&handle_fee_paid,
171         protocol => {
172             2 => {
173                 template     => "A18A2A2A3",
174                 template_len => 25,
175                 fields       => [ (FID_FEE_AMT), (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD), (FID_FEE_ID), (FID_TRANSACTION_ID) ],
176             }
177         }
178     },
179     (ITEM_INFORMATION) => {
180         name     => "Item Information",
181         handler  => \&handle_item_information,
182         protocol => {
183             2 => {
184                 template     => "A18",
185                 template_len => 18,
186                 fields       => [ (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
187             }
188         }
189     },
190     (ITEM_STATUS_UPDATE) => {
191         name     => "Item Status Update",
192         handler  => \&handle_item_status_update,
193         protocol => {
194             2 => {
195                 template     => "A18",
196                 template_len => 18,
197                 fields       => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS) ],
198             }
199         }
200     },
201     (PATRON_ENABLE) => {
202         name     => "Patron Enable",
203         handler  => \&handle_patron_enable,
204         protocol => {
205             2 => {
206                 template     => "A18",
207                 template_len => 18,
208                 fields       => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
209             }
210         }
211     },
212     (HOLD) => {
213         name     => "Hold",
214         handler  => \&handle_hold,
215         protocol => {
216             2 => {
217                 template     => "AA18",
218                 template_len => 19,
219                 fields       => [
220                     (FID_EXPIRATION), (FID_PICKUP_LOCN), (FID_HOLD_TYPE), (FID_INST_ID), (FID_PATRON_ID), (FID_PATRON_PWD),
221                     (FID_ITEM_ID), (FID_TITLE_ID), (FID_TERMINAL_PWD), (FID_FEE_ACK)
222                 ],
223             }
224         }
225     },
226     (RENEW) => {
227         name     => "Renew",
228         handler  => \&handle_renew,
229         protocol => {
230             2 => {
231                 template     => "A1A1A18A18",
232                 template_len => 38,
233                 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             }
235         }
236     },
237     (RENEW_ALL) => {
238         name     => "Renew All",
239         handler  => \&handle_renew_all,
240         protocol => {
241             2 => {
242                 template     => "A18",
243                 template_len => 18,
244                 fields       => [ (FID_INST_ID), (FID_PATRON_ID), (FID_PATRON_PWD), (FID_TERMINAL_PWD), (FID_FEE_ACK) ],
245             }
246         }
247     }
248 );
249
250 #
251 # Now, initialize some of the missing bits of %handlers
252 #
253 foreach my $i ( keys(%handlers) ) {
254     if ( !exists( $handlers{$i}->{protocol}->{2} ) ) {
255         $handlers{$i}->{protocol}->{2} = $handlers{$i}->{protocol}->{1};
256     }
257 }
258
259 sub new {
260     my ( $class, $msg, $seqno ) = @_;
261     my $self = {};
262     my $msgtag = substr( $msg, 0, 2 );
263
264     if ( $msgtag eq LOGIN ) {
265
266         # If the client is using the 2.00-style "Login" message
267         # to authenticate to the server, then we get the Login message
268         # _before_ the client has indicated that it supports 2.00, but
269         # it's using the 2.00 login process, so it must support 2.00.
270         $protocol_version = 2;
271     }
272     siplog( "LOG_DEBUG", "Sip::MsgType::new('%s', '%s...', '%s'): seq.no '%s', protocol %s", $class, substr( $msg, 0, 10 ), $msgtag, $seqno, $protocol_version );
273
274     # warn "SIP PROTOCOL: $protocol_version";
275     if ( !exists( $handlers{$msgtag} ) ) {
276         siplog( "LOG_WARNING", "new Sip::MsgType: Skipping message of unknown type '%s' in '%s'", $msgtag, $msg );
277         return;
278     } elsif ( !exists( $handlers{$msgtag}->{protocol}->{$protocol_version} ) ) {
279         siplog( "LOG_WARNING", "new Sip::MsgType: Skipping message '%s' unsupported by protocol rev. '%d'", $msgtag, $protocol_version );
280         return;
281     }
282
283     bless $self, $class;
284
285     $self->{seqno} = $seqno;
286     $self->_initialize( substr( $msg, 2 ), $handlers{$msgtag} );
287
288     return ($self);
289 }
290
291 sub _initialize {
292     my ( $self, $msg, $control_block ) = @_;
293     my $fn;
294     my $proto = $control_block->{protocol}->{$protocol_version};
295
296     $self->{name}    = $control_block->{name};
297     $self->{handler} = $control_block->{handler};
298
299     $self->{fields}       = {};
300     $self->{fixed_fields} = [];
301
302     chomp($msg);    # These four are probably unnecessary now.
303     $msg =~ tr/\cM//d;
304     $msg =~ s/\^M$//;
305     chomp($msg);
306
307     foreach my $field ( @{ $proto->{fields} } ) {
308         $self->{fields}->{$field} = undef;
309     }
310
311     siplog( "LOG_DEBUG", "Sip::MsgType::_initialize('%s', '%s', '%s', '%s', ...)", $self->{name}, $msg, $proto->{template}, $proto->{template_len} );
312
313     $self->{fixed_fields} = [ unpack( $proto->{template}, $msg ) ];    # see http://perldoc.perl.org/5.8.8/functions/unpack.html
314
315     # Skip over the fixed fields and the split the rest of
316     # the message into fields based on the delimiter and parse them
317     foreach my $field ( split( quotemeta($field_delimiter), substr( $msg, $proto->{template_len} ) ) ) {
318         $fn = substr( $field, 0, 2 );
319
320         if ( !exists( $self->{fields}->{$fn} ) ) {
321             siplog( "LOG_WARNING", "Unsupported field '%s' in %s message '%s'", $fn, $self->{name}, $msg );
322         } elsif ( defined( $self->{fields}->{$fn} ) ) {
323             siplog( "LOG_WARNING", "Duplicate field '%s' (previous value '%s') in %s message '%s'", $fn, $self->{fields}->{$fn}, $self->{name}, $msg );
324         } else {
325             $self->{fields}->{$fn} = substr( $field, 2 );
326         }
327     }
328
329     return ($self);
330 }
331
332 sub handle {
333     my ( $msg, $server, $req ) = @_;
334     my $config = $server->{config};
335     my $self;
336
337     # Set system preference overrides, first global, then account level
338     # Clear overrides from previous message handling first
339     foreach my $key ( keys %ENV ) {
340         delete $ENV{$key} if index($key, 'OVERRIDE_SYSPREF_') > 0;
341     }
342     foreach my $key ( keys %{ $config->{'syspref_overrides'} } ) {
343         $ENV{"OVERRIDE_SYSPREF_$key"} = $config->{'syspref_overrides'}->{$key};
344     }
345     foreach my $key ( keys %{ $server->{account}->{'syspref_overrides'} } ) {
346         $ENV{"OVERRIDE_SYSPREF_$key"} =
347           $server->{account}->{'syspref_overrides'}->{$key};
348     }
349
350     #
351     # What's the field delimiter for variable length fields?
352     # This can't be based on the account, since we need to know
353     # the field delimiter to parse a SIP login message
354     #
355     if ( defined( $server->{config}->{delimiter} ) ) {
356         $field_delimiter = $server->{config}->{delimiter};
357     }
358
359     # error detection is active if this is a REQUEST_ACS_RESEND
360     # message with a checksum, or if the message is long enough
361     # and the last nine characters begin with a sequence number
362     # field
363     if ( $msg eq REQUEST_ACS_RESEND_CKSUM ) {
364
365         # Special case
366         $error_detection = 1;
367         $self = C4::SIP::Sip::MsgType->new( (REQUEST_ACS_RESEND), 0 );
368     } elsif ( ( length($msg) > 11 ) && ( substr( $msg, -9, 2 ) eq "AY" ) ) {
369         $error_detection = 1;
370
371         if ( !verify_cksum($msg) ) {
372             siplog( "LOG_WARNING", "Checksum failed on message '%s'", $msg );
373
374             # REQUEST_SC_RESEND with error detection
375             $last_response = REQUEST_SC_RESEND_CKSUM;
376             print("$last_response\r");
377             return REQUEST_ACS_RESEND;
378         } else {
379
380             # Save the sequence number, then strip off the
381             # error detection data to process the message
382             $self = C4::SIP::Sip::MsgType->new( substr( $msg, 0, -9 ), substr( $msg, -7, 1 ) );
383         }
384     } elsif ($error_detection) {
385
386         # We received a non-ED message when ED is supposed to be active.
387         # Warn about this problem, then process the message anyway.
388         siplog( "LOG_WARNING", "Received message without error detection: '%s'", $msg );
389         $error_detection = 0;
390         $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
391     } else {
392         $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
393     }
394
395     if (   ( substr( $msg, 0, 2 ) ne REQUEST_ACS_RESEND )
396         && $req
397         && ( substr( $msg, 0, 2 ) ne $req ) ) {
398         return substr( $msg, 0, 2 );
399     }
400     unless ( $self->{handler} ) {
401         siplog( "LOG_WARNING", "No handler defined for '%s'", $msg );
402         $last_response = REQUEST_SC_RESEND;
403         print("$last_response\r");
404         return REQUEST_ACS_RESEND;
405     }
406     return ( $self->{handler}->( $self, $server ) );    # FIXME
407                                                         # FIXME: Use of uninitialized value in subroutine entry
408                                                         # Can't use string ("") as a subroutine ref while "strict refs" in use
409 }
410
411 ##
412 ## Message Handlers
413 ##
414
415 #
416 # Patron status messages are produced in response to both
417 # "Request Patron Status" and "Block Patron"
418 #
419 # Request Patron Status requires a patron password, but
420 # Block Patron doesn't (since the patron may never have
421 # provided one before attempting some illegal action).
422 #
423 # ASSUMPTION: If the patron password field is present in the
424 # message, then it must match, otherwise incomplete patron status
425 # information will be returned to the terminal.
426 #
427 sub build_patron_status {
428     my ( $patron, $lang, $fields, $server ) = @_;
429
430     my $patron_pwd = $fields->{ (FID_PATRON_PWD) };
431     my $resp = (PATRON_STATUS_RESP);
432     my $password_rc;
433
434     if ( $patron ) {
435         if ($patron_pwd) {
436             $password_rc = $patron->check_password($patron_pwd);
437         }
438
439         $resp .= patron_status_string( $patron, $server );
440         $resp .= $lang . timestamp();
441         if ( defined $server->{account}->{ae_field_template} ) {
442             $resp .= add_field( FID_PERSONAL_NAME, $patron->format( $server->{account}->{ae_field_template}, $server ) );
443         } else {
444             $resp .= add_field( FID_PERSONAL_NAME, $patron->name, $server );
445         }
446
447
448         # while the patron ID we got from the SC is valid, let's
449         # use the one returned from the ILS, just in case...
450         $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
451
452         if ( $protocol_version >= 2 ) {
453             $resp .= add_field( FID_VALID_PATRON, 'Y', $server );
454
455             # Patron password is a required field.
456             $resp .= add_field( FID_VALID_PATRON_PWD, sipbool($password_rc), $server );
457             $resp .= maybe_add( FID_CURRENCY, $patron->currency, $server );
458             $resp .= maybe_add( FID_FEE_AMT,  $patron->fee_amount, $server );
459         }
460
461         my $msg = $patron->screen_msg;
462         $msg .= ' -- '. INVALID_PW if $patron_pwd && !$password_rc;
463         $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
464
465         $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server )
466           if ( $server->{account}->{send_patron_home_library_in_af} );
467         $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line, $server );
468
469         $resp .= $patron->build_custom_field_string( $server );
470         $resp .= $patron->build_patron_attributes_string( $server );
471
472     } else {
473         # Invalid patron (cardnumber)
474         # Report that the user has no privs.
475
476         # no personal name, and is invalid (if we're using 2.00)
477         $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
478         $resp .= add_field( FID_PERSONAL_NAME, '', $server );
479
480         # the patron ID is invalid, but it's a required field, so
481         # just echo it back
482         $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) }, $server );
483
484         ( $protocol_version >= 2 )
485           and $resp .= add_field( FID_VALID_PATRON, 'N', $server );
486
487         $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
488     }
489
490     $resp .= add_field( FID_INST_ID, $fields->{ (FID_INST_ID) }, $server );
491     return $resp;
492 }
493
494 sub handle_patron_status {
495     my ( $self, $server ) = @_;
496     my $ils = $server->{ils};
497     my $patron;
498     my $resp    = (PATRON_STATUS_RESP);
499     my $account = $server->{account};
500     my ( $lang, $date ) = @{ $self->{fixed_fields} };
501     my $fields = $self->{fields};
502
503     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_patron_status" );
504     $patron = $ils->find_patron( $fields->{ (FID_PATRON_ID) } );
505     $resp = build_patron_status( $patron, $lang, $fields, $server );
506     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
507     return (PATRON_STATUS_REQ);
508 }
509
510 sub handle_checkout {
511     my ( $self, $server ) = @_;
512     my $account = $server->{account};
513     my $ils     = $server->{ils};
514     my $inst    = $ils->institution;
515     my ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date );
516     my $fields;
517     my ( $patron_id, $item_id, $status );
518     my ( $item, $patron );
519     my $resp;
520
521     ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
522     $fields = $self->{fields};
523
524     $patron_id = $fields->{ (FID_PATRON_ID) };
525     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
526     $item_id   = $fields->{ (FID_ITEM_ID) };
527     my $fee_ack = $fields->{ (FID_FEE_ACK) };
528
529     if ( $no_block eq 'Y' ) {
530
531         # Off-line transactions need to be recorded, but there's
532         # not a lot we can do about it
533         siplog( "LOG_WARNING", "received no-block checkout from terminal '%s'", $account->{id} );
534
535         $status = $ils->checkout( $patron_id, $item_id, $sc_renewal_policy, $fee_ack, $account, $nb_due_date );
536     } else {
537
538         # Does the transaction date really matter for items that are
539         # checkout out while the terminal is online?  I'm guessing 'no'
540         $status = $ils->checkout( $patron_id, $item_id, $sc_renewal_policy, $fee_ack, $account );
541     }
542
543     $item   = $status->item;
544     $patron = $status->patron;
545
546     if ( $status->ok ) {
547
548         # Item successfully checked out
549         # Fixed fields
550         $resp = CHECKOUT_RESP . '1';
551         $resp .= sipbool( $status->renew_ok );
552         if ( $ils->supports('magnetic media') ) {
553             $resp .= sipbool( $item->magnetic_media );
554         } else {
555             $resp .= 'U';
556         }
557
558         # We never return the obsolete 'U' value for 'desensitize'
559         $resp .= sipbool(
560             desensitize(
561                 {
562                     item   => $item,
563                     patron => $patron,
564                     server => $server,
565                     status => $status,
566                 }
567             )
568         );
569         $resp .= timestamp;
570
571         # Now for the variable fields
572         $resp .= add_field( FID_INST_ID,   $inst, $server );
573         $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
574         $resp .= add_field( FID_ITEM_ID,   $item_id, $server );
575         $resp .= add_field( FID_TITLE_ID,  $item->title_id, $server );
576         if ( $item->due_date ) {
577             my $due_date =
578               $account->{format_due_date}
579               ? output_pref( { str => $item->due_date, as_due_date => 1 } )
580               : timestamp( $item->due_date );
581             $resp .= add_field( FID_DUE_DATE, $due_date, $server );
582         } else {
583             $resp .= add_field( FID_DUE_DATE, q{}, $server );
584         }
585
586         $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
587         $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
588
589         if ( $protocol_version >= 2 ) {
590             if ( $ils->supports('security inhibit') ) {
591                 $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit, $server );
592             }
593             $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type, $server );
594             $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
595
596         }
597     }
598
599     else {
600
601         # Checkout failed
602         # Checkout Response: not ok, no renewal, don't know mag. media,
603         # no desensitize
604         $resp = sprintf( "120NUN%s", timestamp );
605         $resp .= add_field( FID_INST_ID,   $inst, $server );
606         $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
607         $resp .= add_field( FID_ITEM_ID,   $item_id, $server );
608
609         # If the item is valid, provide the title, otherwise
610         # leave it blank
611         $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : '', $server );
612
613         # Due date is required.  Since it didn't get checked out,
614         # it's not due, so leave the date blank
615         $resp .= add_field( FID_DUE_DATE, '', $server );
616
617         $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
618         $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
619
620         if ( $protocol_version >= 2 ) {
621
622             # Is the patron ID valid?
623             $resp .= add_field( FID_VALID_PATRON, sipbool($patron), $server );
624
625             if ( $patron && exists( $fields->{FID_PATRON_PWD} ) ) {
626
627                 # Password provided, so we can tell if it was valid or not
628                 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password( $fields->{ (FID_PATRON_PWD) } ) ), $server );
629             }
630         }
631     }
632
633     $resp .= $item->build_additional_item_fields_string( $server ) if $item;
634
635     if ( $protocol_version >= 2 ) {
636
637         # Financials : return irrespective of ok status
638         if ( $status->fee_amount ) {
639             $resp .= add_field( FID_FEE_AMT, $status->fee_amount, $server );
640             $resp .= maybe_add( FID_CURRENCY,       $status->sip_currency, $server );
641             $resp .= maybe_add( FID_FEE_TYPE,       $status->sip_fee_type, $server );
642             $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id, $server );
643         }
644     }
645
646     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
647     return (CHECKOUT);
648 }
649
650 sub handle_checkin {
651     my ( $self, $server ) = @_;
652     my $account   = $server->{account};
653     my $ils       = $server->{ils};
654     my $my_branch = $ils->institution;
655     my ( $current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel );
656     my ( $patron, $item, $status );
657     my $resp = CHECKIN_RESP;
658     my ( $no_block, $trans_date, $return_date ) = @{ $self->{fixed_fields} };
659     my $fields = $self->{fields};
660
661     $current_loc = $fields->{ (FID_CURRENT_LOCN) };
662     $inst_id     = $fields->{ (FID_INST_ID) };
663     $item_id     = $fields->{ (FID_ITEM_ID) };
664     $item_props  = $fields->{ (FID_ITEM_PROPS) };
665     $cancel      = $fields->{ (FID_CANCEL) };
666     if ($current_loc) {
667         $my_branch = $current_loc;    # most scm do not set $current_loc
668     }
669
670     $ils->check_inst_id( $inst_id, "handle_checkin" );
671
672     if ( $no_block eq 'Y' ) {
673
674         # Off-line transactions, ick.
675         siplog( "LOG_WARNING", "received no-block checkin from terminal '%s'", $account->{id} );
676         $status = $ils->checkin_no_block( $item_id, $trans_date, $return_date, $item_props, $cancel );
677     } else {
678         $status = $ils->checkin( $item_id, $trans_date, $return_date, $my_branch, $item_props, $cancel, $account );
679     }
680
681     $patron = $status->patron;
682     $item   = $status->item;
683
684     $resp .= $status->ok          ? '1' : '0';
685     $resp .= $status->resensitize ? 'Y' : 'N';
686     if ( $item && $ils->supports('magnetic media') ) {
687         $resp .= sipbool( $item->magnetic_media );
688     } else {
689
690         # item barcode is invalid or system doesn't support 'magnetic media' indicator
691         $resp .= 'U';
692     }
693
694     $resp .= $status->alert ? 'Y' : 'N';
695     $resp .= timestamp;
696     $resp .= add_field( FID_INST_ID, $inst_id, $server );
697     $resp .= add_field( FID_ITEM_ID, $item_id, $server );
698
699     if ($item) {
700         $resp .= add_field( FID_PERM_LOCN, $item->permanent_location, $server );
701         $resp .= maybe_add( FID_TITLE_ID, $item->title_id, $server );
702         $resp .= $item->build_additional_item_fields_string( $server );
703     }
704
705     if ( $protocol_version >= 2 ) {
706         $resp .= maybe_add( FID_SORT_BIN, $status->sort_bin, $server );
707         if ($patron) {
708             $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
709         }
710         if ($item) {
711             $resp .= maybe_add( FID_MEDIA_TYPE,           $item->sip_media_type,      $server );
712             $resp .= maybe_add( FID_ITEM_PROPS,           $item->sip_item_properties, $server );
713             $resp .= maybe_add( FID_CALL_NUMBER,          $item->call_number,         $server );
714             $resp .= maybe_add( FID_HOLD_PATRON_ID,       $item->hold_patron_bcode,   $server );
715             $resp .= add_field( FID_DESTINATION_LOCATION, $item->destination_loc,     $server ) if ( $item->destination_loc || $server->{account}->{ct_always_send} );
716             $resp .= maybe_add( FID_HOLD_PATRON_NAME,     $item->hold_patron_name( $server->{account}->{da_field_template} ), $server );
717
718             if ( my $CR = $server->{account}->{cr_item_field} ) {
719                 $resp .= maybe_add( FID_COLLECTION_CODE, $item->{$CR}, $server );
720             } else {
721                 $resp .= maybe_add( FID_COLLECTION_CODE, $item->collection_code, $server );
722             }
723
724             if ( $status->hold and $status->hold->{branchcode} ne $item->destination_loc ) {
725                 warn 'SIP hold mismatch: $status->hold->{branchcode}=' . $status->hold->{branchcode} . '; $item->destination_loc=' . $item->destination_loc;
726
727                 # just me being paranoid.
728             }
729         }
730     }
731
732     if ( $status->alert && $status->alert_type ) {
733         $resp .= maybe_add( FID_ALERT_TYPE, $status->alert_type, $server );
734     } elsif ( $server->{account}->{cv_send_00_on_success} ) {
735         $resp .= add_field( FID_ALERT_TYPE, '00', $server );
736     }
737     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
738     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
739
740     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
741
742     return (CHECKIN);
743 }
744
745 sub handle_block_patron {
746     my ( $self, $server ) = @_;
747     my $account = $server->{account};
748     my $ils     = $server->{ils};
749     my ( $card_retained, $trans_date );
750     my ( $inst_id, $blocked_card_msg, $patron_id, $terminal_pwd );
751     my ( $fields, $resp, $patron );
752
753     ( $card_retained, $trans_date ) = @{ $self->{fixed_fields} };
754     $fields           = $self->{fields};
755     $inst_id          = $fields->{ (FID_INST_ID) };
756     $blocked_card_msg = $fields->{ (FID_BLOCKED_CARD_MSG) };
757     $patron_id        = $fields->{ (FID_PATRON_ID) };
758     $terminal_pwd     = $fields->{ (FID_TERMINAL_PWD) };
759
760     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
761
762     # Terminal passwords are different from account login
763     # passwords, but I have no idea what to do with them.  So,
764     # I'll just ignore them for now.
765
766     # FIXME ???
767
768     $ils->check_inst_id( $inst_id, "block_patron" );
769     $patron = $ils->find_patron($patron_id);
770
771     # The correct response for a "Block Patron" message is a
772     # "Patron Status Response", so use that handler to generate
773     # the message, but then return the correct code from here.
774     #
775     # Normally, the language is provided by the "Patron Status"
776     # fixed field, but since we're not responding to one of those
777     # we'll just say, "Unspecified", as per the spec.  Let the
778     # terminal default to something that, one hopes, will be
779     # intelligible
780     if ($patron) {
781
782         # Valid patron id
783         $patron->block( $card_retained, $blocked_card_msg );
784     }
785
786     $resp = build_patron_status( $patron, $patron->language, $fields, $server );
787     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
788     return (BLOCK_PATRON);
789 }
790
791 sub handle_sc_status {
792     my ( $self, $server ) = @_;
793     ($server) or warn "handle_sc_status error: no \$server argument received.";
794     my ( $status, $print_width, $sc_protocol_version ) = @{ $self->{fixed_fields} };
795     my ($new_proto);
796
797     if ( $sc_protocol_version =~ /^1\./ ) {
798         $new_proto = 1;
799     } elsif ( $sc_protocol_version =~ /^2\./ ) {
800         $new_proto = 2;
801     } else {
802         siplog( "LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version );
803         $new_proto = 1;
804     }
805
806     if ( $new_proto != $protocol_version ) {
807         siplog( "LOG_INFO", "Setting protocol level to $new_proto" );
808         $protocol_version = $new_proto;
809     }
810
811     if ( $status == SC_STATUS_PAPER ) {
812         siplog( "LOG_WARNING", "Self-Check unit '%s@%s' out of paper", $self->{account}->{id}, $self->{account}->{institution} );
813     } elsif ( $status == SC_STATUS_SHUTDOWN ) {
814         siplog( "LOG_WARNING", "Self-Check unit '%s@%s' shutting down", $self->{account}->{id}, $self->{account}->{institution} );
815     }
816
817     $self->{account}->{print_width} = $print_width;
818     return ( send_acs_status( $self, $server ) ? SC_STATUS : '' );
819 }
820
821 sub handle_request_acs_resend {
822     my ( $self, $server ) = @_;
823
824     if ( !$last_response ) {
825
826         # We haven't sent anything yet, so respond with a
827         # REQUEST_SC_RESEND msg (p. 16)
828         $self->write_msg( REQUEST_SC_RESEND, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
829     } elsif ( ( length($last_response) < 9 )
830         || substr( $last_response, -9, 2 ) ne 'AY' ) {
831
832         # When resending a message, we aren't supposed to include
833         # a sequence number, even if the original had one (p. 4).
834         # If the last message didn't have a sequence number, then
835         # we can just send it.
836         print("$last_response\r");    # not write_msg?
837     } else {
838
839         # Cut out the sequence number and checksum, since the old
840         # checksum is wrong for the resent message.
841         my $rebuilt = substr( $last_response, 0, -9 );
842         $self->write_msg( $rebuilt, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
843     }
844
845     return REQUEST_ACS_RESEND;
846 }
847
848 sub login_core {
849     my $server = shift or return;
850     my $uid    = shift;
851     my $pwd    = shift;
852     my $status = 1;                 # Assume it all works
853     if ( !exists( $server->{config}->{accounts}->{$uid} ) ) {
854         siplog( "LOG_WARNING", "MsgType::login_core: Unknown login '$uid'" );
855         $status = 0;
856     } elsif ( $server->{config}->{accounts}->{$uid}->{password} ne $pwd ) {
857         siplog( "LOG_WARNING", "MsgType::login_core: Invalid password for login '$uid'" );
858         $status = 0;
859     } else {
860
861         # Store the active account someplace handy for everybody else to find.
862         $server->{account} = $server->{config}->{accounts}->{$uid};
863         my $inst = $server->{account}->{institution};
864         $server->{institution}  = $server->{config}->{institutions}->{$inst};
865         $server->{policy}       = $server->{institution}->{policy};
866         $server->{sip_username} = $uid;
867         $server->{sip_password} = $pwd;
868
869         my $auth_status = api_auth( $uid, $pwd, $inst );
870         if ( !$auth_status or $auth_status !~ /^ok$/i ) {
871             siplog( "LOG_WARNING", "api_auth failed for SIP terminal '%s' of '%s': %s", $uid, $inst, ( $auth_status || 'unknown' ) );
872             $status = 0;
873         } else {
874             siplog( "LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst );
875
876             #
877             # initialize connection to ILS
878             #
879             my $module = $server->{config}->{institutions}->{$inst}->{implementation};
880             siplog( "LOG_DEBUG", 'login_core: ' . Dumper($module) );
881
882             # Suspect this is always ILS but so we don't break any eccentic install (for now)
883             if ( $module eq 'ILS' ) {
884                 $module = 'C4::SIP::ILS';
885             }
886             $module->use;
887             if ($@) {
888                 siplog( "LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed", $server->{service}, $module, $inst );
889                 die("Failed to load ILS implementation '$module' for $inst");
890             }
891
892             # like   ILS->new(), I think.
893             $server->{ils} = $module->new( $server->{institution}, $server->{account} );
894             if ( !$server->{ils} ) {
895                 siplog( "LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst );
896                 die("Unable to connect to ILS '$inst'");
897             }
898         }
899     }
900     return $status;
901 }
902
903 sub handle_login {
904     my ( $self, $server ) = @_;
905     my ( $uid_algorithm, $pwd_algorithm );
906     my ( $uid,           $pwd );
907     my $inst;
908     my $fields;
909     my $status = 1;    # Assume it all works
910
911     $fields = $self->{fields};
912     ( $uid_algorithm, $pwd_algorithm ) = @{ $self->{fixed_fields} };
913
914     $uid = $fields->{ (FID_LOGIN_UID) };    # Terminal ID, not patron ID.
915     $pwd = $fields->{ (FID_LOGIN_PWD) };    # Terminal PWD, not patron PWD.
916
917     if ( $uid_algorithm || $pwd_algorithm ) {
918         siplog( "LOG_ERR", "LOGIN: Unsupported non-zero encryption method(s): uid = $uid_algorithm, pwd = $pwd_algorithm" );
919         $status = 0;
920     } else {
921         $status = login_core( $server, $uid, $pwd );
922     }
923
924     $self->write_msg( LOGIN_RESP . $status, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
925     return $status ? LOGIN : '';
926 }
927
928 #
929 # Build the detailed summary information for the Patron
930 # Information Response message based on the first 'Y' that appears
931 # in the 'summary' field of the Patron Information request.  The
932 # specification says that only one 'Y' can appear in that field,
933 # and we're going to believe it.
934 #
935 sub summary_info {
936     my ( $ils, $patron, $summary, $start, $end, $server ) = @_;
937     my $resp = '';
938
939     #
940     # Map from offsets in the "summary" field of the Patron Information
941     # message to the corresponding field and handler
942     #
943     my @summary_map = (
944         { func => $patron->can("hold_items"),    fid => FID_HOLD_ITEMS },
945         { func => $patron->can("overdue_items"), fid => FID_OVERDUE_ITEMS },
946         { func => $patron->can("charged_items"), fid => FID_CHARGED_ITEMS },
947         { func => $patron->can("fine_items"),    fid => FID_FINE_ITEMS },
948         { func => $patron->can("recall_items"),  fid => FID_RECALL_ITEMS },
949         { func => $patron->can("unavail_holds"), fid => FID_UNAVAILABLE_HOLD_ITEMS },
950     );
951
952     my $summary_type = index( $summary, 'Y' );
953     return q{} if $summary_type == -1;    # No detailed information required.
954     return q{} if $summary_type > 5;      # Positions 6-9 are not defined in the sip spec,
955                                           # and we have no extensions to handle them.
956
957     siplog( "LOG_DEBUG", "Summary_info: index == '%d', field '%s'", $summary_type, $summary_map[$summary_type]->{fid} );
958
959     my $func     = $summary_map[$summary_type]->{func};
960     my $fid      = $summary_map[$summary_type]->{fid};
961     my $itemlist = &$func( $patron, $start, $end, $server );
962
963     siplog( "LOG_DEBUG", "summary_info: list = (%s)", join( ", ", map{ $_->{barcode} } @{$itemlist} ) );
964     foreach my $i ( @{$itemlist} ) {
965         $resp .= add_field( $fid, $i->{barcode}, $server );
966     }
967
968     return $resp;
969 }
970
971 sub handle_patron_info {
972     my ( $self, $server ) = @_;
973     my $ils = $server->{ils};
974     my ( $lang, $trans_date, $summary ) = @{ $self->{fixed_fields} };
975     my $fields = $self->{fields};
976     my ( $inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end );
977     my ( $resp, $patron );
978
979     $inst_id      = $fields->{ (FID_INST_ID) };
980     $patron_id    = $fields->{ (FID_PATRON_ID) };
981     $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
982     $patron_pwd   = $fields->{ (FID_PATRON_PWD) };
983     $start        = $fields->{ (FID_START_ITEM) };
984     $end          = $fields->{ (FID_END_ITEM) };
985
986     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
987
988     $patron = $ils->find_patron($patron_id);
989
990     $resp = (PATRON_INFO_RESP);
991     if ($patron) {
992         $patron->update_lastseen();
993         $resp .= patron_status_string( $patron, $server );
994         $resp .= ( defined($lang) and length($lang) == 3 ) ? $lang : $patron->language;
995         $resp .= timestamp();
996
997         $resp .= add_count( 'patron_info/hold_items',    scalar @{ $patron->hold_items } );
998         $resp .= add_count( 'patron_info/overdue_items', scalar @{ $patron->overdue_items } );
999         $resp .= add_count( 'patron_info/charged_items', scalar @{ $patron->charged_items } );
1000         $resp .= add_count( 'patron_info/fine_items',    scalar @{ $patron->fine_items } );
1001         $resp .= add_count( 'patron_info/recall_items',  scalar @{ $patron->recall_items } );
1002         $resp .= add_count( 'patron_info/unavail_holds', scalar @{ $patron->unavail_holds } );
1003
1004         $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ), $server );
1005
1006         # while the patron ID we got from the SC is valid, let's
1007         # use the one returned from the ILS, just in case...
1008         $resp .= add_field( FID_PATRON_ID,     $patron->id, $server );
1009         if ( defined $server->{account}->{ae_field_template} ) {
1010             $resp .= add_field( FID_PERSONAL_NAME, $patron->format( $server->{account}->{ae_field_template} ), $server );
1011         } else {
1012             $resp .= add_field( FID_PERSONAL_NAME, $patron->name, $server );
1013         }
1014
1015         # TODO: add code for the fields
1016         #   hold items limit
1017         #   overdue items limit
1018         #   charged items limit
1019
1020         $resp .= add_field( FID_VALID_PATRON, 'Y', $server );
1021         my $password_rc;
1022         if ( defined($patron_pwd) ) {
1023
1024             # If patron password was provided, report whether it was right or not.
1025             if ( $patron_pwd eq q{} && $server->{account}->{allow_empty_passwords} ) {
1026                 $password_rc = 1;
1027             } else {
1028                 $password_rc = $patron->check_password($patron_pwd);
1029             }
1030             $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $password_rc ), $server );
1031         }
1032
1033         $resp .= maybe_add( FID_CURRENCY, $patron->currency, $server );
1034         $resp .= maybe_add( FID_FEE_AMT,  $patron->fee_amount, $server );
1035         $resp .= add_field( FID_FEE_LMT, $patron->fee_limit, $server );
1036
1037         # TODO: zero or more item details for 2.0 can go here:
1038         #          hold_items
1039         #       overdue_items
1040         #       charged_items
1041         #          fine_items
1042         #        recall_items
1043
1044         $resp .= summary_info( $ils, $patron, $summary, $start, $end, $server );
1045
1046         $resp .= maybe_add( FID_HOME_ADDR,  $patron->address, $server );
1047         $resp .= maybe_add( FID_EMAIL,      $patron->email_addr, $server );
1048         $resp .= maybe_add( FID_HOME_PHONE, $patron->home_phone, $server );
1049
1050         # SIP 2.0 extensions used by Envisionware
1051         # Other terminals will ignore unrecognized fields (unrecognized field identifiers)
1052         $resp .= maybe_add( FID_PATRON_BIRTHDATE, $patron->birthdate, $server );
1053         $resp .= maybe_add( FID_PATRON_CLASS,     $patron->ptype, $server );
1054
1055         # Custom protocol extension to report patron internet privileges
1056         $resp .= maybe_add( FID_INET_PROFILE, $patron->inet_privileges, $server );
1057
1058         my $msg = $patron->screen_msg;
1059         if( defined( $patron_pwd ) && !$password_rc ) {
1060             $msg .= ' -- ' . INVALID_PW;
1061         }
1062         $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
1063         if ( $server->{account}->{send_patron_home_library_in_af} ) {
1064             $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server);
1065         }
1066         $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line, $server );
1067
1068         $resp .= $patron->build_custom_field_string( $server );
1069         $resp .= $patron->build_patron_attributes_string( $server );
1070     } else {
1071
1072         # Invalid patron ID:
1073         # no privileges, no items associated,
1074         # no personal name, and is invalid (if we're using 2.00)
1075         $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
1076         $resp .= '0000' x 6;
1077
1078         $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ), $server );
1079
1080         # patron ID is invalid, but field is required, so just echo it back
1081         $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) }, $server );
1082         $resp .= add_field( FID_PERSONAL_NAME, '', $server );
1083
1084         if ( $protocol_version >= 2 ) {
1085             $resp .= add_field( FID_VALID_PATRON, 'N', $server );
1086         }
1087         $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
1088     }
1089
1090     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1091     return (PATRON_INFO);
1092 }
1093
1094 sub handle_end_patron_session {
1095     my ( $self, $server ) = @_;
1096     my $ils = $server->{ils};
1097     my $trans_date;
1098     my $fields = $self->{fields};
1099     my $resp   = END_SESSION_RESP;
1100     my ( $status, $screen_msg, $print_line );
1101
1102     ($trans_date) = @{ $self->{fixed_fields} };
1103
1104     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, 'handle_end_patron_session' );
1105
1106     ( $status, $screen_msg, $print_line ) = $ils->end_patron_session( $fields->{ (FID_PATRON_ID) } );
1107
1108     $resp .= $status ? 'Y' : 'N';
1109     $resp .= timestamp();
1110
1111     $resp .= add_field( FID_INST_ID, $server->{ils}->institution, $server );
1112     $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) }, $server );
1113
1114     $resp .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1115     $resp .= maybe_add( FID_PRINT_LINE, $print_line, $server );
1116
1117     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1118
1119     return (END_PATRON_SESSION);
1120 }
1121
1122 sub handle_fee_paid {
1123     my ( $self, $server ) = @_;
1124     my $ils = $server->{ils};
1125     my ( $trans_date, $fee_type, $pay_type, $currency ) = @{ $self->{fixed_fields} };
1126     my $fields = $self->{fields};
1127     my ( $fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd );
1128     my ( $fee_id, $trans_id );
1129     my $status;
1130     my $resp = FEE_PAID_RESP;
1131
1132     my $disallow_overpayment  = $server->{account}->{disallow_overpayment};
1133     my $payment_type_writeoff = $server->{account}->{payment_type_writeoff} || q{};
1134     my $register_id           = $server->{account}->{register_id};
1135
1136     my $is_writeoff = $pay_type eq $payment_type_writeoff;
1137
1138     $fee_amt    = $fields->{ (FID_FEE_AMT) };
1139     $inst_id    = $fields->{ (FID_INST_ID) };
1140     $patron_id  = $fields->{ (FID_PATRON_ID) };
1141     $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1142     $fee_id     = $fields->{ (FID_FEE_ID) };
1143     $trans_id   = $fields->{ (FID_TRANSACTION_ID) };
1144
1145     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1146
1147     $ils->check_inst_id( $inst_id, "handle_fee_paid" );
1148
1149     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 );
1150     $status = $pay_result->{status};
1151     my $pay_response = $pay_result->{pay_response};
1152
1153     my $failmap = {
1154         "no_item" => "No matching item could be found",
1155         "no_checkout" => "Item is not checked out",
1156         "too_soon" => "Cannot yet be renewed",
1157         "too_many" => "Renewed the maximum number of times",
1158         "auto_too_soon" => "Scheduled for automatic renewal and cannot yet be renewed",
1159         "auto_too_late" => "Scheduled for automatic renewal and cannot yet be any more",
1160         "auto_account_expired" => "Scheduled for automatic renewal and cannot be renewed because the patron's account has expired",
1161         "auto_renew" => "Scheduled for automatic renewal",
1162         "auto_too_much_oweing" => "Scheduled for automatic renewal",
1163         "on_reserve" => "On hold for another patron",
1164         "patron_restricted" => "Patron is currently restricted",
1165         "item_denied_renewal" => "Item is not allowed renewal",
1166         "onsite_checkout" => "Item is an onsite checkout"
1167     };
1168     my @success = ();
1169     my @fail = ();
1170     foreach my $result( @{$pay_response->{renew_result}} ) {
1171         my $item = Koha::Items->find({ itemnumber => $result->{itemnumber} });
1172         if ($result->{success}) {
1173             push @success, '"' . $item->biblio->title . '"';
1174         } else {
1175             push @fail, '"' . $item->biblio->title . '" : ' . $failmap->{$result->{error}};
1176         }
1177     }
1178
1179     my $msg = "";
1180     if (scalar @success > 0) {
1181         $msg.="The following items were renewed: " . join(", ", @success) . ". ";
1182     }
1183     if (scalar @fail > 0) {
1184         $msg.="The following items were not renewed: " . join(", ", @fail) . ".";
1185     }
1186     if (length $msg > 0) {
1187         $status->screen_msg($status->screen_msg . " $msg");
1188     }
1189
1190     $resp .= ( $status->ok ? 'Y' : 'N' ) . timestamp;
1191     $resp .= add_field( FID_INST_ID,   $inst_id, $server );
1192     $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
1193     $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id, $server );
1194     $resp .= maybe_add( FID_SCREEN_MSG,     $status->screen_msg, $server );
1195     $resp .= maybe_add( FID_PRINT_LINE,     $status->print_line, $server );
1196
1197     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1198
1199     return (FEE_PAID);
1200 }
1201
1202 sub handle_item_information {
1203     my ( $self, $server ) = @_;
1204     my $account = $server->{account};
1205     my $ils     = $server->{ils};
1206     my $fields  = $self->{fields};
1207     my $resp    = ITEM_INFO_RESP;
1208     my $trans_date;
1209     my $item;
1210     my $i;
1211
1212     ($trans_date) = @{ $self->{fixed_fields} };
1213
1214     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_item_information" );
1215
1216     $item = $ils->find_item( $fields->{ (FID_ITEM_ID) } );
1217
1218     if ( !defined($item) ) {
1219
1220         # Invalid Item ID
1221         # "Other" circ stat, "Other" security marker, "Unknown" fee type
1222         $resp .= "010101";
1223         $resp .= timestamp;
1224
1225         # Just echo back the invalid item id
1226         $resp .= add_field( FID_ITEM_ID, $fields->{ (FID_ITEM_ID) }, $server );
1227
1228         # title id is required, but we don't have one
1229         $resp .= add_field( FID_TITLE_ID, '', $server );
1230     } else {
1231
1232         # Valid Item ID, send the good stuff
1233         my $circulation_status = $item->sip_circulation_status;
1234         $resp .= $circulation_status;
1235         $resp .= $item->sip_security_marker;
1236         $resp .= $item->sip_fee_type;
1237         $resp .= timestamp;
1238
1239         if ( $circulation_status eq '01' ) {
1240             $resp .= maybe_add( FID_SCREEN_MSG, "Item is damaged", $server );
1241         }
1242
1243         $resp .= add_field( FID_ITEM_ID,  $item->id, $server );
1244         $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
1245
1246         $resp .= maybe_add( FID_MEDIA_TYPE,   $item->sip_media_type, $server );
1247         $resp .= maybe_add( FID_PERM_LOCN,    $item->permanent_location, $server );
1248         $resp .= maybe_add( FID_CURRENT_LOCN, $item->current_location, $server );
1249         $resp .= maybe_add( FID_ITEM_PROPS,   $item->sip_item_properties, $server );
1250
1251
1252         if ( my $CR = $server->{account}->{cr_item_field} ) {
1253                 $resp .= maybe_add( FID_COLLECTION_CODE, $item->{$CR}, $server );
1254         } else {
1255           $resp .= maybe_add( FID_COLLECTION_CODE, $item->collection_code, $server );
1256         }
1257
1258         if ( ( $i = $item->fee ) != 0 ) {
1259             $resp .= add_field( FID_CURRENCY, $item->fee_currency, $server );
1260             $resp .= add_field( FID_FEE_AMT,  $i, $server );
1261         }
1262         $resp .= maybe_add( FID_OWNER, $item->owner, $server );
1263
1264         if ( ( $i = scalar @{ $item->hold_queue } ) > 0 ) {
1265             $resp .= add_field( FID_HOLD_QUEUE_LEN, $i, $server );
1266         }
1267         if ( $item->due_date ) {
1268             my $due_date =
1269               $account->{format_due_date}
1270               ? output_pref( { str => $item->due_date, as_due_date => 1 } )
1271               : timestamp( $item->due_date );
1272             $resp .= add_field( FID_DUE_DATE, $due_date, $server );
1273         }
1274         if ( ( $i = $item->recall_date ) != 0 ) {
1275             $resp .= add_field( FID_RECALL_DATE, timestamp($i), $server );
1276         }
1277         if ( ( $i = $item->hold_pickup_date ) != 0 ) {
1278             $resp .= add_field( FID_HOLD_PICKUP_DATE, timestamp($i), $server );
1279         }
1280
1281         $resp .= maybe_add( FID_SCREEN_MSG, $item->screen_msg, $server );
1282         $resp .= maybe_add( FID_PRINT_LINE, $item->print_line, $server );
1283
1284         $resp .= $item->build_additional_item_fields_string( $server );
1285         $resp .= $item->build_custom_field_string( $server );
1286     }
1287
1288     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1289
1290     return (ITEM_INFORMATION);
1291 }
1292
1293 sub handle_item_status_update {
1294     my ( $self, $server ) = @_;
1295     my $ils = $server->{ils};
1296     my ( $trans_date, $item_id, $terminal_pwd, $item_props );
1297     my $fields = $self->{fields};
1298     my $status;
1299     my $item;
1300     my $resp = ITEM_STATUS_UPDATE_RESP;
1301
1302     ($trans_date) = @{ $self->{fixed_fields} };
1303
1304     $ils->check_inst_id( $fields->{ (FID_INST_ID) } );
1305
1306     $item_id    = $fields->{ (FID_ITEM_ID) };
1307     $item_props = $fields->{ (FID_ITEM_PROPS) };
1308
1309     if ( !defined($item_id) ) {
1310         siplog( "LOG_WARNING", "handle_item_status: received message without Item ID field" );
1311     } else {
1312         $item = $ils->find_item($item_id);
1313     }
1314
1315     if ( !$item ) {
1316
1317         # Invalid Item ID
1318         $resp .= '0';
1319         $resp .= timestamp;
1320         $resp .= add_field( FID_ITEM_ID, $item_id, $server );
1321     } else {
1322
1323         # Valid Item ID
1324
1325         $status = $item->status_update($item_props);
1326
1327         $resp .= $status->ok ? '1' : '0';
1328         $resp .= timestamp;
1329
1330         $resp .= add_field( FID_ITEM_ID,  $item->id, $server );
1331         $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
1332         $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
1333     }
1334
1335     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1336     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1337
1338     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1339
1340     return (ITEM_STATUS_UPDATE);
1341 }
1342
1343 sub handle_patron_enable {
1344     my ( $self, $server ) = @_;
1345     my $ils    = $server->{ils};
1346     my $fields = $self->{fields};
1347     my ( $trans_date, $patron_id, $terminal_pwd, $patron_pwd );
1348     my ( $status, $patron );
1349     my $resp = PATRON_ENABLE_RESP;
1350
1351     ($trans_date) = @{ $self->{fixed_fields} };
1352     $patron_id  = $fields->{ (FID_PATRON_ID) };
1353     $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1354
1355     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1356
1357     siplog( "LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'", $patron_id, $patron_pwd );
1358
1359     $patron = $ils->find_patron($patron_id);
1360
1361     if ( !defined($patron) ) {
1362
1363         # Invalid patron ID
1364         $resp .= 'YYYY' . ( ' ' x 10 ) . '000' . timestamp();
1365         $resp .= add_field( FID_PATRON_ID,        $patron_id, $server );
1366         $resp .= add_field( FID_PERSONAL_NAME,    '', $server );
1367         $resp .= add_field( FID_VALID_PATRON,     'N', $server );
1368         $resp .= add_field( FID_VALID_PATRON_PWD, 'N', $server );
1369     } else {
1370
1371         # valid patron
1372         if ( !defined($patron_pwd) || $patron->check_password($patron_pwd) ) {
1373
1374             # Don't enable the patron if there was an invalid password
1375             $status = $patron->enable;
1376         }
1377         $resp .= patron_status_string( $patron, $server );
1378         $resp .= $patron->language . timestamp();
1379
1380         $resp .= add_field( FID_PATRON_ID,     $patron->id, $server );
1381         $resp .= add_field( FID_PERSONAL_NAME, $patron->format( $server->{account}->{ae_field_template} ), $server );
1382         if ( defined($patron_pwd) ) {
1383             $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password($patron_pwd) ), $server );
1384         }
1385         $resp .= add_field( FID_VALID_PATRON, 'Y', $server );
1386         $resp .= maybe_add( FID_SCREEN_MSG, $patron->screen_msg, $server );
1387         $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line, $server );
1388     }
1389
1390     $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1391
1392     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1393
1394     return (PATRON_ENABLE);
1395 }
1396
1397 sub handle_hold {
1398     my ( $self, $server ) = @_;
1399     my $ils = $server->{ils};
1400     my ( $hold_mode, $trans_date );
1401     my ( $expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd );
1402     my ( $item_id, $title_id, $fee_ack );
1403     my $fields = $self->{fields};
1404     my $status;
1405     my $resp = HOLD_RESP;
1406
1407     ( $hold_mode, $trans_date ) = @{ $self->{fixed_fields} };
1408
1409     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_hold" );
1410
1411     $patron_id   = $fields->{ (FID_PATRON_ID) };
1412     $expiry_date = $fields->{ (FID_EXPIRATION) } || '';
1413     $pickup_locn = $fields->{ (FID_PICKUP_LOCN) } || '';
1414     $hold_type   = $fields->{ (FID_HOLD_TYPE) } || '2';    # Any copy of title
1415     $patron_pwd  = $fields->{ (FID_PATRON_PWD) };
1416     $item_id     = $fields->{ (FID_ITEM_ID) } || '';
1417     $title_id    = $fields->{ (FID_TITLE_ID) } || '';
1418     $fee_ack     = $fields->{ (FID_FEE_ACK) } || 'N';
1419
1420     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1421
1422     if ( $hold_mode eq '+' ) {
1423         $status = $ils->add_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1424     } elsif ( $hold_mode eq '-' ) {
1425         $status = $ils->cancel_hold( $patron_id, $patron_pwd, $item_id, $title_id );
1426     } elsif ( $hold_mode eq '*' ) {
1427         $status = $ils->alter_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1428     } else {
1429         siplog( "LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'", $hold_mode, $server->{account}->{id} );
1430         $status = $ils->Transaction::Hold;    # new?
1431         $status->screen_msg("System error. Please contact library staff.");
1432     }
1433
1434     $resp .= $status->ok;
1435     $resp .= sipbool( $status->item && $status->item->available($patron_id) );
1436     $resp .= timestamp;
1437
1438     if ( $status->ok ) {
1439         $resp .= add_field( FID_PATRON_ID, $status->patron->id, $server );
1440
1441         ( $status->expiration_date )
1442           and $resp .= maybe_add( FID_EXPIRATION, timestamp( $status->expiration_date ), $server );
1443         $resp .= maybe_add( FID_QUEUE_POS,   $status->queue_position, $server );
1444         $resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location, $server );
1445         $resp .= maybe_add( FID_ITEM_ID,     $status->item->id, $server );
1446         $resp .= maybe_add( FID_TITLE_ID,    $status->item->title_id, $server );
1447     } else {
1448
1449         # Not ok.  still need required fields
1450         $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
1451     }
1452
1453     $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1454     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1455     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1456
1457     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1458
1459     return (HOLD);
1460 }
1461
1462 sub handle_renew {
1463     my ( $self, $server ) = @_;
1464     my $ils = $server->{ils};
1465     my ( $third_party, $no_block, $trans_date, $nb_due_date );
1466     my ( $patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack );
1467     my $fields = $self->{fields};
1468     my $status;
1469     my ( $patron, $item );
1470     my $resp = RENEW_RESP;
1471
1472     ( $third_party, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
1473
1474     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew" );
1475
1476     if ( $no_block eq 'Y' ) {
1477         siplog( "LOG_WARNING", "handle_renew: received 'no block' renewal from terminal '%s'", $server->{account}->{id} );
1478     }
1479
1480     $patron_id  = $fields->{ (FID_PATRON_ID) };
1481     $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1482     $item_id    = $fields->{ (FID_ITEM_ID) };
1483     $title_id   = $fields->{ (FID_TITLE_ID) };
1484     $item_props = $fields->{ (FID_ITEM_PROPS) };
1485     $fee_ack    = $fields->{ (FID_FEE_ACK) };
1486
1487     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1488
1489     $status = $ils->renew( $patron_id, $patron_pwd, $item_id, $title_id, $no_block, $nb_due_date, $third_party, $item_props, $fee_ack );
1490
1491     $patron = $status->patron;
1492     $item   = $status->item;
1493
1494     if ( $status->renewal_ok ) {
1495         $resp .= '1';
1496         $resp .= $status->renewal_ok ? 'Y' : 'N';
1497         if ( $ils->supports('magnetic media') ) {
1498             $resp .= sipbool( $item->magnetic_media );
1499         } else {
1500             $resp .= 'U';
1501         }
1502         $resp .= sipbool( desensitize( { status => $status, patron => $patron, server => $server } ) );
1503         $resp .= timestamp;
1504         $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
1505         $resp .= add_field( FID_ITEM_ID, $item->id, $server );
1506         $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
1507         if ( $item->due_date ) {
1508             $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ), $server );
1509         } else {
1510             $resp .= add_field( FID_DUE_DATE, q{}, $server );
1511         }
1512         if ( $ils->supports('security inhibit') ) {
1513             $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit, $server );
1514         }
1515         $resp .= add_field( FID_MEDIA_TYPE, $item->sip_media_type, $server );
1516         $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
1517     } else {
1518
1519         # renew failed for some reason
1520         # not OK, renewal not OK, Unknown media type (why bother checking?)
1521         $resp .= '0NUN';
1522         $resp .= timestamp;
1523
1524         # If we found the patron or the item, the return the ILS
1525         # information, otherwise echo back the information we received
1526         # from the terminal
1527         $resp .= add_field( FID_PATRON_ID, $patron ? $patron->id     : $patron_id, $server );
1528         $resp .= add_field( FID_ITEM_ID,   $item   ? $item->id       : $item_id, $server );
1529         $resp .= add_field( FID_TITLE_ID,  $item   ? $item->title_id : $title_id, $server );
1530         $resp .= add_field( FID_DUE_DATE,  '', $server );
1531     }
1532
1533     if ( $status->fee_amount ) {
1534         $resp .= add_field( FID_FEE_AMT, $status->fee_amount, $server );
1535         $resp .= maybe_add( FID_CURRENCY,       $status->sip_currency, $server );
1536         $resp .= maybe_add( FID_FEE_TYPE,       $status->sip_fee_type, $server );
1537         $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id, $server );
1538     }
1539
1540     $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1541     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1542     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1543
1544     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1545
1546     return (RENEW);
1547 }
1548
1549 sub handle_renew_all {
1550
1551     # my ($third_party, $no_block, $nb_due_date, $fee_ack, $patron);
1552
1553     my ( $self, $server ) = @_;
1554     my $ils = $server->{ils};
1555     my ( $trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack );
1556     my $fields = $self->{fields};
1557     my $resp   = RENEW_ALL_RESP;
1558     my $status;
1559     my ( @renewed, @unrenewed );
1560
1561     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew_all" );
1562
1563     ($trans_date) = @{ $self->{fixed_fields} };
1564
1565     $patron_id    = $fields->{ (FID_PATRON_ID) };
1566     $patron_pwd   = $fields->{ (FID_PATRON_PWD) };
1567     $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
1568     $fee_ack      = $fields->{ (FID_FEE_ACK) };
1569
1570     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1571
1572     $status = $ils->renew_all( $patron_id, $patron_pwd, $fee_ack );
1573
1574     $resp .= $status->ok ? '1' : '0';
1575
1576     if ( !$status->ok ) {
1577         $resp .= add_count( "renew_all/renewed_count",   0 );
1578         $resp .= add_count( "renew_all/unrenewed_count", 0 );
1579         @renewed   = ();
1580         @unrenewed = ();
1581     } else {
1582         @renewed   = ( @{ $status->renewed } );
1583         @unrenewed = ( @{ $status->unrenewed } );
1584         $resp .= add_count( "renew_all/renewed_count",   scalar @renewed );
1585         $resp .= add_count( "renew_all/unrenewed_count", scalar @unrenewed );
1586     }
1587
1588     $resp .= timestamp;
1589     $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1590
1591     $resp .= join( '', map( add_field( FID_RENEWED_ITEMS,   $_ ), @renewed ), $server );
1592     $resp .= join( '', map( add_field( FID_UNRENEWED_ITEMS, $_ ), @unrenewed ), $server );
1593
1594     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1595     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1596
1597     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1598
1599     return (RENEW_ALL);
1600 }
1601
1602 #
1603 # send_acs_status($self, $server)
1604 #
1605 # Send an ACS Status message, which is contains lots of little fields
1606 # of information gleaned from all sorts of places.
1607 #
1608
1609 my @message_type_names = (
1610     "patron status request",
1611     "checkout",
1612     "checkin",
1613     "block patron",
1614     "acs status",
1615     "request sc/acs resend",
1616     "login",
1617     "patron information",
1618     "end patron session",
1619     "fee paid",
1620     "item information",
1621     "item status update",
1622     "patron enable",
1623     "hold",
1624     "renew",
1625     "renew all",
1626 );
1627
1628 sub send_acs_status {
1629     my ( $self, $server, $screen_msg, $print_line ) = @_;
1630
1631     my $msg = ACS_STATUS;
1632     ($server) or die "send_acs_status error: no \$server argument received";
1633     my $account = $server->{account} or die "send_acs_status error: no 'account' in \$server object:\n" . Dumper($server);
1634     my $policy  = $server->{policy}  or die "send_acs_status error: no 'policy' in \$server object:\n" . Dumper($server);
1635     my $ils     = $server->{ils}     or die "send_acs_status error: no 'ils' in \$server object:\n" . Dumper($server);
1636     my $sip_username = $server->{sip_username} or die "send_acs_status error: no 'sip_username' in \$server object:\n" . Dumper($server);
1637     my ( $online_status,    $checkin_ok, $checkout_ok, $ACS_renewal_policy );
1638     my ( $status_update_ok, $offline_ok, $timeout,     $retries );
1639     my $sip_user = Koha::Patrons->find({ userid => $sip_username });
1640     die "send_acs_status error: sip_username cannot be found in DB or DB cannot be reached" unless $sip_user;
1641
1642     $online_status      = 'Y';
1643     $checkout_ok        = sipbool( $ils->checkout_ok );
1644     $checkin_ok         = sipbool( $ils->checkin_ok );
1645     $ACS_renewal_policy = sipbool( $policy->{renewal} );
1646     $status_update_ok   = sipbool( $ils->status_update_ok );
1647     $offline_ok         = sipbool( $ils->offline_ok );
1648     $timeout            = $server->get_timeout({ policy => 1 });
1649     $retries            = sprintf( "%03d", $policy->{retries} );
1650
1651     if ( length($retries) != 3 ) {
1652         siplog( "LOG_ERR", "handle_acs_status: retries field wrong size: '%s'", $retries );
1653         $retries = '000';
1654     }
1655
1656     $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
1657     $msg .= "$status_update_ok$offline_ok$timeout$retries";
1658     $msg .= timestamp();
1659
1660     if ( $protocol_version == 1 ) {
1661         $msg .= '1.00';
1662     } elsif ( $protocol_version == 2 ) {
1663         $msg .= '2.00';
1664     } else {
1665         siplog( "LOG_ERR", 'Bad setting for $protocol_version, "%s" in send_acs_status', $protocol_version );
1666         $msg .= '1.00';
1667     }
1668
1669     # Institution ID
1670     $msg .= add_field( FID_INST_ID, $account->{institution}, $server );
1671
1672     if ( $protocol_version >= 2 ) {
1673
1674         # Supported messages: we do it all
1675         my $supported_msgs = '';
1676
1677         foreach my $msg_name (@message_type_names) {
1678             if ( $msg_name eq 'request sc/acs resend' ) {
1679                 $supported_msgs .= sipbool(1);
1680             } else {
1681                 $supported_msgs .= sipbool( $ils->supports($msg_name) );
1682             }
1683         }
1684         if ( length($supported_msgs) < 16 ) {
1685             siplog( "LOG_ERR", 'send_acs_status: supported messages "%s" too short', $supported_msgs );
1686         }
1687         $msg .= add_field( FID_SUPPORTED_MSGS, $supported_msgs, $server );
1688     }
1689
1690     $msg .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1691
1692     if (   defined( $account->{print_width} )
1693         && defined($print_line)
1694         && $account->{print_width} < length($print_line) ) {
1695         siplog( "LOG_WARNING", "send_acs_status: print line '%s' too long.  Truncating", $print_line );
1696         $print_line = substr( $print_line, 0, $account->{print_width} );
1697     }
1698
1699     $msg .= maybe_add( FID_PRINT_LINE, $print_line, $server );
1700
1701     # Do we want to tell the terminal its location?
1702
1703     $self->write_msg( $msg, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1704     return 1;
1705 }
1706
1707 #
1708 # build_patron_status: create the 14-char patron status
1709 # string for the Patron Status message
1710 #
1711 sub patron_status_string {
1712     my $patron = shift;
1713     my $server = shift;
1714
1715     my $patron_status;
1716
1717     siplog( "LOG_DEBUG", "patron_status_string: %s charge_ok: %s", $patron->id, $patron->charge_ok );
1718     $patron_status = sprintf(
1719         '%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
1720         denied( $patron->charge_ok ),
1721         denied( $patron->renew_ok ),
1722         denied( $patron->recall_ok ),
1723         denied( $patron->hold_ok ),
1724         boolspace( $patron->card_lost ),
1725         boolspace( $patron->too_many_charged ),
1726         $server->{account}->{overdues_block_checkout} ? boolspace( $patron->too_many_overdue ) : q{ },
1727         boolspace( $patron->too_many_renewal ),
1728         boolspace( $patron->too_many_claim_return ),
1729         boolspace( $patron->too_many_lost ),
1730         boolspace( $patron->excessive_fines ),
1731         boolspace( $patron->excessive_fees ),
1732         boolspace( $patron->recall_overdue ),
1733         boolspace( $patron->too_many_billed )
1734     );
1735     return $patron_status;
1736 }
1737
1738 sub api_auth {
1739     my ( $username, $password, $branch ) = @_;
1740     $ENV{REMOTE_USER} = $username;
1741     my $query = CGI->new();
1742     $query->param( userid   => $username );
1743     $query->param( password => $password );
1744     if ($branch) {
1745         $query->param( branch => $branch );
1746     }
1747     my ( $status, $cookie, $sessionID ) = check_api_auth( $query, { circulate => 1 }, 'intranet' );
1748     return $status;
1749 }
1750
1751 sub desensitize {
1752     my ($params) = @_;
1753
1754     my $status      = $params->{status};
1755     my $desensitize = $status->desensitize();
1756
1757     # If desenstize is already false, no need to do anything
1758     return unless $desensitize;
1759
1760     my $patron = $params->{patron};
1761     my $item   = $params->{item};
1762     my $server = $params->{server};
1763
1764     my $patron_categories = $server->{account}->{inhouse_patron_categories} // q{};
1765     my $item_types = $server->{account}->{inhouse_item_types} // q{};
1766
1767     # If no patron categories or item types are set for never desensitize, no need to do anything
1768     return $desensitize unless $patron_categories || $item_types;
1769
1770     my $patron_category = $patron->ptype();
1771     my @patron_categories = split( /,/, $patron_categories );
1772     my $found_patron_category = grep( /^$patron_category$/, @patron_categories );
1773     return 0 if $found_patron_category;
1774
1775     my $item_type = $item->itemtype;
1776     my @item_types = split( /,/, $item_types );
1777     my $found_item_type = grep( /^$item_type$/, @item_types );
1778     return 0 if $found_item_type;
1779
1780     return 1;
1781 }
1782
1783 1;
1784 __END__
1785