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