1 # ---------------------------------------------------------------
2 # Copyright © 2014 Jason J.A. Stephenson <jason@sigio.com>
4 # This file is part of NCIPServer.
6 # NCIPServer is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # NCIPServer is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with NCIPServer. If not, see <http://www.gnu.org/licenses/>.
18 # ---------------------------------------------------------------
19 package NCIP::ILS::Evergreen;
22 use XML::LibXML::Simple qw(XMLin);
24 use DateTime::Format::ISO8601;
25 use Digest::MD5 qw/md5_hex/;
27 use OpenSRF::AppSession;
28 use OpenSRF::Utils qw/:datetime/;
29 use OpenSRF::Utils::SettingsClient;
30 use OpenILS::Utils::Fieldmapper;
31 use OpenILS::Utils::Normalize qw(clean_marc);
32 use OpenILS::Application::AppUtils;
33 use OpenILS::Const qw/:const/;
37 use List::MoreUtils qw/uniq/;
38 use POSIX qw/strftime/;
40 # We need a bunch of NCIP::* objects.
44 use NCIP::User::OptionalFields;
45 use NCIP::User::AddressInformation;
47 use NCIP::User::BlockOrTrap;
48 use NCIP::User::Privilege;
49 use NCIP::User::PrivilegeStatus;
50 use NCIP::StructuredPersonalUserName;
51 use NCIP::StructuredAddress;
52 use NCIP::ElectronicAddress;
53 use NCIP::PhysicalAddress;
56 use NCIP::Item::OptionalFields;
57 use NCIP::Item::BibliographicDescription;
58 use NCIP::Item::BibliographicItemId;
59 use NCIP::Item::BibliographicRecordId;
60 use NCIP::Item::Description;
62 # Inherit from NCIP::ILS.
63 use parent qw(NCIP::ILS);
67 Evergreen - Evergreen driver for NCIPServer
71 my $ils = NCIP::ILS::Evergreen->new(name => $config->{NCIP.ils.value});
75 NCIP::ILS::Evergreen is the default driver for Evergreen and
76 NCIPServer. It was initially developed to work with Auto-Graphics'
77 SHAREit software using a subset of an unspecified ILL/DCB profile.
81 # Default values we define for things that might be missing in our
82 # runtime environment or configuration file that absolutely must have
85 # OILS_NCIP_CONFIG_DEFAULT is the default location to find our
86 # driver's configuration file. This location can be overridden by
87 # setting the path in the OILS_NCIP_CONFIG environment variable.
89 # BIB_SOURCE_DEFAULT is the config.bib_source.id to use when creating
90 # "short" bibs. It is used only if no entry is supplied in the
91 # configuration file. The provided default is 2, the id of the
92 # "System Local" source that comes with a default Evergreen
95 OILS_NCIP_CONFIG_DEFAULT => '/openils/conf/oils_ncip.xml',
96 BIB_SOURCE_DEFAULT => 2
99 # A common Evergreen code shortcut to use AppUtils:
100 my $U = 'OpenILS::Application::AppUtils';
102 # The usual constructor:
105 $class = ref($class) if (ref $class);
107 # Instantiate our parent with the rest of the arguments. It
108 # creates a blessed hashref.
109 my $self = $class->SUPER::new(@_);
111 # Look for our configuration file, load, and parse it:
114 # Bootstrap OpenSRF and prepare some OpenILS components.
117 # Initialize the rest of our internal state.
123 =head1 HANDLER METHODS
127 $ils->lookupuser($request);
129 Processes a LookupUser request.
137 # Check our session and login if necessary.
138 $self->login() unless ($self->checkauth());
140 my $message_type = $self->parse_request_type($request);
142 # Let's go ahead and create our response object. We need this even
143 # if there is a problem.
144 my $response = NCIP::Response->new({type => $message_type . "Response"});
145 $response->header($self->make_header($request));
147 # Need to parse the request object to get the user barcode.
148 my ($barcode, $idfield) = $self->find_user_barcode($request);
150 # If we did not find a barcode, then report the problem.
151 if (ref($barcode) eq 'NCIP::Problem') {
152 $response->problem($barcode);
156 # Look up our patron by barcode:
157 my $user = $self->retrieve_user_by_barcode($barcode, $idfield);
158 if (ref($user) eq 'NCIP::Problem') {
159 $response->problem($user);
163 # We got the information, so lets fill in our userdata.
164 my $userdata = NCIP::User->new();
166 # Make an array of the user's active barcodes.
168 foreach my $card (@{$user->cards()}) {
169 if ($U->is_true($card->active())) {
170 my $id = NCIP::User::Id->new({
171 UserIdentifierType => 'Barcode',
172 UserIdentifierValue => $card->barcode()
177 $userdata->UserId($ids);
179 # Check if they requested any optional fields and return those.
180 my $elements = $request->{$message_type}->{UserElementType};
182 $elements = [$elements] unless (ref $elements eq 'ARRAY');
183 my $optionalfields = $self->handle_user_elements($user, $elements);
184 $userdata->UserOptionalFields($optionalfields);
187 $response->data($userdata);
194 $ils->acceptitem($request);
196 Processes an AcceptItem request.
204 # Check our session and login if necessary.
205 $self->login() unless ($self->checkauth());
207 # Common preparation.
208 my $message = $self->parse_request_type($request);
209 my $response = NCIP::Response->new({type => $message . 'Response'});
210 $response->header($self->make_header($request));
212 # We only accept holds for the time being.
213 if ($request->{$message}->{RequestedActionType} =~ /^hold\W/i) {
214 # We need the item id or we can't do anything at all.
215 my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
216 if (ref($item_barcode) eq 'NCIP::Problem') {
217 $response->problem($item_barcode);
221 # We need to find a patron barcode or we can't look anyone up
223 my ($user_barcode, $user_idfield) = $self->find_user_barcode($request, 'UserIdentifierValue');
224 if (ref($user_barcode) eq 'NCIP::Problem') {
225 $response->problem($user_barcode);
228 # Look up our patron by barcode:
229 my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
230 if (ref($user) eq 'NCIP::Problem') {
231 $response->problem($user);
234 # We're doing patron checks before looking for bibliographic
235 # information and creating the item because problems with the
236 # patron are more likely to occur.
237 my $problem = $self->check_user_for_problems($user, 'HOLD');
239 $response->problem($problem);
243 # Check if the item barcode already exists:
244 my $item = $self->retrieve_copy_details_by_barcode($item_barcode);
246 # What to do here was not defined in the
247 # specification. Since the copies that we create this way
248 # should get deleted when checked in, it would be an error
249 # if we try to create another one. It means that something
250 # has gone wrong somewhere.
254 ProblemType => 'Duplicate Item',
255 ProblemDetail => "Item with barcode $item_barcode already exists.",
256 ProblemElement => $item_idfield,
257 ProblemValue => $item_barcode
264 # Now, we have to create our new copy and/or bib and call number.
266 # First, we have to gather the necessary information from the
267 # request. Store in a hashref for convenience. We may write a
268 # method to get this information in the future if we find we
269 # need it in other handlers. Such a function would be a
270 # candidate to go into our parent, NCIP::ILS.
272 barcode => $item_barcode,
273 call_number => $request->{$message}->{ItemOptionalFields}->{ItemDescription}->{CallNumber},
274 title => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{Author},
275 author => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{Title},
276 publisher => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{Publisher},
277 publication_date => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{PublicationDate},
278 medium => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{MediumType},
279 electronic => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{ElectronicResource}
282 if ($self->{config}->{items}->{use_precats}) {
283 # We only need to create a precat copy.
284 $item = $self->create_precat_copy($item_info);
286 # We have to create a "partial" bib record, a call number and a copy.
287 $item = $self->create_fuller_copy($item_info);
290 # If we failed to create the copy, report a problem.
294 ProblemType => 'Temporary Processing Failure',
295 ProblemDetail => 'Failed to create the item in the system',
296 ProblemElement => $item_idfield,
297 ProblemValue => $item_barcode
303 # We try to find the pickup location in our database. It's OK
304 # if it does not exist, the user's home library will be used
306 my $location = $request->{$message}->{PickupLocation};
308 $location = $self->retrieve_org_unit_by_shortname($location);
311 # Now, we place the hold on the newly created copy on behalf
312 # of the patron retrieved above.
313 my $hold = $self->place_hold($item, $user, $location);
314 if (ref($hold) eq 'NCIP::Problem') {
315 $response->problem($hold);
319 # We return the RequestId and optionally, the ItemID. We'll
320 # just return what was sent to us, since we ignored all of it
323 $data->{RequestId} = NCIP::RequestId->new(
325 AgencyId => $request->{$message}->{RequestId}->{AgencyId},
326 RequestIdentifierType => $request->{$message}->{RequestId}->{RequestIdentifierType},
327 RequestIdentifierValue => $request->{$message}->{RequestId}->{RequestIdentifierValue}
330 $data->{ItemId} = NCIP::Item::Id->new(
332 AgencyId => $request->{$message}->{ItemId}->{AgencyId},
333 ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
334 ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
337 $response->data($data);
340 my $problem = NCIP::Problem->new();
341 $problem->ProblemType('Unauthorized Combination Of Element Values For System');
342 $problem->ProblemDetail('We only support Hold For Pickup');
343 $problem->ProblemElement('RequestedActionType');
344 $problem->ProblemValue($request->{$message}->{RequestedActionType});
345 $response->problem($problem);
353 $response = $ils->checkinitem($request);
355 Checks the item in if we can find the barcode in the message. It
356 returns problems if it cannot find the item in the system or if the
357 item is not checked out.
359 It could definitely use some more brains at some point as it does not
360 fully support everything that the standard allows. It also does not
361 really check if the checkin succeeded or not.
369 # Check our session and login if necessary:
370 $self->login() unless ($self->checkauth());
373 my $message = $self->parse_request_type($request);
374 my $response = NCIP::Response->new({type => $message . 'Response'});
375 $response->header($self->make_header($request));
377 # We need the copy barcode from the message.
378 my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
379 if (ref($item_barcode) eq 'NCIP::Problem') {
380 $response->problem($item_barcode);
384 # Retrieve the copy details.
385 my $details = $self->retrieve_copy_details_by_barcode($item_barcode);
387 # Return an Unknown Item problem unless we find the copy.
391 ProblemType => 'Unknown Item',
392 ProblemDetail => "Item with barcode $item_barcode is not known.",
393 ProblemElement => $item_idfield,
394 ProblemValue => $item_barcode
401 # Check if a UserId was provided. If so, this is the patron to
402 # whom the copy should be checked out.
404 my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
405 # We ignore the problem, because the UserId is optional.
406 if (ref($user_barcode) ne 'NCIP::Problem') {
407 $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
408 # We don't ignore a problem here, however.
409 if (ref($user) eq 'NCIP::Problem') {
410 $response->problem($user);
416 my $copy = $details->{copy};
418 # Look for a circulation and examine its information:
419 my $circ = $details->{circ};
421 # Check the circ details to see if the copy is checked out and, if
422 # the patron was provided, that it is checked out to the patron in
423 # question. We also verify the copy ownership and circulation
425 my $problem = $self->check_circ_details($circ, $copy, $user);
427 # We need to fill in some information, however.
428 if (!$problem->ProblemValue() && !$problem->ProblemElement()) {
429 $problem->ProblemValue($user_barcode);
430 $problem->ProblemElement($user_idfield);
431 } elsif (!$problem->ProblemElement()) {
432 $problem->ProblemElement($item_idfield);
434 $response->problem($problem);
438 # Checkin parameters. We want to skip hold targeting or making
439 # transits, to force the checkin despite the copy status, as
440 # well as void overdues.
442 copy_barcode => $copy->barcode(),
447 my $result = $U->simplereq(
449 'open-ils.circ.checkin.override',
450 $self->{session}->{authtoken},
453 if (ref($result) eq 'ARRAY') {
454 $result = $result->[0];
456 if ($result->{textcode} eq 'SUCCESS') {
457 # Delete the copy. Since delete_copy checks ownership
458 # before attempting to delete the copy, we don't bother
459 # checking who owns it.
460 $self->delete_copy($copy);
461 # We need the circulation user for the information below, so we retrieve it.
462 my $circ_user = $self->retrieve_user_by_id($circ->usr());
464 ItemId => NCIP::Item::Id->new(
466 AgencyId => $request->{$message}->{ItemId}->{AgencyId},
467 ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
468 ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
471 UserId => NCIP::User::Id->new(
473 UserIdentifierType => 'Barcode Id',
474 UserIdentifierValue => $circ_user->card->barcode()
479 # Look for UserElements requested and add it to the response:
480 my $elements = $request->{$message}->{UserElementType};
482 $elements = [$elements] unless (ref $elements eq 'ARRAY');
483 my $optionalfields = $self->handle_user_elements($circ_user, $elements);
484 $data->{UserOptionalFields} = $optionalfields;
486 $elements = $request->{$message}->{ItemElementType};
488 $elements = [$elements] unless (ref $elements eq 'ARRAY');
489 my $optionalfields = $self->handle_item_elements($copy, $elements);
490 $data->{ItemOptionalFields} = $optionalfields;
493 $response->data($data);
495 # At some point in the future, we should probably check if
496 # they requested optional user or item elements and return
497 # those. For the time being, we ignore those at the risk of
498 # being considered non-compliant.
500 $response->problem(_problem_from_event('Checkin Failed', $result));
508 $response = $ils->renewitem($request);
510 Handle the RenewItem message.
518 # Check our session and login if necessary:
519 $self->login() unless ($self->checkauth());
522 my $message = $self->parse_request_type($request);
523 my $response = NCIP::Response->new({type => $message . 'Response'});
524 $response->header($self->make_header($request));
526 # We need the copy barcode from the message.
527 my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
528 if (ref($item_barcode) eq 'NCIP::Problem') {
529 $response->problem($item_barcode);
533 # Retrieve the copy details.
534 my $details = $self->retrieve_copy_details_by_barcode($item_barcode);
536 # Return an Unknown Item problem unless we find the copy.
540 ProblemType => 'Unknown Item',
541 ProblemDetail => "Item with barcode $item_barcode is not known.",
542 ProblemElement => $item_idfield,
543 ProblemValue => $item_barcode
550 # User is required for RenewItem.
551 my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
552 if (ref($user_barcode) eq 'NCIP::Problem') {
553 $response->problem($user_barcode);
556 my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
557 if (ref($user) eq 'NCIP::Problem') {
558 $response->problem($user);
563 my $copy = $details->{copy};
565 # Look for a circulation and examine its information:
566 my $circ = $details->{circ};
568 # Check the circ details to see if the copy is checked out and, if
569 # the patron was provided, that it is checked out to the patron in
570 # question. We also verify the copy ownership and circulation
572 my $problem = $self->check_circ_details($circ, $copy, $user);
574 # We need to fill in some information, however.
575 if (!$problem->ProblemValue() && !$problem->ProblemElement()) {
576 $problem->ProblemValue($user_barcode);
577 $problem->ProblemElement($user_idfield);
578 } elsif (!$problem->ProblemElement()) {
579 $problem->ProblemElement($item_idfield);
581 $response->problem($problem);
585 # Check if user is blocked from renewals:
586 $problem = $self->check_user_for_problems($user, 'RENEW');
588 # Replace the ProblemElement and ProblemValue fields.
589 $problem->ProblemElement($user_idfield);
590 $problem->ProblemValue($user_barcode);
591 $response->problem($problem);
595 # Check if the duration rule allows renewals. It should have been
596 # fleshed during the copy details retrieve.
597 my $rule = $circ->duration_rule();
598 unless (ref($rule)) {
599 $rule = $U->simplereq(
601 'open-ils.pcrud.retrieve.crcd',
602 $self->{session}->{authtoken},
606 if ($rule->max_renewals() < 1) {
610 ProblemType => 'Item Not Renewable',
611 ProblemDetail => 'Item may not be renewed.',
612 ProblemElement => $item_idfield,
613 ProblemValue => $item_barcode
620 # Check if there are renewals remaining on the latest circ:
621 if ($circ->renewal_remaining() < 1) {
625 ProblemType => 'Maximum Renewals Exceeded',
626 ProblemDetail => 'Renewal cannot proceed because the User has already renewed the Item the maximum number of times permitted.',
627 ProblemElement => $item_idfield,
628 ProblemValue => $item_barcode
635 # Now, we attempt the renewal. If it fails, we simply say that the
636 # user is not allowed to renew this item, without getting into
639 copy_id => $copy->id(),
640 patron_id => $user->id(),
643 my $r = $U->simplereq(
645 'open-ils.circ.renew.override',
646 $self->{session}->{authtoken},
650 # We only look at the first one, since more than one usually means
652 if (ref($r) eq 'ARRAY') {
655 if ($r->{textcode} ne 'SUCCESS') {
656 $problem = _problem_from_event('Renewal Failed', $r);
657 $response->problem($problem);
660 ItemId => NCIP::Item::Id->new(
662 AgencyId => $request->{$message}->{ItemId}->{AgencyId},
663 ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
664 ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
667 UserId => NCIP::User::Id->new(
669 UserIdentifierType => 'Barcode Id',
670 UserIdentifierValue => $user->card->barcode()
674 # We need to retrieve the copy details again to refresh our
675 # circ information to get the new due date.
676 $details = $self->retrieve_copy_details_by_barcode($item_barcode);
677 $circ = $details->{circ};
678 $data->{DateDue} = _fix_date($circ->due_date());
680 # Look for UserElements requested and add it to the response:
681 my $elements = $request->{$message}->{UserElementType};
683 $elements = [$elements] unless (ref $elements eq 'ARRAY');
684 my $optionalfields = $self->handle_user_elements($user, $elements);
685 $data->{UserOptionalFields} = $optionalfields;
687 $elements = $request->{$message}->{ItemElementType};
689 $elements = [$elements] unless (ref $elements eq 'ARRAY');
690 my $optionalfields = $self->handle_item_elements($details->{copy}, $elements);
691 $data->{ItemOptionalFields} = $optionalfields;
694 $response->data($data);
697 # At some point in the future, we should probably check if
698 # they requested optional user or item elements and return
699 # those. For the time being, we ignore those at the risk of
700 # being considered non-compliant.
707 $response = $ils->checkoutitem($request);
709 Handle the Checkoutitem message.
717 # Check our session and login if necessary:
718 $self->login() unless ($self->checkauth());
721 my $message = $self->parse_request_type($request);
722 my $response = NCIP::Response->new({type => $message . 'Response'});
723 $response->header($self->make_header($request));
725 # We need the copy barcode from the message.
726 my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
727 if (ref($item_barcode) eq 'NCIP::Problem') {
728 $response->problem($item_barcode);
732 # Retrieve the copy details.
733 my $details = $self->retrieve_copy_details_by_barcode($item_barcode);
735 # Return an Unknown Item problem unless we find the copy.
739 ProblemType => 'Unknown Item',
740 ProblemDetail => "Item with barcode $item_barcode is not known.",
741 ProblemElement => $item_idfield,
742 ProblemValue => $item_barcode
749 # User is required for CheckOutItem.
750 my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
751 if (ref($user_barcode) eq 'NCIP::Problem') {
752 $response->problem($user_barcode);
755 my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
756 if (ref($user) eq 'NCIP::Problem') {
757 $response->problem($user);
762 my $copy = $details->{copy};
764 # Check if the copy can circulate.
765 unless ($self->copy_can_circulate($copy)) {
769 ProblemType => 'Item Does Not Circulate',
770 ProblemDetail => "Item with barcode $item_barcode does not circulate.",
771 ProblemElement => $item_idfield,
772 ProblemValue => $item_barcode
779 # Look for a circulation and examine its information:
780 my $circ = $details->{circ};
782 # Check if the item is already checked out.
783 if ($circ && !$circ->checkin_time()) {
787 ProblemType => 'Item Already Checked Out',
788 ProblemDetail => "Item with barcode $item_barcode is already checked out.",
789 ProblemElement => $item_idfield,
790 ProblemValue => $item_barcode
797 # Check if user is blocked from circulation:
798 my $problem = $self->check_user_for_problems($user, 'CIRC');
800 # Replace the ProblemElement and ProblemValue fields.
801 $problem->ProblemElement($user_idfield);
802 $problem->ProblemValue($user_barcode);
803 $response->problem($problem);
807 # Now, we attempt the check out. If it fails, we simply say that
808 # the user is not allowed to check out this item, without getting
811 copy_id => $copy->id(),
812 patron_id => $user->id(),
814 my $r = $U->simplereq(
816 'open-ils.circ.checkout.full.override',
817 $self->{session}->{authtoken},
821 # We only look at the first one, since more than one usually means
823 if (ref($r) eq 'ARRAY') {
826 if ($r->{textcode} ne 'SUCCESS') {
827 $problem = _problem_from_event('Check Out Failed', $r);
828 $response->problem($problem);
831 ItemId => NCIP::Item::Id->new(
833 AgencyId => $request->{$message}->{ItemId}->{AgencyId},
834 ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
835 ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
838 UserId => NCIP::User::Id->new(
840 UserIdentifierType => 'Barcode Id',
841 UserIdentifierValue => $user->card->barcode()
845 # We need to retrieve the copy details again to refresh our
846 # circ information to get the due date.
847 $details = $self->retrieve_copy_details_by_barcode($item_barcode);
848 $circ = $details->{circ};
849 $data->{DateDue} = _fix_date($circ->due_date());
851 # Look for UserElements requested and add it to the response:
852 my $elements = $request->{$message}->{UserElementType};
854 $elements = [$elements] unless (ref $elements eq 'ARRAY');
855 my $optionalfields = $self->handle_user_elements($user, $elements);
856 $data->{UserOptionalFields} = $optionalfields;
858 $elements = $request->{$message}->{ItemElementType};
860 $elements = [$elements] unless (ref $elements eq 'ARRAY');
861 my $optionalfields = $self->handle_item_elements($details->{copy}, $elements);
862 $data->{ItemOptionalFields} = $optionalfields;
865 $response->data($data);
868 # At some point in the future, we should probably check if
869 # they requested optional user or item elements and return
870 # those. For the time being, we ignore those at the risk of
871 # being considered non-compliant.
878 $response = $ils->requestitem($request);
880 Handle the NCIP RequestItem message.
887 # Check our session and login if necessary:
888 $self->login() unless ($self->checkauth());
891 my $message = $self->parse_request_type($request);
892 my $response = NCIP::Response->new({type => $message . 'Response'});
893 $response->header($self->make_header($request));
895 # Because we need to have a user to place a hold, because the user
896 # is likely to have problems, and because getting the item
897 # information for the hold is trickier than getting the user
898 # information, we'll do the user first and short circuit out of
899 # the function if there is a problem with the user.
900 my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
901 if (ref($user_barcode) eq 'NCIP::Problem') {
902 $response->problem($user_barcode);
905 my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
906 if (ref($user) eq 'NCIP::Problem') {
907 $response->problem($user);
910 my $problem = $self->check_user_for_problems($user, 'HOLD');
912 $response->problem($problem);
916 # RequestItem is a blast. We need to check if we have a copy
917 # barcode and/or if we have BibliographicIds. If we have both or
918 # either, we then need to figure out what we're placing the hold
919 # on, a copy, a volume or a bib. We don't currently do part holds,
920 # but maybe we should some day. We can also be sent more than 1
921 # BibliographicId, so we look for certain identifiers first, and
922 # then others in decreasing preference: SYSNUMBER, ISBN, and ISSN.
924 # Not to mention that there are two kinds of BibliographicId field
925 # with different field names, and both can be intermixed in an
926 # incoming message! (I just /love/ this nonsense.)
928 # This here is the thing we're going to put on hold:
931 # We need copy details if we find in a couple of places below.
934 # We need the copy barcode from the message.
935 my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
936 if (ref($item_barcode) ne 'NCIP::Problem') {
937 # Retrieve the copy details.
938 $copy_details = $self->retrieve_copy_details_by_barcode($item_barcode);
939 unless ($copy_details) {
940 # Return an Unknown Item problem unless we find the copy.
944 ProblemType => 'Unknown Item',
945 ProblemDetail => "Item with barcode $item_barcode is not known.",
946 ProblemElement => $item_idfield,
947 ProblemValue => $item_barcode
953 $item = $copy_details->{volume}; # We place a volume hold.
956 # We weren't given copy information to target, or we can't find
957 # it, so we need to look for a target via BibliographicId.
959 my @biblio_ids = $self->find_bibliographic_ids($request);
961 $item = $self->find_target_via_bibliographic_id(@biblio_ids);
965 # If we don't have an item, then blow up with a problem that may
966 # have been set when we went looking for the ItemId.
968 if (ref($item_barcode) eq 'NCIP::Problem') {
969 $response->problem($item_barcode);
974 ProblemType => 'Request Item Not Found',
975 ProblemDetail => 'Unable to determine the item to request from input message.',
976 ProblemElement => 'NULL',
977 ProblemValue => 'NULL'
983 } elsif (ref($item) eq 'NCIP::Problem') {
984 $response->problem($item);
988 # See if we were given a PickupLocation.
990 if ($request->{$message}->{PickupLocation}) {
991 my $loc = $request->{$message}->{PickupLocation};
992 $loc =~ s/^.*://; # strip everything up to the last
993 # semi-colon, if any.
994 $location = $self->retrieve_org_unit_by_shortname($loc);
997 # Look for a NeedBeforeDate to use as expiration...
998 my $hold_expiration = $request->{$message}->{NeedBeforeDate};
1001 my $hold = $self->place_hold($item, $user, $location, $hold_expiration);
1002 if (ref($hold) eq 'NCIP::Problem') {
1003 $response->problem($hold);
1006 RequestId => NCIP::RequestId->new(
1008 RequestIdentifierType => 'SYSNUMBER',
1009 RequestIdentifierValue => $hold->id()
1012 UserId => NCIP::User::Id->new(
1014 UserIdentifierType => 'Barcode Id',
1015 UserIdentifierValue => $user->card->barcode()
1018 RequestType => $request->{$message}->{RequestType},
1019 RequestScopeType => ($hold->hold_type() eq 'V') ? "item" : "bibliographic item"
1021 # Look for UserElements requested and add it to the response:
1022 my $elements = $request->{$message}->{UserElementType};
1024 $elements = [$elements] unless (ref $elements eq 'ARRAY');
1025 my $optionalfields = $self->handle_user_elements($user, $elements);
1026 $data->{UserOptionalFields} = $optionalfields;
1028 $elements = $request->{$message}->{ItemElementType};
1030 $copy_details = $self->find_copy_details_by_item($item) unless ($copy_details);
1031 $elements = [$elements] unless (ref($elements) eq 'ARRAY');
1032 my $optionalfields = $self->handle_item_elements($copy_details->{copy}, $elements);
1033 $data->{ItemOptionalFields} = $optionalfields;
1036 $response->data($data);
1042 =head2 cancelrequestitem
1044 $response = $ils->cancelrequestitem($request);
1046 Handle the NCIP CancelRequestItem message.
1050 sub cancelrequestitem {
1052 my $request = shift;
1053 # Check our session and login if necessary:
1054 $self->login() unless ($self->checkauth());
1057 my $message = $self->parse_request_type($request);
1058 my $response = NCIP::Response->new({type => $message . 'Response'});
1059 $response->header($self->make_header($request));
1061 # UserId is required by the standard, but we might not really need it.
1062 my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
1063 if (ref($user_barcode) eq 'NCIP::Problem') {
1064 $response->problem($user_barcode);
1067 my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
1068 if (ref($user) eq 'NCIP::Problem') {
1069 $response->problem($user);
1073 # See if we got a ItemId and a barcode:
1075 my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
1076 if (ref($item_barcode) ne 'NCIP::Problem') {
1077 # Retrieve the copy details.
1078 $copy_details = $self->retrieve_copy_details_by_barcode($item_barcode);
1079 unless ($copy_details) {
1080 # Return an Unknown Item problem unless we find the copy.
1084 ProblemType => 'Unknown Item',
1085 ProblemDetail => "Item with barcode $item_barcode is not known.",
1086 ProblemElement => $item_idfield,
1087 ProblemValue => $item_barcode
1095 # See if we got a RequestId:
1097 if ($request->{$message}->{RequestId}) {
1098 $requestid = NCIP::RequestId->new(
1100 AgencyId => $request->{$message}->{RequestId}->{AgencyId},
1101 RequestIdentifierType => $request->{$message}->{RequestId}->{RequestIdentifierType},
1102 RequestIdentifierValue => $request->{$message}->{RequestId}->{RequestIdentifierValue}
1107 # Just a note: In the below, we cannot rely on the hold or transit
1108 # fields of the copy_details, even if we have retrieved it. This
1109 # is because that hold and transit may not be the ones that we're
1110 # looking for, i.e. they could be for another patron, etc.
1112 # See if we can find the hold:
1115 $hold = $U->simplereq(
1117 'open-ils.pcrud.retrieve.ahr',
1118 $self->{session}->{authtoken},
1119 $requestid->{RequestIdentifierValue},
1120 {flesh => 1, flesh_fields => {ahr => ['transit']}}
1123 # Report a problem that we couldn't find a hold by that id.
1127 ProblemType => 'Unknown Request',
1128 ProblemDetail => 'No request with this identifier found',
1129 ProblemElement => 'RequestIdentifierValue',
1130 ProblemValue => $requestid->{RequestIdentifierValue}
1134 } elsif ($hold->cancel_time()) {
1138 ProblemType => 'Request Already Canceled',
1139 ProblemDetail => 'Request has already been canceled',
1140 ProblemElement => 'RequestIdentifierValue',
1141 ProblemValue => $requestid->{RequestIdentifierValue}
1145 } elsif ($hold->transit()) {
1149 ProblemType => 'Request Already Processed',
1150 ProblemDetail => 'Request has already been processed',
1151 ProblemElement => 'RequestIdentifierValue',
1152 ProblemValue => $requestid->{RequestIdentifierValue}
1156 } elsif ($hold->usr() == $user->id()) {
1157 # Check the target matches the copy information, if any,
1158 # that we were given.
1160 if ($copy_details) {
1161 if ($hold->hold_type() eq 'V') {
1162 $obj_id = $copy_details->{volume}->id();
1163 } elsif ($hold->hold_type() eq 'T') {
1164 $obj_id = $copy_details->{mvr}->doc_id();
1165 } elsif ($hold->hold_type() eq 'C' || $hold->hold_type() eq 'F') {
1166 $obj_id = $copy_details->{copy}->id();
1169 if ($obj_id && $hold->target() != $obj_id) {
1173 ProblemType => 'Request Not For This Item',
1174 ProblemDetail => "Request is not for this item",
1175 ProblemElement => $item_idfield,
1176 ProblemElement => $item_barcode
1181 $self->cancel_hold($hold);
1183 RequestId => $requestid,
1184 UserId => NCIP::User::Id->new(
1186 UserIdentifierType => 'Barcode Id',
1187 UserIdentifierValue => $user->card->barcode()
1191 # Look for UserElements requested and add it to the response:
1192 my $elements = $request->{$message}->{UserElementType};
1194 $elements = [$elements] unless (ref $elements eq 'ARRAY');
1195 my $optionalfields = $self->handle_user_elements($user, $elements);
1196 $data->{UserOptionalFields} = $optionalfields;
1198 $elements = $request->{$message}->{ItemElementType};
1199 if ($elements && $copy_details) {
1200 $elements = [$elements] unless (ref $elements eq 'ARRAY');
1201 my $optionalfields = $self->handle_item_elements($copy_details->{copy}, $elements);
1202 $data->{ItemOptionalFields} = $optionalfields;
1204 $response->data($data);
1207 # Report a problem that the hold is not for this user.
1211 ProblemType => 'Request Not For This User',
1212 ProblemDetail => 'Request is not for this user.',
1213 ProblemElement => $user_idfield,
1214 ProblemValue => $user_barcode
1220 # At this point, we *must have* an ItemId and therefore
1221 # $copy_details, so return the problem from looking up the
1222 # barcode if we don't have $copy_details.
1223 if (!$copy_details) {
1224 $response->problem($item_barcode);
1226 # We have to search for the hold based on the copy details and
1227 # the user. We'll need to search for copy (or force) holds, a
1228 # volume hold, or a title hold.
1229 $hold = $self->_hold_search($user, $copy_details);
1230 if ($hold && $hold->transit()) {
1234 ProblemType => 'Request Already Processed',
1235 ProblemDetail => 'Request has already been processed',
1236 ProblemElement => 'RequestIdentifierValue',
1237 ProblemValue => $requestid->{RequestIdentifierValue}
1242 $self->cancel_hold($hold);
1244 RequestId => NCIP::RequestId->new(
1246 RequestIdentifierType => 'SYSNUMBER',
1247 RequestIdentifierValue => $hold->id()
1250 UserId => NCIP::User::Id->new(
1252 UserIdentifierType => 'Barcode Id',
1253 UserIdentifierValue => $user->card->barcode()
1257 # Look for UserElements requested and add it to the response:
1258 my $elements = $request->{$message}->{UserElementType};
1260 $elements = [$elements] unless (ref $elements eq 'ARRAY');
1261 my $optionalfields = $self->handle_user_elements($user, $elements);
1262 $data->{UserOptionalFields} = $optionalfields;
1264 $elements = $request->{$message}->{ItemElementType};
1265 if ($elements && $copy_details) {
1266 $elements = [$elements] unless (ref $elements eq 'ARRAY');
1267 my $optionalfields = $self->handle_item_elements($copy_details->{copy}, $elements);
1268 $data->{ItemOptionalFields} = $optionalfields;
1270 $response->data($data);
1275 ProblemType => 'Unknown Request',
1276 ProblemDetail => 'No request found for the item and user',
1277 ProblemElement => 'NULL',
1278 ProblemValue => 'NULL'
1289 =head1 METHODS USEFUL to SUBCLASSES
1291 =head2 handle_user_elements
1292 $useroptionalfield = $ils->handle_user_elements($user, $elements);
1294 Returns NCIP::User::OptionalFields for the given user and arrayref of
1299 sub handle_user_elements {
1302 my $elements = shift;
1303 my $optionalfields = NCIP::User::OptionalFields->new();
1305 # First, we'll look for name information.
1306 if (grep {$_ eq 'Name Information'} @$elements) {
1307 my $name = NCIP::StructuredPersonalUserName->new();
1308 $name->Surname($user->family_name());
1309 $name->GivenName($user->first_given_name());
1310 $name->Prefix($user->prefix());
1311 $name->Suffix($user->suffix());
1312 $optionalfields->NameInformation($name);
1315 # Next, check for user address information.
1316 if (grep {$_ eq 'User Address Information'} @$elements) {
1319 # See if the user has any valid, physcial addresses.
1320 foreach my $addr (@{$user->addresses()}) {
1321 next if ($U->is_true($addr->pending()));
1322 my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>$addr->address_type()});
1323 my $structured = NCIP::StructuredAddress->new();
1324 $structured->Line1($addr->street1());
1325 $structured->Line2($addr->street2());
1326 $structured->Locality($addr->city());
1327 $structured->Region($addr->state());
1328 $structured->PostalCode($addr->post_code());
1329 $structured->Country($addr->country());
1330 $address->PhysicalAddress(
1331 NCIP::PhysicalAddress->new(
1333 StructuredAddress => $structured,
1334 Type => 'Postal Address'
1338 push @$addresses, $address;
1341 # Right now, we're only sharing email address if the user
1342 # has it. We don't share phone numbers.
1343 if ($user->email()) {
1344 my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>'Email Address'});
1345 $address->ElectronicAddress(
1346 NCIP::ElectronicAddress->new({
1347 Type=>'Email Address',
1348 Data=>$user->email()
1351 push @$addresses, $address;
1354 $optionalfields->UserAddressInformation($addresses);
1357 # Check for User Privilege.
1358 if (grep {$_ eq 'User Privilege'} @$elements) {
1359 # Get the user's group:
1360 my $pgt = $U->simplereq(
1362 'open-ils.pcrud.retrieve.pgt',
1363 $self->{session}->{authtoken},
1367 my $privilege = NCIP::User::Privilege->new();
1368 $privilege->AgencyId($user->home_ou->shortname());
1369 $privilege->AgencyUserPrivilegeType($pgt->name());
1370 $privilege->ValidToDate(_fix_date($user->expire_date()));
1371 $privilege->ValidFromDate(_fix_date($user->create_date()));
1373 my $status = 'Active';
1374 if (_expired($user)) {
1375 $status = 'Expired';
1376 } elsif ($U->is_true($user->barred())) {
1378 } elsif (!$U->is_true($user->active())) {
1379 $status = 'Inactive';
1382 $privilege->UserPrivilegeStatus(
1383 NCIP::User::PrivilegeStatus->new({
1384 UserPrivilegeStatusType => $status
1389 $optionalfields->UserPrivilege([$privilege]);
1393 # Check for Block Or Trap.
1394 if (grep {$_ eq 'Block Or Trap'} @$elements) {
1397 # First, let's check if the profile is blocked from ILL.
1398 if (grep {$_->id() == $user->profile()} @{$self->{blocked_profiles}}) {
1399 my $block = NCIP::User::BlockOrTrap->new();
1400 $block->AgencyId($user->home_ou->shortname());
1401 $block->BlockOrTrapType('Block Interlibrary Loan');
1402 push @$blocks, $block;
1405 # Next, we loop through the user's standing penalties
1406 # looking for blocks on CIRC, HOLD, and RENEW.
1407 my ($have_circ, $have_renew, $have_hold) = (0,0,0);
1408 foreach my $penalty (@{$user->standing_penalties()}) {
1409 next unless($penalty->standing_penalty->block_list());
1410 my @block_list = split(/\|/, $penalty->standing_penalty->block_list());
1411 my $ou = $U->simplereq(
1413 'open-ils.pcrud.retrieve.aou',
1414 $self->{session}->{authtoken},
1415 $penalty->org_unit()
1419 if (!$have_circ && grep {$_ eq 'CIRC'} @block_list) {
1420 my $bot = NCIP::User::BlockOrTrap->new();
1421 $bot->AgencyId($ou->shortname());
1422 $bot->BlockOrTrapType('Block Checkout');
1423 push @$blocks, $bot;
1428 if (!$have_hold && grep {$_ eq 'HOLD' || $_ eq 'FULFILL'} @block_list) {
1429 my $bot = NCIP::User::BlockOrTrap->new();
1430 $bot->AgencyId($ou->shortname());
1431 $bot->BlockOrTrapType('Block Holds');
1432 push @$blocks, $bot;
1437 if (!$have_renew && grep {$_ eq 'RENEW'} @block_list) {
1438 my $bot = NCIP::User::BlockOrTrap->new();
1439 $bot->AgencyId($ou->shortname());
1440 $bot->BlockOrTrapType('Block Renewals');
1441 push @$blocks, $bot;
1445 # Stop after we report one of each, even if more
1447 last if ($have_circ && $have_renew && $have_hold);
1450 $optionalfields->BlockOrTrap($blocks);
1453 return $optionalfields;
1456 =head2 handle_item_elements
1460 sub handle_item_elements {
1463 my $elements = shift;
1464 my $optionalfields = NCIP::Item::OptionalFields->new();
1466 my $details; # In case we need for more than one.
1468 if (grep {$_ eq 'Bibliographic Description'} @$elements) {
1470 # Check for a precat copy, 'cause it is simple.
1471 if ($copy->dummy_title()) {
1472 $description = NCIP::Item::BibliographicDescription->new();
1473 $description->Title($copy->dummy_title());
1474 $description->Author($copy->dummy_author());
1475 if ($copy->dummy_isbn()) {
1476 $description->BibliographicItemId(
1477 NCIP::Item::BibliographicItemId->new(
1479 BibliographicItemIdentifier => $copy->dummy_isbn(),
1480 BibliographicItemIdentifierCode => 'ISBN'
1486 $details = $self->retrieve_copy_details_by_barcode($copy->barcode()) unless($details);
1487 $description = NCIP::Item::BibliographicDescription->new();
1488 $description->Title($details->{mvr}->title());
1489 $description->Author($details->{mvr}->author());
1490 $description->BibliographicRecordId(
1491 NCIP::Item::BibliographicRecordId->new(
1493 BibliographicRecordIdentifier => $details->{mvr}->doc_id(),
1494 BibliographicRecordIdentifierCode => 'SYSNUMBER'
1498 if ($details->{mvr}->publisher()) {
1499 $description->Publisher($details->{mvr}->publisher());
1501 if ($details->{mvr}->pubdate()) {
1502 $description->PublicationDate($details->{mvr}->pubdate());
1504 if ($details->{mvr}->edition()) {
1505 $description->Edition($details->{mvr}->edition());
1508 $optionalfields->BibliographicDescription($description) if ($description);
1511 if (grep {$_ eq 'Item Description'} @$elements) {
1512 $details = $self->retrieve_copy_details_by_barcode($copy->barcode()) unless($details);
1513 # Call Number is the only field we currently return. We also
1514 # do not attempt to retun a prefix and suffix. Someone else
1515 # can deal with that if they want it.
1516 if ($details->{volume}) {
1517 $optionalfields->ItemDescription(
1518 NCIP::Item::Description->new(
1519 {CallNumber => $details->{volume}->label()}
1525 if (grep {$_ eq 'Circulation Status'} @$elements) {
1526 my $status = $copy->status();
1527 $status = $self->retrieve_copy_status($status) unless (ref($status));
1528 $optionalfields->CirculationStatus($status->name()) if ($status);
1531 if (grep {$_ eq 'Date Due'} @$elements) {
1532 $details = $self->retrieve_copy_details_by_barcode($copy->barcode()) unless($details);
1533 if ($details->{circ}) {
1534 if (!$details->{circ}->checkin_time()) {
1535 $optionalfields->DateDue(_fix_date($details->{circ}->due_date()));
1540 if (grep {$_ eq 'Item Use Restriction Type'} @$elements) {
1541 $optionalfields->ItemUseRestrictionType('None');
1544 if (grep {$_ eq 'Physical Condition'} @$elements) {
1545 $optionalfields->PhysicalCondition(
1546 NCIP::Item::PhysicalCondition->new(
1547 {PhysicalConditionType => 'Unknown'}
1552 return $optionalfields;
1559 Login to Evergreen via OpenSRF. It uses internal state from the
1560 configuration file to login.
1564 # Login via OpenSRF to Evergreen.
1568 # Get the authentication seed.
1569 my $seed = $U->simplereq(
1571 'open-ils.auth.authenticate.init',
1572 $self->{config}->{credentials}->{username}
1577 my $response = $U->simplereq(
1579 'open-ils.auth.authenticate.complete',
1581 username => $self->{config}->{credentials}->{username},
1582 password => md5_hex(
1583 $seed . md5_hex($self->{config}->{credentials}->{password})
1586 workstation => $self->{config}->{credentials}->{workstation}
1590 $self->{session}->{authtoken} = $response->{payload}->{authtoken};
1591 $self->{session}->{authtime} = $response->{payload}->{authtime};
1593 # Set/reset the work_ou and user data in case something changed.
1595 # Retrieve the work_ou as an object.
1596 $self->{session}->{work_ou} = $U->simplereq(
1598 'open-ils.pcrud.search.aou',
1599 $self->{session}->{authtoken},
1600 {shortname => $self->{config}->{credentials}->{work_ou}}
1603 # We need the user information in order to do some things.
1604 $self->{session}->{user} = $U->check_user_session($self->{session}->{authtoken});
1612 $valid = $ils->checkauth();
1614 Returns 1 if the object a 'valid' authtoken, 0 if not.
1621 # We use AppUtils to do the heavy lifting.
1622 if (defined($self->{session})) {
1623 if ($U->check_user_session($self->{session}->{authtoken})) {
1630 # If we reach here, we don't have a session, so we are definitely
1635 =head2 retrieve_user_by_barcode
1637 $user = $ils->retrieve_user_by_barcode($user_barcode, $user_idfield);
1639 Do a fleshed retrieve of a patron by barcode. Return the patron if
1640 found and valid. Return a NCIP::Problem of 'Unknown User' otherwise.
1642 The id field argument is used for the ProblemElement field in the
1643 NCIP::Problem object.
1645 An invalid patron is one where the barcode is not found in the
1646 database, the patron is deleted, or the barcode used to retrieve the
1647 patron is not active. The problem element is also returned if an error
1648 occurs during the retrieval.
1652 sub retrieve_user_by_barcode {
1653 my ($self, $barcode, $idfield) = @_;
1654 my $result = $U->simplereq(
1656 'open-ils.actor.user.fleshed.retrieve_by_barcode',
1657 $self->{session}->{authtoken},
1662 # Check for a failure, or a deleted, inactive, or expired user,
1663 # and if so, return empty userdata.
1664 if (!$result || $U->event_code($result) || $U->is_true($result->deleted())
1665 || !grep {$_->barcode() eq $barcode && $U->is_true($_->active())} @{$result->cards()}) {
1667 my $problem = NCIP::Problem->new();
1668 $problem->ProblemType('Unknown User');
1669 $problem->ProblemDetail("User with barcode $barcode unknown");
1670 $problem->ProblemElement($idfield);
1671 $problem->ProblemValue($barcode);
1678 =head2 retrieve_user_by_id
1680 $user = $ils->retrieve_user_by_id($id);
1682 Similar to C<retrieve_user_by_barcode> but takes the user's database
1683 id rather than barcode. This is useful when you have a circulation or
1684 hold and need to get information about the user's involved in the hold
1687 It returns a fleshed user on success or undef on failure.
1691 sub retrieve_user_by_id {
1692 my ($self, $id) = @_;
1694 # Do a fleshed retrieve of the patron, and flesh the fields that
1695 # we would normally use.
1696 my $result = $U->simplereq(
1698 'open-ils.actor.user.fleshed.retrieve',
1699 $self->{session}->{authtoken},
1701 [ 'card', 'cards', 'standing_penalties', 'addresses', 'home_ou' ]
1703 # Check for an error.
1704 undef($result) if ($result && $U->event_code($result));
1709 =head2 check_user_for_problems
1711 $problem = $ils>check_user_for_problems($user, 'HOLD, 'CIRC', 'RENEW');
1713 This function checks if a user has a blocked profile or any from a
1714 list of provided blocks. If it does, then a NCIP::Problem object is
1715 returned, otherwise an undefined value is returned.
1717 The list of blocks appears as additional arguments after the user. You
1718 can provide any value(s) that might appear in a standing penalty block
1719 lit in Evergreen. The example above checks for HOLD, CIRC, and
1720 RENEW. Any number of such values can be provided. If none are
1721 provided, the function only checks if the patron's profiles appears in
1722 the object's blocked profiles list.
1724 It stops on the first matching block, if any.
1728 sub check_user_for_problems {
1733 # Fill this in if we have a problem, otherwise just return it.
1736 # First, check the user's profile.
1737 if (grep {$_->id() == $user->profile()} @{$self->{blocked_profiles}}) {
1738 $problem = NCIP::Problem->new(
1740 ProblemType => 'User Blocked',
1741 ProblemDetail => 'User blocked from inter-library loan',
1742 ProblemElement => 'NULL',
1743 ProblemValue => 'NULL'
1748 # Next, check if the patron has one of the indicated blocks.
1750 foreach my $penalty (@{$user->standing_penalties()}) {
1751 if ($penalty->standing_penalty->block_list()) {
1752 my @pblocks = split(/\|/, $penalty->standing_penalty->block_list());
1753 foreach my $block (@blocks) {
1754 if (grep {$_ =~ /$block/} @pblocks) {
1755 $problem = NCIP::Problem->new(
1757 ProblemType => 'User Blocked',
1758 ProblemDetail => 'User blocked from ' .
1759 ($block eq 'HOLD') ? 'holds' : (($block eq 'RENEW') ? 'renewals' :
1760 (($block eq 'CIRC') ? 'checkout' : lc($block))),
1761 ProblemElement => 'NULL',
1762 ProblemValue => 'NULL'
1776 =head2 check_circ_details
1778 $problem = $ils->check_circ_details($circ, $copy, $user);
1780 Checks if we can checkin or renew a circulation. That is, the
1781 circulation is still open (i.e. the copy is still checked out), if we
1782 either own the copy or are the circulation location, and if the
1783 circulation is for the optional $user argument. $circ and $copy are
1784 required. $user is optional.
1786 Returns a problem if any of the above conditions fail. Returns undef
1787 if they pass and we can proceed with the checkin or renewal.
1789 If the failure occurred on the copy-related checks, then the
1790 ProblemElement field will be undefined and needs to be filled in with
1791 the item id field name. If the check for the copy being checked out to
1792 the provided user fails, then both ProblemElement and ProblemValue
1793 fields will be empty and need to be filled in by the caller.
1797 sub check_circ_details {
1798 my ($self, $circ, $copy, $user) = @_;
1800 # Shortcut for the next check.
1801 my $ou_id = $self->{session}->{work_ou}->id();
1803 if (!$circ || $circ->checkin_time() || ($circ->circ_lib() != $ou_id && $copy->circ_lib() != $ou_id)) {
1804 # Item isn't checked out.
1805 return NCIP::Problem->new(
1807 ProblemType => 'Item Not Checked Out',
1808 ProblemDetail => 'Item with barcode ' . $copy->barcode() . ' is not checked out.',
1809 ProblemValue => $copy->barcode()
1813 # Get data on the patron who has it checked out.
1814 my $circ_user = $self->retrieve_user_by_id($circ->usr());
1815 if ($user && $circ_user && $user->id() != $circ_user->id()) {
1816 # The ProblemElement and ProblemValue field need to be
1817 # filled in by the caller.
1818 return NCIP::Problem->new(
1820 ProblemType => 'Item Not Checked Out To This User',
1821 ProblemDetail => 'Item with barcode ' . $copy->barcode() . ' is not checked out to this user.',
1826 # If we get here, we're good to go.
1830 =head2 retrieve_copy_details_by_barcode
1832 $copy = $ils->retrieve_copy_details_by_barcode($copy_barcode);
1834 Look up and retrieve some copy details by the copy barcode. This
1835 method returns either a hashref with the copy details or undefined if
1836 no copy exists with that barcode or if some error occurs.
1838 The hashref has the fields copy, hold, transit, circ, volume, and mvr.
1840 This method differs from C<retrieve_user_by_barcode> in that a copy
1841 cannot be invalid if it exists and it is not always an error if no
1842 copy exists. In some cases, when handling AcceptItem, we might prefer
1843 there to be no copy.
1847 sub retrieve_copy_details_by_barcode {
1849 my $barcode = shift;
1851 my $copy = $U->simplereq(
1853 'open-ils.circ.copy_details.retrieve.barcode',
1854 $self->{session}->{authtoken},
1858 # If $copy is an event, return undefined.
1859 if ($copy && $U->event_code($copy)) {
1866 =head2 find_copy_details_by_item
1868 $copy_details = $ils->find_copy_details_by_item($item);
1870 This routine returns a copy_details hashref (See:
1871 retrieve_copy_details_by_barcode) for a given item. It attempts to
1872 find the "first" copy for the given item. If item is a call number it
1873 looks for the first, not deleted copy. If item is a bib, it looks for
1874 the first not deleted copy on the first not deleted call number. If
1875 item is a copy, it simply returns the details for the copy.
1879 sub find_copy_details_by_item {
1885 if (ref($item) eq 'Fieldmapper::biblio::record_entry') {
1886 my $acns = $U->simplereq(
1888 'open-ils.pcrud.search.acn.atomic',
1889 $self->{session}->{authtoken},
1891 record => $item->id(),
1895 ($item) = sort {$a->id() <=> $b->id()} @{$acns};
1898 if (ref($item) eq 'Fieldmapper::asset::call_number') {
1899 my $copies = $U->simplereq(
1901 'open-ils.pcrud.search.acp.atomic',
1902 $self->{session}->{authtoken},
1904 call_number => $item->id(),
1908 ($item) = sort {$a->id() <=> $b->id()} @{$copies};
1911 if (ref($item) eq 'Fieldmapper::asset::copy') {
1912 $details = $self->retrieve_copy_details_by_barcode($item->barcode());
1918 =head2 retrieve_copy_status
1920 $status = $ils->retrieve_copy_status($id);
1922 Retrive a copy status object by database ID.
1926 sub retrieve_copy_status {
1930 my $status = $U->simplereq(
1932 'open-ils.pcrud.retrieve.ccs',
1933 $self->{session}->{authtoken},
1940 =head2 retrieve_org_unit_by_shortname
1942 $org_unit = $ils->retrieve_org_unit_by_shortname($shortname);
1944 Retrieves an org. unit from the database by shortname. Returns the
1945 org. unit as a Fieldmapper object or undefined.
1949 sub retrieve_org_unit_by_shortname {
1951 my $shortname = shift;
1953 my $aou = $U->simplereq(
1955 'open-ils.actor.org_unit.retrieve_by_shortname',
1962 =head2 retrieve_copy_location
1964 $location = $ils->retrieve_copy_location($location_id);
1966 Retrieve a copy location based on id.
1970 sub retrieve_copy_location {
1974 my $location = $U->simplereq(
1976 'open-ils.pcrud.retrieve.acpl',
1977 $self->{session}->{authtoken},
1984 =head2 retrieve_biblio_record_entry
1986 $bre = $ils->retrieve_biblio_record_entry($bre_id);
1988 Given a biblio.record_entry.id, this method retrieves a bre object.
1992 sub retrieve_biblio_record_entry {
1996 my $bre = $U->simplereq(
1998 'open-ils.pcrud.retrieve.bre',
1999 $self->{session}->{authtoken},
2006 =head2 create_precat_copy
2009 barcode => '312340123456789',
2010 author => 'Public, John Q.',
2011 title => 'Magnum Opus',
2012 call_number => '005.82',
2013 publisher => 'Brick House',
2014 publication_date => '2014'
2017 $item = $ils->create_precat_copy($item_info);
2020 Create a "precat" copy to use for the incoming item using a hashref of
2021 item information. At a minimum, the barcode, author and title fields
2022 need to be filled in. The other fields are ignored if provided.
2024 This method is called by the AcceptItem handler if the C<use_precats>
2025 configuration option is turned on.
2029 sub create_precat_copy {
2031 my $item_info = shift;
2033 my $item = Fieldmapper::asset::copy->new();
2034 $item->barcode($item_info->{barcode});
2035 $item->call_number(OILS_PRECAT_CALL_NUMBER);
2036 $item->dummy_title($item_info->{title});
2037 $item->dummy_author($item_info->{author});
2038 $item->circ_lib($self->{session}->{work_ou}->id());
2039 $item->circulate('t');
2040 $item->holdable('t');
2041 $item->opac_visible('f');
2042 $item->deleted('f');
2043 $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2044 $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2047 $item->editor($self->{session}->{user}->id());
2048 $item->creator($self->{session}->{user}->id());
2051 # Actually create it:
2053 my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
2056 $xact = $ses->request(
2057 'open-ils.pcrud.transaction.begin',
2058 $self->{session}->{authtoken}
2060 $item = $ses->request(
2061 'open-ils.pcrud.create.acp',
2062 $self->{session}->{authtoken},
2065 $xact = $ses->request(
2066 'open-ils.pcrud.transaction.commit',
2067 $self->{session}->{authtoken}
2075 'open-ils.pcrud.transaction.rollback',
2076 $self->{session}->{authtoken}
2086 =head2 create_fuller_copy
2089 barcode => '31234003456789',
2090 author => 'Public, John Q.',
2091 title => 'Magnum Opus',
2092 call_number => '005.82',
2093 publisher => 'Brick House',
2094 publication_date => '2014'
2097 $item = $ils->create_fuller_copy($item_info);
2099 Creates a skeletal bibliographic record, call number, and copy for the
2100 incoming item using a hashref with item information in it. At a
2101 minimum, the barcode, author, title, and call_number fields must be
2104 This method is used by the AcceptItem handler if the C<use_precats>
2105 configuration option is NOT set.
2109 sub create_fuller_copy {
2111 my $item_info = shift;
2115 # We do everything in one transaction, because it should be atomic.
2116 my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
2120 $xact = $ses->request(
2121 'open-ils.pcrud.transaction.begin',
2122 $self->{session}->{authtoken}
2129 # The rest depends on there being a transaction.
2132 # Create the MARC record.
2133 my $record = MARC::Record->new();
2134 $record->encoding('UTF-8');
2135 $record->leader('00881nam a2200193 4500');
2136 my $datespec = strftime("%Y%m%d%H%M%S.0", localtime);
2138 push(@fields, MARC::Field->new('005', $datespec));
2139 push(@fields, MARC::Field->new('082', '0', '4', 'a' => $item_info->{call_number}));
2140 push(@fields, MARC::Field->new('245', '0', '0', 'a' => $item_info->{title}));
2141 # Publisher is a little trickier:
2142 if ($item_info->{publisher}) {
2143 my $pub = MARC::Field->new('260', ' ', ' ', 'a' => '[S.l.]', 'b' => $item_info->{publisher});
2144 $pub->add_subfields('c' => $item_info->{publication_date}) if ($item_info->{publication_date});
2145 push(@fields, $pub);
2147 # We have no idea if the author is personal corporate or something else, so we use a 720.
2148 push(@fields, MARC::Field->new('720', ' ', ' ', 'a' => $item_info->{author}, '4' => 'aut'));
2149 $record->append_fields(@fields);
2150 my $marc = clean_marc($record);
2152 # Create the bib object.
2153 my $bib = Fieldmapper::biblio::record_entry->new();
2154 $bib->creator($self->{session}->{user}->id());
2155 $bib->editor($self->{session}->{user}->id());
2156 $bib->source($self->{bib_source}->id());
2163 $bib = $ses->request(
2164 'open-ils.pcrud.create.bre',
2165 $self->{session}->{authtoken},
2173 'open-ils.pcrud.transaction.rollback',
2174 $self->{session}->{authtoken}
2179 # Create the call number
2182 $acn = Fieldmapper::asset::call_number->new();
2183 $acn->creator($self->{session}->{user}->id());
2184 $acn->editor($self->{session}->{user}->id());
2185 $acn->label($item_info->{call_number});
2186 $acn->record($bib->id());
2187 $acn->owning_lib($self->{session}->{work_ou}->id());
2192 $acn = $ses->request(
2193 'open-ils.pcrud.create.acn',
2194 $self->{session}->{authtoken},
2202 'open-ils.pcrud.transaction.rollback',
2203 $self->{session}->{authtoken}
2211 $item = Fieldmapper::asset::copy->new();
2212 $item->barcode($item_info->{barcode});
2213 $item->call_number($acn->id());
2214 $item->circ_lib($self->{session}->{work_ou}->id);
2215 $item->circulate('t');
2216 if ($self->{config}->{items}->{use_force_holds}) {
2217 $item->holdable('f');
2219 $item->holdable('t');
2221 $item->opac_visible('f');
2222 $item->deleted('f');
2223 $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2224 $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2227 $item->editor($self->{session}->{user}->id);
2228 $item->creator($self->{session}->{user}->id);
2232 $item = $ses->request(
2233 'open-ils.pcrud.create.acp',
2234 $self->{session}->{authtoken},
2238 # Cross our fingers and commit the work.
2239 $xact = $ses->request(
2240 'open-ils.pcrud.transaction.commit',
2241 $self->{session}->{authtoken}
2248 'open-ils.pcrud.transaction.rollback',
2249 $self->{session}->{authtoken}
2250 )->gather(1) if ($xact);
2256 # We need to disconnect our session.
2259 # Now, we handle our asset stat_cat entries.
2261 # It would be nice to do these in the above transaction, but
2262 # pcrud does not support the ascecm object, yet.
2263 foreach my $entry (@{$self->{stat_cat_entries}}) {
2264 my $map = Fieldmapper::asset::stat_cat_entry_copy_map->new();
2266 $map->stat_cat($entry->stat_cat());
2267 $map->stat_cat_entry($entry->id());
2268 $map->owning_copy($item->id());
2269 # We don't really worry if it succeeds or not.
2272 'open-ils.circ.stat_cat.asset.copy_map.create',
2273 $self->{session}->{authtoken},
2284 $hold = $ils->place_hold($item, $user, $location, $expiration);
2286 This function places a hold on $item for $user for pickup at
2287 $location. If location is not provided or undefined, the user's home
2288 library is used as a fallback.
2290 The $expiration argument is optional and must be a properly formatted
2291 ISO date time. It will be used as the hold expire time, if
2292 provided. Otherwise the system default time will be used.
2294 $item can be a copy (asset::copy), volume (asset::call_number), or bib
2295 (biblio::record_entry). The appropriate hold type will be placed
2296 depending on the object.
2298 On success, the method returns the object representing the hold. On
2299 failure, a NCIP::Problem object, describing the failure, is returned.
2307 my $location = shift;
2308 my $expiration = shift;
2310 # If $location is undefined, use the user's home_ou, which should
2311 # have been fleshed when the user was retrieved.
2312 $location = $user->home_ou() unless ($location);
2314 # $hold is the hold. $params is for the is_possible check.
2315 my ($hold, $params);
2317 # Prep the hold with fields common to all hold types:
2318 $hold = Fieldmapper::action::hold_request->new();
2319 $hold->isnew(1); # Just to make sure.
2320 $hold->target($item->id());
2321 $hold->usr($user->id());
2322 $hold->pickup_lib($location->id());
2323 $hold->expire_time(cleanse_ISO8601($expiration)) if ($expiration);
2324 if (!$user->email()) {
2325 $hold->email_notify('f');
2326 $hold->phone_notify($user->day_phone()) if ($user->day_phone());
2328 $hold->email_notify('t');
2332 $params = { pickup_lib => $location->id(), patronid => $user->id() };
2334 if (ref($item) eq 'Fieldmapper::asset::copy') {
2335 my $type = ($self->{config}->{items}->{use_force_holds}) ? 'F' : 'C';
2336 $hold->hold_type($type);
2337 $hold->current_copy($item->id());
2338 $params->{hold_type} = $type;
2339 $params->{copy_id} = $item->id();
2340 } elsif (ref($item) eq 'Fieldmapper::asset::call_number') {
2341 $hold->hold_type('V');
2342 $params->{hold_type} = 'V';
2343 $params->{volume_id} = $item->id();
2344 } elsif (ref($item) eq 'Fieldmapper::biblio::record_entry') {
2345 $hold->hold_type('T');
2346 $params->{hold_type} = 'T';
2347 $params->{titleid} = $item->id();
2350 # Check for a duplicate hold:
2351 my $duplicate = $U->simplereq(
2353 'open-ils.pcrud.search.ahr',
2354 $self->{session}->{authtoken},
2356 hold_type => $hold->hold_type(),
2357 target => $hold->target(),
2358 usr => $hold->usr(),
2359 expire_time => {'>' => 'now'},
2360 cancel_time => undef,
2361 fulfillment_time => undef
2365 return NCIP::Problem->new(
2367 ProblemType => 'Duplicate Request',
2368 ProblemDetail => 'A request for this item already exists for this patron.',
2369 ProblemElement => 'NULL',
2370 ProblemValue => 'NULL'
2375 # Check if the hold is possible:
2376 my $r = $U->simplereq(
2378 'open-ils.circ.title_hold.is_possible',
2379 $self->{session}->{authtoken},
2383 if ($r->{success}) {
2384 $hold = $U->simplereq(
2386 'open-ils.circ.holds.create.override',
2387 $self->{session}->{authtoken},
2391 $hold = $hold->[0] if (ref($hold) eq 'ARRAY');
2392 $hold = _problem_from_event('Request Not Possible', $hold);
2394 # open-ils.circ.holds.create.override returns the id on
2395 # success, so we retrieve the full hold object from the
2396 # database to return it.
2397 $hold = $U->simplereq(
2399 'open-ils.pcrud.retrieve.ahr',
2400 $self->{session}->{authtoken},
2404 } elsif ($r->{last_event}) {
2405 $hold = _problem_from_event('Request Not Possible', $r->{last_event});
2406 } elsif ($r->{textcode}) {
2407 $hold = _problem_from_event('Request Not Possible', $r);
2409 $hold = _problem_from_event('Request Not Possible');
2417 $ils->cancel_hold($hold);
2419 This method cancels the hold argument. It makes no checks on the hold,
2420 so if there are certain conditions that need to be fulfilled before
2421 the hold is canceled, then you must check them before calling this
2424 It returns undef on success or failure. If it fails, you've usually
2425 got bigger problems.
2433 my $r = $U->simplereq(
2435 'open-ils.circ.hold.cancel',
2436 $self->{session}->{authtoken},
2439 'Canceled via NCIPServer'
2447 $ils->delete_copy($copy);
2449 Deletes the copy, and if it is owned by our work_ou and not a precat,
2450 we also delete the volume and bib on which the copy depends.
2458 # Shortcut for ownership checks below.
2459 my $ou_id = $self->{session}->{work_ou}->id();
2461 # First, make sure the copy is not already deleted and we own it.
2462 return undef if ($U->is_true($copy->deleted()) || $copy->circ_lib() != $ou_id);
2464 # Indicate we want to delete the copy.
2465 $copy->isdeleted(1);
2467 # Delete the copy using a backend call that will delete the copy,
2468 # the call number, and bib when appropriate.
2469 my $result = $U->simplereq(
2471 'open-ils.cat.asset.copy.fleshed.batch.update.override',
2472 $self->{session}->{authtoken},
2476 # We are currently not checking for succes or failure of the
2477 # above. At some point, someone may want to.
2482 =head2 copy_can_circulate
2484 $can_circulate = $ils->copy_can_circulate($copy);
2486 Check if the copy's location and the copy itself allow
2487 circulation. Return true if they do, and false if they do not.
2491 sub copy_can_circulate {
2495 my $location = $copy->location();
2496 unless (ref($location)) {
2497 $location = $self->retrieve_copy_location($location);
2500 return ($U->is_true($copy->circulate()) && $U->is_true($location->circulate()));
2503 =head2 copy_can_fulfill
2505 $can_fulfill = $ils->copy_can_fulfill($copy);
2507 Check if the copy's location and the copy itself allow
2508 holds. Return true if they do, and false if they do not.
2512 sub copy_can_fulfill {
2516 my $location = $copy->location();
2517 unless (ref($location)) {
2518 $location = $self->retrieve_copy_location($location);
2521 return ($U->is_true($copy->holdable()) && $U->is_true($location->holdable()));
2524 =head1 OVERRIDDEN PARENT METHODS
2526 =head2 find_user_barcode
2528 We dangerously override our parent's C<find_user_barcode> to return
2529 either the $barcode or a Problem object. In list context the barcode
2530 or problem will be the first argument and the id field, if any, will
2531 be the second. We also add a second, optional, argument to indicate a
2532 default value for the id field in the event of a failure to find
2533 anything at all. (Perl lets us get away with this.)
2537 sub find_user_barcode {
2539 my $request = shift;
2540 my $default = shift;
2543 my $message = $self->parse_request_type($request);
2544 if ($message eq 'LookupUser') {
2545 $default = 'AuthenticationInputData';
2547 $default = 'UserIdentifierValue';
2551 my ($value, $idfield) = $self->SUPER::find_user_barcode($request);
2554 $idfield = $default unless ($idfield);
2555 $value = NCIP::Problem->new();
2556 $value->ProblemType('Needed Data Missing');
2557 $value->ProblemDetail('Cannot find user barcode in message.');
2558 $value->ProblemElement($idfield);
2559 $value->ProblemValue('NULL');
2562 return (wantarray) ? ($value, $idfield) : $value;
2565 =head2 find_item_barcode
2567 We do pretty much the same thing as with C<find_user_barcode> for
2568 C<find_item_barcode>.
2572 sub find_item_barcode {
2574 my $request = shift;
2575 my $default = shift || 'ItemIdentifierValue';
2577 my ($value, $idfield) = $self->SUPER::find_item_barcode($request);
2580 $idfield = $default unless ($idfield);
2581 $value = NCIP::Problem->new();
2582 $value->ProblemType('Needed Data Missing');
2583 $value->ProblemDetail('Cannot find item barcode in message.');
2584 $value->ProblemElement($idfield);
2585 $value->ProblemValue('NULL');
2588 return (wantarray) ? ($value, $idfield) : $value;
2591 =head2 find_target_via_bibliographic_id
2593 $item = $ils->find_target_via_bibliographic_id(@biblio_ids);
2595 Searches for a bibliographic record to put on hold and returns an
2596 appropriate hold target item depending upon what it finds. If an
2597 appropriate, single target cannot be found, it returns an
2598 NCIP::Problem with the problem message.
2600 Currently, we only look for SYSNUMBER, ISBN, and ISSN record
2601 identifiers. If nothing is found, this method can return undef. (Gotta
2602 love Perl and untyped/weakly typed languages in general!)
2604 TODO: Figure out how to search OCLC numbers. We probably need to use
2605 "MARC Expert Search" if we don't want to do a JSON query on
2610 sub find_target_via_bibliographic_id {
2612 my @biblio_ids = @_;
2614 # The item that we find:
2617 # Id for our bib in Evergreen:
2620 # First, let's look for a SYSNUMBER:
2622 { ($_->{BibliographicRecordIdentifierCode} && $_->{BibliographicRecordIdentifierCode} eq 'SYSNUMBER')
2623 || ($_->{BibliographicItemIdentifierCode} && $_->{BibliographicItemIdentifierCode} eq 'SYSNUMBER')
2628 # BibliographicRecordId can have an AgencyId field if the
2629 # BibliographicRecordIdentifierCode is absent.
2630 if ($idobj->{AgencyId}) {
2631 $bibid = $idobj->{BibliographicRecordIdentifier};
2632 my $locname = $idobj->{AgencyId};
2634 $locname =~ s/.*://;
2635 $loc = $self->retrieve_org_unit_by_shortname($locname);
2637 } elsif ($idobj->{BibliographicRecordIdentifierCode}) {
2638 $bibid = $idobj->{BibliographicRecordIdentifierCode}
2640 $bibid = $idobj->{BibliographicItemIdentifierCode}
2642 if ($bibid && $loc) {
2643 $item = $self->_call_number_search($bibid, $loc);
2645 $item = $U->simplereq(
2647 'open-ils.pcrud.retrieve.bre',
2648 $self->{session}->{authtoken},
2652 # Check if item is deleted so we'll look for more
2654 undef($item) if ($item && $U->is_true($item->deleted()));
2657 # Build an array of id objects based on the other identifier fields.
2660 ($_->{BibliographicRecordIdentifierCode} && $_->{BibliographicRecordIdentifierCode} eq 'ISBN')
2661 || ($_->{BibliographicItemIdentifierCode} && $_->{BibliographicItemIdentifierCode} eq 'ISBN')
2662 || ($_->{BibliographicRecordIdentifierCode} && $_->{BibliographicRecordIdentifierCode} eq 'ISSN')
2663 || ($_->{BibliographicItemIdentifierCode} && $_->{BibliographicItemIdentifierCode} eq 'ISSN')
2667 my $stashed_problem;
2668 # Reuse $idobj from above.
2669 foreach $idobj (@idobjs) {
2670 my ($idvalue, $idtype, $idfield);
2671 if ($_->{BibliographicItemIdentifier}) {
2672 $idvalue = $_->{BibliographicItemIdentifier};
2673 $idtype = $_->{BibliographicItemIdentifierCode};
2674 $idfield = 'BibliographicItemIdentifier';
2676 $idvalue = $_->{BibliographicRecordIdentifier};
2677 $idtype = $_->{BibliographicRecordIdentifierCode};
2678 $idfield = 'BibliographicRecordIdentifier';
2680 $item = $self->_bib_search($idvalue, $idtype);
2681 if (ref($item) eq 'NCIP::Problem') {
2682 $stashed_problem = $item unless($stashed_problem);
2683 $stashed_problem->ProblemElement($idfield);
2688 $item = $stashed_problem if (!$item && $stashed_problem);
2694 # private subroutines not meant to be used directly by subclasses.
2695 # Most have to do with setup and/or state checking of implementation
2698 # Find, load, and parse our configuration file:
2702 # Find the configuration file via variables:
2703 my $file = OILS_NCIP_CONFIG_DEFAULT;
2704 $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});
2706 $self->{config} = XMLin($file, NormaliseSpace => 2,
2707 ForceArray => ['block_profile', 'stat_cat_entry']);
2710 # Bootstrap OpenSRF::System and load the IDL.
2714 my $bootstrap_config = $self->{config}->{bootstrap};
2715 OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
2717 my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
2718 Fieldmapper->import(IDL => $idl);
2721 # Login and then initialize some object data based on the
2726 # Login to Evergreen.
2729 # Load the barred groups as pgt objects into a blocked_profiles
2731 $self->{blocked_profiles} = [];
2732 foreach (@{$self->{config}->{patrons}->{block_profile}}) {
2735 $pgt = $U->simplereq(
2737 'open-ils.pcrud.retrieve.pgt',
2738 $self->{session}->{authtoken},
2742 $pgt = $U->simplereq(
2744 'open-ils.pcrud.search.pgt',
2745 $self->{session}->{authtoken},
2749 push(@{$self->{blocked_profiles}}, $pgt) if ($pgt);
2752 # Load the bib source if we're not using precats.
2753 unless ($self->{config}->{items}->{use_precats}) {
2754 # Retrieve the default
2755 $self->{bib_source} = $U->simplereq(
2757 'open-ils.pcrud.retrieve.cbs',
2758 $self->{session}->{authtoken},
2759 BIB_SOURCE_DEFAULT);
2760 my $data = $self->{config}->{items}->{bib_source};
2762 $data = $data->[0] if (ref($data) eq 'ARRAY');
2765 $result = $U->simplereq(
2767 'open-ils.pcrud.retrieve.cbs',
2768 $self->{session}->{authtoken},
2772 $result = $U->simplereq(
2774 'open-ils.pcrud.search.cbs',
2775 $self->{session}->{authtoken},
2779 $self->{bib_source} = $result if ($result);
2783 # Load the required asset.stat_cat_entries:
2784 $self->{stat_cat_entries} = [];
2785 # First, make a regex for our ou and ancestors:
2786 my $ancestors = join("|", @{$U->get_org_ancestors($self->{session}->{work_ou}->id())});
2787 my $re = qr/(?:$ancestors)/;
2788 # Get the uniq stat_cat ids from the configuration:
2789 my @cats = uniq map {$_->{stat_cat}} @{$self->{config}->{items}->{stat_cat_entry}};
2790 # Retrieve all of the fleshed stat_cats and entries for the above.
2791 my $stat_cats = $U->simplereq(
2793 'open-ils.circ.stat_cat.asset.retrieve.batch',
2794 $self->{session}->{authtoken},
2797 foreach my $entry (@{$self->{config}->{items}->{stat_cat_entry}}) {
2798 # Must have the stat_cat attr and the name, so we must have a
2800 next unless(ref $entry);
2801 my ($stat) = grep {$_->id() == $entry->{stat_cat}} @$stat_cats;
2802 push(@{$self->{stat_cat_entries}}, grep {$_->owner() =~ $re && $_->value() eq $entry->{content}} @{$stat->entries()});
2806 # Search asset.call_number by a bre.id and location object. Return the
2807 # "closest" call_number if found, undef otherwise.
2808 sub _call_number_search {
2811 my $location = shift;
2813 # At some point, this should be smarter, and we should retrieve
2814 # ancestors and descendants and search with a JSON query or some
2815 # such with results ordered by proximity to the original location,
2816 # but I don't have time to implement that right now.
2817 my $acn = $U->simplereq(
2819 'open-ils.pcrud.search.acn',
2820 $self->{session}->{authtoken},
2821 {record => $bibid, owning_lib => $location->id()}
2827 # Do a multiclass.query to search for items by isbn or issn.
2830 my $idvalue = shift;
2834 my $result = $U->simplereq(
2836 'open-ils.search.biblio.multiclass',
2837 {searches => {lc($idtype) => $idvalue}}
2840 if ($result && $result->{count}) {
2841 if ($result->{count} > 1) {
2842 $item = NCIP::Problem->new(
2844 ProblemType => 'Non-Unique Item',
2845 ProblemDetail => 'More than one item matches the request.',
2846 ProblemElement => '',
2847 ProblemValue => $idvalue
2851 my $bibid = $result->{ids}->[0]->[0];
2852 $item = $U->simplereq(
2854 'open-ils.pcrud.retrieve.bre',
2855 $self->{session}->{authtoken},
2863 # Search for holds using the user and copy_details information:
2867 my $copy_details = shift;
2871 # Retrieve all of the user's active holds, and then search them in Perl.
2872 my $holds_list = $U->simplereq(
2874 'open-ils.circ.holds.retrieve',
2875 $self->{session}->{authtoken},
2880 if ($holds_list && @$holds_list) {
2882 # Look for title holds (the most common), first:
2883 my $targetid = $copy_details->{mvr}->doc_id();
2884 @holds = grep {$_->hold_type eq 'T' && $_->target == $targetid} @{$holds_list};
2886 # Look for volume holds, the next most common:
2887 $targetid = $copy_details->{volume}->id();
2888 @holds = grep {$_->hold_type eq 'V' && $_->target == $targetid} @{$holds_list};
2891 # Look for copy and force holds, the least likely.
2892 $targetid = $copy_details->{copy}->id();
2893 @holds = grep {($_->hold_type eq 'C' || $_->hold_type eq 'F') && $_->target == $targetid} @{$holds_list};
2895 # There should only be 1, at this point, if there are any.
2904 # Standalone, "helper" functions. These do not take an object or
2907 # Check if a user is past their expiration date.
2912 # Users might not expire. If so, they have no expire_date.
2913 if ($user->expire_date()) {
2914 my $expires = DateTime::Format::ISO8601->parse_datetime(
2915 cleanse_ISO8601($user->expire_date())
2917 my $now = DateTime->now()->epoch();
2918 $expired = $now > $expires;
2924 # Creates a NCIP Problem from an event. Takes a string for the problem
2925 # type, the event hashref (or a string to use for the detail), and
2926 # optional arguments for the ProblemElement and ProblemValue fields.
2927 sub _problem_from_event {
2928 my ($type, $evt, $element, $value) = @_;
2934 my ($textcode, $desc);
2936 # Get the textcode, if available. Otherwise, use the ilsevent
2937 # "id," if available.
2938 if ($evt->{textcode}) {
2939 $textcode = $evt->{textcode};
2940 } elsif ($evt->{ilsevent}) {
2941 $textcode = $evt->{ilsevent};
2944 # Get the description. We favor translated descriptions over
2945 # the English in ils_events.xml.
2947 $desc = $evt->{desc};
2950 # Check if $type was set. As an "undocumented" feature, you
2951 # can pass undef, and we'll use the textcode from the event.
2958 # Set the detail from some combination of the above.
2961 } elsif ($textcode eq 'PERM_FAILURE') {
2962 if ($evt->{ilsperm}) {
2963 $detail = "Permission denied: " . $evt->{ilsperm};
2964 $detail =~ s/\.override$//;
2966 } elsif ($textcode) {
2967 $detail = "ILS returned $textcode error.";
2969 $detail = 'Detail not available.';
2976 return NCIP::Problem->new(
2978 ProblemType => ($type) ? $type : 'Temporary Processing Failure',
2979 ProblemDetail => ($detail) ? $detail : 'Detail not available.',
2980 ProblemElement => ($element) ? $element : 'NULL',
2981 ProblemValue => ($value) ? $value : 'NULL'
2986 # "Fix" dates for output so they validate against the schema
2989 my $out = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($date));
2990 $out->set_time_zone('UTC');
2991 return $out->iso8601();