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} );
68 my $ses = $U->simplereq(
70 'open-ils.auth.session.retrieve', $self->{authtoken});
72 return undef if $U->event_code($ses); # auth timed out
73 return $self->{login_session} = $ses;
79 return 1 if $self->fetch_session;
81 syslog('LOG_INFO', "OILS: Logging back after session timeout as user ".$self->{login}->{id});
82 return $self->login( $self->{login}->{id}, $self->{login}->{password} );
86 return $editor = make_editor();
93 sub get_option_value {
94 my($self, $option) = @_;
95 my $ops = $config->{implementation_config}->{options}->{option};
96 $ops = [$ops] unless ref $ops eq 'ARRAY';
97 my @vals = grep { $_->{name} eq $option } @$ops;
98 return @vals ? $vals[0]->{value} : undef;
102 # Creates the global editor object
103 my $cstore_init = 1; # call init on first use
105 OpenILS::Utils::CStoreEditor::init() if $cstore_init;
107 return OpenILS::Utils::CStoreEditor->new;
110 =head2 clean_text(scalar)
112 Evergreen uses the UTF8 encoding for everything from the database up. Perl
113 doesn't know this, however, so we have to convince it to treat our UTF8 strings
114 as UTF8 strings. This may enable OpenNCIP to correctly calculate the checksums
115 for UTF8 text for SIP clients that support such modern options.
117 The target encoding is set in the <encoding> element of the SIPServer.pm
123 my $text = shift || '';
125 # Convert our incoming UTF8 data into Perl's internal string format
127 # Also convert to Normalization Form D, as the ASCII, iso-8859-1,
128 # and latin-1 encodings (at least) require this to substitute
129 # characters rather than simply returning a string truncated
130 # after the first non-ASCII character
131 $text = NFD(decode_utf8($text));
133 if ($target_encoding eq 'ascii') {
135 # Try to maintain a reasonable version of the content by
136 # stripping diacritics from the text, given that the SIP client
137 # wants just plain ASCII. This is the base requirement according
138 # to the SIP2 specification.
140 # Stripping the combining characters converts ""béè♁ts"
141 # into "bee?ts" instead of "b???ts" - better, eh?
145 # Characters that cannot be represented in the target encoding will
146 # generally be replaced with a question mark (?) character.
147 $text = encode($target_encoding, $text);
152 sub shortname_from_id {
153 my $id = shift or return;
154 return editor()->search_actor_org_unit({id => $id})->[0]->shortname;
156 sub patron_barcode_from_id {
157 my $id = shift or return;
158 return editor()->search_actor_card({ usr => $id, active => 't' })->[0]->barcode;
164 my $type = shift || 'dob';
166 return "" unless $date;
168 $date = DateTime::Format::ISO8601->new->
169 parse_datetime(OpenSRF::Utils::cleanse_ISO8601($date));
170 my @time = localtime($date->epoch);
172 my $year = $time[5]+1900;
173 my $mon = $time[4]+1;
176 my $minute = $time[1];
177 my $second = $time[0];
179 $date = sprintf("%04d%02d%02d", $year, $mon, $day);
181 # Due dates need hyphen separators and time of day as well
182 if ($type eq 'due') {
183 $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year, $mon, $day, $hour, $minute, $second);
186 syslog('LOG_DEBUG', "OILS: formatted date [type=$type]: $date");
193 my( $self, $username, $password ) = @_;
194 syslog('LOG_DEBUG', "OILS: Logging in with username $username");
196 my $seed = $U->simplereq(
198 'open-ils.auth.authenticate.init', $username );
200 my $response = $U->simplereq(
202 'open-ils.auth.authenticate.complete',
204 username => $username,
205 password => md5_hex($seed . md5_hex($password)),
210 if( my $code = $U->event_code($response) ) {
211 my $txt = $response->{textcode};
212 syslog('LOG_WARNING', "OILS: Login failed for $username. $txt:$code");
216 my $key = $response->{payload}->{authtoken};
217 syslog('LOG_INFO', "OILS: Login succeeded for $username : authkey = $key");
219 $self->fetch_session; # to cache the login
221 return $self->{authtoken} = $key;
227 return OpenILS::SIP::Patron->new(@_);
233 return OpenILS::SIP::Item->new(@_);
239 return $self->{institution}->{id}; # consider making this return the whole institution
244 return $self->{institution}->{id}; # then use this for just the ID
248 my ($self, $op) = @_;
249 my ($i) = grep { $_->{name} eq $op }
250 @{$config->{implementation_config}->{supports}->{item}};
251 return to_bool($i->{value});
255 my ($self, $id, $whence) = @_;
256 if ($id ne $self->{institution}->{id}) {
257 syslog("LOG_WARNING", "OILS: %s: received institution '%s', expected '%s'", $whence, $id, $self->{institution}->{id});
258 # Just an FYI check, we don't expect the user to change location from that in SIPconfig.xml
265 # If it's defined, and matches a true sort of string, or is
266 # a non-zero number, then it's true.
267 defined($bool) or return; # false
268 ($bool =~ /true|y|yes/i) and return 1; # true
269 return ($bool =~ /^\d+$/ and $bool != 0); # true for non-zero numbers, false otherwise
273 return to_bool($config->{policy}->{checkout});
277 return to_bool($config->{policy}->{checkin});
281 return to_bool($config->{policy}->{renew});
284 sub status_update_ok {
285 return to_bool($config->{policy}->{status_update});
289 return to_bool($config->{policy}->{offline});
295 ## Checkout(patron_id, item_id, sc_renew):
296 ## patron_id & item_id are the identifiers send by the terminal
297 ## sc_renew is the renewal policy configured on the terminal
298 ## returns a status opject that can be queried for the various bits
299 ## of information that the protocol (SIP or NCIP) needs to generate
304 my ($self, $patron_id, $item_id, $sc_renew) = @_;
307 $self->verify_session;
309 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout attempt: patron=$patron_id, item=$item_id");
311 my $xact = OpenILS::SIP::Transaction::Checkout->new( authtoken => $self->{authtoken} );
312 my $patron = $self->find_patron($patron_id);
313 my $item = $self->find_item($item_id);
315 $xact->patron($patron);
319 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
323 if (!$patron->charge_ok) {
324 $xact->screen_msg("Patron Blocked");
329 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
333 syslog('LOG_DEBUG', "OILS: OpenILS::Checkout data loaded OK, checking out...");
335 if ($item->{patron} && ($item->{patron} eq $patron_id)) {
336 syslog('LOG_INFO', "OILS: OpenILS::Checkout data loaded OK, doing renew...");
338 } elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
339 # I can't deal with this right now
340 # XXX check in then check out?
341 $xact->screen_msg("Item checked out to another patron");
345 $xact->do_checkout($sc_renew);
346 $xact->desensitize(!$item->magnetic);
350 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
351 "patron %s checkout %s succeeded", $patron_id, $item_id);
353 #editor()->xact_rollback;
354 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
355 "patron %s checkout %s FAILED, rolling back xact...", $patron_id, $item_id);
363 my ($self, $item_id, $inst_id, $trans_date, $return_date,
364 $current_loc, $item_props, $cancel) = @_;
366 $self->verify_session;
368 syslog('LOG_DEBUG', "OILS: OpenILS::Checkin of item=$item_id (to $inst_id)");
370 my $xact = OpenILS::SIP::Transaction::Checkin->new(authtoken => $self->{authtoken});
371 my $item = OpenILS::SIP::Item->new($item_id);
373 unless ( $xact->item($item) ) {
375 # $circ->alert(1); $circ->alert_type(99);
376 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
377 syslog('LOG_INFO', "OILS: Checkin failed. " . $xact->screen_msg() );
381 $xact->do_checkin( $self, $inst_id, $trans_date, $return_date, $current_loc, $item_props );
384 $xact->patron($self->find_patron($item->{patron}));
385 delete $item->{patron};
386 delete $item->{due_date};
387 syslog('LOG_INFO', "OILS: Checkin succeeded");
390 #editor()->xact_rollback;
391 syslog('LOG_WARNING', "OILS: Checkin failed");
397 ## If the ILS caches patron information, this lets it free it up.
398 ## Also, this could be used for centrally logging session duration.
399 ## We don't do anything with it.
400 sub end_patron_session {
401 my ($self, $patron_id) = @_;
402 return (1, 'Thank you!', '');
407 # my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
408 # $pay_type, $fee_id, $trans_id, $currency) = @_;
412 # $trans = new ILS::Transaction::FeePayment;
414 # $patron = new ILS::Patron $patron_id;
416 # $trans->transaction_id($trans_id);
417 # $trans->patron($patron);
424 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
425 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
426 # my ($patron, $item);
431 # $trans = new ILS::Transaction::Hold;
433 # # BEGIN TRANSACTION
434 # $patron = new ILS::Patron $patron_id;
436 # || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
437 # $trans->screen_msg("Invalid Patron.");
442 # $item = new ILS::Item ($item_id || $title_id);
444 # $trans->screen_msg("No such item.");
446 # # END TRANSACTION (conditionally)
448 # } elsif ($item->fee && ($fee_ack ne 'Y')) {
449 # $trans->screen_msg = "Fee required to place hold.";
451 # # END TRANSACTION (conditionally)
456 # item_id => $item->id,
457 # patron_id => $patron->id,
458 # expiration_date => $expiry_date,
459 # pickup_location => $pickup_location,
460 # hold_type => $hold_type,
464 # $trans->patron($patron);
465 # $trans->item($item);
466 # $trans->pickup_location($pickup_location);
468 # push(@{$item->hold_queue}, $hold);
469 # push(@{$patron->{hold_items}}, $hold);
477 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
478 # my ($patron, $item, $hold);
481 # $trans = new ILS::Transaction::Hold;
483 # # BEGIN TRANSACTION
484 # $patron = new ILS::Patron $patron_id;
486 # $trans->screen_msg("Invalid patron barcode.");
489 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
490 # $trans->screen_msg('Invalid patron password.');
495 # $item = new ILS::Item ($item_id || $title_id);
497 # $trans->screen_msg("No such item.");
499 # # END TRANSACTION (conditionally)
503 # # Remove the hold from the patron's record first
504 # $trans->ok($patron->drop_hold($item_id));
507 # # We didn't find it on the patron record
508 # $trans->screen_msg("No such hold on patron record.");
510 # # END TRANSACTION (conditionally)
514 # # Now, remove it from the item record. If it was on the patron
515 # # record but not on the item record, we'll treat that as success.
516 # foreach my $i (0 .. scalar @{$item->hold_queue}) {
517 # $hold = $item->hold_queue->[$i];
519 # if ($hold->{patron_id} eq $patron->id) {
520 # # found it: delete it.
521 # splice @{$item->hold_queue}, $i, 1;
526 # $trans->screen_msg("Hold Cancelled.");
527 # $trans->patron($patron);
528 # $trans->item($item);
534 ## The patron and item id's can't be altered, but the
535 ## date, location, and type can.
537 # my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
538 # $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
539 # my ($patron, $item);
543 # $trans = new ILS::Transaction::Hold;
545 # # BEGIN TRANSACTION
546 # $patron = new ILS::Patron $patron_id;
548 # $trans->screen_msg("Invalid patron barcode.");
553 # foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
554 # $hold = $patron->{hold_items}[$i];
556 # if ($hold->{item_id} eq $item_id) {
557 # # Found it. So fix it.
558 # $hold->{expiration_date} = $expiry_date if $expiry_date;
559 # $hold->{pickup_location} = $pickup_location if $pickup_location;
560 # $hold->{hold_type} = $hold_type if $hold_type;
563 # $trans->screen_msg("Hold updated.");
564 # $trans->patron($patron);
565 # $trans->item(new ILS::Item $hold->{item_id});
570 # # The same hold structure is linked into both the patron's
571 # # list of hold items and into the queue of outstanding holds
572 # # for the item, so we don't need to search the hold queue for
573 # # the item, since it's already been updated by the patron code.
576 # $trans->screen_msg("No such outstanding hold.");
584 my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
585 $no_block, $nb_due_date, $third_party, $item_props, $fee_ack) = @_;
587 $self->verify_session;
589 my $trans = OpenILS::SIP::Transaction::Renew->new( authtoken => $self->{authtoken} );
590 $trans->patron($self->find_patron($patron_id));
591 $trans->item($self->find_item($item_id));
593 if(!$trans->patron) {
594 $trans->screen_msg("Invalid patron barcode.");
599 if(!$trans->patron->renew_ok) {
600 $trans->screen_msg("Renewals not allowed.");
607 $trans->screen_msg("Title ID renewal not supported. Use item barcode.");
609 $trans->screen_msg("Invalid item barcode.");
615 if(!$trans->item->{patron} or
616 $trans->item->{patron} ne $patron_id) {
617 $trans->screen_msg("Item not checked out to " . $trans->patron->name);
622 # Perform the renewal
625 $trans->desensitize(0); # It's already checked out
626 $trans->item->{due_date} = $nb_due_date if $no_block eq 'Y';
627 $trans->item->{sip_item_properties} = $item_props if $item_props;
638 # my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
639 # my ($patron, $item_id);
642 # $trans = new ILS::Transaction::RenewAll;
644 # $trans->patron($patron = new ILS::Patron $patron_id);
645 # if (defined $patron) {
646 # syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
647 # $patron->name, $patron->renew_ok);
649 # syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
653 # if (!defined($patron)) {
654 # $trans->screen_msg("Invalid patron barcode.");
656 # } elsif (!$patron->renew_ok) {
657 # $trans->screen_msg("Renewals not allowed.");
659 # } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
660 # $trans->screen_msg("Invalid patron password.");
664 # foreach $item_id (@{$patron->{items}}) {
665 # my $item = new ILS::Item $item_id;
667 # if (!defined($item)) {
668 # syslog("LOG_WARNING",
669 # "renew_all: Invalid item id associated with patron '%s'",
674 # if (@{$item->hold_queue}) {
675 # # Can't renew if there are outstanding holds
676 # push @{$trans->unrenewed}, $item_id;
678 # $item->{due_date} = time + (14*24*60*60); # two weeks hence
679 # push @{$trans->renewed}, $item_id;