Add OILS_SIP_MSG_BILL_ERR for when an error occurs getting bills.
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / SIP.pm
1 #
2 # ILS.pm: Test ILS interface module
3 #
4
5 package OpenILS::SIP;
6 use warnings; use strict;
7
8 use Sys::Syslog qw(syslog);
9 use Time::HiRes q/time/;
10
11 use OpenILS::SIP::Item;
12 use OpenILS::SIP::Patron;
13 use OpenILS::SIP::Transaction;
14 use OpenILS::SIP::Transaction::Checkout;
15 use OpenILS::SIP::Transaction::Checkin;
16 use OpenILS::SIP::Transaction::Renew;
17 use OpenILS::SIP::Transaction::FeePayment;
18
19 use OpenSRF::System;
20 use OpenILS::Utils::Fieldmapper;
21 use OpenSRF::Utils::SettingsClient;
22 use OpenILS::Application::AppUtils;
23 use OpenSRF::Utils qw/:datetime/;
24 use DateTime::Format::ISO8601;
25 use Encode;
26 use Unicode::Normalize;
27 my $U = 'OpenILS::Application::AppUtils';
28
29 my $editor;
30 my $config;
31 my $target_encoding;    # FIXME: this is configured at the institution level. 
32
33 use Digest::MD5 qw(md5_hex);
34
35 sub new {
36         my ($class, $institution, $login) = @_;
37         my $type = ref($class) || $class;
38         my $self = {};
39
40         $self->{login} = $login;
41
42         $config = $institution;
43         syslog("LOG_DEBUG", "OILS: new ILS '%s'", $institution->{id});
44         $self->{institution} = $institution;
45
46         my $bsconfig     = $institution->{implementation_config}->{bootstrap};
47         $target_encoding = $institution->{implementation_config}->{encoding} || 'ascii';
48
49         syslog('LOG_DEBUG', "OILS: loading bootstrap config: $bsconfig");
50         
51         local $/ = "\n";    # why?
52         OpenSRF::System->bootstrap_client(config_file => $bsconfig);
53         syslog('LOG_DEBUG', "OILS: bootstrap loaded..");
54
55         $self->{osrf_config} = OpenSRF::Utils::SettingsClient->new;
56
57         Fieldmapper->import($self->{osrf_config}->config_value('IDL'));
58
59         bless( $self, $type );
60
61         return undef unless 
62                 $self->login( $login->{id}, $login->{password} );
63
64         return $self;
65 }
66
67 sub fetch_session {
68     my $self = shift;
69
70         my $ses = $U->simplereq( 
71                 'open-ils.auth',
72                 'open-ils.auth.session.retrieve',  $self->{authtoken});
73
74     return undef if $U->event_code($ses); # auth timed out
75     return $self->{login_session} = $ses;
76 }
77
78 sub verify_session {
79         my $self = shift;
80
81     return 1 if $self->fetch_session;
82
83     syslog('LOG_INFO', "OILS: Logging back after session timeout as user ".$self->{login}->{id});
84     return $self->login( $self->{login}->{id}, $self->{login}->{password} );
85 }
86
87 sub editor {
88         return $editor = make_editor();
89 }
90
91 sub config {
92         return $config;
93 }
94
95 sub get_option_value {
96     my($self, $option) = @_;
97     my $ops = $config->{implementation_config}->{options}->{option};
98     $ops = [$ops] unless ref $ops eq 'ARRAY';
99     my @vals = grep { $_->{name} eq $option } @$ops;
100     return @vals ? $vals[0]->{value} : undef;
101 }
102
103
104 # Creates the global editor object
105 my $cstore_init = 1; # call init on first use
106 sub make_editor {
107     OpenILS::Utils::CStoreEditor::init() if $cstore_init;
108     $cstore_init = 0;
109         return OpenILS::Utils::CStoreEditor->new;
110 }
111
112 =head2 clean_text(scalar)
113
114 Evergreen uses the UTF8 encoding for everything from the database up. Perl
115 doesn't know this, however, so we have to convince it to treat our UTF8 strings
116 as UTF8 strings. This may enable OpenNCIP to correctly calculate the checksums
117 for UTF8 text for SIP clients that support such modern options.
118
119 The target encoding is set in the <encoding> element of the SIPServer.pm
120 configuration file.
121
122 =cut
123
124 sub clean_text {
125     my $text = shift || '';
126
127     # Convert our incoming UTF8 data into Perl's internal string format
128
129     # Also convert to Normalization Form D, as the ASCII, iso-8859-1,
130     # and latin-1 encodings (at least) require this to substitute
131     # characters rather than simply returning a string truncated
132     # after the first non-ASCII character
133     $text = NFD(decode_utf8($text));
134
135     if ($target_encoding eq 'ascii') {
136
137         # Try to maintain a reasonable version of the content by
138         # stripping diacritics from the text, given that the SIP client
139         # wants just plain ASCII. This is the base requirement according
140         # to the SIP2 specification.
141
142         # Stripping the combining characters converts ""béè♁ts"
143         # into "bee?ts" instead of "b???ts" - better, eh?
144         $text =~ s/\pM+//og;
145     }
146
147     # Characters that cannot be represented in the target encoding will
148     # generally be replaced with a question mark (?) character.
149     $text = encode($target_encoding, $text);
150
151     return $text;
152 }
153
154 my %org_sn_cache;
155 sub shortname_from_id {
156     my $id = shift or return;
157     return $id->shortname if ref $id;
158     return $org_sn_cache{$id} if $org_sn_cache{$id};
159     return $org_sn_cache{$id} = editor()->retrieve_actor_org_unit($id)->shortname;
160 }
161 sub patron_barcode_from_id {
162     my $id = shift or return;
163     return editor()->search_actor_card({ usr => $id, active => 't' })->[0]->barcode;
164 }
165
166 sub format_date {
167         my $class = shift;
168         my $date = shift;
169         my $type = shift || 'dob';
170
171         return "" unless $date;
172
173         $date = DateTime::Format::ISO8601->new->
174                 parse_datetime(OpenSRF::Utils::cleanse_ISO8601($date));
175         my @time = localtime($date->epoch);
176
177         my $year   = $time[5]+1900;
178         my $mon    = $time[4]+1;
179         my $day    = $time[3];
180         my $hour   = $time[2];
181         my $minute = $time[1];
182         my $second = $time[0];
183   
184         $date = sprintf("%04d%02d%02d", $year, $mon, $day);
185
186         # Due dates need hyphen separators and time of day as well
187         if ($type eq 'due') {
188                 $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year, $mon, $day, $hour, $minute, $second);
189         }
190
191         syslog('LOG_DEBUG', "OILS: formatted date [type=$type]: $date");
192         return $date;
193 }
194
195
196
197 sub login {
198         my( $self, $username, $password ) = @_;
199         syslog('LOG_DEBUG', "OILS: Logging in with username $username");
200
201         my $seed = $U->simplereq( 
202                 'open-ils.auth',
203                 'open-ils.auth.authenticate.init', $username );
204
205         my $response = $U->simplereq(
206                 'open-ils.auth', 
207                 'open-ils.auth.authenticate.complete', 
208                 {       
209                         username => $username, 
210                         password => md5_hex($seed . md5_hex($password)), 
211                         type     => 'opac',
212                 }
213         );
214
215         if( my $code = $U->event_code($response) ) {
216                 my $txt = $response->{textcode};
217                 syslog('LOG_WARNING', "OILS: Login failed for $username.  $txt:$code");
218                 return undef;
219         }
220
221         my $key = $response->{payload}->{authtoken};
222         syslog('LOG_INFO', "OILS: Login succeeded for $username : authkey = $key");
223
224     $self->fetch_session; # to cache the login
225
226         return $self->{authtoken} = $key;
227 }
228
229
230 sub find_patron {
231         my $self = shift;
232         return OpenILS::SIP::Patron->new(@_);
233 }
234
235
236 sub find_item {
237         my $self = shift;
238         return OpenILS::SIP::Item->new(@_);
239 }
240
241
242 sub institution {
243     my $self = shift;
244     return $self->{institution}->{id};  # consider making this return the whole institution
245 }
246
247 sub institution_id {
248     my $self = shift;
249     return $self->{institution}->{id};  # then use this for just the ID
250 }
251
252 sub supports {
253         my ($self, $op) = @_;
254         my ($i) = grep { $_->{name} eq $op }  
255                 @{$config->{implementation_config}->{supports}->{item}};
256         return to_bool($i->{value});
257 }
258
259 sub check_inst_id {
260     my ($self, $id, $whence) = @_;
261     if ($id ne $self->{institution}->{id}) {
262         syslog("LOG_WARNING", "OILS: %s: received institution '%s', expected '%s'", $whence, $id, $self->{institution}->{id});
263         # Just an FYI check, we don't expect the user to change location from that in SIPconfig.xml
264     }
265 }
266
267
268 sub to_bool {
269     my $bool = shift;
270     # If it's defined, and matches a true sort of string, or is
271     # a non-zero number, then it's true.
272     defined($bool) or return;                   # false
273     ($bool =~ /true|y|yes/i) and return 1;      # true
274     return ($bool =~ /^\d+$/ and $bool != 0);   # true for non-zero numbers, false otherwise
275 }
276
277 sub checkout_ok {
278         return to_bool($config->{policy}->{checkout});
279 }
280
281 sub checkin_ok {
282         return to_bool($config->{policy}->{checkin});
283 }
284
285 sub renew_ok {
286         return to_bool($config->{policy}->{renew});
287 }
288
289 sub status_update_ok {
290         return to_bool($config->{policy}->{status_update});
291 }
292
293 sub offline_ok {
294         return to_bool($config->{policy}->{offline});
295 }
296
297
298
299 ##
300 ## Checkout(patron_id, item_id, sc_renew):
301 ##    patron_id & item_id are the identifiers send by the terminal
302 ##    sc_renew is the renewal policy configured on the terminal
303 ## returns a status opject that can be queried for the various bits
304 ## of information that the protocol (SIP or NCIP) needs to generate
305 ## the response.
306 ##
307
308 sub checkout {
309         my ($self, $patron_id, $item_id, $sc_renew) = @_;
310         $sc_renew = 0;
311
312         $self->verify_session;
313
314         syslog('LOG_DEBUG', "OILS: OpenILS::Checkout attempt: patron=$patron_id, item=$item_id");
315
316     my $xact   = OpenILS::SIP::Transaction::Checkout->new( authtoken => $self->{authtoken} );
317     my $patron = $self->find_patron($patron_id);
318     my $item   = $self->find_item($item_id);
319
320         $xact->patron($patron);
321         $xact->item($item);
322
323         if (!$patron) {
324                 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
325                 return $xact;
326         }
327
328         if (!$patron->charge_ok) {
329                 $xact->screen_msg("Patron Blocked");
330                 return $xact;
331         }
332
333         if( !$item ) {
334                 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
335                 return $xact;
336         }
337
338         syslog('LOG_DEBUG', "OILS: OpenILS::Checkout data loaded OK, checking out...");
339
340         if ($item->{patron} && ($item->{patron} eq $patron_id)) {
341                 syslog('LOG_INFO', "OILS: OpenILS::Checkout data loaded OK, doing renew...");
342                 $sc_renew = 1;
343         } elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
344                 # I can't deal with this right now
345                 # XXX check in then check out?
346                 $xact->screen_msg("Item checked out to another patron");
347                 $xact->ok(0);
348         } 
349
350         $xact->do_checkout($sc_renew);
351         $xact->desensitize(!$item->magnetic);
352
353         if( $xact->ok ) {
354                 #editor()->commit;
355                 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
356                         "patron %s checkout %s succeeded", $patron_id, $item_id);
357         } else {
358                 #editor()->xact_rollback;
359                 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
360                         "patron %s checkout %s FAILED, rolling back xact...", $patron_id, $item_id);
361         }
362
363         return $xact;
364 }
365
366
367 sub checkin {
368         my ($self, $item_id, $inst_id, $trans_date, $return_date,
369         $current_loc, $item_props, $cancel) = @_;
370
371     my $start_time = time();
372
373         $self->verify_session;
374
375         syslog('LOG_DEBUG', "OILS: OpenILS::Checkin of item=$item_id (to $inst_id)");
376         
377     my $xact = OpenILS::SIP::Transaction::Checkin->new(authtoken => $self->{authtoken});
378     my $item = OpenILS::SIP::Item->new($item_id);
379
380     unless ( $xact->item($item) ) {
381         $xact->ok(0);
382         # $circ->alert(1); $circ->alert_type(99);
383         $xact->screen_msg("Invalid Item Barcode: '$item_id'");
384         syslog('LOG_INFO', "OILS: Checkin failed.  " . $xact->screen_msg() );
385         return $xact;
386     }
387
388         $xact->do_checkin( $self, $inst_id, $trans_date, $return_date, $current_loc, $item_props );
389         
390         if ($xact->ok) {
391         $xact->patron($self->find_patron(usr => $xact->{circ_user_id}, slim_user => 1)) if $xact->{circ_user_id};
392         delete $item->{patron};
393         delete $item->{due_date};
394         syslog('LOG_INFO', "OILS: Checkin succeeded");
395     } else {
396         syslog('LOG_WARNING', "OILS: Checkin failed");
397     }
398
399     syslog('LOG_INFO', "OILS: SIP Checkin request took %0.3f seconds", (time() - $start_time));
400         return $xact;
401 }
402
403 ## If the ILS caches patron information, this lets it free it up.
404 ## Also, this could be used for centrally logging session duration.
405 ## We don't do anything with it.
406 sub end_patron_session {
407     my ($self, $patron_id) = @_;
408     return (1, 'Thank you!', '');
409 }
410
411
412 sub pay_fee {
413     my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
414         $pay_type, $fee_id, $trans_id, $currency) = @_;
415
416     my $xact = OpenILS::SIP::Transaction::FeePayment->new(authtoken => $self->{authtoken});
417     my $patron = $self->find_patron($patron_id);
418
419     if (!$patron) {
420         $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
421         $xact->ok(0);
422         return $xact;
423     }
424
425     $xact->patron($patron);
426     $xact->sip_currency($currency);
427     $xact->fee_amount($fee_amt);
428     $xact->sip_fee_type($fee_type);
429     $xact->transaction_id($trans_id);
430     $xact->fee_id($fee_id);
431     # We don't presently use these, but we might in the future.
432     $xact->patron_password($patron_pwd);
433     $xact->sip_payment_type($pay_type);
434
435     $xact->do_fee_payment();
436
437     return $xact;
438 }
439
440 #sub add_hold {
441 #    my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
442 #       $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
443 #    my ($patron, $item);
444 #    my $hold;
445 #    my $trans;
446 #
447 #
448 #    $trans = new ILS::Transaction::Hold;
449 #
450 #    # BEGIN TRANSACTION
451 #    $patron = new ILS::Patron $patron_id;
452 #    if (!$patron
453 #       || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
454 #       $trans->screen_msg("Invalid Patron.");
455 #
456 #       return $trans;
457 #    }
458 #
459 #    $item = new ILS::Item ($item_id || $title_id);
460 #    if (!$item) {
461 #       $trans->screen_msg("No such item.");
462 #
463 #       # END TRANSACTION (conditionally)
464 #       return $trans;
465 #    } elsif ($item->fee && ($fee_ack ne 'Y')) {
466 #       $trans->screen_msg = "Fee required to place hold.";
467 #
468 #       # END TRANSACTION (conditionally)
469 #       return $trans;
470 #    }
471 #
472 #    $hold = {
473 #       item_id         => $item->id,
474 #       patron_id       => $patron->id,
475 #       expiration_date => $expiry_date,
476 #       pickup_location => $pickup_location,
477 #       hold_type       => $hold_type,
478 #    };
479 #
480 #    $trans->ok(1);
481 #    $trans->patron($patron);
482 #    $trans->item($item);
483 #    $trans->pickup_location($pickup_location);
484 #
485 #    push(@{$item->hold_queue}, $hold);
486 #    push(@{$patron->{hold_items}}, $hold);
487 #
488 #
489 #    # END TRANSACTION
490 #    return $trans;
491 #}
492 #
493 #sub cancel_hold {
494 #    my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
495 #    my ($patron, $item, $hold);
496 #    my $trans;
497 #
498 #    $trans = new ILS::Transaction::Hold;
499 #
500 #    # BEGIN TRANSACTION
501 #    $patron = new ILS::Patron $patron_id;
502 #    if (!$patron) {
503 #       $trans->screen_msg("Invalid patron barcode.");
504 #
505 #       return $trans;
506 #    } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
507 #       $trans->screen_msg('Invalid patron password.');
508 #
509 #       return $trans;
510 #    }
511 #
512 #    $item = new ILS::Item ($item_id || $title_id);
513 #    if (!$item) {
514 #       $trans->screen_msg("No such item.");
515 #
516 #       # END TRANSACTION (conditionally)
517 #       return $trans;
518 #    }
519 #
520 #    # Remove the hold from the patron's record first
521 #    $trans->ok($patron->drop_hold($item_id));
522 #
523 #    if (!$trans->ok) {
524 #       # We didn't find it on the patron record
525 #       $trans->screen_msg("No such hold on patron record.");
526 #
527 #       # END TRANSACTION (conditionally)
528 #       return $trans;
529 #    }
530 #
531 #    # Now, remove it from the item record.  If it was on the patron
532 #    # record but not on the item record, we'll treat that as success.
533 #    foreach my $i (0 .. scalar @{$item->hold_queue}) {
534 #       $hold = $item->hold_queue->[$i];
535 #
536 #       if ($hold->{patron_id} eq $patron->id) {
537 #           # found it: delete it.
538 #           splice @{$item->hold_queue}, $i, 1;
539 #           last;
540 #       }
541 #    }
542 #
543 #    $trans->screen_msg("Hold Cancelled.");
544 #    $trans->patron($patron);
545 #    $trans->item($item);
546 #
547 #    return $trans;
548 #}
549 #
550 #
551 ## The patron and item id's can't be altered, but the
552 ## date, location, and type can.
553 #sub alter_hold {
554 #    my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
555 #       $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
556 #    my ($patron, $item);
557 #    my $hold;
558 #    my $trans;
559 #
560 #    $trans = new ILS::Transaction::Hold;
561 #
562 #    # BEGIN TRANSACTION
563 #    $patron = new ILS::Patron $patron_id;
564 #    if (!$patron) {
565 #       $trans->screen_msg("Invalid patron barcode.");
566 #
567 #       return $trans;
568 #    }
569 #
570 #    foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
571 #       $hold = $patron->{hold_items}[$i];
572 #
573 #       if ($hold->{item_id} eq $item_id) {
574 #           # Found it.  So fix it.
575 #           $hold->{expiration_date} = $expiry_date if $expiry_date;
576 #           $hold->{pickup_location} = $pickup_location if $pickup_location;
577 #           $hold->{hold_type} = $hold_type if $hold_type;
578 #
579 #           $trans->ok(1);
580 #           $trans->screen_msg("Hold updated.");
581 #           $trans->patron($patron);
582 #           $trans->item(new ILS::Item $hold->{item_id});
583 #           last;
584 #       }
585 #    }
586 #
587 #    # The same hold structure is linked into both the patron's
588 #    # list of hold items and into the queue of outstanding holds
589 #    # for the item, so we don't need to search the hold queue for
590 #    # the item, since it's already been updated by the patron code.
591 #
592 #    if (!$trans->ok) {
593 #       $trans->screen_msg("No such outstanding hold.");
594 #    }
595 #
596 #    return $trans;
597 #}
598
599
600 sub renew {
601         my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
602                 $no_block, $nb_due_date, $third_party, $item_props, $fee_ack) = @_;
603
604         $self->verify_session;
605
606         my $trans = OpenILS::SIP::Transaction::Renew->new( authtoken => $self->{authtoken} );
607         $trans->patron($self->find_patron($patron_id));
608         $trans->item($self->find_item($item_id));
609
610         if(!$trans->patron) {
611                 $trans->screen_msg("Invalid patron barcode.");
612                 $trans->ok(0);
613                 return $trans;
614         }
615
616         if(!$trans->patron->renew_ok) {
617                 $trans->screen_msg("Renewals not allowed.");
618                 $trans->ok(0);
619                 return $trans;
620         }
621
622         if(!$trans->item) {
623                 if( $title_id ) {
624                         $trans->screen_msg("Title ID renewal not supported.  Use item barcode.");
625                 } else {
626                         $trans->screen_msg("Invalid item barcode.");
627                 }
628                 $trans->ok(0);
629                 return $trans;
630         }
631
632         if(!$trans->item->{patron} or 
633                         $trans->item->{patron} ne $patron_id) {
634                 $trans->screen_msg("Item not checked out to " . $trans->patron->name);
635                 $trans->ok(0);
636                 return $trans;
637         }
638
639         # Perform the renewal
640         $trans->do_renew();
641
642         $trans->desensitize(0); # It's already checked out
643         $trans->item->{due_date} = $nb_due_date if $no_block eq 'Y';
644         $trans->item->{sip_item_properties} = $item_props if $item_props;
645
646         return $trans;
647 }
648
649
650
651
652
653 #
654 #sub renew_all {
655 #    my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
656 #    my ($patron, $item_id);
657 #    my $trans;
658 #
659 #    $trans = new ILS::Transaction::RenewAll;
660 #
661 #    $trans->patron($patron = new ILS::Patron $patron_id);
662 #    if (defined $patron) {
663 #       syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
664 #              $patron->name, $patron->renew_ok);
665 #    } else {
666 #       syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
667 #              $patron_id);
668 #    }
669 #
670 #    if (!defined($patron)) {
671 #       $trans->screen_msg("Invalid patron barcode.");
672 #       return $trans;
673 #    } elsif (!$patron->renew_ok) {
674 #       $trans->screen_msg("Renewals not allowed.");
675 #       return $trans;
676 #    } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
677 #       $trans->screen_msg("Invalid patron password.");
678 #       return $trans;
679 #    }
680 #
681 #    foreach $item_id (@{$patron->{items}}) {
682 #       my $item = new ILS::Item $item_id;
683 #
684 #       if (!defined($item)) {
685 #           syslog("LOG_WARNING",
686 #                  "renew_all: Invalid item id associated with patron '%s'",
687 #                  $patron->id);
688 #           next;
689 #       }
690 #
691 #       if (@{$item->hold_queue}) {
692 #           # Can't renew if there are outstanding holds
693 #           push @{$trans->unrenewed}, $item_id;
694 #       } else {
695 #           $item->{due_date} = time + (14*24*60*60); # two weeks hence
696 #           push @{$trans->renewed}, $item_id;
697 #       }
698 #    }
699 #
700 #    $trans->ok(1);
701 #
702 #    return $trans;
703 #}
704
705 1;