2 # ILS.pm: Test ILS interface module
6 use warnings; use strict;
8 use Sys::Syslog qw(syslog);
9 use Time::HiRes q/time/;
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::RenewAll;
18 use OpenILS::SIP::Transaction::FeePayment;
21 use OpenSRF::AppSession;
22 use OpenILS::Utils::Fieldmapper;
23 use OpenSRF::Utils::SettingsClient;
24 use OpenILS::Application::AppUtils;
25 use OpenSRF::Utils qw/:datetime/;
26 use DateTime::Format::ISO8601;
28 use Unicode::Normalize;
30 my $U = 'OpenILS::Application::AppUtils';
35 my $target_encoding; # FIXME: this is configured at the institution level.
37 use Digest::MD5 qw(md5_hex);
39 # Copied from Sip::Constants
41 SIP_DATETIME => "%Y%m%d %H%M%S",
45 my ($class, $institution, $login) = @_;
46 my $type = ref($class) || $class;
49 $self->{login} = $login_account = $login;
51 $config = $institution;
52 syslog("LOG_DEBUG", "OILS: new ILS '%s'", $institution->{id});
53 $self->{institution} = $institution;
55 my $bsconfig = $institution->{implementation_config}->{bootstrap};
56 $target_encoding = $institution->{implementation_config}->{encoding} || 'ascii';
58 syslog('LOG_DEBUG', "OILS: loading bootstrap config: $bsconfig");
60 # ingress will persist throughout
61 OpenSRF::AppSession->ingress('sip2');
63 local $/ = "\n"; # why?
64 OpenSRF::System->bootstrap_client(config_file => $bsconfig);
65 syslog('LOG_DEBUG', "OILS: bootstrap loaded..");
67 $self->{osrf_config} = OpenSRF::Utils::SettingsClient->new;
69 Fieldmapper->import($self->{osrf_config}->config_value('IDL'));
71 bless( $self, $type );
74 $self->login( $login->{id}, $login->{password} );
82 my $ses = $U->simplereq(
84 'open-ils.auth.session.retrieve', $self->{authtoken});
86 return undef if $U->event_code($ses); # auth timed out
87 return $self->{login_session} = $ses;
93 return 1 if $self->fetch_session;
95 syslog('LOG_INFO', "OILS: Logging back after session timeout as user ".$self->{login}->{id});
96 return $self->login( $self->{login}->{id}, $self->{login}->{password} );
100 return $editor = make_editor();
107 return $login_account;
110 sub get_option_value {
111 my($self, $option) = @_;
112 my $ops = $config->{implementation_config}->{options}->{option};
113 $ops = [$ops] unless ref $ops eq 'ARRAY';
114 my @vals = grep { $_->{name} eq $option } @$ops;
115 return @vals ? $vals[0]->{value} : undef;
119 # Creates the global editor object
120 my $cstore_init = 1; # call init on first use
122 OpenILS::Utils::CStoreEditor::init() if $cstore_init;
124 return OpenILS::Utils::CStoreEditor->new;
127 =head2 clean_text(scalar)
129 Evergreen uses the UTF8 encoding for everything from the database up. Perl
130 doesn't know this, however, so we have to convince it to treat our UTF8 strings
131 as UTF8 strings. This may enable OpenNCIP to correctly calculate the checksums
132 for UTF8 text for SIP clients that support such modern options.
134 The target encoding is set in the <encoding> element of the SIPServer.pm
140 my $text = shift || '';
142 # Convert our incoming UTF8 data into Perl's internal string format
144 # Also convert to Normalization Form D, as the ASCII, iso-8859-1,
145 # and latin-1 encodings (at least) require this to substitute
146 # characters rather than simply returning a string truncated
147 # after the first non-ASCII character
148 $text = NFD(decode_utf8($text));
150 if ($target_encoding eq 'ascii') {
152 # Try to maintain a reasonable version of the content by
153 # stripping diacritics from the text, given that the SIP client
154 # wants just plain ASCII. This is the base requirement according
155 # to the SIP2 specification.
157 # Stripping the combining characters converts ""béè♁ts"
158 # into "bee?ts" instead of "b???ts" - better, eh?
162 # Characters that cannot be represented in the target encoding will
163 # generally be replaced with a question mark (?) character.
164 $text = encode($target_encoding, $text);
170 sub shortname_from_id {
171 my $id = shift or return;
172 return $id->shortname if ref $id;
173 return $org_sn_cache{$id} if $org_sn_cache{$id};
174 return $org_sn_cache{$id} = editor()->retrieve_actor_org_unit($id)->shortname;
176 sub patron_barcode_from_id {
177 my $id = shift or return;
178 return editor()->search_actor_card({ usr => $id, active => 't' })->[0]->barcode;
184 my $type = shift || 'dob';
186 return "" unless $date;
188 my $dt = DateTime::Format::ISO8601->new->
189 parse_datetime(OpenSRF::Utils::cleanse_ISO8601($date));
190 my @time = localtime($dt->epoch);
192 my $year = $time[5]+1900;
193 my $mon = $time[4]+1;
196 my $minute = $time[1];
197 my $second = $time[0];
199 $date = sprintf("%04d%02d%02d", $year, $mon, $day);
201 # Due dates need hyphen separators and time of day as well
202 if ($type eq 'due') {
204 my $use_sdf = $class->get_option_value('use_sip_date_format') | '';
206 if ($use_sdf =~ /true/i) {
207 $date = $dt->strftime(SIP_DATETIME);
210 $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d",
211 $year, $mon, $day, $hour, $minute, $second);
215 syslog('LOG_DEBUG', "OILS: formatted date [type=$type]: $date");
222 my( $self, $username, $password ) = @_;
223 syslog('LOG_DEBUG', "OILS: Logging in with username $username");
225 my $seed = $U->simplereq(
227 'open-ils.auth.authenticate.init', $username );
229 my $response = $U->simplereq(
231 'open-ils.auth.authenticate.complete',
233 username => $username,
234 password => md5_hex($seed . md5_hex($password)),
239 if( my $code = $U->event_code($response) ) {
240 my $txt = $response->{textcode};
241 syslog('LOG_WARNING', "OILS: Login failed for $username. $txt:$code");
245 my $key = $response->{payload}->{authtoken};
246 syslog('LOG_INFO', "OILS: Login succeeded for $username : authkey = $key");
248 $self->{authtoken} = $key;
250 $self->fetch_session; # to cache the login
256 # find_patron($barcode);
257 # find_patron(barcode => $barcode); # same as above
258 # find_patron(usr => $id);
262 my $key = (@_ > 1) ? shift : 'barcode'; # if we have multiple args, the first is the key index (default barcode)
263 my $patron_id = shift;
265 return OpenILS::SIP::Patron->new($key => $patron_id, authtoken => $self->{authtoken}, @_);
271 return OpenILS::SIP::Item->new(@_);
277 return $self->{institution}->{id}; # consider making this return the whole institution
282 return $self->{institution}->{id}; # then use this for just the ID
286 my ($self, $op) = @_;
287 my ($i) = grep { $_->{name} eq $op }
288 @{$config->{implementation_config}->{supports}->{item}};
289 return to_bool($i->{value});
293 my ($self, $id, $whence) = @_;
294 if ($id ne $self->{institution}->{id}) {
295 syslog("LOG_WARNING", "OILS: %s: received institution '%s', expected '%s'", $whence, $id, $self->{institution}->{id});
296 # Just an FYI check, we don't expect the user to change location from that in SIPconfig.xml
303 # If it's defined, and matches a true sort of string, or is
304 # a non-zero number, then it's true.
305 defined($bool) or return; # false
306 ($bool =~ /true|y|yes/i) and return 1; # true
307 return ($bool =~ /^\d+$/ and $bool != 0); # true for non-zero numbers, false otherwise
311 return to_bool($config->{policy}->{checkout});
315 return to_bool($config->{policy}->{checkin});
319 return to_bool($config->{policy}->{renewal});
322 sub status_update_ok {
323 return to_bool($config->{policy}->{status_update});
327 return to_bool($config->{policy}->{offline});
333 ## Checkout(patron_id, item_id, sc_renew, fee_ack):
334 ## patron_id & item_id are the identifiers send by the terminal
335 ## sc_renew is the renewal policy configured on the terminal
336 ## returns a status opject that can be queried for the various bits
337 ## of information that the protocol (SIP or NCIP) needs to generate
339 ## fee_ack is the fee_acknowledged field (BO) sent from the sc
340 ## when doing chargeable loans.
344 my ($self, $patron_id, $item_id, $sc_renew, $fee_ack) = @_;
345 # In order to allow renewals the selfcheck AND the config have to say they are allowed
346 $sc_renew = (chr($sc_renew) eq 'Y' && $self->renew_ok());
348 $self->verify_session;
350 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout attempt: patron=$patron_id, item=$item_id");
352 my $xact = OpenILS::SIP::Transaction::Checkout->new( authtoken => $self->{authtoken} );
353 my $patron = $self->find_patron($patron_id);
354 my $item = $self->find_item($item_id);
356 $xact->patron($patron);
360 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
364 if (!$patron->charge_ok) {
365 $xact->screen_msg("Patron Blocked");
370 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
374 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout data loaded OK, checking out...");
376 if ($item->{patron} && ($item->{patron} eq $patron_id)) {
377 $xact->renew_ok(1); # So that accept/reject responses have the correct value later
379 syslog('LOG_INFO', "OILS: OpenILS::Checkout data loaded OK, doing renew...");
381 syslog('LOG_INFO', "OILS: OpenILS::Checkout appears to be renew, but renewal disallowed...");
382 $xact->screen_msg("Renewals not permitted");
384 return $xact; # Don't attempt later
386 } elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
387 # I can't deal with this right now
388 # XXX check in then check out?
389 $xact->screen_msg("Item checked out to another patron");
391 return $xact; # Don't wipe out the screen message later
396 # Check for fee and $fee_ack. If there is a fee, and $fee_ack
397 # is 'Y', we proceed, otherwise we reject the checkout.
398 if ($item->fee > 0.0) {
399 $xact->fee_amount($item->fee);
400 $xact->sip_fee_type($item->sip_fee_type);
401 $xact->sip_currency($item->fee_currency);
402 if ($fee_ack && $fee_ack eq 'Y') {
405 $xact->screen_msg('Fee required');
411 $xact->do_checkout($sc_renew);
412 $xact->desensitize(!$item->magnetic);
416 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
417 "patron %s checkout %s succeeded", $patron_id, $item_id);
419 #editor()->xact_rollback;
420 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
421 "patron %s checkout %s FAILED, rolling back xact...", $patron_id, $item_id);
429 my ($self, $item_id, $inst_id, $trans_date, $return_date,
430 $current_loc, $item_props, $cancel) = @_;
432 my $start_time = time();
434 $self->verify_session;
436 syslog('LOG_DEBUG', "OILS: OpenILS::Checkin of item=$item_id (to $inst_id)");
438 my $xact = OpenILS::SIP::Transaction::Checkin->new(authtoken => $self->{authtoken});
439 my $item = OpenILS::SIP::Item->new($item_id);
441 unless ( $xact->item($item) ) {
443 # $circ->alert(1); $circ->alert_type(99);
444 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
445 syslog('LOG_INFO', "OILS: Checkin failed. " . $xact->screen_msg() );
449 $xact->do_checkin( $self, $inst_id, $trans_date, $return_date, $current_loc, $item_props );
452 $xact->patron($self->find_patron(usr => $xact->{circ_user_id}, slim_user => 1)) if $xact->{circ_user_id};
453 delete $item->{patron};
454 delete $item->{due_date};
455 syslog('LOG_INFO', "OILS: Checkin succeeded");
457 syslog('LOG_WARNING', "OILS: Checkin failed");
460 syslog('LOG_INFO', "OILS: SIP Checkin request took %0.3f seconds", (time() - $start_time));
464 ## If the ILS caches patron information, this lets it free it up.
465 ## Also, this could be used for centrally logging session duration.
466 ## We don't do anything with it.
467 sub end_patron_session {
468 my ($self, $patron_id) = @_;
469 return (1, 'Thank you!', '');
474 my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
475 $pay_type, $fee_id, $trans_id, $currency) = @_;
477 $self->verify_session;
479 my $xact = OpenILS::SIP::Transaction::FeePayment->new(authtoken => $self->{authtoken});
480 my $patron = $self->find_patron($patron_id);
483 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
488 $xact->patron($patron);
489 $xact->sip_currency($currency);
490 $xact->fee_amount($fee_amt);
491 $xact->sip_fee_type($fee_type);
492 $xact->transaction_id($trans_id);
493 $xact->fee_id($fee_id);
494 $xact->sip_payment_type($pay_type);
495 # We don't presently use this, but we might in the future.
496 $xact->patron_password($patron_pwd);
498 $xact->do_fee_payment();
504 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
505 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
506 # my ($patron, $item);
511 # $trans = new ILS::Transaction::Hold;
513 # # BEGIN TRANSACTION
514 # $patron = new ILS::Patron $patron_id;
516 # || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
517 # $trans->screen_msg("Invalid Patron.");
522 # $item = new ILS::Item ($item_id || $title_id);
524 # $trans->screen_msg("No such item.");
526 # # END TRANSACTION (conditionally)
528 # } elsif ($item->fee && ($fee_ack ne 'Y')) {
529 # $trans->screen_msg = "Fee required to place hold.";
531 # # END TRANSACTION (conditionally)
536 # item_id => $item->id,
537 # patron_id => $patron->id,
538 # expiration_date => $expiry_date,
539 # pickup_location => $pickup_location,
540 # hold_type => $hold_type,
544 # $trans->patron($patron);
545 # $trans->item($item);
546 # $trans->pickup_location($pickup_location);
548 # push(@{$item->hold_queue}, $hold);
549 # push(@{$patron->{hold_items}}, $hold);
557 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
558 # my ($patron, $item, $hold);
561 # $trans = new ILS::Transaction::Hold;
563 # # BEGIN TRANSACTION
564 # $patron = new ILS::Patron $patron_id;
566 # $trans->screen_msg("Invalid patron barcode.");
569 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
570 # $trans->screen_msg('Invalid patron password.');
575 # $item = new ILS::Item ($item_id || $title_id);
577 # $trans->screen_msg("No such item.");
579 # # END TRANSACTION (conditionally)
583 # # Remove the hold from the patron's record first
584 # $trans->ok($patron->drop_hold($item_id));
587 # # We didn't find it on the patron record
588 # $trans->screen_msg("No such hold on patron record.");
590 # # END TRANSACTION (conditionally)
594 # # Now, remove it from the item record. If it was on the patron
595 # # record but not on the item record, we'll treat that as success.
596 # foreach my $i (0 .. scalar @{$item->hold_queue}) {
597 # $hold = $item->hold_queue->[$i];
599 # if ($hold->{patron_id} eq $patron->id) {
600 # # found it: delete it.
601 # splice @{$item->hold_queue}, $i, 1;
606 # $trans->screen_msg("Hold Cancelled.");
607 # $trans->patron($patron);
608 # $trans->item($item);
614 ## The patron and item id's can't be altered, but the
615 ## date, location, and type can.
617 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
618 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
619 # my ($patron, $item);
623 # $trans = new ILS::Transaction::Hold;
625 # # BEGIN TRANSACTION
626 # $patron = new ILS::Patron $patron_id;
628 # $trans->screen_msg("Invalid patron barcode.");
633 # foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
634 # $hold = $patron->{hold_items}[$i];
636 # if ($hold->{item_id} eq $item_id) {
637 # # Found it. So fix it.
638 # $hold->{expiration_date} = $expiry_date if $expiry_date;
639 # $hold->{pickup_location} = $pickup_location if $pickup_location;
640 # $hold->{hold_type} = $hold_type if $hold_type;
643 # $trans->screen_msg("Hold updated.");
644 # $trans->patron($patron);
645 # $trans->item(new ILS::Item $hold->{item_id});
650 # # The same hold structure is linked into both the patron's
651 # # list of hold items and into the queue of outstanding holds
652 # # for the item, so we don't need to search the hold queue for
653 # # the item, since it's already been updated by the patron code.
656 # $trans->screen_msg("No such outstanding hold.");
664 my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
665 $no_block, $nb_due_date, $third_party, $item_props, $fee_ack) = @_;
667 $self->verify_session;
669 my $trans = OpenILS::SIP::Transaction::Renew->new( authtoken => $self->{authtoken} );
670 $trans->patron($self->find_patron($patron_id));
671 $trans->item($self->find_item($item_id));
673 if(!$trans->patron) {
674 $trans->screen_msg("Invalid patron barcode.");
679 if(!$trans->patron->renew_ok) {
680 $trans->screen_msg("Renewals not allowed.");
687 $trans->screen_msg("Title ID renewal not supported. Use item barcode.");
689 $trans->screen_msg("Invalid item barcode.");
695 if(!$trans->item->{patron} or
696 $trans->item->{patron} ne $patron_id) {
697 $trans->screen_msg("Item not checked out to " . $trans->patron->name);
702 # Perform the renewal
705 $trans->desensitize(0); # It's already checked out
706 $trans->item->{due_date} = $nb_due_date if $no_block eq 'Y';
707 $trans->item->{sip_item_properties} = $item_props if $item_props;
714 my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
716 $self->verify_session;
718 my $trans = OpenILS::SIP::Transaction::RenewAll->new(authtoken => $self->{authtoken});
719 $trans->patron($self->find_patron($patron_id));
721 if(!$trans->patron) {
722 $trans->screen_msg("Invalid patron barcode.");
727 if(!$trans->patron->renew_ok) {
728 $trans->screen_msg("Renewals not allowed.");
733 $trans->do_renew_all($self);
740 # my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
741 # my ($patron, $item_id);
744 # $trans = new ILS::Transaction::RenewAll;
746 # $trans->patron($patron = new ILS::Patron $patron_id);
747 # if (defined $patron) {
748 # syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
749 # $patron->name, $patron->renew_ok);
751 # syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
755 # if (!defined($patron)) {
756 # $trans->screen_msg("Invalid patron barcode.");
758 # } elsif (!$patron->renew_ok) {
759 # $trans->screen_msg("Renewals not allowed.");
761 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
762 # $trans->screen_msg("Invalid patron password.");
766 # foreach $item_id (@{$patron->{items}}) {
767 # my $item = new ILS::Item $item_id;
769 # if (!defined($item)) {
770 # syslog("LOG_WARNING",
771 # "renew_all: Invalid item id associated with patron '%s'",
776 # if (@{$item->hold_queue}) {
777 # # Can't renew if there are outstanding holds
778 # push @{$trans->unrenewed}, $item_id;
780 # $item->{due_date} = time + (14*24*60*60); # two weeks hence
781 # push @{$trans->renewed}, $item_id;