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