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;
19 use OpenILS::SIP::Transaction::Hold;
22 use OpenSRF::AppSession;
23 use OpenILS::Utils::Fieldmapper;
24 use OpenSRF::Utils::SettingsClient;
25 use OpenILS::Application::AppUtils;
26 use OpenSRF::Utils qw/:datetime/;
27 use DateTime::Format::ISO8601;
29 use Unicode::Normalize;
31 my $U = 'OpenILS::Application::AppUtils';
36 my $target_encoding; # FIXME: this is configured at the institution level.
38 use Digest::MD5 qw(md5_hex);
40 # Copied from Sip::Constants
42 SIP_DATETIME => "%Y%m%d %H%M%S",
46 OpenSRF::Transport::PeerHandle->retrieve->disconnect
50 my ($class, $institution, $login, $state) = @_;
51 my $type = ref($class) || $class;
54 $self->{login} = $login_account = $login;
56 $config = $institution;
57 syslog("LOG_DEBUG", "OILS: new ILS '%s'", $institution->{id});
58 $self->{institution} = $institution;
60 my $bsconfig = $institution->{implementation_config}->{bootstrap};
61 $target_encoding = $institution->{implementation_config}->{encoding} || 'ascii';
63 syslog('LOG_DEBUG', "OILS: loading bootstrap config: $bsconfig");
65 # ingress will persist throughout
66 OpenSRF::AppSession->ingress('sip2');
68 local $/ = "\n"; # why?
69 OpenSRF::System->bootstrap_client(config_file => $bsconfig);
70 syslog('LOG_DEBUG', "OILS: bootstrap loaded..");
72 $self->{osrf_config} = OpenSRF::Utils::SettingsClient->new;
74 Fieldmapper->import($self->{osrf_config}->config_value('IDL'));
76 bless( $self, $type );
79 $self->login( $login->{id}, $login->{password}, $state );
87 my $ses = $U->simplereq(
89 'open-ils.auth.session.retrieve', $self->{authtoken});
91 return undef if $U->event_code($ses); # auth timed out
92 return $self->{login_session} = $ses;
98 return 1 if $self->fetch_session;
100 syslog('LOG_INFO', "OILS: Logging back after session timeout as user ".$self->{login}->{id});
101 return $self->login( $self->{login}->{id}, $self->{login}->{password} );
105 return $editor = make_editor();
112 return $login_account;
115 sub get_option_value {
116 my($self, $option) = @_;
117 my $ops = $config->{implementation_config}->{options}->{option};
118 $ops = [$ops] unless ref $ops eq 'ARRAY';
119 my @vals = grep { $_->{name} eq $option } @$ops;
120 return @vals ? $vals[0]->{value} : undef;
124 # Creates the global editor object
125 my $cstore_init = 1; # call init on first use
127 OpenILS::Utils::CStoreEditor::init() if $cstore_init;
129 return OpenILS::Utils::CStoreEditor->new;
132 =head2 clean_text(scalar)
134 Evergreen uses the UTF8 encoding for everything from the database up. Perl
135 doesn't know this, however, so we have to convince it to treat our UTF8 strings
136 as UTF8 strings. This may enable OpenNCIP to correctly calculate the checksums
137 for UTF8 text for SIP clients that support such modern options.
139 The target encoding is set in the <encoding> element of the SIPServer.pm
145 my $text = shift || '';
147 # Convert our incoming UTF8 data into Perl's internal string format
149 # Also convert to Normalization Form D, as the ASCII, iso-8859-1,
150 # and latin-1 encodings (at least) require this to substitute
151 # characters rather than simply returning a string truncated
152 # after the first non-ASCII character
153 $text = NFD(decode_utf8($text));
155 if ($target_encoding eq 'ascii') {
157 # Try to maintain a reasonable version of the content by
158 # stripping diacritics from the text, given that the SIP client
159 # wants just plain ASCII. This is the base requirement according
160 # to the SIP2 specification.
162 # Stripping the combining characters converts ""béè♁ts"
163 # into "bee?ts" instead of "b???ts" - better, eh?
167 # Characters that cannot be represented in the target encoding will
168 # generally be replaced with a question mark (?) character.
169 $text = encode($target_encoding, $text);
175 sub shortname_from_id {
176 my $id = shift or return;
177 return $id->shortname if ref $id;
178 return $org_sn_cache{$id} if $org_sn_cache{$id};
179 return $org_sn_cache{$id} = editor()->retrieve_actor_org_unit($id)->shortname;
181 sub patron_barcode_from_id {
182 my $id = shift or return;
183 return editor()->search_actor_card({ usr => $id, active => 't' })->[0]->barcode;
189 my $type = shift || '';
191 return "" unless $date;
193 my $dt = DateTime::Format::ISO8601->new->
194 parse_datetime(OpenSRF::Utils::cleanse_ISO8601($date));
196 # actor.usr.dob stores dates without time/timezone, which causes
197 # DateTime to assume the date is stored as UTC. Tell DateTime
198 # to use the local time zone, instead.
199 # Other dates will have time zones and should be parsed as-is.
200 $dt->set_time_zone('local') if $type eq 'dob';
202 my @time = localtime($dt->epoch);
204 my $year = $time[5]+1900;
205 my $mon = $time[4]+1;
208 my $minute = $time[1];
209 my $second = $time[0];
211 $date = sprintf("%04d%02d%02d", $year, $mon, $day);
213 # Due dates need hyphen separators and time of day as well
214 if ($type eq 'due') {
216 my $use_sdf = $class->get_option_value('use_sip_date_format') || '';
218 if ($use_sdf =~ /true/i) {
219 $date = $dt->strftime(SIP_DATETIME);
222 $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d",
223 $year, $mon, $day, $hour, $minute, $second);
227 syslog('LOG_DEBUG', "OILS: formatted date [type=$type]: $date");
234 my( $self, $username, $password, $state ) = @_;
235 syslog('LOG_DEBUG', "OILS: Logging in with username $username");
238 if ($state and ref $state and $$state{authtoken}) {
239 $self->{authtoken} = $$state{authtoken};
240 return $self->{authtoken} if ($self->fetch_session); # fetch the session
243 my $nonce = rand($$);
245 my $seed = $U->simplereq(
247 'open-ils.auth.authenticate.init', $username, $nonce );
251 username => $username,
252 password => md5_hex($seed . md5_hex($password)),
257 if ($self->{login}->{location}) {
258 $opts->{workstation} = $self->{login}->{location};
261 my $response = $U->simplereq(
263 'open-ils.auth.authenticate.complete',
267 if( my $code = $U->event_code($response) ) {
268 my $txt = $response->{textcode};
269 syslog('LOG_WARNING', "OILS: Login failed for $username. $txt:$code");
273 my $key = $response->{payload}->{authtoken};
274 syslog('LOG_INFO', "OILS: Login succeeded for $username : authkey = $key");
276 $self->{authtoken} = $key;
278 $self->fetch_session; # to cache the login
285 return { authtoken => $self->{authtoken} };
289 # find_patron($barcode);
290 # find_patron(barcode => $barcode); # same as above
291 # find_patron(usr => $id);
295 my $key = (@_ > 1) ? shift : 'barcode'; # if we have multiple args, the first is the key index (default barcode)
296 my $patron_id = shift;
298 return OpenILS::SIP::Patron->new($key => $patron_id, authtoken => $self->{authtoken}, @_);
304 return OpenILS::SIP::Item->new(@_);
310 return $self->{institution}->{id}; # consider making this return the whole institution
315 return $self->{institution}->{id}; # then use this for just the ID
319 my ($self, $op) = @_;
320 my ($i) = grep { $_->{name} eq $op }
321 @{$config->{implementation_config}->{supports}->{item}};
322 return to_bool($i->{value});
326 my ($self, $id, $whence) = @_;
327 if ($id ne $self->{institution}->{id}) {
328 syslog("LOG_WARNING", "OILS: %s: received institution '%s', expected '%s'", $whence, $id, $self->{institution}->{id});
329 # Just an FYI check, we don't expect the user to change location from that in SIPconfig.xml
336 # If it's defined, and matches a true sort of string, or is
337 # a non-zero number, then it's true.
338 defined($bool) or return; # false
339 ($bool =~ /true|y|yes/i) and return 1; # true
340 return ($bool =~ /^\d+$/ and $bool != 0); # true for non-zero numbers, false otherwise
344 return to_bool($config->{policy}->{checkout});
348 return to_bool($config->{policy}->{checkin});
352 return to_bool($config->{policy}->{renewal});
355 sub status_update_ok {
356 return to_bool($config->{policy}->{status_update});
360 return to_bool($config->{policy}->{offline});
366 ## Checkout(patron_id, item_id, sc_renew, fee_ack):
367 ## patron_id & item_id are the identifiers send by the terminal
368 ## sc_renew is the renewal policy configured on the terminal
369 ## returns a status opject that can be queried for the various bits
370 ## of information that the protocol (SIP or NCIP) needs to generate
372 ## fee_ack is the fee_acknowledged field (BO) sent from the sc
373 ## when doing chargeable loans.
377 my ($self, $patron_id, $item_id, $sc_renew, $fee_ack) = @_;
378 # In order to allow renewals the selfcheck AND the config have to say they are allowed
379 $sc_renew = (chr($sc_renew) eq 'Y' && $self->renew_ok());
381 $self->verify_session;
383 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout attempt: patron=$patron_id, item=$item_id");
385 my $xact = OpenILS::SIP::Transaction::Checkout->new( authtoken => $self->{authtoken} );
386 my $patron = $self->find_patron($patron_id);
387 my $item = $self->find_item($item_id);
389 $xact->patron($patron);
393 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
397 if (!$patron->charge_ok) {
398 $xact->screen_msg("Patron Blocked");
403 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
407 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout data loaded OK, checking out...");
409 if ($item->{patron} && ($item->{patron} eq $patron_id)) {
410 $xact->renew_ok(1); # So that accept/reject responses have the correct value later
412 syslog('LOG_INFO', "OILS: OpenILS::Checkout data loaded OK, doing renew...");
414 syslog('LOG_INFO', "OILS: OpenILS::Checkout appears to be renew, but renewal disallowed...");
415 $xact->screen_msg("Renewals not permitted");
417 return $xact; # Don't attempt later
419 } elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
420 # I can't deal with this right now
421 # XXX check in then check out?
422 $xact->screen_msg("Item checked out to another patron");
424 return $xact; # Don't wipe out the screen message later
429 # Check for fee and $fee_ack. If there is a fee, and $fee_ack
430 # is 'Y', we proceed, otherwise we reject the checkout.
431 if ($item->fee > 0.0) {
432 $xact->fee_amount($item->fee);
433 $xact->sip_fee_type($item->sip_fee_type);
434 $xact->sip_currency($item->fee_currency);
435 if ($fee_ack && $fee_ack eq 'Y') {
438 $xact->screen_msg('Fee required');
444 $xact->do_checkout($sc_renew);
445 $xact->desensitize(!$item->magnetic);
449 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
450 "patron %s checkout %s succeeded", $patron_id, $item_id);
452 #editor()->xact_rollback;
453 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
454 "patron %s checkout %s FAILED, rolling back xact...", $patron_id, $item_id);
462 my ($self, $item_id, $inst_id, $trans_date, $return_date,
463 $current_loc, $item_props, $cancel) = @_;
465 my $start_time = time();
467 $self->verify_session;
469 syslog('LOG_DEBUG', "OILS: OpenILS::Checkin of item=$item_id (to $inst_id)");
471 my $xact = OpenILS::SIP::Transaction::Checkin->new(authtoken => $self->{authtoken});
472 my $item = OpenILS::SIP::Item->new($item_id);
474 unless ( $xact->item($item) ) {
476 # $circ->alert(1); $circ->alert_type(99);
477 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
478 syslog('LOG_INFO', "OILS: Checkin failed. " . $xact->screen_msg() );
482 $xact->do_checkin( $self, $inst_id, $trans_date, $return_date, $current_loc, $item_props );
485 $xact->patron($self->find_patron(usr => $xact->{circ_user_id}, slim_user => 1)) if $xact->{circ_user_id};
486 delete $item->{patron};
487 delete $item->{due_date};
488 syslog('LOG_INFO', "OILS: Checkin succeeded");
490 syslog('LOG_WARNING', "OILS: Checkin failed");
493 syslog('LOG_INFO', "OILS: SIP Checkin request took %0.3f seconds", (time() - $start_time));
497 ## If the ILS caches patron information, this lets it free it up.
498 ## Also, this could be used for centrally logging session duration.
499 ## We don't do anything with it.
500 sub end_patron_session {
501 my ($self, $patron_id) = @_;
502 return (1, 'Thank you!', '');
507 my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
508 $pay_type, $fee_id, $trans_id, $currency) = @_;
510 $self->verify_session;
512 my $xact = OpenILS::SIP::Transaction::FeePayment->new(authtoken => $self->{authtoken});
513 my $patron = $self->find_patron($patron_id);
516 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
521 $xact->patron($patron);
522 $xact->sip_currency($currency);
523 $xact->fee_amount($fee_amt);
524 $xact->sip_fee_type($fee_type);
525 $xact->transaction_id($trans_id);
526 $xact->fee_id($fee_id);
527 $xact->sip_payment_type($pay_type);
528 # We don't presently use this, but we might in the future.
529 $xact->patron_password($patron_pwd);
531 $xact->do_fee_payment();
537 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
538 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
539 # my ($patron, $item);
544 # $trans = new ILS::Transaction::Hold;
546 # # BEGIN TRANSACTION
547 # $patron = new ILS::Patron $patron_id;
549 # || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
550 # $trans->screen_msg("Invalid Patron.");
555 # $item = new ILS::Item ($item_id || $title_id);
557 # $trans->screen_msg("No such item.");
559 # # END TRANSACTION (conditionally)
561 # } elsif ($item->fee && ($fee_ack ne 'Y')) {
562 # $trans->screen_msg = "Fee required to place hold.";
564 # # END TRANSACTION (conditionally)
569 # item_id => $item->id,
570 # patron_id => $patron->id,
571 # expiration_date => $expiry_date,
572 # pickup_location => $pickup_location,
573 # hold_type => $hold_type,
577 # $trans->patron($patron);
578 # $trans->item($item);
579 # $trans->pickup_location($pickup_location);
581 # push(@{$item->hold_queue}, $hold);
582 # push(@{$patron->{hold_items}}, $hold);
590 # Note: item_id in this context is the hold id
592 my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
594 my $trans = OpenILS::SIP::Transaction::Hold->new(authtoken => $self->{authtoken});
595 my $patron = $self->find_patron($patron_id);
598 $trans->screen_msg("Invalid patron barcode.");
603 if (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
604 $trans->screen_msg('Invalid patron password.');
609 $trans->patron($patron);
610 my $hold = $patron->find_hold_from_copy($item_id);
613 syslog('LOG_WARNING', "OILS: No hold found from copy $item_id");
614 $trans->screen_msg("No such hold.");
619 if ($hold->usr ne $patron->{user}->id) {
620 $trans->screen_msg("No such hold on patron record.");
626 $trans->do_hold_cancel($self);
628 if ($trans->cancel_ok) {
629 $trans->screen_msg("Hold Cancelled.");
631 $trans->screen_msg("Hold was not cancelled.");
634 # if the hold had no current_copy, use the representative
635 # item as the item for the hold. Without this, the SIP
637 $trans->item($self->find_item($item_id)) unless $trans->item;
643 ## The patron and item id's can't be altered, but the
644 ## date, location, and type can.
646 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
647 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
648 # my ($patron, $item);
652 # $trans = new ILS::Transaction::Hold;
654 # # BEGIN TRANSACTION
655 # $patron = new ILS::Patron $patron_id;
657 # $trans->screen_msg("Invalid patron barcode.");
662 # foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
663 # $hold = $patron->{hold_items}[$i];
665 # if ($hold->{item_id} eq $item_id) {
666 # # Found it. So fix it.
667 # $hold->{expiration_date} = $expiry_date if $expiry_date;
668 # $hold->{pickup_location} = $pickup_location if $pickup_location;
669 # $hold->{hold_type} = $hold_type if $hold_type;
672 # $trans->screen_msg("Hold updated.");
673 # $trans->patron($patron);
674 # $trans->item(new ILS::Item $hold->{item_id});
679 # # The same hold structure is linked into both the patron's
680 # # list of hold items and into the queue of outstanding holds
681 # # for the item, so we don't need to search the hold queue for
682 # # the item, since it's already been updated by the patron code.
685 # $trans->screen_msg("No such outstanding hold.");
693 my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
694 $no_block, $nb_due_date, $third_party, $item_props, $fee_ack) = @_;
696 $self->verify_session;
698 my $trans = OpenILS::SIP::Transaction::Renew->new( authtoken => $self->{authtoken} );
699 $trans->patron($self->find_patron($patron_id));
700 $trans->item($self->find_item($item_id));
702 if(!$trans->patron) {
703 $trans->screen_msg("Invalid patron barcode.");
708 if(!$trans->patron->renew_ok) {
709 $trans->screen_msg("Renewals not allowed.");
716 $trans->screen_msg("Title ID renewal not supported. Use item barcode.");
718 $trans->screen_msg("Invalid item barcode.");
724 if(!$trans->item->{patron} or
725 $trans->item->{patron} ne $patron_id) {
726 $trans->screen_msg("Item not checked out to " . $trans->patron->name);
731 # Perform the renewal
734 $trans->desensitize(0); # It's already checked out
735 $trans->item->{due_date} = $nb_due_date if $no_block eq 'Y';
736 $trans->item->{sip_item_properties} = $item_props if $item_props;
743 my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
745 $self->verify_session;
747 my $trans = OpenILS::SIP::Transaction::RenewAll->new(authtoken => $self->{authtoken});
748 $trans->patron($self->find_patron($patron_id));
750 if(!$trans->patron) {
751 $trans->screen_msg("Invalid patron barcode.");
756 if(!$trans->patron->renew_ok) {
757 $trans->screen_msg("Renewals not allowed.");
762 $trans->do_renew_all($self);
769 # my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
770 # my ($patron, $item_id);
773 # $trans = new ILS::Transaction::RenewAll;
775 # $trans->patron($patron = new ILS::Patron $patron_id);
776 # if (defined $patron) {
777 # syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
778 # $patron->name, $patron->renew_ok);
780 # syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
784 # if (!defined($patron)) {
785 # $trans->screen_msg("Invalid patron barcode.");
787 # } elsif (!$patron->renew_ok) {
788 # $trans->screen_msg("Renewals not allowed.");
790 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
791 # $trans->screen_msg("Invalid patron password.");
795 # foreach $item_id (@{$patron->{items}}) {
796 # my $item = new ILS::Item $item_id;
798 # if (!defined($item)) {
799 # syslog("LOG_WARNING",
800 # "renew_all: Invalid item id associated with patron '%s'",
805 # if (@{$item->hold_queue}) {
806 # # Can't renew if there are outstanding holds
807 # push @{$trans->unrenewed}, $item_id;
809 # $item->{due_date} = time + (14*24*60*60); # two weeks hence
810 # push @{$trans->renewed}, $item_id;