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::FeePayment;
20 use OpenSRF::AppSession;
21 use OpenILS::Utils::Fieldmapper;
22 use OpenSRF::Utils::SettingsClient;
23 use OpenILS::Application::AppUtils;
24 use OpenSRF::Utils qw/:datetime/;
25 use DateTime::Format::ISO8601;
27 use Unicode::Normalize;
29 my $U = 'OpenILS::Application::AppUtils';
34 my $target_encoding; # FIXME: this is configured at the institution level.
36 use Digest::MD5 qw(md5_hex);
38 # Copied from Sip::Constants
40 SIP_DATETIME => "%Y%m%d %H%M%S",
44 my ($class, $institution, $login) = @_;
45 my $type = ref($class) || $class;
48 $self->{login} = $login_account = $login;
50 $config = $institution;
51 syslog("LOG_DEBUG", "OILS: new ILS '%s'", $institution->{id});
52 $self->{institution} = $institution;
54 my $bsconfig = $institution->{implementation_config}->{bootstrap};
55 $target_encoding = $institution->{implementation_config}->{encoding} || 'ascii';
57 syslog('LOG_DEBUG', "OILS: loading bootstrap config: $bsconfig");
59 # ingress will persist throughout
60 OpenSRF::AppSession->ingress('sip2');
62 local $/ = "\n"; # why?
63 OpenSRF::System->bootstrap_client(config_file => $bsconfig);
64 syslog('LOG_DEBUG', "OILS: bootstrap loaded..");
66 $self->{osrf_config} = OpenSRF::Utils::SettingsClient->new;
68 Fieldmapper->import($self->{osrf_config}->config_value('IDL'));
70 bless( $self, $type );
73 $self->login( $login->{id}, $login->{password} );
81 my $ses = $U->simplereq(
83 'open-ils.auth.session.retrieve', $self->{authtoken});
85 return undef if $U->event_code($ses); # auth timed out
86 return $self->{login_session} = $ses;
92 return 1 if $self->fetch_session;
94 syslog('LOG_INFO', "OILS: Logging back after session timeout as user ".$self->{login}->{id});
95 return $self->login( $self->{login}->{id}, $self->{login}->{password} );
99 return $editor = make_editor();
106 return $login_account;
109 sub get_option_value {
110 my($self, $option) = @_;
111 my $ops = $config->{implementation_config}->{options}->{option};
112 $ops = [$ops] unless ref $ops eq 'ARRAY';
113 my @vals = grep { $_->{name} eq $option } @$ops;
114 return @vals ? $vals[0]->{value} : undef;
118 # Creates the global editor object
119 my $cstore_init = 1; # call init on first use
121 OpenILS::Utils::CStoreEditor::init() if $cstore_init;
123 return OpenILS::Utils::CStoreEditor->new;
126 =head2 clean_text(scalar)
128 Evergreen uses the UTF8 encoding for everything from the database up. Perl
129 doesn't know this, however, so we have to convince it to treat our UTF8 strings
130 as UTF8 strings. This may enable OpenNCIP to correctly calculate the checksums
131 for UTF8 text for SIP clients that support such modern options.
133 The target encoding is set in the <encoding> element of the SIPServer.pm
139 my $text = shift || '';
141 # Convert our incoming UTF8 data into Perl's internal string format
143 # Also convert to Normalization Form D, as the ASCII, iso-8859-1,
144 # and latin-1 encodings (at least) require this to substitute
145 # characters rather than simply returning a string truncated
146 # after the first non-ASCII character
147 $text = NFD(decode_utf8($text));
149 if ($target_encoding eq 'ascii') {
151 # Try to maintain a reasonable version of the content by
152 # stripping diacritics from the text, given that the SIP client
153 # wants just plain ASCII. This is the base requirement according
154 # to the SIP2 specification.
156 # Stripping the combining characters converts ""béè♁ts"
157 # into "bee?ts" instead of "b???ts" - better, eh?
161 # Characters that cannot be represented in the target encoding will
162 # generally be replaced with a question mark (?) character.
163 $text = encode($target_encoding, $text);
169 sub shortname_from_id {
170 my $id = shift or return;
171 return $id->shortname if ref $id;
172 return $org_sn_cache{$id} if $org_sn_cache{$id};
173 return $org_sn_cache{$id} = editor()->retrieve_actor_org_unit($id)->shortname;
175 sub patron_barcode_from_id {
176 my $id = shift or return;
177 return editor()->search_actor_card({ usr => $id, active => 't' })->[0]->barcode;
183 my $type = shift || 'dob';
185 return "" unless $date;
187 my $dt = DateTime::Format::ISO8601->new->
188 parse_datetime(OpenSRF::Utils::cleanse_ISO8601($date));
189 my @time = localtime($dt->epoch);
191 my $year = $time[5]+1900;
192 my $mon = $time[4]+1;
195 my $minute = $time[1];
196 my $second = $time[0];
198 $date = sprintf("%04d%02d%02d", $year, $mon, $day);
200 # Due dates need hyphen separators and time of day as well
201 if ($type eq 'due') {
203 my $use_sdf = $class->get_option_value('use_sip_date_format') | '';
205 if ($use_sdf =~ /true/i) {
206 $date = $dt->strftime(SIP_DATETIME);
209 $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d",
210 $year, $mon, $day, $hour, $minute, $second);
214 syslog('LOG_DEBUG', "OILS: formatted date [type=$type]: $date");
221 my( $self, $username, $password ) = @_;
222 syslog('LOG_DEBUG', "OILS: Logging in with username $username");
224 my $seed = $U->simplereq(
226 'open-ils.auth.authenticate.init', $username );
228 my $response = $U->simplereq(
230 'open-ils.auth.authenticate.complete',
232 username => $username,
233 password => md5_hex($seed . md5_hex($password)),
238 if( my $code = $U->event_code($response) ) {
239 my $txt = $response->{textcode};
240 syslog('LOG_WARNING', "OILS: Login failed for $username. $txt:$code");
244 my $key = $response->{payload}->{authtoken};
245 syslog('LOG_INFO', "OILS: Login succeeded for $username : authkey = $key");
247 $self->fetch_session; # to cache the login
249 return $self->{authtoken} = $key;
253 # find_patron($barcode);
254 # find_patron(barcode => $barcode); # same as above
255 # find_patron(usr => $id);
259 my $key = (@_ > 1) ? shift : 'barcode'; # if we have multiple args, the first is the key index (default barcode)
260 my $patron_id = shift;
262 return OpenILS::SIP::Patron->new($key => $patron_id, authtoken => $self->{authtoken}, @_);
268 return OpenILS::SIP::Item->new(@_);
274 return $self->{institution}->{id}; # consider making this return the whole institution
279 return $self->{institution}->{id}; # then use this for just the ID
283 my ($self, $op) = @_;
284 my ($i) = grep { $_->{name} eq $op }
285 @{$config->{implementation_config}->{supports}->{item}};
286 return to_bool($i->{value});
290 my ($self, $id, $whence) = @_;
291 if ($id ne $self->{institution}->{id}) {
292 syslog("LOG_WARNING", "OILS: %s: received institution '%s', expected '%s'", $whence, $id, $self->{institution}->{id});
293 # Just an FYI check, we don't expect the user to change location from that in SIPconfig.xml
300 # If it's defined, and matches a true sort of string, or is
301 # a non-zero number, then it's true.
302 defined($bool) or return; # false
303 ($bool =~ /true|y|yes/i) and return 1; # true
304 return ($bool =~ /^\d+$/ and $bool != 0); # true for non-zero numbers, false otherwise
308 return to_bool($config->{policy}->{checkout});
312 return to_bool($config->{policy}->{checkin});
316 return to_bool($config->{policy}->{renewal});
319 sub status_update_ok {
320 return to_bool($config->{policy}->{status_update});
324 return to_bool($config->{policy}->{offline});
330 ## Checkout(patron_id, item_id, sc_renew, fee_ack):
331 ## patron_id & item_id are the identifiers send by the terminal
332 ## sc_renew is the renewal policy configured on the terminal
333 ## returns a status opject that can be queried for the various bits
334 ## of information that the protocol (SIP or NCIP) needs to generate
336 ## fee_ack is the fee_acknowledged field (BO) sent from the sc
337 ## when doing chargeable loans.
341 my ($self, $patron_id, $item_id, $sc_renew, $fee_ack) = @_;
342 # In order to allow renewals the selfcheck AND the config have to say they are allowed
343 $sc_renew = (chr($sc_renew) eq 'Y' && $self->renew_ok());
345 $self->verify_session;
347 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout attempt: patron=$patron_id, item=$item_id");
349 my $xact = OpenILS::SIP::Transaction::Checkout->new( authtoken => $self->{authtoken} );
350 my $patron = $self->find_patron($patron_id);
351 my $item = $self->find_item($item_id);
353 $xact->patron($patron);
357 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
361 if (!$patron->charge_ok) {
362 $xact->screen_msg("Patron Blocked");
367 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
371 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout data loaded OK, checking out...");
373 if ($item->{patron} && ($item->{patron} eq $patron_id)) {
374 $xact->renew_ok(1); # So that accept/reject responses have the correct value later
376 syslog('LOG_INFO', "OILS: OpenILS::Checkout data loaded OK, doing renew...");
378 syslog('LOG_INFO', "OILS: OpenILS::Checkout appears to be renew, but renewal disallowed...");
379 $xact->screen_msg("Renewals not permitted");
381 return $xact; # Don't attempt later
383 } elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
384 # I can't deal with this right now
385 # XXX check in then check out?
386 $xact->screen_msg("Item checked out to another patron");
388 return $xact; # Don't wipe out the screen message later
393 # Check for fee and $fee_ack. If there is a fee, and $fee_ack
394 # is 'Y', we proceed, otherwise we reject the checkout.
395 if ($item->fee > 0.0) {
396 $xact->fee_amount($item->fee);
397 $xact->sip_fee_type($item->sip_fee_type);
398 $xact->sip_currency($item->fee_currency);
399 if ($fee_ack && $fee_ack eq 'Y') {
402 $xact->screen_msg('Fee required');
408 $xact->do_checkout($sc_renew);
409 $xact->desensitize(!$item->magnetic);
413 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
414 "patron %s checkout %s succeeded", $patron_id, $item_id);
416 #editor()->xact_rollback;
417 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
418 "patron %s checkout %s FAILED, rolling back xact...", $patron_id, $item_id);
426 my ($self, $item_id, $inst_id, $trans_date, $return_date,
427 $current_loc, $item_props, $cancel) = @_;
429 my $start_time = time();
431 $self->verify_session;
433 syslog('LOG_DEBUG', "OILS: OpenILS::Checkin of item=$item_id (to $inst_id)");
435 my $xact = OpenILS::SIP::Transaction::Checkin->new(authtoken => $self->{authtoken});
436 my $item = OpenILS::SIP::Item->new($item_id);
438 unless ( $xact->item($item) ) {
440 # $circ->alert(1); $circ->alert_type(99);
441 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
442 syslog('LOG_INFO', "OILS: Checkin failed. " . $xact->screen_msg() );
446 $xact->do_checkin( $self, $inst_id, $trans_date, $return_date, $current_loc, $item_props );
449 $xact->patron($self->find_patron(usr => $xact->{circ_user_id}, slim_user => 1)) if $xact->{circ_user_id};
450 delete $item->{patron};
451 delete $item->{due_date};
452 syslog('LOG_INFO', "OILS: Checkin succeeded");
454 syslog('LOG_WARNING', "OILS: Checkin failed");
457 syslog('LOG_INFO', "OILS: SIP Checkin request took %0.3f seconds", (time() - $start_time));
461 ## If the ILS caches patron information, this lets it free it up.
462 ## Also, this could be used for centrally logging session duration.
463 ## We don't do anything with it.
464 sub end_patron_session {
465 my ($self, $patron_id) = @_;
466 return (1, 'Thank you!', '');
471 my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
472 $pay_type, $fee_id, $trans_id, $currency) = @_;
474 $self->verify_session;
476 my $xact = OpenILS::SIP::Transaction::FeePayment->new(authtoken => $self->{authtoken});
477 my $patron = $self->find_patron($patron_id);
480 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
485 $xact->patron($patron);
486 $xact->sip_currency($currency);
487 $xact->fee_amount($fee_amt);
488 $xact->sip_fee_type($fee_type);
489 $xact->transaction_id($trans_id);
490 $xact->fee_id($fee_id);
491 $xact->sip_payment_type($pay_type);
492 # We don't presently use this, but we might in the future.
493 $xact->patron_password($patron_pwd);
495 $xact->do_fee_payment();
501 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
502 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
503 # my ($patron, $item);
508 # $trans = new ILS::Transaction::Hold;
510 # # BEGIN TRANSACTION
511 # $patron = new ILS::Patron $patron_id;
513 # || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
514 # $trans->screen_msg("Invalid Patron.");
519 # $item = new ILS::Item ($item_id || $title_id);
521 # $trans->screen_msg("No such item.");
523 # # END TRANSACTION (conditionally)
525 # } elsif ($item->fee && ($fee_ack ne 'Y')) {
526 # $trans->screen_msg = "Fee required to place hold.";
528 # # END TRANSACTION (conditionally)
533 # item_id => $item->id,
534 # patron_id => $patron->id,
535 # expiration_date => $expiry_date,
536 # pickup_location => $pickup_location,
537 # hold_type => $hold_type,
541 # $trans->patron($patron);
542 # $trans->item($item);
543 # $trans->pickup_location($pickup_location);
545 # push(@{$item->hold_queue}, $hold);
546 # push(@{$patron->{hold_items}}, $hold);
554 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
555 # my ($patron, $item, $hold);
558 # $trans = new ILS::Transaction::Hold;
560 # # BEGIN TRANSACTION
561 # $patron = new ILS::Patron $patron_id;
563 # $trans->screen_msg("Invalid patron barcode.");
566 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
567 # $trans->screen_msg('Invalid patron password.');
572 # $item = new ILS::Item ($item_id || $title_id);
574 # $trans->screen_msg("No such item.");
576 # # END TRANSACTION (conditionally)
580 # # Remove the hold from the patron's record first
581 # $trans->ok($patron->drop_hold($item_id));
584 # # We didn't find it on the patron record
585 # $trans->screen_msg("No such hold on patron record.");
587 # # END TRANSACTION (conditionally)
591 # # Now, remove it from the item record. If it was on the patron
592 # # record but not on the item record, we'll treat that as success.
593 # foreach my $i (0 .. scalar @{$item->hold_queue}) {
594 # $hold = $item->hold_queue->[$i];
596 # if ($hold->{patron_id} eq $patron->id) {
597 # # found it: delete it.
598 # splice @{$item->hold_queue}, $i, 1;
603 # $trans->screen_msg("Hold Cancelled.");
604 # $trans->patron($patron);
605 # $trans->item($item);
611 ## The patron and item id's can't be altered, but the
612 ## date, location, and type can.
614 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
615 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
616 # my ($patron, $item);
620 # $trans = new ILS::Transaction::Hold;
622 # # BEGIN TRANSACTION
623 # $patron = new ILS::Patron $patron_id;
625 # $trans->screen_msg("Invalid patron barcode.");
630 # foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
631 # $hold = $patron->{hold_items}[$i];
633 # if ($hold->{item_id} eq $item_id) {
634 # # Found it. So fix it.
635 # $hold->{expiration_date} = $expiry_date if $expiry_date;
636 # $hold->{pickup_location} = $pickup_location if $pickup_location;
637 # $hold->{hold_type} = $hold_type if $hold_type;
640 # $trans->screen_msg("Hold updated.");
641 # $trans->patron($patron);
642 # $trans->item(new ILS::Item $hold->{item_id});
647 # # The same hold structure is linked into both the patron's
648 # # list of hold items and into the queue of outstanding holds
649 # # for the item, so we don't need to search the hold queue for
650 # # the item, since it's already been updated by the patron code.
653 # $trans->screen_msg("No such outstanding hold.");
661 my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
662 $no_block, $nb_due_date, $third_party, $item_props, $fee_ack) = @_;
664 $self->verify_session;
666 my $trans = OpenILS::SIP::Transaction::Renew->new( authtoken => $self->{authtoken} );
667 $trans->patron($self->find_patron($patron_id));
668 $trans->item($self->find_item($item_id));
670 if(!$trans->patron) {
671 $trans->screen_msg("Invalid patron barcode.");
676 if(!$trans->patron->renew_ok) {
677 $trans->screen_msg("Renewals not allowed.");
684 $trans->screen_msg("Title ID renewal not supported. Use item barcode.");
686 $trans->screen_msg("Invalid item barcode.");
692 if(!$trans->item->{patron} or
693 $trans->item->{patron} ne $patron_id) {
694 $trans->screen_msg("Item not checked out to " . $trans->patron->name);
699 # Perform the renewal
702 $trans->desensitize(0); # It's already checked out
703 $trans->item->{due_date} = $nb_due_date if $no_block eq 'Y';
704 $trans->item->{sip_item_properties} = $item_props if $item_props;
715 # my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
716 # my ($patron, $item_id);
719 # $trans = new ILS::Transaction::RenewAll;
721 # $trans->patron($patron = new ILS::Patron $patron_id);
722 # if (defined $patron) {
723 # syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
724 # $patron->name, $patron->renew_ok);
726 # syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
730 # if (!defined($patron)) {
731 # $trans->screen_msg("Invalid patron barcode.");
733 # } elsif (!$patron->renew_ok) {
734 # $trans->screen_msg("Renewals not allowed.");
736 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
737 # $trans->screen_msg("Invalid patron password.");
741 # foreach $item_id (@{$patron->{items}}) {
742 # my $item = new ILS::Item $item_id;
744 # if (!defined($item)) {
745 # syslog("LOG_WARNING",
746 # "renew_all: Invalid item id associated with patron '%s'",
751 # if (@{$item->hold_queue}) {
752 # # Can't renew if there are outstanding holds
753 # push @{$trans->unrenewed}, $item_id;
755 # $item->{due_date} = time + (14*24*60*60); # two weeks hence
756 # push @{$trans->renewed}, $item_id;