Bug 32515: Ignore no-block flag in check-in message
[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' - no-block checkin not supported", $account->{id} );
676         #FIXME We need to write the routine called below
677         #$status = $ils->checkin_no_block( $item_id, $trans_date, $return_date, $item_props, $cancel );
678         #Until we do, lets just checkin the item
679         $status = $ils->checkin( $item_id, $trans_date, $return_date, $my_branch, $item_props, $cancel, $account );
680     } else {
681         $status = $ils->checkin( $item_id, $trans_date, $return_date, $my_branch, $item_props, $cancel, $account );
682     }
683
684     $patron = $status->patron;
685     $item   = $status->item;
686
687     $resp .= $status->ok          ? '1' : '0';
688     $resp .= $status->resensitize ? 'Y' : 'N';
689     if ( $item && $ils->supports('magnetic media') ) {
690         $resp .= sipbool( $item->magnetic_media );
691     } else {
692
693         # item barcode is invalid or system doesn't support 'magnetic media' indicator
694         $resp .= 'U';
695     }
696
697     $resp .= $status->alert ? 'Y' : 'N';
698     $resp .= timestamp;
699     $resp .= add_field( FID_INST_ID, $inst_id, $server );
700     $resp .= add_field( FID_ITEM_ID, $item_id, $server );
701
702     if ($item) {
703         $resp .= add_field( FID_PERM_LOCN, $item->permanent_location, $server );
704         $resp .= maybe_add( FID_TITLE_ID, $item->title_id, $server );
705         $resp .= $item->build_additional_item_fields_string( $server );
706     }
707
708     if ( $protocol_version >= 2 ) {
709         $resp .= maybe_add( FID_SORT_BIN, $status->sort_bin, $server );
710         if ($patron) {
711             $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
712         }
713         if ($item) {
714             $resp .= maybe_add( FID_MEDIA_TYPE,           $item->sip_media_type,      $server );
715             $resp .= maybe_add( FID_ITEM_PROPS,           $item->sip_item_properties, $server );
716             $resp .= maybe_add( FID_CALL_NUMBER,          $item->call_number,         $server );
717             $resp .= maybe_add( FID_HOLD_PATRON_ID,       $item->hold_patron_bcode,   $server );
718             $resp .= add_field( FID_DESTINATION_LOCATION, $item->destination_loc,     $server ) if ( $item->destination_loc || $server->{account}->{ct_always_send} );
719             $resp .= maybe_add( FID_HOLD_PATRON_NAME,     $item->hold_patron_name( $server->{account}->{da_field_template} ), $server );
720
721             if ( my $CR = $server->{account}->{cr_item_field} ) {
722                 $resp .= maybe_add( FID_COLLECTION_CODE, $item->{$CR}, $server );
723             } else {
724                 $resp .= maybe_add( FID_COLLECTION_CODE, $item->collection_code, $server );
725             }
726
727             if ( $status->hold and $status->hold->{branchcode} ne $item->destination_loc ) {
728                 warn 'SIP hold mismatch: $status->hold->{branchcode}=' . $status->hold->{branchcode} . '; $item->destination_loc=' . $item->destination_loc;
729
730                 # just me being paranoid.
731             }
732         }
733     }
734
735     if ( $status->alert && $status->alert_type ) {
736         $resp .= maybe_add( FID_ALERT_TYPE, $status->alert_type, $server );
737     } elsif ( $server->{account}->{cv_send_00_on_success} ) {
738         $resp .= add_field( FID_ALERT_TYPE, '00', $server );
739     }
740     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
741     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
742
743     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
744
745     return (CHECKIN);
746 }
747
748 sub handle_block_patron {
749     my ( $self, $server ) = @_;
750     my $account = $server->{account};
751     my $ils     = $server->{ils};
752     my ( $card_retained, $trans_date );
753     my ( $inst_id, $blocked_card_msg, $patron_id, $terminal_pwd );
754     my ( $fields, $resp, $patron );
755
756     ( $card_retained, $trans_date ) = @{ $self->{fixed_fields} };
757     $fields           = $self->{fields};
758     $inst_id          = $fields->{ (FID_INST_ID) };
759     $blocked_card_msg = $fields->{ (FID_BLOCKED_CARD_MSG) };
760     $patron_id        = $fields->{ (FID_PATRON_ID) };
761     $terminal_pwd     = $fields->{ (FID_TERMINAL_PWD) };
762
763     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
764
765     # Terminal passwords are different from account login
766     # passwords, but I have no idea what to do with them.  So,
767     # I'll just ignore them for now.
768
769     # FIXME ???
770
771     $ils->check_inst_id( $inst_id, "block_patron" );
772     $patron = $ils->find_patron($patron_id);
773
774     # The correct response for a "Block Patron" message is a
775     # "Patron Status Response", so use that handler to generate
776     # the message, but then return the correct code from here.
777     #
778     # Normally, the language is provided by the "Patron Status"
779     # fixed field, but since we're not responding to one of those
780     # we'll just say, "Unspecified", as per the spec.  Let the
781     # terminal default to something that, one hopes, will be
782     # intelligible
783     if ($patron) {
784
785         # Valid patron id
786         $patron->block( $card_retained, $blocked_card_msg );
787     }
788
789     $resp = build_patron_status( $patron, $patron->language, $fields, $server );
790     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
791     return (BLOCK_PATRON);
792 }
793
794 sub handle_sc_status {
795     my ( $self, $server ) = @_;
796     ($server) or warn "handle_sc_status error: no \$server argument received.";
797     my ( $status, $print_width, $sc_protocol_version ) = @{ $self->{fixed_fields} };
798     my ($new_proto);
799
800     if ( $sc_protocol_version =~ /^1\./ ) {
801         $new_proto = 1;
802     } elsif ( $sc_protocol_version =~ /^2\./ ) {
803         $new_proto = 2;
804     } else {
805         siplog( "LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version );
806         $new_proto = 1;
807     }
808
809     if ( $new_proto != $protocol_version ) {
810         siplog( "LOG_INFO", "Setting protocol level to $new_proto" );
811         $protocol_version = $new_proto;
812     }
813
814     if ( $status == SC_STATUS_PAPER ) {
815         siplog( "LOG_WARNING", "Self-Check unit '%s@%s' out of paper", $self->{account}->{id}, $self->{account}->{institution} );
816     } elsif ( $status == SC_STATUS_SHUTDOWN ) {
817         siplog( "LOG_WARNING", "Self-Check unit '%s@%s' shutting down", $self->{account}->{id}, $self->{account}->{institution} );
818     }
819
820     $self->{account}->{print_width} = $print_width;
821     return ( send_acs_status( $self, $server ) ? SC_STATUS : '' );
822 }
823
824 sub handle_request_acs_resend {
825     my ( $self, $server ) = @_;
826
827     if ( !$last_response ) {
828
829         # We haven't sent anything yet, so respond with a
830         # REQUEST_SC_RESEND msg (p. 16)
831         $self->write_msg( REQUEST_SC_RESEND, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
832     } elsif ( ( length($last_response) < 9 )
833         || substr( $last_response, -9, 2 ) ne 'AY' ) {
834
835         # When resending a message, we aren't supposed to include
836         # a sequence number, even if the original had one (p. 4).
837         # If the last message didn't have a sequence number, then
838         # we can just send it.
839         print("$last_response\r");    # not write_msg?
840     } else {
841
842         # Cut out the sequence number and checksum, since the old
843         # checksum is wrong for the resent message.
844         my $rebuilt = substr( $last_response, 0, -9 );
845         $self->write_msg( $rebuilt, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
846     }
847
848     return REQUEST_ACS_RESEND;
849 }
850
851 sub login_core {
852     my $server = shift or return;
853     my $uid    = shift;
854     my $pwd    = shift;
855     my $status = 1;                 # Assume it all works
856     if ( !exists( $server->{config}->{accounts}->{$uid} ) ) {
857         siplog( "LOG_WARNING", "MsgType::login_core: Unknown login '$uid'" );
858         $status = 0;
859     } elsif ( $server->{config}->{accounts}->{$uid}->{password} ne $pwd ) {
860         siplog( "LOG_WARNING", "MsgType::login_core: Invalid password for login '$uid'" );
861         $status = 0;
862     } else {
863
864         # Store the active account someplace handy for everybody else to find.
865         $server->{account} = $server->{config}->{accounts}->{$uid};
866         my $inst = $server->{account}->{institution};
867         $server->{institution}  = $server->{config}->{institutions}->{$inst};
868         $server->{policy}       = $server->{institution}->{policy};
869         $server->{sip_username} = $uid;
870         $server->{sip_password} = $pwd;
871
872         my $auth_status = api_auth( $uid, $pwd, $inst );
873         if ( !$auth_status or $auth_status !~ /^ok$/i ) {
874             siplog( "LOG_WARNING", "api_auth failed for SIP terminal '%s' of '%s': %s", $uid, $inst, ( $auth_status || 'unknown' ) );
875             $status = 0;
876         } else {
877             siplog( "LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst );
878
879             #
880             # initialize connection to ILS
881             #
882             my $module = $server->{config}->{institutions}->{$inst}->{implementation};
883             siplog( "LOG_DEBUG", 'login_core: ' . Dumper($module) );
884
885             # Suspect this is always ILS but so we don't break any eccentic install (for now)
886             if ( $module eq 'ILS' ) {
887                 $module = 'C4::SIP::ILS';
888             }
889             $module->use;
890             if ($@) {
891                 siplog( "LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed", $server->{service}, $module, $inst );
892                 die("Failed to load ILS implementation '$module' for $inst");
893             }
894
895             # like   ILS->new(), I think.
896             $server->{ils} = $module->new( $server->{institution}, $server->{account} );
897             if ( !$server->{ils} ) {
898                 siplog( "LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst );
899                 die("Unable to connect to ILS '$inst'");
900             }
901         }
902     }
903     return $status;
904 }
905
906 sub handle_login {
907     my ( $self, $server ) = @_;
908     my ( $uid_algorithm, $pwd_algorithm );
909     my ( $uid,           $pwd );
910     my $inst;
911     my $fields;
912     my $status = 1;    # Assume it all works
913
914     $fields = $self->{fields};
915     ( $uid_algorithm, $pwd_algorithm ) = @{ $self->{fixed_fields} };
916
917     $uid = $fields->{ (FID_LOGIN_UID) };    # Terminal ID, not patron ID.
918     $pwd = $fields->{ (FID_LOGIN_PWD) };    # Terminal PWD, not patron PWD.
919
920     if ( $uid_algorithm || $pwd_algorithm ) {
921         siplog( "LOG_ERR", "LOGIN: Unsupported non-zero encryption method(s): uid = $uid_algorithm, pwd = $pwd_algorithm" );
922         $status = 0;
923     } else {
924         $status = login_core( $server, $uid, $pwd );
925     }
926
927     $self->write_msg( LOGIN_RESP . $status, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
928     return $status ? LOGIN : '';
929 }
930
931 #
932 # Build the detailed summary information for the Patron
933 # Information Response message based on the first 'Y' that appears
934 # in the 'summary' field of the Patron Information request.  The
935 # specification says that only one 'Y' can appear in that field,
936 # and we're going to believe it.
937 #
938 sub summary_info {
939     my ( $ils, $patron, $summary, $start, $end, $server ) = @_;
940     my $resp = '';
941
942     #
943     # Map from offsets in the "summary" field of the Patron Information
944     # message to the corresponding field and handler
945     #
946     my @summary_map = (
947         { func => $patron->can("hold_items"),    fid => FID_HOLD_ITEMS },
948         { func => $patron->can("overdue_items"), fid => FID_OVERDUE_ITEMS },
949         { func => $patron->can("charged_items"), fid => FID_CHARGED_ITEMS },
950         { func => $patron->can("fine_items"),    fid => FID_FINE_ITEMS },
951         { func => $patron->can("recall_items"),  fid => FID_RECALL_ITEMS },
952         { func => $patron->can("unavail_holds"), fid => FID_UNAVAILABLE_HOLD_ITEMS },
953     );
954
955     my $summary_type = index( $summary, 'Y' );
956     return q{} if $summary_type == -1;    # No detailed information required.
957     return q{} if $summary_type > 5;      # Positions 6-9 are not defined in the sip spec,
958                                           # and we have no extensions to handle them.
959
960     siplog( "LOG_DEBUG", "Summary_info: index == '%d', field '%s'", $summary_type, $summary_map[$summary_type]->{fid} );
961
962     my $func     = $summary_map[$summary_type]->{func};
963     my $fid      = $summary_map[$summary_type]->{fid};
964     my $itemlist = &$func( $patron, $start, $end, $server );
965
966     siplog( "LOG_DEBUG", "summary_info: list = (%s)", join( ", ", map{ $_->{barcode} } @{$itemlist} ) );
967     foreach my $i ( @{$itemlist} ) {
968         $resp .= add_field( $fid, $i->{barcode}, $server );
969     }
970
971     return $resp;
972 }
973
974 sub handle_patron_info {
975     my ( $self, $server ) = @_;
976     my $ils = $server->{ils};
977     my ( $lang, $trans_date, $summary ) = @{ $self->{fixed_fields} };
978     my $fields = $self->{fields};
979     my ( $inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end );
980     my ( $resp, $patron );
981
982     $inst_id      = $fields->{ (FID_INST_ID) };
983     $patron_id    = $fields->{ (FID_PATRON_ID) };
984     $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
985     $patron_pwd   = $fields->{ (FID_PATRON_PWD) };
986     $start        = $fields->{ (FID_START_ITEM) };
987     $end          = $fields->{ (FID_END_ITEM) };
988
989     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
990
991     $patron = $ils->find_patron($patron_id);
992
993     $resp = (PATRON_INFO_RESP);
994     if ($patron) {
995         $patron->update_lastseen();
996         $resp .= patron_status_string( $patron, $server );
997         $resp .= ( defined($lang) and length($lang) == 3 ) ? $lang : $patron->language;
998         $resp .= timestamp();
999
1000         $resp .= add_count( 'patron_info/hold_items',    scalar @{ $patron->hold_items } );
1001         $resp .= add_count( 'patron_info/overdue_items', scalar @{ $patron->overdue_items } );
1002         $resp .= add_count( 'patron_info/charged_items', scalar @{ $patron->charged_items } );
1003         $resp .= add_count( 'patron_info/fine_items',    scalar @{ $patron->fine_items } );
1004         $resp .= add_count( 'patron_info/recall_items',  scalar @{ $patron->recall_items } );
1005         $resp .= add_count( 'patron_info/unavail_holds', scalar @{ $patron->unavail_holds } );
1006
1007         $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ), $server );
1008
1009         # while the patron ID we got from the SC is valid, let's
1010         # use the one returned from the ILS, just in case...
1011         $resp .= add_field( FID_PATRON_ID,     $patron->id, $server );
1012         if ( defined $server->{account}->{ae_field_template} ) {
1013             $resp .= add_field( FID_PERSONAL_NAME, $patron->format( $server->{account}->{ae_field_template} ), $server );
1014         } else {
1015             $resp .= add_field( FID_PERSONAL_NAME, $patron->name, $server );
1016         }
1017
1018         # TODO: add code for the fields
1019         #   hold items limit
1020         #   overdue items limit
1021         #   charged items limit
1022
1023         $resp .= add_field( FID_VALID_PATRON, 'Y', $server );
1024         my $password_rc;
1025         if ( defined($patron_pwd) ) {
1026
1027             # If patron password was provided, report whether it was right or not.
1028             if ( $patron_pwd eq q{} && $server->{account}->{allow_empty_passwords} ) {
1029                 $password_rc = 1;
1030             } else {
1031                 $password_rc = $patron->check_password($patron_pwd);
1032             }
1033             $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $password_rc ), $server );
1034         }
1035
1036         $resp .= maybe_add( FID_CURRENCY, $patron->currency, $server );
1037         $resp .= maybe_add( FID_FEE_AMT,  $patron->fee_amount, $server );
1038         $resp .= add_field( FID_FEE_LMT, $patron->fee_limit, $server );
1039
1040         # TODO: zero or more item details for 2.0 can go here:
1041         #          hold_items
1042         #       overdue_items
1043         #       charged_items
1044         #          fine_items
1045         #        recall_items
1046
1047         $resp .= summary_info( $ils, $patron, $summary, $start, $end, $server );
1048
1049         $resp .= maybe_add( FID_HOME_ADDR,  $patron->address, $server );
1050         $resp .= maybe_add( FID_EMAIL,      $patron->email_addr, $server );
1051         $resp .= maybe_add( FID_HOME_PHONE, $patron->home_phone, $server );
1052
1053         # SIP 2.0 extensions used by Envisionware
1054         # Other terminals will ignore unrecognized fields (unrecognized field identifiers)
1055         $resp .= maybe_add( FID_PATRON_BIRTHDATE, $patron->birthdate, $server );
1056         $resp .= maybe_add( FID_PATRON_CLASS,     $patron->ptype, $server );
1057
1058         # Custom protocol extension to report patron internet privileges
1059         $resp .= maybe_add( FID_INET_PROFILE, $patron->inet_privileges, $server );
1060
1061         my $msg = $patron->screen_msg;
1062         if( defined( $patron_pwd ) && !$password_rc ) {
1063             $msg .= ' -- ' . INVALID_PW;
1064         }
1065         $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
1066         if ( $server->{account}->{send_patron_home_library_in_af} ) {
1067             $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server);
1068         }
1069         $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line, $server );
1070
1071         $resp .= $patron->build_custom_field_string( $server );
1072         $resp .= $patron->build_patron_attributes_string( $server );
1073     } else {
1074
1075         # Invalid patron ID:
1076         # no privileges, no items associated,
1077         # no personal name, and is invalid (if we're using 2.00)
1078         $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
1079         $resp .= '0000' x 6;
1080
1081         $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ), $server );
1082
1083         # patron ID is invalid, but field is required, so just echo it back
1084         $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) }, $server );
1085         $resp .= add_field( FID_PERSONAL_NAME, '', $server );
1086
1087         if ( $protocol_version >= 2 ) {
1088             $resp .= add_field( FID_VALID_PATRON, 'N', $server );
1089         }
1090         $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
1091     }
1092
1093     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1094     return (PATRON_INFO);
1095 }
1096
1097 sub handle_end_patron_session {
1098     my ( $self, $server ) = @_;
1099     my $ils = $server->{ils};
1100     my $trans_date;
1101     my $fields = $self->{fields};
1102     my $resp   = END_SESSION_RESP;
1103     my ( $status, $screen_msg, $print_line );
1104
1105     ($trans_date) = @{ $self->{fixed_fields} };
1106
1107     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, 'handle_end_patron_session' );
1108
1109     ( $status, $screen_msg, $print_line ) = $ils->end_patron_session( $fields->{ (FID_PATRON_ID) } );
1110
1111     $resp .= $status ? 'Y' : 'N';
1112     $resp .= timestamp();
1113
1114     $resp .= add_field( FID_INST_ID, $server->{ils}->institution, $server );
1115     $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) }, $server );
1116
1117     $resp .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1118     $resp .= maybe_add( FID_PRINT_LINE, $print_line, $server );
1119
1120     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1121
1122     return (END_PATRON_SESSION);
1123 }
1124
1125 sub handle_fee_paid {
1126     my ( $self, $server ) = @_;
1127     my $ils = $server->{ils};
1128     my ( $trans_date, $fee_type, $pay_type, $currency ) = @{ $self->{fixed_fields} };
1129     my $fields = $self->{fields};
1130     my ( $fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd );
1131     my ( $fee_id, $trans_id );
1132     my $status;
1133     my $resp = FEE_PAID_RESP;
1134
1135     my $disallow_overpayment  = $server->{account}->{disallow_overpayment};
1136     my $payment_type_writeoff = $server->{account}->{payment_type_writeoff} || q{};
1137     my $register_id           = $server->{account}->{register_id};
1138
1139     my $is_writeoff = $pay_type eq $payment_type_writeoff;
1140
1141     $fee_amt    = $fields->{ (FID_FEE_AMT) };
1142     $inst_id    = $fields->{ (FID_INST_ID) };
1143     $patron_id  = $fields->{ (FID_PATRON_ID) };
1144     $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1145     $fee_id     = $fields->{ (FID_FEE_ID) };
1146     $trans_id   = $fields->{ (FID_TRANSACTION_ID) };
1147
1148     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1149
1150     $ils->check_inst_id( $inst_id, "handle_fee_paid" );
1151
1152     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 );
1153     $status = $pay_result->{status};
1154     my $pay_response = $pay_result->{pay_response};
1155
1156     my $failmap = {
1157         "no_item" => "No matching item could be found",
1158         "no_checkout" => "Item is not checked out",
1159         "too_soon" => "Cannot yet be renewed",
1160         "too_many" => "Renewed the maximum number of times",
1161         "auto_too_soon" => "Scheduled for automatic renewal and cannot yet be renewed",
1162         "auto_too_late" => "Scheduled for automatic renewal and cannot yet be any more",
1163         "auto_account_expired" => "Scheduled for automatic renewal and cannot be renewed because the patron's account has expired",
1164         "auto_renew" => "Scheduled for automatic renewal",
1165         "auto_too_much_oweing" => "Scheduled for automatic renewal",
1166         "on_reserve" => "On hold for another patron",
1167         "patron_restricted" => "Patron is currently restricted",
1168         "item_denied_renewal" => "Item is not allowed renewal",
1169         "onsite_checkout" => "Item is an onsite checkout"
1170     };
1171     my @success = ();
1172     my @fail = ();
1173     foreach my $result( @{$pay_response->{renew_result}} ) {
1174         my $item = Koha::Items->find({ itemnumber => $result->{itemnumber} });
1175         if ($result->{success}) {
1176             push @success, '"' . $item->biblio->title . '"';
1177         } else {
1178             push @fail, '"' . $item->biblio->title . '" : ' . $failmap->{$result->{error}};
1179         }
1180     }
1181
1182     my $msg = "";
1183     if (scalar @success > 0) {
1184         $msg.="The following items were renewed: " . join(", ", @success) . ". ";
1185     }
1186     if (scalar @fail > 0) {
1187         $msg.="The following items were not renewed: " . join(", ", @fail) . ".";
1188     }
1189     if (length $msg > 0) {
1190         $status->screen_msg($status->screen_msg . " $msg");
1191     }
1192
1193     $resp .= ( $status->ok ? 'Y' : 'N' ) . timestamp;
1194     $resp .= add_field( FID_INST_ID,   $inst_id, $server );
1195     $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
1196     $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id, $server );
1197     $resp .= maybe_add( FID_SCREEN_MSG,     $status->screen_msg, $server );
1198     $resp .= maybe_add( FID_PRINT_LINE,     $status->print_line, $server );
1199
1200     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1201
1202     return (FEE_PAID);
1203 }
1204
1205 sub handle_item_information {
1206     my ( $self, $server ) = @_;
1207     my $account = $server->{account};
1208     my $ils     = $server->{ils};
1209     my $fields  = $self->{fields};
1210     my $resp    = ITEM_INFO_RESP;
1211     my $trans_date;
1212     my $item;
1213     my $i;
1214
1215     ($trans_date) = @{ $self->{fixed_fields} };
1216
1217     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_item_information" );
1218
1219     $item = $ils->find_item( $fields->{ (FID_ITEM_ID) } );
1220
1221     if ( !defined($item) ) {
1222
1223         # Invalid Item ID
1224         # "Other" circ stat, "Other" security marker, "Unknown" fee type
1225         $resp .= "010101";
1226         $resp .= timestamp;
1227
1228         # Just echo back the invalid item id
1229         $resp .= add_field( FID_ITEM_ID, $fields->{ (FID_ITEM_ID) }, $server );
1230
1231         # title id is required, but we don't have one
1232         $resp .= add_field( FID_TITLE_ID, '', $server );
1233     } else {
1234
1235         # Valid Item ID, send the good stuff
1236         my $circulation_status = $item->sip_circulation_status;
1237         $resp .= $circulation_status;
1238         $resp .= $item->sip_security_marker;
1239         $resp .= $item->sip_fee_type;
1240         $resp .= timestamp;
1241
1242         if ( $circulation_status eq '01' ) {
1243             $resp .= maybe_add( FID_SCREEN_MSG, "Item is damaged", $server );
1244         }
1245
1246         $resp .= add_field( FID_ITEM_ID,  $item->id, $server );
1247         $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
1248
1249         $resp .= maybe_add( FID_MEDIA_TYPE,   $item->sip_media_type, $server );
1250         $resp .= maybe_add( FID_PERM_LOCN,    $item->permanent_location, $server );
1251         $resp .= maybe_add( FID_CURRENT_LOCN, $item->current_location, $server );
1252         $resp .= maybe_add( FID_ITEM_PROPS,   $item->sip_item_properties, $server );
1253
1254
1255         if ( my $CR = $server->{account}->{cr_item_field} ) {
1256                 $resp .= maybe_add( FID_COLLECTION_CODE, $item->{$CR}, $server );
1257         } else {
1258           $resp .= maybe_add( FID_COLLECTION_CODE, $item->collection_code, $server );
1259         }
1260
1261         if ( ( $i = $item->fee ) != 0 ) {
1262             $resp .= add_field( FID_CURRENCY, $item->fee_currency, $server );
1263             $resp .= add_field( FID_FEE_AMT,  $i, $server );
1264         }
1265         $resp .= maybe_add( FID_OWNER, $item->owner, $server );
1266
1267         if ( ( $i = scalar @{ $item->hold_queue } ) > 0 ) {
1268             $resp .= add_field( FID_HOLD_QUEUE_LEN, $i, $server );
1269         }
1270         if ( $item->due_date ) {
1271             my $due_date =
1272               $account->{format_due_date}
1273               ? output_pref( { str => $item->due_date, as_due_date => 1 } )
1274               : timestamp( $item->due_date );
1275             $resp .= add_field( FID_DUE_DATE, $due_date, $server );
1276         }
1277         if ( ( $i = $item->recall_date ) != 0 ) {
1278             $resp .= add_field( FID_RECALL_DATE, timestamp($i), $server );
1279         }
1280         if ( ( $i = $item->hold_pickup_date ) != 0 ) {
1281             $resp .= add_field( FID_HOLD_PICKUP_DATE, timestamp($i), $server );
1282         }
1283
1284         $resp .= maybe_add( FID_SCREEN_MSG, $item->screen_msg, $server );
1285         $resp .= maybe_add( FID_PRINT_LINE, $item->print_line, $server );
1286
1287         $resp .= $item->build_additional_item_fields_string( $server );
1288         $resp .= $item->build_custom_field_string( $server );
1289     }
1290
1291     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1292
1293     return (ITEM_INFORMATION);
1294 }
1295
1296 sub handle_item_status_update {
1297     my ( $self, $server ) = @_;
1298     my $ils = $server->{ils};
1299     my ( $trans_date, $item_id, $terminal_pwd, $item_props );
1300     my $fields = $self->{fields};
1301     my $status;
1302     my $item;
1303     my $resp = ITEM_STATUS_UPDATE_RESP;
1304
1305     ($trans_date) = @{ $self->{fixed_fields} };
1306
1307     $ils->check_inst_id( $fields->{ (FID_INST_ID) } );
1308
1309     $item_id    = $fields->{ (FID_ITEM_ID) };
1310     $item_props = $fields->{ (FID_ITEM_PROPS) };
1311
1312     if ( !defined($item_id) ) {
1313         siplog( "LOG_WARNING", "handle_item_status: received message without Item ID field" );
1314     } else {
1315         $item = $ils->find_item($item_id);
1316     }
1317
1318     if ( !$item ) {
1319
1320         # Invalid Item ID
1321         $resp .= '0';
1322         $resp .= timestamp;
1323         $resp .= add_field( FID_ITEM_ID, $item_id, $server );
1324     } else {
1325
1326         # Valid Item ID
1327
1328         $status = $item->status_update($item_props);
1329
1330         $resp .= $status->ok ? '1' : '0';
1331         $resp .= timestamp;
1332
1333         $resp .= add_field( FID_ITEM_ID,  $item->id, $server );
1334         $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
1335         $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
1336     }
1337
1338     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1339     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1340
1341     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1342
1343     return (ITEM_STATUS_UPDATE);
1344 }
1345
1346 sub handle_patron_enable {
1347     my ( $self, $server ) = @_;
1348     my $ils    = $server->{ils};
1349     my $fields = $self->{fields};
1350     my ( $trans_date, $patron_id, $terminal_pwd, $patron_pwd );
1351     my ( $status, $patron );
1352     my $resp = PATRON_ENABLE_RESP;
1353
1354     ($trans_date) = @{ $self->{fixed_fields} };
1355     $patron_id  = $fields->{ (FID_PATRON_ID) };
1356     $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1357
1358     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1359
1360     siplog( "LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'", $patron_id, $patron_pwd );
1361
1362     $patron = $ils->find_patron($patron_id);
1363
1364     if ( !defined($patron) ) {
1365
1366         # Invalid patron ID
1367         $resp .= 'YYYY' . ( ' ' x 10 ) . '000' . timestamp();
1368         $resp .= add_field( FID_PATRON_ID,        $patron_id, $server );
1369         $resp .= add_field( FID_PERSONAL_NAME,    '', $server );
1370         $resp .= add_field( FID_VALID_PATRON,     'N', $server );
1371         $resp .= add_field( FID_VALID_PATRON_PWD, 'N', $server );
1372     } else {
1373
1374         # valid patron
1375         if ( !defined($patron_pwd) || $patron->check_password($patron_pwd) ) {
1376
1377             # Don't enable the patron if there was an invalid password
1378             $status = $patron->enable;
1379         }
1380         $resp .= patron_status_string( $patron, $server );
1381         $resp .= $patron->language . timestamp();
1382
1383         $resp .= add_field( FID_PATRON_ID,     $patron->id, $server );
1384         $resp .= add_field( FID_PERSONAL_NAME, $patron->format( $server->{account}->{ae_field_template} ), $server );
1385         if ( defined($patron_pwd) ) {
1386             $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password($patron_pwd) ), $server );
1387         }
1388         $resp .= add_field( FID_VALID_PATRON, 'Y', $server );
1389         $resp .= maybe_add( FID_SCREEN_MSG, $patron->screen_msg, $server );
1390         $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line, $server );
1391     }
1392
1393     $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1394
1395     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1396
1397     return (PATRON_ENABLE);
1398 }
1399
1400 sub handle_hold {
1401     my ( $self, $server ) = @_;
1402     my $ils = $server->{ils};
1403     my ( $hold_mode, $trans_date );
1404     my ( $expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd );
1405     my ( $item_id, $title_id, $fee_ack );
1406     my $fields = $self->{fields};
1407     my $status;
1408     my $resp = HOLD_RESP;
1409
1410     ( $hold_mode, $trans_date ) = @{ $self->{fixed_fields} };
1411
1412     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_hold" );
1413
1414     $patron_id   = $fields->{ (FID_PATRON_ID) };
1415     $expiry_date = $fields->{ (FID_EXPIRATION) } || '';
1416     $pickup_locn = $fields->{ (FID_PICKUP_LOCN) } || '';
1417     $hold_type   = $fields->{ (FID_HOLD_TYPE) } || '2';    # Any copy of title
1418     $patron_pwd  = $fields->{ (FID_PATRON_PWD) };
1419     $item_id     = $fields->{ (FID_ITEM_ID) } || '';
1420     $title_id    = $fields->{ (FID_TITLE_ID) } || '';
1421     $fee_ack     = $fields->{ (FID_FEE_ACK) } || 'N';
1422
1423     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1424
1425     if ( $hold_mode eq '+' ) {
1426         $status = $ils->add_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1427     } elsif ( $hold_mode eq '-' ) {
1428         $status = $ils->cancel_hold( $patron_id, $patron_pwd, $item_id, $title_id );
1429     } elsif ( $hold_mode eq '*' ) {
1430         $status = $ils->alter_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1431     } else {
1432         siplog( "LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'", $hold_mode, $server->{account}->{id} );
1433         $status = $ils->Transaction::Hold;    # new?
1434         $status->screen_msg("System error. Please contact library staff.");
1435     }
1436
1437     $resp .= $status->ok;
1438     $resp .= sipbool( $status->item && $status->item->available($patron_id) );
1439     $resp .= timestamp;
1440
1441     if ( $status->ok ) {
1442         $resp .= add_field( FID_PATRON_ID, $status->patron->id, $server );
1443
1444         ( $status->expiration_date )
1445           and $resp .= maybe_add( FID_EXPIRATION, timestamp( $status->expiration_date ), $server );
1446         $resp .= maybe_add( FID_QUEUE_POS,   $status->queue_position, $server );
1447         $resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location, $server );
1448         $resp .= maybe_add( FID_ITEM_ID,     $status->item->id, $server );
1449         $resp .= maybe_add( FID_TITLE_ID,    $status->item->title_id, $server );
1450     } else {
1451
1452         # Not ok.  still need required fields
1453         $resp .= add_field( FID_PATRON_ID, $patron_id, $server );
1454     }
1455
1456     $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1457     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1458     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1459
1460     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1461
1462     return (HOLD);
1463 }
1464
1465 sub handle_renew {
1466     my ( $self, $server ) = @_;
1467     my $ils = $server->{ils};
1468     my ( $third_party, $no_block, $trans_date, $nb_due_date );
1469     my ( $patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack );
1470     my $fields = $self->{fields};
1471     my $status;
1472     my ( $patron, $item );
1473     my $resp = RENEW_RESP;
1474
1475     ( $third_party, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
1476
1477     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew" );
1478
1479     if ( $no_block eq 'Y' ) {
1480         siplog( "LOG_WARNING", "handle_renew: received 'no block' renewal from terminal '%s'", $server->{account}->{id} );
1481     }
1482
1483     $patron_id  = $fields->{ (FID_PATRON_ID) };
1484     $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1485     $item_id    = $fields->{ (FID_ITEM_ID) };
1486     $title_id   = $fields->{ (FID_TITLE_ID) };
1487     $item_props = $fields->{ (FID_ITEM_PROPS) };
1488     $fee_ack    = $fields->{ (FID_FEE_ACK) };
1489
1490     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1491
1492     $status = $ils->renew( $patron_id, $patron_pwd, $item_id, $title_id, $no_block, $nb_due_date, $third_party, $item_props, $fee_ack );
1493
1494     $patron = $status->patron;
1495     $item   = $status->item;
1496
1497     if ( $status->renewal_ok ) {
1498         $resp .= '1';
1499         $resp .= $status->renewal_ok ? 'Y' : 'N';
1500         if ( $ils->supports('magnetic media') ) {
1501             $resp .= sipbool( $item->magnetic_media );
1502         } else {
1503             $resp .= 'U';
1504         }
1505         $resp .= sipbool( desensitize( { status => $status, patron => $patron, server => $server } ) );
1506         $resp .= timestamp;
1507         $resp .= add_field( FID_PATRON_ID, $patron->id, $server );
1508         $resp .= add_field( FID_ITEM_ID, $item->id, $server );
1509         $resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
1510         if ( $item->due_date ) {
1511             $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ), $server );
1512         } else {
1513             $resp .= add_field( FID_DUE_DATE, q{}, $server );
1514         }
1515         if ( $ils->supports('security inhibit') ) {
1516             $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit, $server );
1517         }
1518         $resp .= add_field( FID_MEDIA_TYPE, $item->sip_media_type, $server );
1519         $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
1520     } else {
1521
1522         # renew failed for some reason
1523         # not OK, renewal not OK, Unknown media type (why bother checking?)
1524         $resp .= '0NUN';
1525         $resp .= timestamp;
1526
1527         # If we found the patron or the item, the return the ILS
1528         # information, otherwise echo back the information we received
1529         # from the terminal
1530         $resp .= add_field( FID_PATRON_ID, $patron ? $patron->id     : $patron_id, $server );
1531         $resp .= add_field( FID_ITEM_ID,   $item   ? $item->id       : $item_id, $server );
1532         $resp .= add_field( FID_TITLE_ID,  $item   ? $item->title_id : $title_id, $server );
1533         $resp .= add_field( FID_DUE_DATE,  '', $server );
1534     }
1535
1536     if ( $status->fee_amount ) {
1537         $resp .= add_field( FID_FEE_AMT, $status->fee_amount, $server );
1538         $resp .= maybe_add( FID_CURRENCY,       $status->sip_currency, $server );
1539         $resp .= maybe_add( FID_FEE_TYPE,       $status->sip_fee_type, $server );
1540         $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id, $server );
1541     }
1542
1543     $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1544     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1545     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1546
1547     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1548
1549     return (RENEW);
1550 }
1551
1552 sub handle_renew_all {
1553
1554     # my ($third_party, $no_block, $nb_due_date, $fee_ack, $patron);
1555
1556     my ( $self, $server ) = @_;
1557     my $ils = $server->{ils};
1558     my ( $trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack );
1559     my $fields = $self->{fields};
1560     my $resp   = RENEW_ALL_RESP;
1561     my $status;
1562     my ( @renewed, @unrenewed );
1563
1564     $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew_all" );
1565
1566     ($trans_date) = @{ $self->{fixed_fields} };
1567
1568     $patron_id    = $fields->{ (FID_PATRON_ID) };
1569     $patron_pwd   = $fields->{ (FID_PATRON_PWD) };
1570     $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
1571     $fee_ack      = $fields->{ (FID_FEE_ACK) };
1572
1573     Koha::Plugins->call('patron_barcode_transform', \$patron_id );
1574
1575     $status = $ils->renew_all( $patron_id, $patron_pwd, $fee_ack );
1576
1577     $resp .= $status->ok ? '1' : '0';
1578
1579     if ( !$status->ok ) {
1580         $resp .= add_count( "renew_all/renewed_count",   0 );
1581         $resp .= add_count( "renew_all/unrenewed_count", 0 );
1582         @renewed   = ();
1583         @unrenewed = ();
1584     } else {
1585         @renewed   = ( @{ $status->renewed } );
1586         @unrenewed = ( @{ $status->unrenewed } );
1587         $resp .= add_count( "renew_all/renewed_count",   scalar @renewed );
1588         $resp .= add_count( "renew_all/unrenewed_count", scalar @unrenewed );
1589     }
1590
1591     $resp .= timestamp;
1592     $resp .= add_field( FID_INST_ID, $ils->institution, $server );
1593
1594     $resp .= join( '', map( add_field( FID_RENEWED_ITEMS,   $_ ), @renewed ), $server );
1595     $resp .= join( '', map( add_field( FID_UNRENEWED_ITEMS, $_ ), @unrenewed ), $server );
1596
1597     $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1598     $resp .= maybe_add( FID_PRINT_LINE, $status->print_line, $server );
1599
1600     $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1601
1602     return (RENEW_ALL);
1603 }
1604
1605 #
1606 # send_acs_status($self, $server)
1607 #
1608 # Send an ACS Status message, which is contains lots of little fields
1609 # of information gleaned from all sorts of places.
1610 #
1611
1612 my @message_type_names = (
1613     "patron status request",
1614     "checkout",
1615     "checkin",
1616     "block patron",
1617     "acs status",
1618     "request sc/acs resend",
1619     "login",
1620     "patron information",
1621     "end patron session",
1622     "fee paid",
1623     "item information",
1624     "item status update",
1625     "patron enable",
1626     "hold",
1627     "renew",
1628     "renew all",
1629 );
1630
1631 sub send_acs_status {
1632     my ( $self, $server, $screen_msg, $print_line ) = @_;
1633
1634     my $msg = ACS_STATUS;
1635     ($server) or die "send_acs_status error: no \$server argument received";
1636     my $account = $server->{account} or die "send_acs_status error: no 'account' in \$server object:\n" . Dumper($server);
1637     my $policy  = $server->{policy}  or die "send_acs_status error: no 'policy' in \$server object:\n" . Dumper($server);
1638     my $ils     = $server->{ils}     or die "send_acs_status error: no 'ils' in \$server object:\n" . Dumper($server);
1639     my $sip_username = $server->{sip_username} or die "send_acs_status error: no 'sip_username' in \$server object:\n" . Dumper($server);
1640     my ( $online_status,    $checkin_ok, $checkout_ok, $ACS_renewal_policy );
1641     my ( $status_update_ok, $offline_ok, $timeout,     $retries );
1642     my $sip_user = Koha::Patrons->find({ userid => $sip_username });
1643     die "send_acs_status error: sip_username cannot be found in DB or DB cannot be reached" unless $sip_user;
1644
1645     $online_status      = 'Y';
1646     $checkout_ok        = sipbool( $ils->checkout_ok );
1647     $checkin_ok         = sipbool( $ils->checkin_ok );
1648     $ACS_renewal_policy = sipbool( $policy->{renewal} );
1649     $status_update_ok   = sipbool( $ils->status_update_ok );
1650     $offline_ok         = sipbool( $ils->offline_ok );
1651     $timeout            = $server->get_timeout({ policy => 1 });
1652     $retries            = sprintf( "%03d", $policy->{retries} );
1653
1654     if ( length($retries) != 3 ) {
1655         siplog( "LOG_ERR", "handle_acs_status: retries field wrong size: '%s'", $retries );
1656         $retries = '000';
1657     }
1658
1659     $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
1660     $msg .= "$status_update_ok$offline_ok$timeout$retries";
1661     $msg .= timestamp();
1662
1663     if ( $protocol_version == 1 ) {
1664         $msg .= '1.00';
1665     } elsif ( $protocol_version == 2 ) {
1666         $msg .= '2.00';
1667     } else {
1668         siplog( "LOG_ERR", 'Bad setting for $protocol_version, "%s" in send_acs_status', $protocol_version );
1669         $msg .= '1.00';
1670     }
1671
1672     # Institution ID
1673     $msg .= add_field( FID_INST_ID, $account->{institution}, $server );
1674
1675     if ( $protocol_version >= 2 ) {
1676
1677         # Supported messages: we do it all
1678         my $supported_msgs = '';
1679
1680         foreach my $msg_name (@message_type_names) {
1681             if ( $msg_name eq 'request sc/acs resend' ) {
1682                 $supported_msgs .= sipbool(1);
1683             } else {
1684                 $supported_msgs .= sipbool( $ils->supports($msg_name) );
1685             }
1686         }
1687         if ( length($supported_msgs) < 16 ) {
1688             siplog( "LOG_ERR", 'send_acs_status: supported messages "%s" too short', $supported_msgs );
1689         }
1690         $msg .= add_field( FID_SUPPORTED_MSGS, $supported_msgs, $server );
1691     }
1692
1693     $msg .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1694
1695     if (   defined( $account->{print_width} )
1696         && defined($print_line)
1697         && $account->{print_width} < length($print_line) ) {
1698         siplog( "LOG_WARNING", "send_acs_status: print line '%s' too long.  Truncating", $print_line );
1699         $print_line = substr( $print_line, 0, $account->{print_width} );
1700     }
1701
1702     $msg .= maybe_add( FID_PRINT_LINE, $print_line, $server );
1703
1704     # Do we want to tell the terminal its location?
1705
1706     $self->write_msg( $msg, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1707     return 1;
1708 }
1709
1710 #
1711 # build_patron_status: create the 14-char patron status
1712 # string for the Patron Status message
1713 #
1714 sub patron_status_string {
1715     my $patron = shift;
1716     my $server = shift;
1717
1718     my $patron_status;
1719
1720     siplog( "LOG_DEBUG", "patron_status_string: %s charge_ok: %s", $patron->id, $patron->charge_ok );
1721     $patron_status = sprintf(
1722         '%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
1723         denied( $patron->charge_ok ),
1724         denied( $patron->renew_ok ),
1725         denied( $patron->recall_ok ),
1726         denied( $patron->hold_ok ),
1727         boolspace( $patron->card_lost ),
1728         boolspace( $patron->too_many_charged ),
1729         $server->{account}->{overdues_block_checkout} ? boolspace( $patron->too_many_overdue ) : q{ },
1730         boolspace( $patron->too_many_renewal ),
1731         boolspace( $patron->too_many_claim_return ),
1732         boolspace( $patron->too_many_lost ),
1733         boolspace( $patron->excessive_fines ),
1734         boolspace( $patron->excessive_fees ),
1735         boolspace( $patron->recall_overdue ),
1736         boolspace( $patron->too_many_billed )
1737     );
1738     return $patron_status;
1739 }
1740
1741 sub api_auth {
1742     my ( $username, $password, $branch ) = @_;
1743     $ENV{REMOTE_USER} = $username;
1744     my $query = CGI->new();
1745     $query->param( userid   => $username );
1746     $query->param( password => $password );
1747     if ($branch) {
1748         $query->param( branch => $branch );
1749     }
1750     my ( $status, $cookie, $sessionID ) = check_api_auth( $query, { circulate => 1 }, 'intranet' );
1751     return $status;
1752 }
1753
1754 sub desensitize {
1755     my ($params) = @_;
1756
1757     my $status      = $params->{status};
1758     my $desensitize = $status->desensitize();
1759
1760     # If desenstize is already false, no need to do anything
1761     return unless $desensitize;
1762
1763     my $patron = $params->{patron};
1764     my $item   = $params->{item};
1765     my $server = $params->{server};
1766
1767     my $patron_categories = $server->{account}->{inhouse_patron_categories} // q{};
1768     my $item_types = $server->{account}->{inhouse_item_types} // q{};
1769
1770     # If no patron categories or item types are set for never desensitize, no need to do anything
1771     return $desensitize unless $patron_categories || $item_types;
1772
1773     my $patron_category = $patron->ptype();
1774     my @patron_categories = split( /,/, $patron_categories );
1775     my $found_patron_category = grep( /^$patron_category$/, @patron_categories );
1776     return 0 if $found_patron_category;
1777
1778     my $item_type = $item->itemtype;
1779     my @item_types = split( /,/, $item_types );
1780     my $found_item_type = grep( /^$item_type$/, @item_types );
1781     return 0 if $found_item_type;
1782
1783     return 1;
1784 }
1785
1786 1;
1787 __END__
1788