2 # ILS.pm: Test ILS interface module
6 use warnings; use strict;
8 use Sys::Syslog qw(syslog);
10 use OpenILS::SIP::Item;
11 use OpenILS::SIP::Patron;
12 use OpenILS::SIP::Transaction;
13 use OpenILS::SIP::Transaction::Checkout;
14 use OpenILS::SIP::Transaction::Checkin;
15 use OpenILS::SIP::Transaction::Renew;
18 use OpenILS::Utils::Fieldmapper;
19 use OpenSRF::Utils::SettingsClient;
20 use OpenILS::Application::AppUtils;
21 use OpenSRF::Utils qw/:datetime/;
22 use DateTime::Format::ISO8601;
24 use Unicode::Normalize;
25 my $U = 'OpenILS::Application::AppUtils';
31 use Digest::MD5 qw(md5_hex);
34 my ($class, $institution, $login) = @_;
35 my $type = ref($class) || $class;
38 $self->{login} = $login;
40 $config = $institution;
41 syslog("LOG_DEBUG", "OILS: new ILS '%s'", $institution->{id});
42 $self->{institution} = $institution;
44 my $bsconfig = $institution->{implementation_config}->{bootstrap};
45 $target_encoding = $institution->{implementation_config}->{encoding} || 'ascii';
47 syslog('LOG_DEBUG', "OILS: loading bootstrap config: $bsconfig");
50 OpenSRF::System->bootstrap_client(config_file => $bsconfig);
51 syslog('LOG_DEBUG', "OILS: bootstrap loaded..");
53 $self->{osrf_config} = OpenSRF::Utils::SettingsClient->new;
55 Fieldmapper->import($self->{osrf_config}->config_value('IDL'));
57 bless( $self, $type );
60 $self->login( $login->{id}, $login->{password} );
67 my $ses = $U->simplereq(
69 'open-ils.auth.session.retrieve', $self->{authtoken} );
70 return 1 unless $U->event_code($ses);
71 syslog('LOG_INFO', "OILS: Logging back after session timeout as user ".$self->{login}->{id});
72 return $self->login( $self->{login}->{id}, $self->{login}->{password} );
77 return ($val and $val =~ /true/io);
81 return $editor = make_editor();
89 # Creates the global editor object
90 my $cstore_init = 1; # call init on first use
92 OpenILS::Utils::CStoreEditor::init() if $cstore_init;
94 return OpenILS::Utils::CStoreEditor->new;
97 =head2 clean_text(scalar)
99 Evergreen uses the UTF8 encoding for everything from the database up. Perl
100 doesn't know this, however, so we have to convince it to treat our UTF8 strings
101 as UTF8 strings. This may enable OpenNCIP to correctly calculate the checksums
102 for UTF8 text for SIP clients that support such modern options.
104 The target encoding is set in the <encoding> element of the SIPServer.pm
110 my $text = shift || '';
112 # Convert our incoming UTF8 data into Perl's internal string format
114 # Also convert to Normalization Form D, as the ASCII, iso-8859-1,
115 # and latin-1 encodings (at least) require this to substitute
116 # characters rather than simply returning a string truncated
117 # after the first non-ASCII character
118 $text = NFD(decode_utf8($text));
120 if ($target_encoding eq 'ascii') {
122 # Try to maintain a reasonable version of the content by
123 # stripping diacritics from the text, given that the SIP client
124 # wants just plain ASCII. This is the base requirement according
125 # to the SIP2 specification.
127 # Stripping the combining characters converts ""béè♁ts"
128 # into "bee?ts" instead of "b???ts" - better, eh?
132 # Characters that cannot be represented in the target encoding will
133 # generally be replaced with a question mark (?) character.
134 $text = encode($target_encoding, $text);
142 my $type = shift || 'dob';
144 return "" unless $date;
146 $date = DateTime::Format::ISO8601->new->
147 parse_datetime(OpenSRF::Utils::cleanse_ISO8601($date));
148 my @time = localtime($date->epoch);
150 my $year = $time[5]+1900;
151 my $mon = $time[4]+1;
154 my $minute = $time[1];
155 my $second = $time[0];
157 $date = sprintf("%04d%02d%02d", $year, $mon, $day);
159 # Due dates need hyphen separators and time of day as well
160 if ($type eq 'due') {
161 $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year, $mon, $day, $hour, $minute, $second);
164 syslog('LOG_DEBUG', "OILS: formatted date [type=$type]: $date");
171 my( $self, $username, $password ) = @_;
172 syslog('LOG_DEBUG', "OILS: Logging in with username $username");
174 my $seed = $U->simplereq(
176 'open-ils.auth.authenticate.init', $username );
178 my $response = $U->simplereq(
180 'open-ils.auth.authenticate.complete',
182 username => $username,
183 password => md5_hex($seed . md5_hex($password)),
188 if( my $code = $U->event_code($response) ) {
189 my $txt = $response->{textcode};
190 syslog('LOG_WARNING', "OILS: Login failed for $username. $txt:$code");
194 my $key = $response->{payload}->{authtoken};
195 syslog('LOG_INFO', "OILS: Login succeeded for $username : authkey = $key");
196 return $self->{authtoken} = $key;
202 return OpenILS::SIP::Patron->new(@_);
208 return OpenILS::SIP::Item->new(@_);
214 return $self->{institution}->{id};
218 my ($self, $op) = @_;
219 my ($i) = grep { $_->{name} eq $op }
220 @{$config->{implementation_config}->{supports}->{item}};
221 return to_bool($i->{value});
225 my ($self, $id, $whence) = @_;
226 if ($id ne $self->{institution}->{id}) {
227 syslog("LOG_WARNING",
228 "OILS: %s: received institution '%s', expected '%s'",
229 $whence, $id, $self->{institution}->{id});
234 return to_bool($config->{policy}->{checkout});
238 return to_bool($config->{policy}->{checkin});
243 return to_bool($config->{policy}->{renew});
246 sub status_update_ok {
247 return to_bool($config->{policy}->{status_update});
251 return to_bool($config->{policy}->{offline});
257 ## Checkout(patron_id, item_id, sc_renew):
258 ## patron_id & item_id are the identifiers send by the terminal
259 ## sc_renew is the renewal policy configured on the terminal
260 ## returns a status opject that can be queried for the various bits
261 ## of information that the protocol (SIP or NCIP) needs to generate
266 my ($self, $patron_id, $item_id, $sc_renew) = @_;
269 $self->verify_session;
271 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout attempt: patron=$patron_id, item=$item_id");
273 my $xact = OpenILS::SIP::Transaction::Checkout->new( authtoken => $self->{authtoken} );
274 my $patron = $self->find_patron($patron_id);
275 my $item = $self->find_item($item_id);
277 $xact->patron($patron);
281 $xact->screen_msg("Invalid Patron");
285 if (!$patron->charge_ok) {
286 $xact->screen_msg("Patron Blocked");
291 $xact->screen_msg("Invalid Item");
295 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout data loaded OK, checking out...");
297 if ($item->{patron} && ($item->{patron} eq $patron_id)) {
298 syslog('LOG_INFO', "OILS: OpenILS::Checkout data loaded OK, doing renew...");
300 } elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
301 # I can't deal with this right now
302 # XXX check in then check out?
303 $xact->screen_msg("Item checked out to another patron");
307 $xact->do_checkout($sc_renew);
308 $xact->desensitize(!$item->magnetic);
313 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
314 "patron %s checkout %s succeeded", $patron_id, $item_id);
318 #editor()->xact_rollback;
319 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
320 "patron %s checkout %s FAILED, rolling back xact...", $patron_id, $item_id);
328 my ($self, $item_id, $trans_date, $return_date,
329 $current_loc, $item_props, $cancel) = @_;
331 $self->verify_session;
333 syslog('LOG_DEBUG', "OILS: OpenILS::Checkin on item=$item_id");
336 my $xact = OpenILS::SIP::Transaction::Checkin->new(authtoken => $self->{authtoken});
337 my $item = $self->find_item($item_id);
342 $xact->screen_msg("Invalid item barcode: $item_id");
347 $xact->do_checkin( $trans_date, $return_date, $current_loc, $item_props );
351 $xact->patron($patron = $self->find_patron($item->{patron}));
352 delete $item->{patron};
353 delete $item->{due_date};
354 syslog('LOG_INFO', "OILS: Checkin succeeded");
359 #editor()->xact_rollback;
360 syslog('LOG_WARNING', "OILS: Checkin failed");
367 ## If the ILS caches patron information, this lets it free it up
368 sub end_patron_session {
369 my ($self, $patron_id) = @_;
370 return (1, 'Thank you for using OpenILS!', '');
375 # my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
376 # $pay_type, $fee_id, $trans_id, $currency) = @_;
380 # $trans = new ILS::Transaction::FeePayment;
382 # $patron = new ILS::Patron $patron_id;
384 # $trans->transaction_id($trans_id);
385 # $trans->patron($patron);
392 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
393 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
394 # my ($patron, $item);
399 # $trans = new ILS::Transaction::Hold;
401 # # BEGIN TRANSACTION
402 # $patron = new ILS::Patron $patron_id;
404 # || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
405 # $trans->screen_msg("Invalid Patron.");
410 # $item = new ILS::Item ($item_id || $title_id);
412 # $trans->screen_msg("No such item.");
414 # # END TRANSACTION (conditionally)
416 # } elsif ($item->fee && ($fee_ack ne 'Y')) {
417 # $trans->screen_msg = "Fee required to place hold.";
419 # # END TRANSACTION (conditionally)
424 # item_id => $item->id,
425 # patron_id => $patron->id,
426 # expiration_date => $expiry_date,
427 # pickup_location => $pickup_location,
428 # hold_type => $hold_type,
432 # $trans->patron($patron);
433 # $trans->item($item);
434 # $trans->pickup_location($pickup_location);
436 # push(@{$item->hold_queue}, $hold);
437 # push(@{$patron->{hold_items}}, $hold);
445 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
446 # my ($patron, $item, $hold);
449 # $trans = new ILS::Transaction::Hold;
451 # # BEGIN TRANSACTION
452 # $patron = new ILS::Patron $patron_id;
454 # $trans->screen_msg("Invalid patron barcode.");
457 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
458 # $trans->screen_msg('Invalid patron password.');
463 # $item = new ILS::Item ($item_id || $title_id);
465 # $trans->screen_msg("No such item.");
467 # # END TRANSACTION (conditionally)
471 # # Remove the hold from the patron's record first
472 # $trans->ok($patron->drop_hold($item_id));
475 # # We didn't find it on the patron record
476 # $trans->screen_msg("No such hold on patron record.");
478 # # END TRANSACTION (conditionally)
482 # # Now, remove it from the item record. If it was on the patron
483 # # record but not on the item record, we'll treat that as success.
484 # foreach my $i (0 .. scalar @{$item->hold_queue}) {
485 # $hold = $item->hold_queue->[$i];
487 # if ($hold->{patron_id} eq $patron->id) {
488 # # found it: delete it.
489 # splice @{$item->hold_queue}, $i, 1;
494 # $trans->screen_msg("Hold Cancelled.");
495 # $trans->patron($patron);
496 # $trans->item($item);
502 ## The patron and item id's can't be altered, but the
503 ## date, location, and type can.
505 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
506 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
507 # my ($patron, $item);
511 # $trans = new ILS::Transaction::Hold;
513 # # BEGIN TRANSACTION
514 # $patron = new ILS::Patron $patron_id;
516 # $trans->screen_msg("Invalid patron barcode.");
521 # foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
522 # $hold = $patron->{hold_items}[$i];
524 # if ($hold->{item_id} eq $item_id) {
525 # # Found it. So fix it.
526 # $hold->{expiration_date} = $expiry_date if $expiry_date;
527 # $hold->{pickup_location} = $pickup_location if $pickup_location;
528 # $hold->{hold_type} = $hold_type if $hold_type;
531 # $trans->screen_msg("Hold updated.");
532 # $trans->patron($patron);
533 # $trans->item(new ILS::Item $hold->{item_id});
538 # # The same hold structure is linked into both the patron's
539 # # list of hold items and into the queue of outstanding holds
540 # # for the item, so we don't need to search the hold queue for
541 # # the item, since it's already been updated by the patron code.
544 # $trans->screen_msg("No such outstanding hold.");
552 my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
553 $no_block, $nb_due_date, $third_party, $item_props, $fee_ack) = @_;
555 $self->verify_session;
557 my $trans = OpenILS::SIP::Transaction::Renew->new( authtoken => $self->{authtoken} );
558 $trans->patron($self->find_patron($patron_id));
559 $trans->item($self->find_item($item_id));
561 if(!$trans->patron) {
562 $trans->screen_msg("Invalid patron barcode.");
567 if(!$trans->patron->renew_ok) {
568 $trans->screen_msg("Renewals not allowed.");
575 $trans->screen_msg("Item Id renewal not supported.");
577 $trans->screen_msg("Invalid item barcode.");
583 if(!$trans->item->{patron} or
584 $trans->item->{patron} ne $patron_id) {
585 $trans->screen_msg("Item not checked out to " . $trans->patron->name);
590 # Perform the renewal
593 $trans->desensitize(0); # It's already checked out
594 $trans->item->{due_date} = $nb_due_date if $no_block eq 'Y';
595 $trans->item->{sip_item_properties} = $item_props if $item_props;
606 # my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
607 # my ($patron, $item_id);
610 # $trans = new ILS::Transaction::RenewAll;
612 # $trans->patron($patron = new ILS::Patron $patron_id);
613 # if (defined $patron) {
614 # syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
615 # $patron->name, $patron->renew_ok);
617 # syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
621 # if (!defined($patron)) {
622 # $trans->screen_msg("Invalid patron barcode.");
624 # } elsif (!$patron->renew_ok) {
625 # $trans->screen_msg("Renewals not allowed.");
627 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
628 # $trans->screen_msg("Invalid patron password.");
632 # foreach $item_id (@{$patron->{items}}) {
633 # my $item = new ILS::Item $item_id;
635 # if (!defined($item)) {
636 # syslog("LOG_WARNING",
637 # "renew_all: Invalid item id associated with patron '%s'",
642 # if (@{$item->hold_queue}) {
643 # # Can't renew if there are outstanding holds
644 # push @{$trans->unrenewed}, $item_id;
646 # $item->{due_date} = time + (14*24*60*60); # two weeks hence
647 # push @{$trans->renewed}, $item_id;