4abfe54ae23e98540798e80a7dbba619959621eb
[working/NCIPServer.git] / lib / NCIP / ILS / Evergreen.pm
1 # ---------------------------------------------------------------
2 # Copyright © 2014 Jason J.A. Stephenson <jason@sigio.com>
3 #
4 # This file is part of NCIPServer.
5 #
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.
10 #
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.
15 #
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;
20
21 use Modern::Perl;
22 use XML::LibXML::Simple qw(XMLin);
23 use DateTime;
24 use DateTime::Format::ISO8601;
25 use Digest::MD5 qw/md5_hex/;
26 use OpenSRF::System;
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/;
34 use MARC::Record;
35 use MARC::Field;
36 use MARC::File::XML;
37 use List::MoreUtils qw/uniq/;
38 use POSIX qw/strftime/;
39
40 # We need a bunch of NCIP::* objects.
41 use NCIP::Response;
42 use NCIP::Problem;
43 use NCIP::User;
44 use NCIP::User::OptionalFields;
45 use NCIP::User::AddressInformation;
46 use NCIP::User::Id;
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;
54 use NCIP::RequestId;
55 use NCIP::Item::Id;
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;
61
62 # Inherit from NCIP::ILS.
63 use parent qw(NCIP::ILS);
64
65 =head1 NAME
66
67 Evergreen - Evergreen driver for NCIPServer
68
69 =head1 SYNOPSIS
70
71     my $ils = NCIP::ILS::Evergreen->new(name => $config->{NCIP.ils.value});
72
73 =head1 DESCRIPTION
74
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.
78
79 =cut
80
81 # Default values we define for things that might be missing in our
82 # runtime environment or configuration file that absolutely must have
83 # values.
84 #
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.
88 #
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
93 # installation.
94 use constant {
95     OILS_NCIP_CONFIG_DEFAULT => '/openils/conf/oils_ncip.xml',
96     BIB_SOURCE_DEFAULT => 2
97 };
98
99 # A common Evergreen code shortcut to use AppUtils:
100 my $U = 'OpenILS::Application::AppUtils';
101
102 # The usual constructor:
103 sub new {
104     my $class = shift;
105     $class = ref($class) if (ref $class);
106
107     # Instantiate our parent with the rest of the arguments.  It
108     # creates a blessed hashref.
109     my $self = $class->SUPER::new(@_);
110
111     # Look for our configuration file, load, and parse it:
112     $self->_configure();
113
114     # Bootstrap OpenSRF and prepare some OpenILS components.
115     $self->_bootstrap();
116
117     # Initialize the rest of our internal state.
118     $self->_init();
119
120     return $self;
121 }
122
123 =head1 HANDLER METHODS
124
125 =head2 lookupuser
126
127     $ils->lookupuser($request);
128
129 Processes a LookupUser request.
130
131 =cut
132
133 sub lookupuser {
134     my $self = shift;
135     my $request = shift;
136
137     # Check our session and login if necessary.
138     $self->login() unless ($self->checkauth());
139
140     my $message_type = $self->parse_request_type($request);
141
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));
146
147     # Need to parse the request object to get the user barcode.
148     my ($barcode, $idfield) = $self->find_user_barcode($request);
149
150     # If we did not find a barcode, then report the problem.
151     if (ref($barcode) eq 'NCIP::Problem') {
152         $response->problem($barcode);
153         return $response;
154     }
155
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);
160         return $response;
161     }
162
163     # We got the information, so lets fill in our userdata.
164     my $userdata = NCIP::User->new();
165
166     # Use the user's main card as the UserId.
167     my $id = NCIP::User::Id->new({
168         UserIdentifierType => 'Barcode',
169         UserIdnetifierValue => $user->card->barcode()
170     });
171     $userdata->UserId($id);
172
173     # Check if they requested any optional fields and return those.
174     my $elements = $request->{$message_type}->{UserElementType};
175     if ($elements) {
176         $elements = [$elements] unless (ref $elements eq 'ARRAY');
177         my $optionalfields = $self->handle_user_elements($user, $elements);
178         $userdata->UserOptionalFields($optionalfields);
179     }
180
181     $response->data($userdata);
182
183     return $response;
184 }
185
186 =head2 acceptitem
187
188     $ils->acceptitem($request);
189
190 Processes an AcceptItem request.
191
192 =cut
193
194 sub acceptitem {
195     my $self = shift;
196     my $request = shift;
197
198     # Check our session and login if necessary.
199     $self->login() unless ($self->checkauth());
200
201     # Common preparation.
202     my $message = $self->parse_request_type($request);
203     my $response = NCIP::Response->new({type => $message . 'Response'});
204     $response->header($self->make_header($request));
205
206     # We only accept holds for the time being.
207     if ($request->{$message}->{RequestedActionType} =~ /^hold\W/i) {
208         # We need the item id or we can't do anything at all.
209         my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
210         if (ref($item_barcode) eq 'NCIP::Problem') {
211             $response->problem($item_barcode);
212             return $response;
213         }
214
215         # We need to find a patron barcode or we can't look anyone up
216         # to place a hold.
217         my ($user_barcode, $user_idfield) = $self->find_user_barcode($request, 'UserIdentifierValue');
218         if (ref($user_barcode) eq 'NCIP::Problem') {
219             $response->problem($user_barcode);
220             return $response;
221         }
222         # Look up our patron by barcode:
223         my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
224         if (ref($user) eq 'NCIP::Problem') {
225             $response->problem($user);
226             return $response;
227         }
228         # We're doing patron checks before looking for bibliographic
229         # information and creating the item because problems with the
230         # patron are more likely to occur.
231         my $problem = $self->check_user_for_problems($user, 'HOLD');
232         if ($problem) {
233             $response->problem($problem);
234             return $response;
235         }
236
237         # Check if the item barcode already exists:
238         my $item = $self->retrieve_copy_details_by_barcode($item_barcode);
239         if ($item) {
240             # What to do here was not defined in the
241             # specification. Since the copies that we create this way
242             # should get deleted when checked in, it would be an error
243             # if we try to create another one. It means that something
244             # has gone wrong somewhere.
245             $response->problem(
246                 NCIP::Problem->new(
247                     {
248                         ProblemType => 'Duplicate Item',
249                         ProblemDetail => "Item with barcode $item_barcode already exists.",
250                         ProblemElement => $item_idfield,
251                         ProblemValue => $item_barcode
252                     }
253                 )
254             );
255             return $response;
256         }
257
258         # Now, we have to create our new copy and/or bib and call number.
259
260         # First, we have to gather the necessary information from the
261         # request.  Store in a hashref for convenience. We may write a
262         # method to get this information in the future if we find we
263         # need it in other handlers. Such a function would be a
264         # candidate to go into our parent, NCIP::ILS.
265         my $item_info = {
266             barcode => $item_barcode,
267             call_number => $request->{$message}->{ItemOptionalFields}->{ItemDescription}->{CallNumber},
268             title => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{Title},
269             author => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{Author},
270             publisher => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{Publisher},
271             publication_date => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{PublicationDate},
272             medium => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{MediumType},
273             electronic => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{ElectronicResource}
274         };
275
276         if ($self->{config}->{items}->{use_precats}) {
277             # We only need to create a precat copy.
278             $item = $self->create_precat_copy($item_info);
279         } else {
280             # We have to create a "partial" bib record, a call number and a copy.
281             $item = $self->create_fuller_copy($item_info);
282         }
283
284         # If we failed to create the copy, report a problem.
285         unless ($item) {
286             $response->problem(
287                 {
288                     ProblemType => 'Temporary Processing Failure',
289                     ProblemDetail => 'Failed to create the item in the system',
290                     ProblemElement => $item_idfield,
291                     ProblemValue => $item_barcode
292                 }
293             );
294             return $response;
295         }
296
297         # We try to find the pickup location in our database. It's OK
298         # if it does not exist, the user's home library will be used
299         # instead.
300         my $location = $self->find_location_failover($request->{$message}->{PickupLocation}, $request, $message);
301
302         # Now, we place the hold on the newly created copy on behalf
303         # of the patron retrieved above.
304         my $hold = $self->place_hold($item, $user, $location);
305         if (ref($hold) eq 'NCIP::Problem') {
306             $response->problem($hold);
307             return $response;
308         }
309
310         # We return the RequestId and optionally, the ItemID. We'll
311         # just return what was sent to us, since we ignored all of it
312         # but the barcode.
313         my $data = {};
314         $data->{RequestId} = NCIP::RequestId->new(
315             {
316                 AgencyId => $request->{$message}->{RequestId}->{AgencyId},
317                 RequestIdentifierType => $request->{$message}->{RequestId}->{RequestIdentifierType},
318                 RequestIdentifierValue => $request->{$message}->{RequestId}->{RequestIdentifierValue}
319             }
320         );
321         $data->{ItemId} = NCIP::Item::Id->new(
322             {
323                 AgencyId => $request->{$message}->{ItemId}->{AgencyId},
324                 ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
325                 ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
326             }
327         );
328         $response->data($data);
329
330     } else {
331         my $problem = NCIP::Problem->new();
332         $problem->ProblemType('Unauthorized Combination Of Element Values For System');
333         $problem->ProblemDetail('We only support Hold For Pickup');
334         $problem->ProblemElement('RequestedActionType');
335         $problem->ProblemValue($request->{$message}->{RequestedActionType});
336         $response->problem($problem);
337     }
338
339     return $response;
340 }
341
342 =head2 checkinitem
343
344     $response = $ils->checkinitem($request);
345
346 Checks the item in if we can find the barcode in the message. It
347 returns problems if it cannot find the item in the system or if the
348 item is not checked out.
349
350 It could definitely use some more brains at some point as it does not
351 fully support everything that the standard allows. It also does not
352 really check if the checkin succeeded or not.
353
354 =cut
355
356 sub checkinitem {
357     my $self = shift;
358     my $request = shift;
359
360     # Check our session and login if necessary:
361     $self->login() unless ($self->checkauth());
362
363     # Common stuff:
364     my $message = $self->parse_request_type($request);
365     my $response = NCIP::Response->new({type => $message . 'Response'});
366     $response->header($self->make_header($request));
367
368     # We need the copy barcode from the message.
369     my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
370     if (ref($item_barcode) eq 'NCIP::Problem') {
371         $response->problem($item_barcode);
372         return $response;
373     }
374
375     # Retrieve the copy details.
376     my $details = $self->retrieve_copy_details_by_barcode($item_barcode);
377     unless ($details) {
378         # Return an Unknown Item problem unless we find the copy.
379         $response->problem(
380             NCIP::Problem->new(
381                 {
382                     ProblemType => 'Unknown Item',
383                     ProblemDetail => "Item with barcode $item_barcode is not known.",
384                     ProblemElement => $item_idfield,
385                     ProblemValue => $item_barcode
386                 }
387             )
388         );
389         return $response;
390     }
391
392     # Check if a UserId was provided. If so, this is the patron to
393     # whom the copy should be checked out.
394     my $user;
395     my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
396     # We ignore the problem, because the UserId is optional.
397     if (ref($user_barcode) ne 'NCIP::Problem') {
398         $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
399         # We don't ignore a problem here, however.
400         if (ref($user) eq 'NCIP::Problem') {
401             $response->problem($user);
402             return $response;
403         }
404     }
405
406     # Isolate the copy.
407     my $copy = $details->{copy};
408
409     # Look for a circulation and examine its information:
410     my $circ = $details->{circ};
411
412     # Check the circ details to see if the copy is checked out and, if
413     # the patron was provided, that it is checked out to the patron in
414     # question. We also verify the copy ownership and circulation
415     # location.
416     my $problem = $self->check_circ_details($circ, $copy, $user);
417     if ($problem) {
418         # We need to fill in some information, however.
419         if (!$problem->ProblemValue() && !$problem->ProblemElement()) {
420             $problem->ProblemValue($user_barcode);
421             $problem->ProblemElement($user_idfield);
422         } elsif (!$problem->ProblemElement()) {
423             $problem->ProblemElement($item_idfield);
424         }
425         $response->problem($problem);
426         return $response;
427     }
428
429     # Checkin parameters. We want to skip hold targeting or making
430     # transits, to force the checkin despite the copy status, as
431     # well as void overdues.
432     my $params = {
433         copy_barcode => $copy->barcode(),
434         force => 1,
435         noop => 1,
436         void_overdues => 1
437     };
438     my $result = $U->simplereq(
439         'open-ils.circ',
440         'open-ils.circ.checkin.override',
441         $self->{session}->{authtoken},
442         $params
443     );
444     if (ref($result) eq 'ARRAY') {
445         $result = $result->[0];
446     }
447     if ($result->{textcode} eq 'SUCCESS') {
448         # Delete the copy. Since delete_copy checks ownership
449         # before attempting to delete the copy, we don't bother
450         # checking who owns it.
451         $self->delete_copy($copy);
452         # We need the circulation user for the information below, so we retrieve it.
453         my $circ_user = $self->retrieve_user_by_id($circ->usr());
454         my $data = {
455             ItemId => NCIP::Item::Id->new(
456                 {
457                     AgencyId => $request->{$message}->{ItemId}->{AgencyId},
458                     ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
459                     ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
460                 }
461             ),
462             UserId => NCIP::User::Id->new(
463                 {
464                     UserIdentifierType => 'Barcode Id',
465                     UserIdentifierValue => $circ_user->card->barcode()
466                 }
467             )
468         };
469
470         # Look for UserElements requested and add it to the response:
471         my $elements = $request->{$message}->{UserElementType};
472         if ($elements) {
473             $elements = [$elements] unless (ref $elements eq 'ARRAY');
474             my $optionalfields = $self->handle_user_elements($circ_user, $elements);
475             $data->{UserOptionalFields} = $optionalfields;
476         }
477         $elements = $request->{$message}->{ItemElementType};
478         if ($elements) {
479             $elements = [$elements] unless (ref $elements eq 'ARRAY');
480             my $optionalfields = $self->handle_item_elements($copy, $elements);
481             $data->{ItemOptionalFields} = $optionalfields;
482         }
483
484         $response->data($data);
485
486         # At some point in the future, we should probably check if
487         # they requested optional user or item elements and return
488         # those. For the time being, we ignore those at the risk of
489         # being considered non-compliant.
490     } else {
491         $response->problem(_problem_from_event('Checkin Failed', $result));
492     }
493
494     return $response
495 }
496
497 =head2 renewitem
498
499     $response = $ils->renewitem($request);
500
501 Handle the RenewItem message.
502
503 =cut
504
505 sub renewitem {
506     my $self = shift;
507     my $request = shift;
508
509     # Check our session and login if necessary:
510     $self->login() unless ($self->checkauth());
511
512     # Common stuff:
513     my $message = $self->parse_request_type($request);
514     my $response = NCIP::Response->new({type => $message . 'Response'});
515     $response->header($self->make_header($request));
516
517     # We need the copy barcode from the message.
518     my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
519     if (ref($item_barcode) eq 'NCIP::Problem') {
520         $response->problem($item_barcode);
521         return $response;
522     }
523
524     # Retrieve the copy details.
525     my $details = $self->retrieve_copy_details_by_barcode($item_barcode);
526     unless ($details) {
527         # Return an Unknown Item problem unless we find the copy.
528         $response->problem(
529             NCIP::Problem->new(
530                 {
531                     ProblemType => 'Unknown Item',
532                     ProblemDetail => "Item with barcode $item_barcode is not known.",
533                     ProblemElement => $item_idfield,
534                     ProblemValue => $item_barcode
535                 }
536             )
537         );
538         return $response;
539     }
540
541     # User is required for RenewItem.
542     my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
543     if (ref($user_barcode) eq 'NCIP::Problem') {
544         $response->problem($user_barcode);
545         return $response;
546     }
547     my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
548     if (ref($user) eq 'NCIP::Problem') {
549         $response->problem($user);
550         return $response;
551     }
552
553     # Isolate the copy.
554     my $copy = $details->{copy};
555
556     # Look for a circulation and examine its information:
557     my $circ = $details->{circ};
558
559     # Check the circ details to see if the copy is checked out and, if
560     # the patron was provided, that it is checked out to the patron in
561     # question. We also verify the copy ownership and circulation
562     # location.
563     my $problem = $self->check_circ_details($circ, $copy, $user);
564     if ($problem) {
565         # We need to fill in some information, however.
566         if (!$problem->ProblemValue() && !$problem->ProblemElement()) {
567             $problem->ProblemValue($user_barcode);
568             $problem->ProblemElement($user_idfield);
569         } elsif (!$problem->ProblemElement()) {
570             $problem->ProblemElement($item_idfield);
571         }
572         $response->problem($problem);
573         return $response;
574     }
575
576     # Check if user is blocked from renewals:
577     $problem = $self->check_user_for_problems($user, 'RENEW');
578     if ($problem) {
579         # Replace the ProblemElement and ProblemValue fields.
580         $problem->ProblemElement($user_idfield);
581         $problem->ProblemValue($user_barcode);
582         $response->problem($problem);
583         return $response;
584     }
585
586     # Check if the duration rule allows renewals. It should have been
587     # fleshed during the copy details retrieve.
588     my $rule = $circ->duration_rule();
589     unless (ref($rule)) {
590         $rule = $U->simplereq(
591             'open-ils.pcrud',
592             'open-ils.pcrud.retrieve.crcd',
593             $self->{session}->{authtoken},
594             $rule
595         )->gather(1);
596     }
597     if ($rule->max_renewals() < 1) {
598         $response->problem(
599             NCIP::Problem->new(
600                 {
601                     ProblemType => 'Item Not Renewable',
602                     ProblemDetail => 'Item may not be renewed.',
603                     ProblemElement => $item_idfield,
604                     ProblemValue => $item_barcode
605                 }
606             )
607         );
608         return $response;
609     }
610
611     # Check if there are renewals remaining on the latest circ:
612     if ($circ->renewal_remaining() < 1) {
613         $response->problem(
614             NCIP::Problem->new(
615                 {
616                     ProblemType => 'Maximum Renewals Exceeded',
617                     ProblemDetail => 'Renewal cannot proceed because the User has already renewed the Item the maximum number of times permitted.',
618                     ProblemElement => $item_idfield,
619                     ProblemValue => $item_barcode
620                 }
621             )
622         );
623         return $response;
624     }
625
626     # Now, we attempt the renewal. If it fails, we simply say that the
627     # user is not allowed to renew this item, without getting into
628     # details.
629     my $params = {
630         copy_id => $copy->id(),
631         patron_id => $user->id(),
632         sip_renewal => 1
633     };
634     my $r = $U->simplereq(
635         'open-ils.circ',
636         'open-ils.circ.renew.override',
637         $self->{session}->{authtoken},
638         $params
639     )->gather(1);
640
641     # We only look at the first one, since more than one usually means
642     # failure.
643     if (ref($r) eq 'ARRAY') {
644         $r = $r->[0];
645     }
646     if ($r->{textcode} ne 'SUCCESS') {
647         $problem = _problem_from_event('Renewal Failed', $r);
648         $response->problem($problem);
649     } else {
650         my $data = {
651             ItemId => NCIP::Item::Id->new(
652                 {
653                     AgencyId => $request->{$message}->{ItemId}->{AgencyId},
654                     ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
655                     ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
656                 }
657             ),
658             UserId => NCIP::User::Id->new(
659                 {
660                     UserIdentifierType => 'Barcode Id',
661                     UserIdentifierValue => $user->card->barcode()
662                 }
663             )
664         };
665         # We need to retrieve the copy details again to refresh our
666         # circ information to get the new due date.
667         $details = $self->retrieve_copy_details_by_barcode($item_barcode);
668         $circ = $details->{circ};
669         $data->{DateDue} = _fix_date($circ->due_date());
670
671         # Look for UserElements requested and add it to the response:
672         my $elements = $request->{$message}->{UserElementType};
673         if ($elements) {
674             $elements = [$elements] unless (ref $elements eq 'ARRAY');
675             my $optionalfields = $self->handle_user_elements($user, $elements);
676             $data->{UserOptionalFields} = $optionalfields;
677         }
678         $elements = $request->{$message}->{ItemElementType};
679         if ($elements) {
680             $elements = [$elements] unless (ref $elements eq 'ARRAY');
681             my $optionalfields = $self->handle_item_elements($details->{copy}, $elements);
682             $data->{ItemOptionalFields} = $optionalfields;
683         }
684
685         $response->data($data);
686     }
687
688     # At some point in the future, we should probably check if
689     # they requested optional user or item elements and return
690     # those. For the time being, we ignore those at the risk of
691     # being considered non-compliant.
692
693     return $response;
694 }
695
696 =head2 checkoutitem
697
698     $response = $ils->checkoutitem($request);
699
700 Handle the Checkoutitem message.
701
702 =cut
703
704 sub checkoutitem {
705     my $self = shift;
706     my $request = shift;
707
708     # Check our session and login if necessary:
709     $self->login() unless ($self->checkauth());
710
711     # Common stuff:
712     my $message = $self->parse_request_type($request);
713     my $response = NCIP::Response->new({type => $message . 'Response'});
714     $response->header($self->make_header($request));
715
716     # We need the copy barcode from the message.
717     my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
718     if (ref($item_barcode) eq 'NCIP::Problem') {
719         $response->problem($item_barcode);
720         return $response;
721     }
722
723     # Retrieve the copy details.
724     my $details = $self->retrieve_copy_details_by_barcode($item_barcode);
725     unless ($details) {
726         # Return an Unknown Item problem unless we find the copy.
727         $response->problem(
728             NCIP::Problem->new(
729                 {
730                     ProblemType => 'Unknown Item',
731                     ProblemDetail => "Item with barcode $item_barcode is not known.",
732                     ProblemElement => $item_idfield,
733                     ProblemValue => $item_barcode
734                 }
735             )
736         );
737         return $response;
738     }
739
740     # User is required for CheckOutItem.
741     my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
742     if (ref($user_barcode) eq 'NCIP::Problem') {
743         $response->problem($user_barcode);
744         return $response;
745     }
746     my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
747     if (ref($user) eq 'NCIP::Problem') {
748         $response->problem($user);
749         return $response;
750     }
751
752     # Isolate the copy.
753     my $copy = $details->{copy};
754
755     # Check if the copy can circulate.
756     unless ($self->copy_can_circulate($copy)) {
757         $response->problem(
758             NCIP::Problem->new(
759                 {
760                     ProblemType => 'Item Does Not Circulate',
761                     ProblemDetail => "Item with barcode $item_barcode does not circulate.",
762                     ProblemElement => $item_idfield,
763                     ProblemValue => $item_barcode
764                 }
765             )
766         );
767         return $response;
768     }
769
770     # Look for a circulation and examine its information:
771     my $circ = $details->{circ};
772
773     # Check if the item is already checked out.
774     if ($circ && !$circ->checkin_time()) {
775         $response->problem(
776             NCIP::Problem->new(
777                 {
778                     ProblemType => 'Item Already Checked Out',
779                     ProblemDetail => "Item with barcode $item_barcode is already checked out.",
780                     ProblemElement => $item_idfield,
781                     ProblemValue => $item_barcode
782                 }
783             )
784         );
785         return $response;
786     }
787
788     # Check if user is blocked from circulation:
789     my $problem = $self->check_user_for_problems($user, 'CIRC');
790     if ($problem) {
791         # Replace the ProblemElement and ProblemValue fields.
792         $problem->ProblemElement($user_idfield);
793         $problem->ProblemValue($user_barcode);
794         $response->problem($problem);
795         return $response;
796     }
797
798     # Check for the copy being in transit and receive or abort it.
799     my $transit = $U->simplereq(
800         'open-ils.circ.open_copy_transit.retrieve',
801         $self->{session}->{authtoken},
802         $copy->id()
803     );
804     if (ref($transit) eq 'Fieldmapper::action::transit_copy') {
805         if ($transit->dest() == $self->{session}->{work_ou}->id()) {
806             my $r = $U->simplereq(
807                 'open-ils.circ',
808                 'open-ils.circ.copy_transit.receive',
809                 $self->{session}->{authtoken},
810                 {copyid => $copy->id()}
811             );
812         } elsif ($transit->source() == $self->{session}->{work_ou}->id()) {
813             my $r = $U->simplereq(
814                 'open-ils.circ',
815                 'open-ils.circ.transit.abort',
816                 $self->{session}->{authtoken},
817                 {copyid => $copy->id()}
818             );
819         }
820     }
821
822     # Now, we attempt the check out. If it fails, we simply say that
823     # the user is not allowed to check out this item, without getting
824     # into details.
825     my $params = {
826         copy_id => $copy->id(),
827         patron_id => $user->id(),
828     };
829     my $r = $U->simplereq(
830         'open-ils.circ',
831         'open-ils.circ.checkout.full.override',
832         $self->{session}->{authtoken},
833         $params
834     );
835
836     # We only look at the first one, since more than one usually means
837     # failure.
838     if (ref($r) eq 'ARRAY') {
839         $r = $r->[0];
840     }
841     if ($r->{textcode} ne 'SUCCESS') {
842         $problem = _problem_from_event('Check Out Failed', $r);
843         $response->problem($problem);
844     } else {
845         my $data = {
846             ItemId => NCIP::Item::Id->new(
847                 {
848                     AgencyId => $request->{$message}->{ItemId}->{AgencyId},
849                     ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
850                     ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
851                 }
852             ),
853             UserId => NCIP::User::Id->new(
854                 {
855                     UserIdentifierType => 'Barcode Id',
856                     UserIdentifierValue => $user->card->barcode()
857                 }
858             )
859         };
860         # We need to retrieve the copy details again to refresh our
861         # circ information to get the due date.
862         $details = $self->retrieve_copy_details_by_barcode($item_barcode);
863         $circ = $details->{circ};
864         $data->{DateDue} = _fix_date($circ->due_date());
865
866         # Look for UserElements requested and add it to the response:
867         my $elements = $request->{$message}->{UserElementType};
868         if ($elements) {
869             $elements = [$elements] unless (ref $elements eq 'ARRAY');
870             my $optionalfields = $self->handle_user_elements($user, $elements);
871             $data->{UserOptionalFields} = $optionalfields;
872         }
873         $elements = $request->{$message}->{ItemElementType};
874         if ($elements) {
875             $elements = [$elements] unless (ref $elements eq 'ARRAY');
876             my $optionalfields = $self->handle_item_elements($details->{copy}, $elements);
877             $data->{ItemOptionalFields} = $optionalfields;
878         }
879
880         $response->data($data);
881     }
882
883     # At some point in the future, we should probably check if
884     # they requested optional user or item elements and return
885     # those. For the time being, we ignore those at the risk of
886     # being considered non-compliant.
887
888     return $response;
889 }
890
891 =head2 requestitem
892
893     $response = $ils->requestitem($request);
894
895 Handle the NCIP RequestItem message.
896
897 =cut
898
899 sub requestitem {
900     my $self = shift;
901     my $request = shift;
902     # Check our session and login if necessary:
903     $self->login() unless ($self->checkauth());
904
905     # Common stuff:
906     my $message = $self->parse_request_type($request);
907     my $response = NCIP::Response->new({type => $message . 'Response'});
908     $response->header($self->make_header($request));
909
910     # Because we need to have a user to place a hold, because the user
911     # is likely to have problems, and because getting the item
912     # information for the hold is trickier than getting the user
913     # information, we'll do the user first and short circuit out of
914     # the function if there is a problem with the user.
915     my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
916     if (ref($user_barcode) eq 'NCIP::Problem') {
917         $response->problem($user_barcode);
918         return $response;
919     }
920     my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
921     if (ref($user) eq 'NCIP::Problem') {
922         $response->problem($user);
923         return $response;
924     }
925     my $problem = $self->check_user_for_problems($user, 'HOLD');
926     if ($problem) {
927         $response->problem($problem);
928         return $response;
929     }
930
931     # RequestItem is a blast. We need to check if we have a copy
932     # barcode and/or if we have BibliographicIds. If we have both or
933     # either, we then need to figure out what we're placing the hold
934     # on, a copy, a volume or a bib. We don't currently do part holds,
935     # but maybe we should some day. We can also be sent more than 1
936     # BibliographicId, so we look for certain identifiers first, and
937     # then others in decreasing preference: SYSNUMBER, ISBN, and ISSN.
938
939     # Not to mention that there are two kinds of BibliographicId field
940     # with different field names, and both can be intermixed in an
941     # incoming message! (I just /love/ this nonsense.)
942
943     # This here is the thing we're going to put on hold:
944     my $item;
945
946     # We need copy details if we find in a couple of places below.
947     my $copy_details;
948
949     # We need the copy barcode from the message.
950     my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
951     if (ref($item_barcode) ne 'NCIP::Problem') {
952         # Retrieve the copy details.
953         $copy_details = $self->retrieve_copy_details_by_barcode($item_barcode);
954         unless ($copy_details) {
955             # Return an Unknown Item problem unless we find the copy.
956             $response->problem(
957                 NCIP::Problem->new(
958                     {
959                         ProblemType => 'Unknown Item',
960                         ProblemDetail => "Item with barcode $item_barcode is not known.",
961                         ProblemElement => $item_idfield,
962                         ProblemValue => $item_barcode
963                     }
964                 )
965             );
966             return $response;
967         }
968         $item = $copy_details->{volume}; # We place a volume hold.
969     }
970
971     # We weren't given copy information to target, or we can't find
972     # it, so we need to look for a target via BibliographicId.
973     unless ($item) {
974         my @biblio_ids = $self->find_bibliographic_ids($request);
975         if (@biblio_ids) {
976             $item = $self->find_target_via_bibliographic_id(@biblio_ids);
977         }
978     }
979
980     # If we don't have an item, then blow up with a problem that may
981     # have been set when we went looking for the ItemId.
982     unless ($item) {
983         if (ref($item_barcode) eq 'NCIP::Problem') {
984             $response->problem($item_barcode);
985         } else {
986             $response->problem(
987                 NCIP::Problem->new(
988                     {
989                         ProblemType => 'Request Item Not Found',
990                         ProblemDetail => 'Unable to determine the item to request from input message.',
991                         ProblemElement => 'NULL',
992                         ProblemValue => 'NULL'
993                     }
994                 )
995             );
996         }
997         return $response;
998     } elsif (ref($item) eq 'NCIP::Problem') {
999         $response->problem($item);
1000         return $response;
1001     }
1002
1003     # We have an item.  If we have a biblio.record_entry object,
1004     # Auto-Graphics expects us to place the hold such that it is
1005     # limited to the branch or system whose id they have given us.
1006     # We, therefore, look for an org_unit based on the ToAgencyId from
1007     # the InitiationHeader.
1008     my $ou;
1009     if (ref($item) eq 'Fieldmapper::biblio::record_entry') {
1010         $ou = $self->find_location_failover(undef, $request, $message);
1011     }
1012
1013     # See if we were given a PickupLocation.
1014     my $location;
1015     if ($request->{$message}->{PickupLocation}) {
1016         my $loc = $request->{$message}->{PickupLocation};
1017         $loc =~ s/^.*://; # strip everything up to the last
1018                           # semi-colon, if any.
1019         $location = $self->retrieve_org_unit_by_shortname($loc);
1020     }
1021
1022     # Look for a NeedBeforeDate to use as expiration...
1023     my $hold_expiration = $request->{$message}->{NeedBeforeDate};
1024
1025     # Place the hold.
1026     my $hold = $self->place_hold($item, $user, $location, $hold_expiration, $ou);
1027     if (ref($hold) eq 'NCIP::Problem') {
1028         $response->problem($hold);
1029     } else {
1030         my $data = {
1031             RequestId => NCIP::RequestId->new(
1032                 {
1033                     AgencyId => $request->{$message}->{RequestId}->{AgencyId},
1034                     RequestIdentifierType => $request->{$message}->{RequestId}->{RequestIdentifierType},
1035                     RequestIdentifierValue => $request->{$message}->{RequestId}->{RequestIdentifierValue}
1036                 }
1037             ),
1038             UserId => NCIP::User::Id->new(
1039                 {
1040                     UserIdentifierType => 'Barcode Id',
1041                     UserIdentifierValue => $user->card->barcode()
1042                 }
1043             ),
1044             RequestType => $request->{$message}->{RequestType},
1045             RequestScopeType => $request->{$message}->{RequestScopeType}
1046         };
1047         # Fill in the ItemId based on what we have.
1048         if ($item_barcode && ref($item_barcode) ne 'NCIP::Problem') {
1049             $data->{ItemId} = NCIP::Item::Id->new({
1050                 ItemIdentifierValue => $item_barcode,
1051                 ItemIdentifierType => 'Barcode'
1052             });
1053         } else {
1054             $data->{ItemId} = NCIP::Item::Id->new({
1055                 ItemIdentifierValue => $item->id(),
1056             });
1057             # if we have an $ou, we return the AgencyId, else set
1058             # ItemIdentifierType.
1059             if ($ou) {
1060                 $data->{ItemId}->AgencyId($ou->shortname());
1061             } else {
1062                 $data->{ItemId}->ItemIdentifierType('SYSNUMBER');
1063             }
1064         }
1065
1066         # Look for UserElements requested and add it to the response:
1067         my $elements = $request->{$message}->{UserElementType};
1068         if ($elements) {
1069             $elements = [$elements] unless (ref $elements eq 'ARRAY');
1070             my $optionalfields = $self->handle_user_elements($user, $elements);
1071             $data->{UserOptionalFields} = $optionalfields;
1072         }
1073         $elements = $request->{$message}->{ItemElementType};
1074         if ($elements) {
1075             $copy_details = $self->find_copy_details_by_item($item) unless ($copy_details);
1076             $elements = [$elements] unless (ref($elements) eq 'ARRAY');
1077             my $optionalfields = $self->handle_item_elements($copy_details->{copy}, $elements);
1078             $data->{ItemOptionalFields} = $optionalfields;
1079         }
1080
1081         $response->data($data);
1082     }
1083
1084     return $response;
1085 }
1086
1087 =head2 cancelrequestitem
1088
1089     $response = $ils->cancelrequestitem($request);
1090
1091 Handle the NCIP CancelRequestItem message.
1092
1093 =cut
1094
1095 sub cancelrequestitem {
1096     my $self = shift;
1097     my $request = shift;
1098     # Check our session and login if necessary:
1099     $self->login() unless ($self->checkauth());
1100
1101     # Common stuff:
1102     my $message = $self->parse_request_type($request);
1103     my $response = NCIP::Response->new({type => $message . 'Response'});
1104     $response->header($self->make_header($request));
1105
1106     # UserId is required by the standard, but we might not really need it.
1107     my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
1108     if (ref($user_barcode) eq 'NCIP::Problem') {
1109         $response->problem($user_barcode);
1110         return $response;
1111     }
1112     my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
1113     if (ref($user) eq 'NCIP::Problem') {
1114         $response->problem($user);
1115         return $response;
1116     }
1117
1118     # Auto-Graphics has agreed to return the ItemId that we sent them
1119     # in the RequestItemResponse when they attempt CancelRequestItem
1120     # for that same request.  For the sake of time, we're only going
1121     # to support that method of looking up the hold request in
1122     # Evergreen.  We leave it as future enhancement to make this
1123     # "portable" to other vendors.  (Frankly, that's a fool's errand.
1124     # NCIP is one of those "standards" where you neeed a separate
1125     # implementation for every vendor.)
1126     my $item_id = $request->{$message}->{ItemId};
1127     unless ($item_id) {
1128         # We'll throw a problem that we're missing needed information.
1129         my $problem = NCIP::Problem->new();
1130         $problem->ProblemType('Needed Data Missing');
1131         $problem->ProblemDetail('Cannot find ItemId in message.');
1132         $problem->ProblemElement('ItemId');
1133         $problem->ProblemValue('NULL');
1134         $response->problem($problem);
1135         return $response;
1136     }
1137     my $idvalue = $item_id->{ItemIdentifierValue};
1138     my $itemagy = $item_id->{AgencyId};
1139     my $selection_ou = $self->find_location_failover($itemagy, $request, $message);
1140     # We should support looking up holds by barcode, since we still
1141     # support placing them by barcode, but that is not how it is going
1142     # to work with Auto-Graphics, apparently.  I'll leave the
1143     # reimplementation of that for a future enhancement.
1144
1145     # See if we can find the hold:
1146     my $hold = $self->_hold_search($user, $idvalue, $selection_ou);
1147     if ($hold && $hold->transit()) {
1148         $response->problem(
1149             NCIP::Problem->new(
1150                 {
1151                     ProblemType => 'Request Already Processed',
1152                     ProblemDetail => 'Request has already been processed',
1153                     ProblemElement => 'RequestIdentifierValue',
1154                     ProblemValue => $request->{message}->{RequestId}->{RequestIdentifierValue}
1155                 }
1156             )
1157        );
1158     } elsif ($hold) {
1159         $self->cancel_hold($hold);
1160         my $data = {
1161             RequestId => NCIP::RequestId->new(
1162                 {
1163                     AgencyId => $request->{$message}->{RequestId}->{AgencyId},
1164                     RequestIdentifierType => $request->{$message}->{RequestId}->{RequestIdentifierType},
1165                     RequestIdentifierValue => $request->{$message}->{RequestId}->{RequestIdentifierValue}
1166                 }
1167             ),
1168             UserId => NCIP::User::Id->new(
1169                 {
1170                     UserIdentifierType => 'Barcode Id',
1171                     UserIdentifierValue => $user->card->barcode()
1172                 }
1173             ),
1174             ItemId => NCIP::Item::Id->new(
1175                 {
1176                     AgencyId => $request->{$message}->{ItemId}->{AgencyId},
1177                     ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
1178                     ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
1179                 }
1180             )
1181         };
1182         # Look for UserElements requested and add it to the response:
1183         my $elements = $request->{$message}->{UserElementType};
1184         if ($elements) {
1185             $elements = [$elements] unless (ref $elements eq 'ARRAY');
1186             my $optionalfields = $self->handle_user_elements($user, $elements);
1187             $data->{UserOptionalFields} = $optionalfields;
1188         }
1189         $elements = $request->{$message}->{ItemElementType};
1190         if ($elements && $hold->current_copy()) {
1191             $elements = [$elements] unless (ref $elements eq 'ARRAY');
1192             my $copy_details = $self->retrieve_copy_details_by_id($hold->current_copy());
1193             if ($copy_details) {
1194                 my $optionalfields = $self->handle_item_elements($copy_details->{copy}, $elements);
1195                 $data->{ItemOptionalFields} = $optionalfields;
1196             }
1197         }
1198         $response->data($data);
1199     } else {
1200         $response->problem(
1201             NCIP::Problem->new(
1202                 {
1203                     ProblemType => 'Unknown Request',
1204                     ProblemDetail => 'No request found for the item and user',
1205                     ProblemElement => 'NULL',
1206                     ProblemValue => 'NULL'
1207                 }
1208             )
1209         )
1210     }
1211
1212     return $response;
1213 }
1214
1215 =head1 METHODS USEFUL to SUBCLASSES
1216
1217 =head2 handle_user_elements
1218     $useroptionalfield = $ils->handle_user_elements($user, $elements);
1219
1220 Returns NCIP::User::OptionalFields for the given user and arrayref of
1221 UserElement.
1222
1223 =cut
1224
1225 sub handle_user_elements {
1226     my $self = shift;
1227     my $user = shift;
1228     my $elements = shift;
1229     my $optionalfields = NCIP::User::OptionalFields->new();
1230
1231     # First, we'll look for name information.
1232     if (grep {$_ eq 'Name Information'} @$elements) {
1233         my $name = NCIP::StructuredPersonalUserName->new();
1234         $name->Surname($user->family_name());
1235         $name->GivenName($user->first_given_name());
1236         $name->Prefix($user->prefix());
1237         $name->Suffix($user->suffix());
1238         $optionalfields->NameInformation($name);
1239     }
1240
1241     # Next, check for user address information.
1242     if (grep {$_ eq 'User Address Information'} @$elements) {
1243         my $addresses = [];
1244
1245         # See if the user has any valid, physcial addresses.
1246         foreach my $addr (@{$user->addresses()}) {
1247             next if ($U->is_true($addr->pending()));
1248             my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>$addr->address_type()});
1249             my $structured = NCIP::StructuredAddress->new();
1250             $structured->Line1($addr->street1());
1251             $structured->Line2($addr->street2());
1252             $structured->Locality($addr->city());
1253             $structured->Region($addr->state());
1254             $structured->PostalCode($addr->post_code());
1255             $structured->Country($addr->country());
1256             $address->PhysicalAddress(
1257                 NCIP::PhysicalAddress->new(
1258                     {
1259                         StructuredAddress => $structured,
1260                         Type => 'Postal Address'
1261                     }
1262                 )
1263             );
1264             push @$addresses, $address;
1265         }
1266
1267         # Right now, we're only sharing email address if the user
1268         # has it.
1269         if ($user->email()) {
1270             my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>'Email Address'});
1271             $address->ElectronicAddress(
1272                 NCIP::ElectronicAddress->new({
1273                     Type=>'mailto',
1274                     Data=>$user->email()
1275                 })
1276                 );
1277             push @$addresses, $address;
1278         }
1279         # Auto-graphics asked for the phone numbers.
1280         if ($user->day_phone()) {
1281             my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>'Day Phone'});
1282             $address->ElectronicAddress(
1283                 NCIP::ElectronicAddress->new(
1284                     {
1285                         Type=>'Day Phone',
1286                         Data=>$user->day_phone()
1287                     }
1288                 )
1289             );
1290             push @$addresses, $address;
1291         }
1292         if ($user->evening_phone()) {
1293             my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>'Evening Phone'});
1294             $address->ElectronicAddress(
1295                 NCIP::ElectronicAddress->new(
1296                     {
1297                         Type=>'Evening Phone',
1298                         Data=>$user->evening_phone()
1299                     }
1300                 )
1301             );
1302             push @$addresses, $address;
1303         }
1304         if ($user->other_phone()) {
1305             my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>'Other Phone'});
1306             $address->ElectronicAddress(
1307                 NCIP::ElectronicAddress->new(
1308                     {
1309                         Type=>'Other Phone',
1310                         Data=>$user->other_phone()
1311                     }
1312                 )
1313             );
1314             push @$addresses, $address;
1315         }
1316
1317         $optionalfields->UserAddressInformation($addresses);
1318     }
1319
1320     # Check for User Privilege.
1321     if (grep {$_ eq 'User Privilege'} @$elements) {
1322         # Get the user's group:
1323         my $pgt = $U->simplereq(
1324             'open-ils.pcrud',
1325             'open-ils.pcrud.retrieve.pgt',
1326             $self->{session}->{authtoken},
1327             $user->profile()
1328         );
1329         if ($pgt) {
1330             my $privilege = NCIP::User::Privilege->new();
1331             $privilege->AgencyId($user->home_ou->shortname());
1332             $privilege->AgencyUserPrivilegeType($pgt->name());
1333             $privilege->ValidToDate(_fix_date($user->expire_date()));
1334             $privilege->ValidFromDate(_fix_date($user->create_date()));
1335
1336             my $status = 'Active';
1337             if (_expired($user)) {
1338                 $status = 'Expired';
1339             } elsif ($U->is_true($user->barred())) {
1340                 $status = 'Barred';
1341             } elsif (!$U->is_true($user->active())) {
1342                 $status = 'Inactive';
1343             }
1344             if ($status) {
1345                 $privilege->UserPrivilegeStatus(
1346                     NCIP::User::PrivilegeStatus->new({
1347                         UserPrivilegeStatusType => $status
1348                     })
1349                 );
1350             }
1351
1352             $optionalfields->UserPrivilege([$privilege]);
1353         }
1354     }
1355
1356     # Check for Block Or Trap.
1357     if (grep {$_ eq 'Block Or Trap'} @$elements) {
1358         my $blocks = [];
1359
1360         # First, let's check if the profile is blocked from ILL.
1361         if (grep {$_->id() == $user->profile()} @{$self->{blocked_profiles}}) {
1362             my $block = NCIP::User::BlockOrTrap->new();
1363             $block->AgencyId($user->home_ou->shortname());
1364             $block->BlockOrTrapType('Block Interlibrary Loan');
1365             push @$blocks, $block;
1366         }
1367
1368         # Next, we loop through the user's standing penalties
1369         # looking for blocks on CIRC, HOLD, and RENEW.
1370         my ($have_circ, $have_renew, $have_hold) = (0,0,0);
1371         foreach my $penalty (@{$user->standing_penalties()}) {
1372             next unless($penalty->standing_penalty->block_list());
1373             my @block_list = split(/\|/, $penalty->standing_penalty->block_list());
1374             my $ou = $U->simplereq(
1375                 'open-ils.pcrud',
1376                 'open-ils.pcrud.retrieve.aou',
1377                 $self->{session}->{authtoken},
1378                 $penalty->org_unit()
1379             );
1380
1381             # Block checkout.
1382             if (!$have_circ && grep {$_ eq 'CIRC'} @block_list) {
1383                 my $bot = NCIP::User::BlockOrTrap->new();
1384                 $bot->AgencyId($ou->shortname());
1385                 $bot->BlockOrTrapType('Block Checkout');
1386                 push @$blocks, $bot;
1387                 $have_circ = 1;
1388             }
1389
1390             # Block holds.
1391             if (!$have_hold && grep {$_ eq 'HOLD' || $_ eq 'FULFILL'} @block_list) {
1392                 my $bot = NCIP::User::BlockOrTrap->new();
1393                 $bot->AgencyId($ou->shortname());
1394                 $bot->BlockOrTrapType('Block Holds');
1395                 push @$blocks, $bot;
1396                 $have_hold = 1;
1397             }
1398
1399             # Block renewals.
1400             if (!$have_renew && grep {$_ eq 'RENEW'} @block_list) {
1401                 my $bot = NCIP::User::BlockOrTrap->new();
1402                 $bot->AgencyId($ou->shortname());
1403                 $bot->BlockOrTrapType('Block Renewals');
1404                 push @$blocks, $bot;
1405                 $have_renew = 1;
1406             }
1407
1408             # Stop after we report one of each, even if more
1409             # blocks remain.
1410             last if ($have_circ && $have_renew && $have_hold);
1411         }
1412
1413         $optionalfields->BlockOrTrap($blocks);
1414     }
1415
1416     return $optionalfields;
1417 }
1418
1419 =head2 handle_item_elements
1420
1421 =cut
1422
1423 sub handle_item_elements {
1424     my $self = shift;
1425     my $copy = shift;
1426     my $elements = shift;
1427     my $optionalfields = NCIP::Item::OptionalFields->new();
1428
1429     my $details; # In case we need for more than one.
1430
1431     if (grep {$_ eq 'Bibliographic Description'} @$elements) {
1432         my $description;
1433         # Check for a precat copy, 'cause it is simple.
1434         if ($copy->dummy_title()) {
1435             $description = NCIP::Item::BibliographicDescription->new();
1436             $description->Title($copy->dummy_title());
1437             $description->Author($copy->dummy_author());
1438             if ($copy->dummy_isbn()) {
1439                 $description->BibliographicItemId(
1440                     NCIP::Item::BibliographicItemId->new(
1441                         {
1442                             BibliographicItemIdentifier => $copy->dummy_isbn(),
1443                             BibliographicItemIdentifierCode => 'ISBN'
1444                         }
1445                     )
1446                 );
1447             }
1448         } else {
1449             $details = $self->retrieve_copy_details_by_barcode($copy->barcode()) unless($details);
1450             $description = NCIP::Item::BibliographicDescription->new();
1451             $description->Title($details->{mvr}->title());
1452             $description->Author($details->{mvr}->author());
1453             $description->BibliographicRecordId(
1454                 NCIP::Item::BibliographicRecordId->new(
1455                     {
1456                         BibliographicRecordIdentifier => $details->{mvr}->doc_id(),
1457                         BibliographicRecordIdentifierCode => 'SYSNUMBER'
1458                     }
1459                 )
1460             );
1461             if ($details->{mvr}->publisher()) {
1462                 $description->Publisher($details->{mvr}->publisher());
1463             }
1464             if ($details->{mvr}->pubdate()) {
1465                 $description->PublicationDate($details->{mvr}->pubdate());
1466             }
1467             if ($details->{mvr}->edition()) {
1468                 $description->Edition($details->{mvr}->edition());
1469             }
1470         }
1471         $optionalfields->BibliographicDescription($description) if ($description);
1472     }
1473
1474     if (grep {$_ eq 'Item Description'} @$elements) {
1475         $details = $self->retrieve_copy_details_by_barcode($copy->barcode()) unless($details);
1476         # Call Number is the only field we currently return. We also
1477         # do not attempt to retun a prefix and suffix. Someone else
1478         # can deal with that if they want it.
1479         if ($details->{volume}) {
1480             $optionalfields->ItemDescription(
1481                 NCIP::Item::Description->new(
1482                     {CallNumber => $details->{volume}->label()}
1483                 )
1484             );
1485         }
1486     }
1487
1488     if (grep {$_ eq 'Circulation Status'} @$elements) {
1489         my $status = $copy->status();
1490         $status = $self->retrieve_copy_status($status) unless (ref($status));
1491         $optionalfields->CirculationStatus($status->name()) if ($status);
1492     }
1493
1494     if (grep {$_ eq 'Date Due'} @$elements) {
1495         $details = $self->retrieve_copy_details_by_barcode($copy->barcode()) unless($details);
1496         if ($details->{circ}) {
1497             if (!$details->{circ}->checkin_time()) {
1498                 $optionalfields->DateDue(_fix_date($details->{circ}->due_date()));
1499             }
1500         }
1501     }
1502
1503     if (grep {$_ eq 'Item Use Restriction Type'} @$elements) {
1504         $optionalfields->ItemUseRestrictionType('None');
1505     }
1506
1507     if (grep {$_ eq 'Physical Condition'} @$elements) {
1508         $optionalfields->PhysicalCondition(
1509             NCIP::Item::PhysicalCondition->new(
1510                 {PhysicalConditionType => 'Unknown'}
1511             )
1512         );
1513     }
1514
1515     return $optionalfields;
1516 }
1517
1518 =head2 login
1519
1520     $ils->login();
1521
1522 Login to Evergreen via OpenSRF. It uses internal state from the
1523 configuration file to login.
1524
1525 =cut
1526
1527 # Login via OpenSRF to Evergreen.
1528 sub login {
1529     my $self = shift;
1530
1531     # Get the authentication seed.
1532     my $seed = $U->simplereq(
1533         'open-ils.auth',
1534         'open-ils.auth.authenticate.init',
1535         $self->{config}->{credentials}->{username}
1536     );
1537
1538     # Actually login.
1539     if ($seed) {
1540         my $response = $U->simplereq(
1541             'open-ils.auth',
1542             'open-ils.auth.authenticate.complete',
1543             {
1544                 username => $self->{config}->{credentials}->{username},
1545                 password => md5_hex(
1546                     $seed . md5_hex($self->{config}->{credentials}->{password})
1547                 ),
1548                 type => 'staff',
1549                 workstation => $self->{config}->{credentials}->{workstation}
1550             }
1551         );
1552         if ($response) {
1553             $self->{session}->{authtoken} = $response->{payload}->{authtoken};
1554             $self->{session}->{authtime} = $response->{payload}->{authtime};
1555
1556             # Set/reset the work_ou and user data in case something changed.
1557
1558             # Retrieve the work_ou as an object.
1559             $self->{session}->{work_ou} = $U->simplereq(
1560                 'open-ils.pcrud',
1561                 'open-ils.pcrud.search.aou',
1562                 $self->{session}->{authtoken},
1563                 {shortname => $self->{config}->{credentials}->{work_ou}}
1564             );
1565
1566             # We need the user information in order to do some things.
1567             $self->{session}->{user} = $U->check_user_session($self->{session}->{authtoken});
1568
1569         }
1570     }
1571 }
1572
1573 =head2 checkauth
1574
1575     $valid = $ils->checkauth();
1576
1577 Returns 1 if the object a 'valid' authtoken, 0 if not.
1578
1579 =cut
1580
1581 sub checkauth {
1582     my $self = shift;
1583
1584     # We use AppUtils to do the heavy lifting.
1585     if (defined($self->{session})) {
1586         if ($U->check_user_session($self->{session}->{authtoken})) {
1587             return 1;
1588         } else {
1589             return 0;
1590         }
1591     }
1592
1593     # If we reach here, we don't have a session, so we are definitely
1594     # not logged in.
1595     return 0;
1596 }
1597
1598 =head2 retrieve_user_by_barcode
1599
1600     $user = $ils->retrieve_user_by_barcode($user_barcode, $user_idfield);
1601
1602 Do a fleshed retrieve of a patron by barcode. Return the patron if
1603 found and valid. Return a NCIP::Problem of 'Unknown User' otherwise.
1604
1605 The id field argument is used for the ProblemElement field in the
1606 NCIP::Problem object.
1607
1608 An invalid patron is one where the barcode is not found in the
1609 database, the patron is deleted, or the barcode used to retrieve the
1610 patron is not active. The problem element is also returned if an error
1611 occurs during the retrieval.
1612
1613 =cut
1614
1615 sub retrieve_user_by_barcode {
1616     my ($self, $barcode, $idfield) = @_;
1617     my $result = $U->simplereq(
1618         'open-ils.actor',
1619         'open-ils.actor.user.fleshed.retrieve_by_barcode',
1620         $self->{session}->{authtoken},
1621         $barcode,
1622         1
1623     );
1624
1625     # Check for a failure, or a deleted, inactive, or expired user,
1626     # and if so, return empty userdata.
1627     if (!$result || $U->event_code($result) || $U->is_true($result->deleted())
1628             || !grep {$_->barcode() eq $barcode && $U->is_true($_->active())} @{$result->cards()}) {
1629
1630         my $problem = NCIP::Problem->new();
1631         $problem->ProblemType('Unknown User');
1632         $problem->ProblemDetail("User with barcode $barcode unknown");
1633         $problem->ProblemElement($idfield);
1634         $problem->ProblemValue($barcode);
1635         $result = $problem;
1636     }
1637
1638     return $result;
1639 }
1640
1641 =head2 retrieve_user_by_id
1642
1643     $user = $ils->retrieve_user_by_id($id);
1644
1645 Similar to C<retrieve_user_by_barcode> but takes the user's database
1646 id rather than barcode. This is useful when you have a circulation or
1647 hold and need to get information about the user's involved in the hold
1648 or circulaiton.
1649
1650 It returns a fleshed user on success or undef on failure.
1651
1652 =cut
1653
1654 sub retrieve_user_by_id {
1655     my ($self, $id) = @_;
1656
1657     # Do a fleshed retrieve of the patron, and flesh the fields that
1658     # we would normally use.
1659     my $result = $U->simplereq(
1660         'open-ils.actor',
1661         'open-ils.actor.user.fleshed.retrieve',
1662         $self->{session}->{authtoken},
1663         $id,
1664         [ 'card', 'cards', 'standing_penalties', 'addresses', 'home_ou' ]
1665     );
1666     # Check for an error.
1667     undef($result) if ($result && $U->event_code($result));
1668
1669     return $result;
1670 }
1671
1672 =head2 check_user_for_problems
1673
1674     $problem = $ils>check_user_for_problems($user, 'HOLD, 'CIRC', 'RENEW');
1675
1676 This function checks if a user has a blocked profile or any from a
1677 list of provided blocks. If it does, then a NCIP::Problem object is
1678 returned, otherwise an undefined value is returned.
1679
1680 The list of blocks appears as additional arguments after the user. You
1681 can provide any value(s) that might appear in a standing penalty block
1682 lit in Evergreen. The example above checks for HOLD, CIRC, and
1683 RENEW. Any number of such values can be provided. If none are
1684 provided, the function only checks if the patron's profiles appears in
1685 the object's blocked profiles list.
1686
1687 It stops on the first matching block, if any.
1688
1689 =cut
1690
1691 sub check_user_for_problems {
1692     my $self = shift;
1693     my $user = shift;
1694     my @blocks = @_;
1695
1696     # Fill this in if we have a problem, otherwise just return it.
1697     my $problem;
1698
1699     # First, check the user's profile.
1700     if (grep {$_->id() == $user->profile()} @{$self->{blocked_profiles}}) {
1701         $problem = NCIP::Problem->new(
1702             {
1703                 ProblemType => 'User Blocked',
1704                 ProblemDetail => 'User blocked from inter-library loan',
1705                 ProblemElement => 'NULL',
1706                 ProblemValue => 'NULL'
1707             }
1708         );
1709     }
1710
1711     # Next, check if the patron has one of the indicated blocks.
1712     unless ($problem) {
1713         foreach my $penalty (@{$user->standing_penalties()}) {
1714             if ($penalty->standing_penalty->block_list()) {
1715                 my @pblocks = split(/\|/, $penalty->standing_penalty->block_list());
1716                 foreach my $block (@blocks) {
1717                     if (grep {$_ =~ /$block/} @pblocks) {
1718                         $problem = NCIP::Problem->new(
1719                             {
1720                                 ProblemType => 'User Blocked',
1721                                 ProblemDetail => 'User blocked from ' .
1722                                     ($block eq 'HOLD') ? 'holds' : (($block eq 'RENEW') ? 'renewals' :
1723                                                                         (($block eq 'CIRC') ? 'checkout' : lc($block))),
1724                                 ProblemElement => 'NULL',
1725                                 ProblemValue => 'NULL'
1726                             }
1727                         );
1728                         last;
1729                     }
1730                 }
1731                 last if ($problem);
1732             }
1733         }
1734     }
1735
1736     return $problem;
1737 }
1738
1739 =head2 check_circ_details
1740
1741     $problem = $ils->check_circ_details($circ, $copy, $user);
1742
1743 Checks if we can checkin or renew a circulation. That is, the
1744 circulation is still open (i.e. the copy is still checked out), if we
1745 either own the copy or are the circulation location, and if the
1746 circulation is for the optional $user argument. $circ and $copy are
1747 required. $user is optional.
1748
1749 Returns a problem if any of the above conditions fail. Returns undef
1750 if they pass and we can proceed with the checkin or renewal.
1751
1752 If the failure occurred on the copy-related checks, then the
1753 ProblemElement field will be undefined and needs to be filled in with
1754 the item id field name. If the check for the copy being checked out to
1755 the provided user fails, then both ProblemElement and ProblemValue
1756 fields will be empty and need to be filled in by the caller.
1757
1758 =cut
1759
1760 sub check_circ_details {
1761     my ($self, $circ, $copy, $user) = @_;
1762
1763     # Shortcut for the next check.
1764     my $ou_id = $self->{session}->{work_ou}->id();
1765
1766     if (!$circ || $circ->checkin_time() || ($circ->circ_lib() != $ou_id && $copy->circ_lib() != $ou_id)) {
1767         # Item isn't checked out.
1768         return NCIP::Problem->new(
1769             {
1770                 ProblemType => 'Item Not Checked Out',
1771                 ProblemDetail => 'Item with barcode ' . $copy->barcode() . ' is not checked out.',
1772                 ProblemValue => $copy->barcode()
1773             }
1774         );
1775     } else {
1776         # Get data on the patron who has it checked out.
1777         my $circ_user = $self->retrieve_user_by_id($circ->usr());
1778         if ($user && $circ_user && $user->id() != $circ_user->id()) {
1779             # The ProblemElement and ProblemValue field need to be
1780             # filled in by the caller.
1781             return NCIP::Problem->new(
1782                 {
1783                     ProblemType => 'Item Not Checked Out To This User',
1784                     ProblemDetail => 'Item with barcode ' . $copy->barcode() . ' is not checked out to this user.',
1785                 }
1786             );
1787         }
1788     }
1789     # If we get here, we're good to go.
1790     return undef;
1791 }
1792
1793 =head2 retrieve_copy_details_by_barcode
1794
1795     $copy = $ils->retrieve_copy_details_by_barcode($copy_barcode);
1796
1797 Look up and retrieve some copy details by the copy barcode. This
1798 method returns either a hashref with the copy details or undefined if
1799 no copy exists with that barcode or if some error occurs.
1800
1801 The hashref has the fields copy, hold, transit, circ, volume, and mvr.
1802
1803 This method differs from C<retrieve_user_by_barcode> in that a copy
1804 cannot be invalid if it exists and it is not always an error if no
1805 copy exists. In some cases, when handling AcceptItem, we might prefer
1806 there to be no copy.
1807
1808 =cut
1809
1810 sub retrieve_copy_details_by_barcode {
1811     my $self = shift;
1812     my $barcode = shift;
1813
1814     my $copy = $U->simplereq(
1815         'open-ils.circ',
1816         'open-ils.circ.copy_details.retrieve.barcode',
1817         $self->{session}->{authtoken},
1818         $barcode
1819     );
1820
1821     # If $copy is an event, return undefined.
1822     if ($copy && $U->event_code($copy)) {
1823         undef($copy);
1824     }
1825
1826     return $copy;
1827 }
1828
1829 =head2 retrieve_copy_details_by_id
1830
1831     $copy = $ils->retrieve_copy_details_by_id($copy_id);
1832
1833 Retrieve copy_details by copy id. Same as the above, but with a copy
1834 id instead of barcode.
1835
1836 =cut
1837
1838 sub retrieve_copy_details_by_id {
1839     my $self = shift;
1840     my $copy_id = shift;
1841
1842     my $copy = $U->simplereq(
1843         'open-ils.circ',
1844         'open-ils.circ.copy_details.retrieve',
1845         $self->{session}->{authtoken},
1846         $copy_id
1847     );
1848
1849     # If $copy is an event, return undefined.
1850     if ($copy && $U->event_code($copy)) {
1851         undef($copy);
1852     }
1853
1854     return $copy;
1855 }
1856
1857 =head2 find_copy_details_by_item
1858
1859     $copy_details = $ils->find_copy_details_by_item($item);
1860
1861 This routine returns a copy_details hashref (See:
1862 retrieve_copy_details_by_barcode) for a given item. It attempts to
1863 find the "first" copy for the given item. If item is a call number it
1864 looks for the first, not deleted copy. If item is a bib, it looks for
1865 the first not deleted copy on the first not deleted call number. If
1866 item is a copy, it simply returns the details for the copy.
1867
1868 =cut
1869
1870 sub find_copy_details_by_item {
1871     my $self = shift;
1872     my $item = shift;
1873
1874     my ($details);
1875
1876     if (ref($item) eq 'Fieldmapper::biblio::record_entry') {
1877         my $acns = $U->simplereq(
1878             'open-ils.pcrud',
1879             'open-ils.pcrud.search.acn.atomic',
1880             $self->{session}->{authtoken},
1881             {
1882                 record => $item->id(),
1883                 deleted => 'f'
1884             }
1885         );
1886         ($item) = sort {$a->id() <=> $b->id()} @{$acns};
1887     }
1888
1889     if (ref($item) eq 'Fieldmapper::asset::call_number') {
1890         my $copies = $U->simplereq(
1891             'open-ils.pcrud',
1892             'open-ils.pcrud.search.acp.atomic',
1893             $self->{session}->{authtoken},
1894             {
1895                 call_number => $item->id(),
1896                 deleted => 'f'
1897             }
1898         );
1899         ($item) = sort {$a->id() <=> $b->id()} @{$copies};
1900     }
1901
1902     if (ref($item) eq 'Fieldmapper::asset::copy') {
1903         $details = $self->retrieve_copy_details_by_barcode($item->barcode());
1904     }
1905
1906     return $details;
1907 }
1908
1909 =head2 retrieve_copy_status
1910
1911     $status = $ils->retrieve_copy_status($id);
1912
1913 Retrive a copy status object by database ID.
1914
1915 =cut
1916
1917 sub retrieve_copy_status {
1918     my $self = shift;
1919     my $id = shift;
1920
1921     my $status = $U->simplereq(
1922         'open-ils.pcrud',
1923         'open-ils.pcrud.retrieve.ccs',
1924         $self->{session}->{authtoken},
1925         $id
1926     );
1927
1928     return $status;
1929 }
1930
1931 =head2 retrieve_org_unit_by_shortname
1932
1933     $org_unit = $ils->retrieve_org_unit_by_shortname($shortname);
1934
1935 Retrieves an org. unit from the database by shortname, and fleshes the
1936 ou_type field. Returns the org. unit as a Fieldmapper object or
1937 undefined.
1938
1939 =cut
1940
1941 sub retrieve_org_unit_by_shortname {
1942     my $self = shift;
1943     my $shortname = shift;
1944
1945     my $aou = $U->simplereq(
1946         'open-ils.actor',
1947         'open-ils.actor.org_unit.retrieve_by_shortname',
1948         $shortname
1949     );
1950
1951     # We want to retrieve the type and manually "flesh" the object.
1952     if ($aou) {
1953         my $type = $U->simplereq(
1954             'open-ils.pcrud',
1955             'open-ils.pcrud.retrieve.aout',
1956             $self->{session}->{authtoken},
1957             $aou->ou_type()
1958         );
1959         $aou->ou_type($type) if ($type);
1960     }
1961
1962     return $aou;
1963 }
1964
1965 =head2 retrieve_copy_location
1966
1967     $location = $ils->retrieve_copy_location($location_id);
1968
1969 Retrieve a copy location based on id.
1970
1971 =cut
1972
1973 sub retrieve_copy_location {
1974     my $self = shift;
1975     my $id = shift;
1976
1977     my $location = $U->simplereq(
1978         'open-ils.pcrud',
1979         'open-ils.pcrud.retrieve.acpl',
1980         $self->{session}->{authtoken},
1981         $id
1982     );
1983
1984     return $location;
1985 }
1986
1987 =head2 retrieve_biblio_record_entry
1988
1989     $bre = $ils->retrieve_biblio_record_entry($bre_id);
1990
1991 Given a biblio.record_entry.id, this method retrieves a bre object.
1992
1993 =cut
1994
1995 sub retrieve_biblio_record_entry {
1996     my $self = shift;
1997     my $id = shift;
1998
1999     my $bre = $U->simplereq(
2000         'open-ils.pcrud',
2001         'open-ils.pcrud.retrieve.bre',
2002         $self->{session}->{authtoken},
2003         $id
2004     );
2005
2006     return $bre;
2007 }
2008
2009 =head2 create_precat_copy
2010
2011     $item_info->{
2012         barcode => '312340123456789',
2013         author => 'Public, John Q.',
2014         title => 'Magnum Opus',
2015         call_number => '005.82',
2016         publisher => 'Brick House',
2017         publication_date => '2014'
2018     };
2019
2020     $item = $ils->create_precat_copy($item_info);
2021
2022
2023 Create a "precat" copy to use for the incoming item using a hashref of
2024 item information. At a minimum, the barcode, author and title fields
2025 need to be filled in. The other fields are ignored if provided.
2026
2027 This method is called by the AcceptItem handler if the C<use_precats>
2028 configuration option is turned on.
2029
2030 =cut
2031
2032 sub create_precat_copy {
2033     my $self = shift;
2034     my $item_info = shift;
2035
2036     my $item = Fieldmapper::asset::copy->new();
2037     $item->barcode($item_info->{barcode});
2038     $item->call_number(OILS_PRECAT_CALL_NUMBER);
2039     $item->dummy_title($item_info->{title});
2040     $item->dummy_author($item_info->{author});
2041     $item->circ_lib($self->{session}->{work_ou}->id());
2042     $item->circulate('t');
2043     $item->holdable('t');
2044     $item->opac_visible('f');
2045     $item->deleted('f');
2046     $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2047     $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2048     $item->location(1);
2049     $item->status(0);
2050     $item->editor($self->{session}->{user}->id());
2051     $item->creator($self->{session}->{user}->id());
2052     $item->isnew(1);
2053
2054     # Actually create it:
2055     my $xact;
2056     my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
2057     $ses->connect();
2058     eval {
2059         $xact = $ses->request(
2060             'open-ils.pcrud.transaction.begin',
2061             $self->{session}->{authtoken}
2062         )->gather(1);
2063         $item = $ses->request(
2064             'open-ils.pcrud.create.acp',
2065             $self->{session}->{authtoken},
2066             $item
2067         )->gather(1);
2068         $xact = $ses->request(
2069             'open-ils.pcrud.transaction.commit',
2070             $self->{session}->{authtoken}
2071         )->gather(1);
2072     };
2073     if ($@) {
2074         undef($item);
2075         if ($xact) {
2076             eval {
2077                 $ses->request(
2078                     'open-ils.pcrud.transaction.rollback',
2079                     $self->{session}->{authtoken}
2080                 )->gather(1);
2081             };
2082         }
2083     }
2084     $ses->disconnect();
2085
2086     return $item;
2087 }
2088
2089 =head2 create_fuller_copy
2090
2091     $item_info->{
2092         barcode => '31234003456789',
2093         author => 'Public, John Q.',
2094         title => 'Magnum Opus',
2095         call_number => '005.82',
2096         publisher => 'Brick House',
2097         publication_date => '2014'
2098     };
2099
2100     $item = $ils->create_fuller_copy($item_info);
2101
2102 Creates a skeletal bibliographic record, call number, and copy for the
2103 incoming item using a hashref with item information in it. At a
2104 minimum, the barcode, author, title, and call_number fields must be
2105 filled in.
2106
2107 This method is used by the AcceptItem handler if the C<use_precats>
2108 configuration option is NOT set.
2109
2110 =cut
2111
2112 sub create_fuller_copy {
2113     my $self = shift;
2114     my $item_info = shift;
2115
2116     my $item;
2117
2118     # We do everything in one transaction, because it should be atomic.
2119     my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
2120     $ses->connect();
2121     my $xact;
2122     eval {
2123         $xact = $ses->request(
2124             'open-ils.pcrud.transaction.begin',
2125             $self->{session}->{authtoken}
2126         )->gather(1);
2127     };
2128     if ($@) {
2129         undef($xact);
2130     }
2131
2132     # The rest depends on there being a transaction.
2133     if ($xact) {
2134
2135         # Create the MARC record.
2136         my $record = MARC::Record->new();
2137         $record->encoding('UTF-8');
2138         $record->leader('00881nam a2200193   4500');
2139         my $datespec = strftime("%Y%m%d%H%M%S.0", localtime);
2140         my @fields = ();
2141         push(@fields, MARC::Field->new('005', $datespec));
2142         push(@fields, MARC::Field->new('082', '0', '4', 'a' => $item_info->{call_number}));
2143         push(@fields, MARC::Field->new('245', '0', '0', 'a' => $item_info->{title}));
2144         # Publisher is a little trickier:
2145         if ($item_info->{publisher}) {
2146             my $pub = MARC::Field->new('260', ' ', ' ', 'a' => '[S.l.]', 'b' => $item_info->{publisher});
2147             $pub->add_subfields('c' => $item_info->{publication_date}) if ($item_info->{publication_date});
2148             push(@fields, $pub);
2149         }
2150         # We have no idea if the author is personal corporate or something else, so we use a 720.
2151         push(@fields, MARC::Field->new('720', ' ', ' ', 'a' => $item_info->{author}, '4' => 'aut'));
2152         $record->append_fields(@fields);
2153         my $marc = clean_marc($record);
2154
2155         # Create the bib object.
2156         my $bib = Fieldmapper::biblio::record_entry->new();
2157         $bib->creator($self->{session}->{user}->id());
2158         $bib->editor($self->{session}->{user}->id());
2159         $bib->source($self->{bib_source}->id());
2160         $bib->active('t');
2161         $bib->deleted('f');
2162         $bib->marc($marc);
2163         $bib->isnew(1);
2164
2165         eval {
2166             $bib = $ses->request(
2167                 'open-ils.pcrud.create.bre',
2168                 $self->{session}->{authtoken},
2169                 $bib
2170             )->gather(1);
2171         };
2172         if ($@) {
2173             undef($bib);
2174             eval {
2175                 $ses->request(
2176                     'open-ils.pcrud.transaction.rollback',
2177                     $self->{session}->{authtoken}
2178                 )->gather(1);
2179             };
2180         }
2181
2182         # Create the call number
2183         my $acn;
2184         if ($bib) {
2185             $acn = Fieldmapper::asset::call_number->new();
2186             $acn->creator($self->{session}->{user}->id());
2187             $acn->editor($self->{session}->{user}->id());
2188             $acn->label($item_info->{call_number});
2189             $acn->record($bib->id());
2190             $acn->owning_lib($self->{session}->{work_ou}->id());
2191             $acn->deleted('f');
2192             $acn->isnew(1);
2193
2194             eval {
2195                 $acn = $ses->request(
2196                     'open-ils.pcrud.create.acn',
2197                     $self->{session}->{authtoken},
2198                     $acn
2199                 )->gather(1);
2200             };
2201             if ($@) {
2202                 undef($acn);
2203                 eval {
2204                     $ses->request(
2205                         'open-ils.pcrud.transaction.rollback',
2206                         $self->{session}->{authtoken}
2207                     )->gather(1);
2208                 };
2209             }
2210         }
2211
2212         # create the copy
2213         if ($acn) {
2214             $item = Fieldmapper::asset::copy->new();
2215             $item->barcode($item_info->{barcode});
2216             $item->call_number($acn->id());
2217             $item->circ_lib($self->{session}->{work_ou}->id);
2218             $item->circulate('t');
2219             if ($self->{config}->{items}->{use_force_holds}) {
2220                 $item->holdable('f');
2221             } else {
2222                 $item->holdable('t');
2223             }
2224             $item->opac_visible('f');
2225             $item->deleted('f');
2226             $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2227             $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2228             $item->location(1);
2229             $item->status(0);
2230             $item->editor($self->{session}->{user}->id);
2231             $item->creator($self->{session}->{user}->id);
2232             $item->isnew(1);
2233
2234             eval {
2235                 $item = $ses->request(
2236                     'open-ils.pcrud.create.acp',
2237                     $self->{session}->{authtoken},
2238                     $item
2239                 )->gather(1);
2240
2241                 # Cross our fingers and commit the work.
2242                 $xact = $ses->request(
2243                     'open-ils.pcrud.transaction.commit',
2244                     $self->{session}->{authtoken}
2245                 )->gather(1);
2246             };
2247             if ($@) {
2248                 undef($item);
2249                 eval {
2250                     $ses->request(
2251                         'open-ils.pcrud.transaction.rollback',
2252                         $self->{session}->{authtoken}
2253                     )->gather(1) if ($xact);
2254                 };
2255             }
2256         }
2257     }
2258
2259     # We need to disconnect our session.
2260     $ses->disconnect();
2261
2262     # Now, we handle our asset stat_cat entries.
2263     if ($item) {
2264         # It would be nice to do these in the above transaction, but
2265         # pcrud does not support the ascecm object, yet.
2266         foreach my $entry (@{$self->{stat_cat_entries}}) {
2267             my $map = Fieldmapper::asset::stat_cat_entry_copy_map->new();
2268             $map->isnew(1);
2269             $map->stat_cat($entry->stat_cat());
2270             $map->stat_cat_entry($entry->id());
2271             $map->owning_copy($item->id());
2272             # We don't really worry if it succeeds or not.
2273             $U->simplereq(
2274                 'open-ils.circ',
2275                 'open-ils.circ.stat_cat.asset.copy_map.create',
2276                 $self->{session}->{authtoken},
2277                 $map
2278             );
2279         }
2280     }
2281
2282     return $item;
2283 }
2284
2285 =head2 place_hold
2286
2287     $hold = $ils->place_hold($item, $user, $location, $expiration, $org_unit);
2288
2289 This function places a hold on $item for $user for pickup at
2290 $location. If location is not provided or undefined, the user's home
2291 library is used as a fallback.
2292
2293 The $expiration argument is optional and must be a properly formatted
2294 ISO date time. It will be used as the hold expire time, if
2295 provided. Otherwise the system default time will be used.
2296
2297 The $org_unit parameter is only consulted in the event of $item being
2298 a biblio::record_entry object.  In which case, it is expected to be
2299 undefined or an actor::org_unit object.  If it is present, then its id
2300 and ou_type depth (if the ou_type field is fleshed) will be used to
2301 control the selection ou and selection depth for the hold.  This
2302 essentially limits the hold to being filled by copies belonging to the
2303 specified org_unit or its children.
2304
2305 $item can be a copy (asset::copy), volume (asset::call_number), or bib
2306 (biblio::record_entry). The appropriate hold type will be placed
2307 depending on the object.
2308
2309 On success, the method returns the object representing the hold. On
2310 failure, a NCIP::Problem object, describing the failure, is returned.
2311
2312 =cut
2313
2314 sub place_hold {
2315     my $self = shift;
2316     my $item = shift;
2317     my $user = shift;
2318     my $location = shift;
2319     my $expiration = shift;
2320     my $org_unit = shift;
2321
2322     # If $location is undefined, use the user's home_ou, which should
2323     # have been fleshed when the user was retrieved.
2324     $location = $user->home_ou() unless ($location);
2325
2326     # $hold is the hold. $params is for the is_possible check.
2327     my ($hold, $params);
2328
2329     # Prep the hold with fields common to all hold types:
2330     $hold = Fieldmapper::action::hold_request->new();
2331     $hold->isnew(1); # Just to make sure.
2332     $hold->target($item->id());
2333     $hold->usr($user->id());
2334     $hold->pickup_lib($location->id());
2335     $hold->expire_time(cleanse_ISO8601($expiration)) if ($expiration);
2336     if (!$user->email()) {
2337         $hold->email_notify('f');
2338         $hold->phone_notify($user->day_phone()) if ($user->day_phone());
2339     } else {
2340         $hold->email_notify('t');
2341     }
2342
2343     # Ditto the params:
2344     $params = { pickup_lib => $location->id(), patronid => $user->id() };
2345
2346     if (ref($item) eq 'Fieldmapper::asset::copy') {
2347         my $type = ($self->{config}->{items}->{use_force_holds}) ? 'F' : 'C';
2348         $hold->hold_type($type);
2349         $hold->current_copy($item->id());
2350         $params->{hold_type} = $type;
2351         $params->{copy_id} = $item->id();
2352     } elsif (ref($item) eq 'Fieldmapper::asset::call_number') {
2353         $hold->hold_type('V');
2354         $params->{hold_type} = 'V';
2355         $params->{volume_id} = $item->id();
2356     } elsif (ref($item) eq 'Fieldmapper::biblio::record_entry') {
2357         $hold->hold_type('T');
2358         $params->{hold_type} = 'T';
2359         $params->{titleid} = $item->id();
2360         if ($org_unit && ref($org_unit) eq 'Fieldmapper::actor::org_unit') {
2361             $hold->selection_ou($org_unit->id());
2362             $hold->selection_depth($org_unit->ou_type->depth()) if (ref($org_unit->ou_type()));
2363         }
2364     }
2365
2366     # Check for a duplicate hold:
2367     my $duplicate = $U->simplereq(
2368         'open-ils.pcrud',
2369         'open-ils.pcrud.search.ahr',
2370         $self->{session}->{authtoken},
2371         {
2372             hold_type => $hold->hold_type(),
2373             target => $hold->target(),
2374             usr => $hold->usr(),
2375             expire_time => {'>' => 'now'},
2376             cancel_time => undef,
2377             fulfillment_time => undef
2378         }
2379     );
2380     if ($duplicate) {
2381         return NCIP::Problem->new(
2382             {
2383                 ProblemType => 'Duplicate Request',
2384                 ProblemDetail => 'A request for this item already exists for this patron.',
2385                 ProblemElement => 'NULL',
2386                 ProblemValue => 'NULL'
2387             }
2388         );
2389     }
2390
2391     # Check if the hold is possible:
2392     my $r = $U->simplereq(
2393         'open-ils.circ',
2394         'open-ils.circ.title_hold.is_possible',
2395         $self->{session}->{authtoken},
2396         $params
2397     );
2398
2399     if ($r->{success}) {
2400         $hold = $U->simplereq(
2401             'open-ils.circ',
2402             'open-ils.circ.holds.create.override',
2403             $self->{session}->{authtoken},
2404             $hold
2405         );
2406         if (ref($hold)) {
2407             $hold = $hold->[0] if (ref($hold) eq 'ARRAY');
2408             $hold = _problem_from_event('Request Not Possible', $hold);
2409         } else {
2410             # open-ils.circ.holds.create.override returns the id on
2411             # success, so we retrieve the full hold object from the
2412             # database to return it.
2413             $hold = $U->simplereq(
2414                 'open-ils.pcrud',
2415                 'open-ils.pcrud.retrieve.ahr',
2416                 $self->{session}->{authtoken},
2417                 $hold
2418             );
2419         }
2420     } elsif ($r->{last_event}) {
2421         $hold = _problem_from_event('Request Not Possible', $r->{last_event});
2422     } elsif ($r->{textcode}) {
2423         $hold = _problem_from_event('Request Not Possible', $r);
2424     } else {
2425         $hold = _problem_from_event('Request Not Possible');
2426     }
2427
2428     return $hold;
2429 }
2430
2431 =head2 cancel_hold
2432
2433     $ils->cancel_hold($hold);
2434
2435 This method cancels the hold argument. It makes no checks on the hold,
2436 so if there are certain conditions that need to be fulfilled before
2437 the hold is canceled, then you must check them before calling this
2438 method.
2439
2440 It returns undef on success or failure. If it fails, you've usually
2441 got bigger problems.
2442
2443 =cut
2444
2445 sub cancel_hold {
2446     my $self = shift;
2447     my $hold = shift;
2448
2449     my $r = $U->simplereq(
2450         'open-ils.circ',
2451         'open-ils.circ.hold.cancel',
2452         $self->{session}->{authtoken},
2453         $hold->id(),
2454         '5',
2455         'Canceled via NCIPServer'
2456     );
2457
2458     return undef;
2459 }
2460
2461 =head2 delete_copy
2462
2463     $ils->delete_copy($copy);
2464
2465 Deletes the copy, and if it is owned by our work_ou and not a precat,
2466 we also delete the volume and bib on which the copy depends.
2467
2468 =cut
2469
2470 sub delete_copy {
2471     my $self = shift;
2472     my $copy = shift;
2473
2474     # Shortcut for ownership checks below.
2475     my $ou_id = $self->{session}->{work_ou}->id();
2476
2477     # First, make sure the copy is not already deleted and we own it.
2478     return undef if ($U->is_true($copy->deleted()) || $copy->circ_lib() != $ou_id);
2479
2480     # Indicate we want to delete the copy.
2481     $copy->isdeleted(1);
2482
2483     # Delete the copy using a backend call that will delete the copy,
2484     # the call number, and bib when appropriate.
2485     my $result = $U->simplereq(
2486         'open-ils.cat',
2487         'open-ils.cat.asset.copy.fleshed.batch.update.override',
2488         $self->{session}->{authtoken},
2489         [$copy]
2490     );
2491
2492     # We are currently not checking for succes or failure of the
2493     # above. At some point, someone may want to.
2494
2495     return undef;
2496 }
2497
2498 =head2 copy_can_circulate
2499
2500     $can_circulate = $ils->copy_can_circulate($copy);
2501
2502 Check if the copy's location and the copy itself allow
2503 circulation. Return true if they do, and false if they do not.
2504
2505 =cut
2506
2507 sub copy_can_circulate {
2508     my $self = shift;
2509     my $copy = shift;
2510
2511     my $location = $copy->location();
2512     unless (ref($location)) {
2513         $location = $self->retrieve_copy_location($location);
2514     }
2515
2516     return ($U->is_true($copy->circulate()) && $U->is_true($location->circulate()));
2517 }
2518
2519 =head2 copy_can_fulfill
2520
2521     $can_fulfill = $ils->copy_can_fulfill($copy);
2522
2523 Check if the copy's location and the copy itself allow
2524 holds. Return true if they do, and false if they do not.
2525
2526 =cut
2527
2528 sub copy_can_fulfill {
2529     my $self = shift;
2530     my $copy = shift;
2531
2532     my $location = $copy->location();
2533     unless (ref($location)) {
2534         $location = $self->retrieve_copy_location($location);
2535     }
2536
2537     return ($U->is_true($copy->holdable()) && $U->is_true($location->holdable()));
2538 }
2539
2540 =head1 OVERRIDDEN PARENT METHODS
2541
2542 =head2 find_user_barcode
2543
2544 We dangerously override our parent's C<find_user_barcode> to return
2545 either the $barcode or a Problem object. In list context the barcode
2546 or problem will be the first argument and the id field, if any, will
2547 be the second. We also add a second, optional, argument to indicate a
2548 default value for the id field in the event of a failure to find
2549 anything at all. (Perl lets us get away with this.)
2550
2551 =cut
2552
2553 sub find_user_barcode {
2554     my $self = shift;
2555     my $request = shift;
2556     my $default = shift;
2557
2558     unless ($default) {
2559         my $message = $self->parse_request_type($request);
2560         if ($message eq 'LookupUser') {
2561             $default = 'AuthenticationInputData';
2562         } else {
2563             $default = 'UserIdentifierValue';
2564         }
2565     }
2566
2567     my ($value, $idfield) = $self->SUPER::find_user_barcode($request);
2568
2569     unless ($value) {
2570         $idfield = $default unless ($idfield);
2571         $value = NCIP::Problem->new();
2572         $value->ProblemType('Needed Data Missing');
2573         $value->ProblemDetail('Cannot find user barcode in message.');
2574         $value->ProblemElement($idfield);
2575         $value->ProblemValue('NULL');
2576     }
2577
2578     return (wantarray) ? ($value, $idfield) : $value;
2579 }
2580
2581 =head2 find_item_barcode
2582
2583 We do pretty much the same thing as with C<find_user_barcode> for
2584 C<find_item_barcode>.
2585
2586 =cut
2587
2588 sub find_item_barcode {
2589     my $self = shift;
2590     my $request = shift;
2591     my $default = shift || 'ItemIdentifierValue';
2592
2593     my ($value, $idfield) = $self->SUPER::find_item_barcode($request);
2594
2595     unless ($value) {
2596         $idfield = $default unless ($idfield);
2597         $value = NCIP::Problem->new();
2598         $value->ProblemType('Needed Data Missing');
2599         $value->ProblemDetail('Cannot find item barcode in message.');
2600         $value->ProblemElement($idfield);
2601         $value->ProblemValue('NULL');
2602     }
2603
2604     return (wantarray) ? ($value, $idfield) : $value;
2605 }
2606
2607 =head2 find_target_via_bibliographic_id
2608
2609     $item = $ils->find_target_via_bibliographic_id(@biblio_ids);
2610
2611 Searches for a bibliographic record to put on hold and returns an
2612 appropriate hold target item depending upon what it finds. If an
2613 appropriate, single target cannot be found, it returns an
2614 NCIP::Problem with the problem message.
2615
2616 Currently, we only look for SYSNUMBER, ISBN, and ISSN record
2617 identifiers. If nothing is found, this method can return undef. (Gotta
2618 love Perl and untyped/weakly typed languages in general!)
2619
2620 TODO: Figure out how to search OCLC numbers. We probably need to use
2621 "MARC Expert Search" if we don't want to do a JSON query on
2622 metabib.full_rec.
2623
2624 =cut
2625
2626 sub find_target_via_bibliographic_id {
2627     my $self = shift;
2628     my @biblio_ids = @_;
2629
2630     # The item that we find:
2631     my $item;
2632
2633     # Id for our bib in Evergreen:
2634     my $bibid;
2635
2636     # First, let's look for a SYSNUMBER:
2637     my ($idobj) = grep
2638         { ($_->{BibliographicRecordIdentifierCode} && $_->{BibliographicRecordIdentifierCode} eq 'SYSNUMBER')
2639               || ($_->{BibliographicItemIdentifierCode} && $_->{BibliographicItemIdentifierCode} eq 'SYSNUMBER')
2640               || $_->{AgencyId} }
2641             @biblio_ids;
2642     if ($idobj) {
2643         my $loc;
2644         # BibliographicRecordId can have an AgencyId field if the
2645         # BibliographicRecordIdentifierCode is absent.
2646         if ($idobj->{AgencyId}) {
2647             $bibid = $idobj->{BibliographicRecordIdentifier};
2648             my $locname = $idobj->{AgencyId};
2649             if ($locname) {
2650                 $locname =~ s/^.*://;
2651                 $loc = $self->retrieve_org_unit_by_shortname($locname);
2652             }
2653         } elsif ($idobj->{BibliographicRecordIdentifierCode}) {
2654             $bibid = $idobj->{BibliographicRecordIdentifier};
2655         } else {
2656             $bibid = $idobj->{BibliographicItemIdentifier};
2657         }
2658         if ($bibid && $loc) {
2659             $item = $self->_call_number_search($bibid, $loc);
2660         } else {
2661             $item = $U->simplereq(
2662                 'open-ils.pcrud',
2663                 'open-ils.pcrud.retrieve.bre',
2664                 $self->{session}->{authtoken},
2665                 $bibid
2666             );
2667         }
2668         # Check if item is deleted so we'll look for more
2669         # possibilties.
2670         undef($item) if ($item && $U->is_true($item->deleted()));
2671     }
2672
2673     # Build an array of id objects based on the other identifier fields.
2674     my @idobjs = grep
2675         {
2676             ($_->{BibliographicRecordIdentifierCode} && $_->{BibliographicRecordIdentifierCode} eq 'ISBN')
2677                 || ($_->{BibliographicItemIdentifierCode} && $_->{BibliographicItemIdentifierCode} eq 'ISBN')
2678                 || ($_->{BibliographicRecordIdentifierCode} && $_->{BibliographicRecordIdentifierCode} eq 'ISSN')
2679                 || ($_->{BibliographicItemIdentifierCode} && $_->{BibliographicItemIdentifierCode} eq 'ISSN')
2680         } @biblio_ids;
2681
2682     if (@idobjs) {
2683         my $stashed_problem;
2684         # Reuse $idobj from above.
2685         foreach $idobj (@idobjs) {
2686             my ($idvalue, $idtype, $idfield);
2687             if ($_->{BibliographicItemIdentifier}) {
2688                 $idvalue = $_->{BibliographicItemIdentifier};
2689                 $idtype = $_->{BibliographicItemIdentifierCode};
2690                 $idfield = 'BibliographicItemIdentifier';
2691             } else {
2692                 $idvalue = $_->{BibliographicRecordIdentifier};
2693                 $idtype = $_->{BibliographicRecordIdentifierCode};
2694                 $idfield = 'BibliographicRecordIdentifier';
2695             }
2696             $item = $self->_bib_search($idvalue, $idtype);
2697             if (ref($item) eq 'NCIP::Problem') {
2698                 $stashed_problem = $item unless($stashed_problem);
2699                 $stashed_problem->ProblemElement($idfield);
2700                 undef($item);
2701             }
2702             last if ($item);
2703         }
2704         $item = $stashed_problem if (!$item && $stashed_problem);
2705     }
2706
2707     return $item;
2708 }
2709
2710 =head2 find_location_failover
2711
2712     $location = $ils->find_location_failover($location, $request, $message);
2713
2714 Attempts to retrieve an org_unit by shortname from the passed in
2715 $location.  If that fails, $request and $message are used to lookup
2716 the ToAgencyId/AgencyId field and that is used.  Returns an org_unit
2717 as retrieved by retrieve_org_unit_by_shortname if successful and undef
2718 on failure.
2719
2720 =cut
2721
2722 sub find_location_failover {
2723     my ($self, $location, $request, $message) = @_;
2724     if ($request && !$message) {
2725         $message = $self->parse_request_type($request);
2726     }
2727     my $org_unit;
2728     if ($location) {
2729         # Because Auto-Graphics. (This should be configured somehow.)
2730         $location =~ s/^[^-]+-//;
2731         $org_unit = $self->retrieve_org_unit_by_shortname($location);
2732     }
2733     if ($request && $message && !$org_unit) {
2734         $location = $request->{$message}->{InitiationHeader}->{ToAgencyId}->{AgencyId};
2735         if ($location) {
2736             # Because Auto-Graphics. (This should be configured somehow.)
2737             $location =~ s/^[^-]+-//;
2738             $org_unit = $self->retrieve_org_unit_by_shortname($location);
2739         }
2740     }
2741
2742     return $org_unit;
2743 }
2744
2745 # private subroutines not meant to be used directly by subclasses.
2746 # Most have to do with setup and/or state checking of implementation
2747 # components.
2748
2749 # Find, load, and parse our configuration file:
2750 sub _configure {
2751     my $self = shift;
2752
2753     # Find the configuration file via variables:
2754     my $file = OILS_NCIP_CONFIG_DEFAULT;
2755     $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});
2756
2757     $self->{config} = XMLin($file, NormaliseSpace => 2,
2758                             ForceArray => ['block_profile', 'stat_cat_entry']);
2759 }
2760
2761 # Bootstrap OpenSRF::System and load the IDL.
2762 sub _bootstrap {
2763     my $self = shift;
2764
2765     my $bootstrap_config = $self->{config}->{bootstrap};
2766     OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
2767
2768     my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
2769     Fieldmapper->import(IDL => $idl);
2770 }
2771
2772 # Login and then initialize some object data based on the
2773 # configuration.
2774 sub _init {
2775     my $self = shift;
2776
2777     # Login to Evergreen.
2778     $self->login();
2779
2780     # Load the barred groups as pgt objects into a blocked_profiles
2781     # list.
2782     $self->{blocked_profiles} = [];
2783     foreach (@{$self->{config}->{patrons}->{block_profile}}) {
2784         my $pgt;
2785         if (ref $_) {
2786             $pgt = $U->simplereq(
2787                 'open-ils.pcrud',
2788                 'open-ils.pcrud.retrieve.pgt',
2789                 $self->{session}->{authtoken},
2790                 $_->{grp}
2791             );
2792         } else {
2793             $pgt = $U->simplereq(
2794                 'open-ils.pcrud',
2795                 'open-ils.pcrud.search.pgt',
2796                 $self->{session}->{authtoken},
2797                 {name => $_}
2798             );
2799         }
2800         push(@{$self->{blocked_profiles}}, $pgt) if ($pgt);
2801     }
2802
2803     # Load the bib source if we're not using precats.
2804     unless ($self->{config}->{items}->{use_precats}) {
2805         # Retrieve the default
2806         $self->{bib_source} = $U->simplereq(
2807             'open-ils.pcrud',
2808             'open-ils.pcrud.retrieve.cbs',
2809             $self->{session}->{authtoken},
2810             BIB_SOURCE_DEFAULT);
2811         my $data = $self->{config}->{items}->{bib_source};
2812         if ($data) {
2813             $data = $data->[0] if (ref($data) eq 'ARRAY');
2814             my $result;
2815             if (ref $data) {
2816                 $result = $U->simplereq(
2817                     'open-ils.pcrud',
2818                     'open-ils.pcrud.retrieve.cbs',
2819                     $self->{session}->{authtoken},
2820                     $data->{cbs}
2821                 );
2822             } else {
2823                 $result = $U->simplereq(
2824                     'open-ils.pcrud',
2825                     'open-ils.pcrud.search.cbs',
2826                     $self->{session}->{authtoken},
2827                     {source => $data}
2828                 );
2829             }
2830             $self->{bib_source} = $result if ($result);
2831         }
2832     }
2833
2834     # Load the required asset.stat_cat_entries:
2835     $self->{stat_cat_entries} = [];
2836     # First, make a regex for our ou and ancestors:
2837     my $ancestors = join("|", @{$U->get_org_ancestors($self->{session}->{work_ou}->id())});
2838     my $re = qr/(?:$ancestors)/;
2839     # Get the uniq stat_cat ids from the configuration:
2840     my @cats = uniq map {$_->{stat_cat}} @{$self->{config}->{items}->{stat_cat_entry}};
2841     # Retrieve all of the fleshed stat_cats and entries for the above.
2842     my $stat_cats = $U->simplereq(
2843         'open-ils.circ',
2844         'open-ils.circ.stat_cat.asset.retrieve.batch',
2845         $self->{session}->{authtoken},
2846         @cats
2847     );
2848     foreach my $entry (@{$self->{config}->{items}->{stat_cat_entry}}) {
2849         # Must have the stat_cat attr and the name, so we must have a
2850         # reference.
2851         next unless(ref $entry);
2852         my ($stat) = grep {$_->id() == $entry->{stat_cat}} @$stat_cats;
2853         push(@{$self->{stat_cat_entries}}, grep {$_->owner() =~ $re && $_->value() eq $entry->{content}} @{$stat->entries()});
2854     }
2855 }
2856
2857 # Search asset.call_number by a bre.id and location object. Return the
2858 # "closest" call_number if found, undef otherwise.
2859 sub _call_number_search {
2860     my $self = shift;
2861     my $bibid = shift;
2862     my $location = shift;
2863
2864     # At some point, this should be smarter, and we should retrieve
2865     # ancestors and descendants and search with a JSON query or some
2866     # such with results ordered by proximity to the original location,
2867     # but I don't have time to implement that right now.
2868     my $acn = $U->simplereq(
2869         'open-ils.pcrud',
2870         'open-ils.pcrud.search.acn',
2871         $self->{session}->{authtoken},
2872         {record => $bibid, owning_lib => $location->id()}
2873     );
2874
2875     return $acn;
2876 }
2877
2878 # Do a multiclass.query to search for items by isbn or issn.
2879 sub _bib_search {
2880     my $self = shift;
2881     my $idvalue = shift;
2882     my $idtype = shift;
2883     my $item;
2884
2885     my $result = $U->simplereq(
2886         'open-ils.search',
2887         'open-ils.search.biblio.multiclass',
2888         {searches => {lc($idtype) => $idvalue}}
2889     );
2890
2891     if ($result && $result->{count}) {
2892         if ($result->{count} > 1) {
2893             $item = NCIP::Problem->new(
2894                 {
2895                     ProblemType => 'Non-Unique Item',
2896                     ProblemDetail => 'More than one item matches the request.',
2897                     ProblemElement => '',
2898                     ProblemValue => $idvalue
2899                 }
2900             );
2901         }
2902         my $bibid = $result->{ids}->[0]->[0];
2903         $item = $U->simplereq(
2904             'open-ils.pcrud',
2905             'open-ils.pcrud.retrieve.bre',
2906             $self->{session}->{authtoken},
2907             $bibid
2908         );
2909     }
2910
2911     return $item;
2912 }
2913
2914 # Search for holds using the user, idvalue and selection_ou.
2915 sub _hold_search {
2916     my $self = shift;
2917     my $user = shift;
2918     my $target = shift;
2919     my $selection_ou = shift;
2920
2921     my $hold;
2922
2923     # Retrieve all of the user's active holds, and then search them in Perl.
2924     my $holds_list = $U->simplereq(
2925         'open-ils.circ',
2926         'open-ils.circ.holds.retrieve',
2927         $self->{session}->{authtoken},
2928         $user->id(),
2929         0
2930     );
2931
2932     if ($holds_list && @$holds_list) {
2933         my @holds = grep {$_->target == $target && $_->selection_ou == $selection_ou->id()} @{$holds_list};
2934         # There should only be 1, at this point, if there are any.
2935         if (@holds) {
2936             $hold = $holds[0];
2937         }
2938     }
2939
2940     return $hold;
2941 }
2942
2943 # Standalone, "helper" functions.  These do not take an object or
2944 # class reference.
2945
2946 # Check if a user is past their expiration date.
2947 sub _expired {
2948     my $user = shift;
2949     my $expired = 0;
2950
2951     # Users might not expire.  If so, they have no expire_date.
2952     if ($user->expire_date()) {
2953         my $expires = DateTime::Format::ISO8601->parse_datetime(
2954             cleanse_ISO8601($user->expire_date())
2955         )->epoch();
2956         my $now = DateTime->now()->epoch();
2957         $expired = $now > $expires;
2958     }
2959
2960     return $expired;
2961 }
2962
2963 # Creates a NCIP Problem from an event. Takes a string for the problem
2964 # type, the event hashref (or a string to use for the detail), and
2965 # optional arguments for the ProblemElement and ProblemValue fields.
2966 sub _problem_from_event {
2967     my ($type, $evt, $element, $value) = @_;
2968
2969     my $detail;
2970
2971     # Check the event.
2972     if (ref($evt)) {
2973         my ($textcode, $desc);
2974
2975         # Get the textcode, if available. Otherwise, use the ilsevent
2976         # "id," if available.
2977         if ($evt->{textcode}) {
2978             $textcode = $evt->{textcode};
2979         } elsif ($evt->{ilsevent}) {
2980             $textcode = $evt->{ilsevent};
2981         }
2982
2983         # Get the description. We favor translated descriptions over
2984         # the English in ils_events.xml.
2985         if ($evt->{desc}) {
2986             $desc = $evt->{desc};
2987         }
2988
2989         # Check if $type was set. As an "undocumented" feature, you
2990         # can pass undef, and we'll use the textcode from the event.
2991         unless ($type) {
2992             if ($textcode) {
2993                 $type = $textcode;
2994             }
2995         }
2996
2997         # Set the detail from some combination of the above.
2998         if ($desc) {
2999             $detail = $desc;
3000         } elsif ($textcode eq 'PERM_FAILURE') {
3001             if ($evt->{ilsperm}) {
3002                 $detail = "Permission denied: " . $evt->{ilsperm};
3003                 $detail =~ s/\.override$//;
3004             }
3005         } elsif ($textcode) {
3006             $detail = "ILS returned $textcode error.";
3007         } else {
3008             $detail = 'Detail not available.';
3009         }
3010
3011     } else {
3012         $detail = $evt;
3013     }
3014
3015     return NCIP::Problem->new(
3016         {
3017             ProblemType => ($type) ? $type : 'Temporary Processing Failure',
3018             ProblemDetail => ($detail) ? $detail : 'Detail not available.',
3019             ProblemElement => ($element) ? $element : 'NULL',
3020             ProblemValue => ($value) ? $value : 'NULL'
3021         }
3022     );
3023 }
3024
3025 # "Fix" dates for output so they validate against the schema
3026 sub _fix_date {
3027     my $date = shift;
3028     my $out = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($date));
3029     $out->set_time_zone('UTC');
3030     return $out->iso8601();
3031 }
3032
3033 1;