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';
29 my $target_encoding; # FIXME: this is configured at the institution level.
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");
49 local $/ = "\n"; # why?
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} );
76 return $editor = make_editor();
84 # Creates the global editor object
85 my $cstore_init = 1; # call init on first use
87 OpenILS::Utils::CStoreEditor::init() if $cstore_init;
89 return OpenILS::Utils::CStoreEditor->new;
92 =head2 clean_text(scalar)
94 Evergreen uses the UTF8 encoding for everything from the database up. Perl
95 doesn't know this, however, so we have to convince it to treat our UTF8 strings
96 as UTF8 strings. This may enable OpenNCIP to correctly calculate the checksums
97 for UTF8 text for SIP clients that support such modern options.
99 The target encoding is set in the <encoding> element of the SIPServer.pm
105 my $text = shift || '';
107 # Convert our incoming UTF8 data into Perl's internal string format
109 # Also convert to Normalization Form D, as the ASCII, iso-8859-1,
110 # and latin-1 encodings (at least) require this to substitute
111 # characters rather than simply returning a string truncated
112 # after the first non-ASCII character
113 $text = NFD(decode_utf8($text));
115 if ($target_encoding eq 'ascii') {
117 # Try to maintain a reasonable version of the content by
118 # stripping diacritics from the text, given that the SIP client
119 # wants just plain ASCII. This is the base requirement according
120 # to the SIP2 specification.
122 # Stripping the combining characters converts ""béè♁ts"
123 # into "bee?ts" instead of "b???ts" - better, eh?
127 # Characters that cannot be represented in the target encoding will
128 # generally be replaced with a question mark (?) character.
129 $text = encode($target_encoding, $text);
134 sub shortname_from_id {
135 my $id = shift or return;
136 return editor()->search_actor_org_unit({id => $id})->[0]->shortname;
138 sub patron_barcode_from_id {
139 my $id = shift or return;
140 return editor()->search_actor_card({ usr => $id, active => 't' })->[0]->barcode;
146 my $type = shift || 'dob';
148 return "" unless $date;
150 $date = DateTime::Format::ISO8601->new->
151 parse_datetime(OpenSRF::Utils::cleanse_ISO8601($date));
152 my @time = localtime($date->epoch);
154 my $year = $time[5]+1900;
155 my $mon = $time[4]+1;
158 my $minute = $time[1];
159 my $second = $time[0];
161 $date = sprintf("%04d%02d%02d", $year, $mon, $day);
163 # Due dates need hyphen separators and time of day as well
164 if ($type eq 'due') {
165 $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year, $mon, $day, $hour, $minute, $second);
168 syslog('LOG_DEBUG', "OILS: formatted date [type=$type]: $date");
175 my( $self, $username, $password ) = @_;
176 syslog('LOG_DEBUG', "OILS: Logging in with username $username");
178 my $seed = $U->simplereq(
180 'open-ils.auth.authenticate.init', $username );
182 my $response = $U->simplereq(
184 'open-ils.auth.authenticate.complete',
186 username => $username,
187 password => md5_hex($seed . md5_hex($password)),
192 if( my $code = $U->event_code($response) ) {
193 my $txt = $response->{textcode};
194 syslog('LOG_WARNING', "OILS: Login failed for $username. $txt:$code");
198 my $key = $response->{payload}->{authtoken};
199 syslog('LOG_INFO', "OILS: Login succeeded for $username : authkey = $key");
200 return $self->{authtoken} = $key;
206 return OpenILS::SIP::Patron->new(@_);
212 return OpenILS::SIP::Item->new(@_);
218 return $self->{institution}->{id}; # consider making this return the whole institution
223 return $self->{institution}->{id}; # then use this for just the ID
227 my ($self, $op) = @_;
228 my ($i) = grep { $_->{name} eq $op }
229 @{$config->{implementation_config}->{supports}->{item}};
230 return to_bool($i->{value});
234 my ($self, $id, $whence) = @_;
235 if ($id ne $self->{institution}->{id}) {
236 syslog("LOG_WARNING", "OILS: %s: received institution '%s', expected '%s'", $whence, $id, $self->{institution}->{id});
237 # Just an FYI check, we don't expect the user to change location from that in SIPconfig.xml
244 # If it's defined, and matches a true sort of string, or is
245 # a non-zero number, then it's true.
246 defined($bool) or return; # false
247 ($bool =~ /true|y|yes/i) and return 1; # true
248 return ($bool =~ /^\d+$/ and $bool != 0); # true for non-zero numbers, false otherwise
252 return to_bool($config->{policy}->{checkout});
256 return to_bool($config->{policy}->{checkin});
260 return to_bool($config->{policy}->{renew});
263 sub status_update_ok {
264 return to_bool($config->{policy}->{status_update});
268 return to_bool($config->{policy}->{offline});
274 ## Checkout(patron_id, item_id, sc_renew):
275 ## patron_id & item_id are the identifiers send by the terminal
276 ## sc_renew is the renewal policy configured on the terminal
277 ## returns a status opject that can be queried for the various bits
278 ## of information that the protocol (SIP or NCIP) needs to generate
283 my ($self, $patron_id, $item_id, $sc_renew) = @_;
286 $self->verify_session;
288 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout attempt: patron=$patron_id, item=$item_id");
290 my $xact = OpenILS::SIP::Transaction::Checkout->new( authtoken => $self->{authtoken} );
291 my $patron = $self->find_patron($patron_id);
292 my $item = $self->find_item($item_id);
294 $xact->patron($patron);
298 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
302 if (!$patron->charge_ok) {
303 $xact->screen_msg("Patron Blocked");
308 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
312 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout data loaded OK, checking out...");
314 if ($item->{patron} && ($item->{patron} eq $patron_id)) {
315 syslog('LOG_INFO', "OILS: OpenILS::Checkout data loaded OK, doing renew...");
317 } elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
318 # I can't deal with this right now
319 # XXX check in then check out?
320 $xact->screen_msg("Item checked out to another patron");
324 $xact->do_checkout($sc_renew);
325 $xact->desensitize(!$item->magnetic);
329 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
330 "patron %s checkout %s succeeded", $patron_id, $item_id);
332 #editor()->xact_rollback;
333 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
334 "patron %s checkout %s FAILED, rolling back xact...", $patron_id, $item_id);
342 my ($self, $item_id, $inst_id, $trans_date, $return_date,
343 $current_loc, $item_props, $cancel) = @_;
345 $self->verify_session;
347 syslog('LOG_DEBUG', "OILS: OpenILS::Checkin of item=$item_id (to $inst_id)");
349 my $xact = OpenILS::SIP::Transaction::Checkin->new(authtoken => $self->{authtoken});
350 my $item = OpenILS::SIP::Item->new($item_id);
352 unless ( $xact->item($item) ) {
354 # $circ->alert(1); $circ->alert_type(99);
355 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
356 syslog('LOG_INFO', "OILS: Checkin failed. " . $xact->screen_msg() );
360 $xact->do_checkin( $inst_id, $trans_date, $return_date, $current_loc, $item_props );
363 $xact->patron($self->find_patron($item->{patron}));
364 delete $item->{patron};
365 delete $item->{due_date};
366 syslog('LOG_INFO', "OILS: Checkin succeeded");
369 #editor()->xact_rollback;
370 syslog('LOG_WARNING', "OILS: Checkin failed");
376 ## If the ILS caches patron information, this lets it free it up.
377 ## Also, this could be used for centrally logging session duration.
378 ## We don't do anything with it.
379 sub end_patron_session {
380 my ($self, $patron_id) = @_;
381 return (1, 'Thank you!', '');
386 # my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
387 # $pay_type, $fee_id, $trans_id, $currency) = @_;
391 # $trans = new ILS::Transaction::FeePayment;
393 # $patron = new ILS::Patron $patron_id;
395 # $trans->transaction_id($trans_id);
396 # $trans->patron($patron);
403 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
404 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
405 # my ($patron, $item);
410 # $trans = new ILS::Transaction::Hold;
412 # # BEGIN TRANSACTION
413 # $patron = new ILS::Patron $patron_id;
415 # || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
416 # $trans->screen_msg("Invalid Patron.");
421 # $item = new ILS::Item ($item_id || $title_id);
423 # $trans->screen_msg("No such item.");
425 # # END TRANSACTION (conditionally)
427 # } elsif ($item->fee && ($fee_ack ne 'Y')) {
428 # $trans->screen_msg = "Fee required to place hold.";
430 # # END TRANSACTION (conditionally)
435 # item_id => $item->id,
436 # patron_id => $patron->id,
437 # expiration_date => $expiry_date,
438 # pickup_location => $pickup_location,
439 # hold_type => $hold_type,
443 # $trans->patron($patron);
444 # $trans->item($item);
445 # $trans->pickup_location($pickup_location);
447 # push(@{$item->hold_queue}, $hold);
448 # push(@{$patron->{hold_items}}, $hold);
456 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
457 # my ($patron, $item, $hold);
460 # $trans = new ILS::Transaction::Hold;
462 # # BEGIN TRANSACTION
463 # $patron = new ILS::Patron $patron_id;
465 # $trans->screen_msg("Invalid patron barcode.");
468 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
469 # $trans->screen_msg('Invalid patron password.');
474 # $item = new ILS::Item ($item_id || $title_id);
476 # $trans->screen_msg("No such item.");
478 # # END TRANSACTION (conditionally)
482 # # Remove the hold from the patron's record first
483 # $trans->ok($patron->drop_hold($item_id));
486 # # We didn't find it on the patron record
487 # $trans->screen_msg("No such hold on patron record.");
489 # # END TRANSACTION (conditionally)
493 # # Now, remove it from the item record. If it was on the patron
494 # # record but not on the item record, we'll treat that as success.
495 # foreach my $i (0 .. scalar @{$item->hold_queue}) {
496 # $hold = $item->hold_queue->[$i];
498 # if ($hold->{patron_id} eq $patron->id) {
499 # # found it: delete it.
500 # splice @{$item->hold_queue}, $i, 1;
505 # $trans->screen_msg("Hold Cancelled.");
506 # $trans->patron($patron);
507 # $trans->item($item);
513 ## The patron and item id's can't be altered, but the
514 ## date, location, and type can.
516 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
517 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
518 # my ($patron, $item);
522 # $trans = new ILS::Transaction::Hold;
524 # # BEGIN TRANSACTION
525 # $patron = new ILS::Patron $patron_id;
527 # $trans->screen_msg("Invalid patron barcode.");
532 # foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
533 # $hold = $patron->{hold_items}[$i];
535 # if ($hold->{item_id} eq $item_id) {
536 # # Found it. So fix it.
537 # $hold->{expiration_date} = $expiry_date if $expiry_date;
538 # $hold->{pickup_location} = $pickup_location if $pickup_location;
539 # $hold->{hold_type} = $hold_type if $hold_type;
542 # $trans->screen_msg("Hold updated.");
543 # $trans->patron($patron);
544 # $trans->item(new ILS::Item $hold->{item_id});
549 # # The same hold structure is linked into both the patron's
550 # # list of hold items and into the queue of outstanding holds
551 # # for the item, so we don't need to search the hold queue for
552 # # the item, since it's already been updated by the patron code.
555 # $trans->screen_msg("No such outstanding hold.");
563 my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
564 $no_block, $nb_due_date, $third_party, $item_props, $fee_ack) = @_;
566 $self->verify_session;
568 my $trans = OpenILS::SIP::Transaction::Renew->new( authtoken => $self->{authtoken} );
569 $trans->patron($self->find_patron($patron_id));
570 $trans->item($self->find_item($item_id));
572 if(!$trans->patron) {
573 $trans->screen_msg("Invalid patron barcode.");
578 if(!$trans->patron->renew_ok) {
579 $trans->screen_msg("Renewals not allowed.");
586 $trans->screen_msg("Title ID renewal not supported. Use item barcode.");
588 $trans->screen_msg("Invalid item barcode.");
594 if(!$trans->item->{patron} or
595 $trans->item->{patron} ne $patron_id) {
596 $trans->screen_msg("Item not checked out to " . $trans->patron->name);
601 # Perform the renewal
604 $trans->desensitize(0); # It's already checked out
605 $trans->item->{due_date} = $nb_due_date if $no_block eq 'Y';
606 $trans->item->{sip_item_properties} = $item_props if $item_props;
617 # my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
618 # my ($patron, $item_id);
621 # $trans = new ILS::Transaction::RenewAll;
623 # $trans->patron($patron = new ILS::Patron $patron_id);
624 # if (defined $patron) {
625 # syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
626 # $patron->name, $patron->renew_ok);
628 # syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
632 # if (!defined($patron)) {
633 # $trans->screen_msg("Invalid patron barcode.");
635 # } elsif (!$patron->renew_ok) {
636 # $trans->screen_msg("Renewals not allowed.");
638 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
639 # $trans->screen_msg("Invalid patron password.");
643 # foreach $item_id (@{$patron->{items}}) {
644 # my $item = new ILS::Item $item_id;
646 # if (!defined($item)) {
647 # syslog("LOG_WARNING",
648 # "renew_all: Invalid item id associated with patron '%s'",
653 # if (@{$item->hold_queue}) {
654 # # Can't renew if there are outstanding holds
655 # push @{$trans->unrenewed}, $item_id;
657 # $item->{due_date} = time + (14*24*60*60); # two weeks hence
658 # push @{$trans->renewed}, $item_id;