]> git.evergreen-ils.org Git - working/NCIPServer.git/blob - lib/NCIP/ILS/Evergreen.pm
Fix ProblemDescription for ProblemDetail in NCIP::ILS::Evergreen.
[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::RequestId;
54 use NCIP::Item::Id;
55
56 # Inherit from NCIP::ILS.
57 use parent qw(NCIP::ILS);
58
59 =head1 NAME
60
61 Evergreen - Evergreen driver for NCIPServer
62
63 =head1 SYNOPSIS
64
65     my $ils = NCIP::ILS::Evergreen->new(name => $config->{NCIP.ils.value});
66
67 =head1 DESCRIPTION
68
69 NCIP::ILS::Evergreen is the default driver for Evergreen and
70 NCIPServer. It was initially developed to work with Auto-Graphics'
71 SHAREit software using a subset of an unspecified ILL/DCB profile.
72
73 =cut
74
75 # Default values we define for things that might be missing in our
76 # runtime environment or configuration file that absolutely must have
77 # values.
78 #
79 # OILS_NCIP_CONFIG_DEFAULT is the default location to find our
80 # driver's configuration file.  This location can be overridden by
81 # setting the path in the OILS_NCIP_CONFIG environment variable.
82 #
83 # BIB_SOURCE_DEFAULT is the config.bib_source.id to use when creating
84 # "short" bibs.  It is used only if no entry is supplied in the
85 # configuration file.  The provided default is 2, the id of the
86 # "System Local" source that comes with a default Evergreen
87 # installation.
88 use constant {
89     OILS_NCIP_CONFIG_DEFAULT => '/openils/conf/oils_ncip.xml',
90     BIB_SOURCE_DEFAULT => 2
91 };
92
93 # A common Evergreen code shortcut to use AppUtils:
94 my $U = 'OpenILS::Application::AppUtils';
95
96 # The usual constructor:
97 sub new {
98     my $class = shift;
99     $class = ref($class) if (ref $class);
100
101     # Instantiate our parent with the rest of the arguments.  It
102     # creates a blessed hashref.
103     my $self = $class->SUPER::new(@_);
104
105     # Look for our configuration file, load, and parse it:
106     $self->_configure();
107
108     # Bootstrap OpenSRF and prepare some OpenILS components.
109     $self->_bootstrap();
110
111     # Initialize the rest of our internal state.
112     $self->_init();
113
114     return $self;
115 }
116
117 =head1 HANDLER METHODS
118
119 =head2 lookupuser
120
121     $ils->lookupuser($request);
122
123 Processes a LookupUser request.
124
125 =cut
126
127 sub lookupuser {
128     my $self = shift;
129     my $request = shift;
130
131     # Check our session and login if necessary.
132     $self->login() unless ($self->checkauth());
133
134     my $message_type = $self->parse_request_type($request);
135
136     # Let's go ahead and create our response object. We need this even
137     # if there is a problem.
138     my $response = NCIP::Response->new({type => $message_type . "Response"});
139     $response->header($self->make_header($request));
140
141     # Need to parse the request object to get the user barcode.
142     my ($barcode, $idfield) = $self->find_user_barcode($request);
143
144     # If we did not find a barcode, then report the problem.
145     if (ref($barcode) eq 'NCIP::Problem') {
146         $response->problem($barcode);
147         return $response;
148     }
149
150     # Look up our patron by barcode:
151     my $user = $self->retrieve_user_by_barcode($barcode, $idfield);
152     if (ref($user) eq 'NCIP::Problem') {
153         $response->problem($user);
154         return $response;
155     }
156
157     # We got the information, so lets fill in our userdata.
158     my $userdata = NCIP::User->new();
159
160     # Make an array of the user's active barcodes.
161     my $ids = [];
162     foreach my $card (@{$user->cards()}) {
163         if ($U->is_true($card->active())) {
164             my $id = NCIP::User::Id->new({
165                 UserIdentifierType => 'Barcode',
166                 UserIdentifierValue => $card->barcode()
167             });
168             push(@$ids, $id);
169         }
170     }
171     $userdata->UserId($ids);
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 = NCIP::User::OptionalFields->new();
178
179         # First, we'll look for name information.
180         if (grep {$_ eq 'Name Information'} @$elements) {
181             my $name = NCIP::StructuredPersonalUserName->new();
182             $name->Surname($user->family_name());
183             $name->GivenName($user->first_given_name());
184             $name->Prefix($user->prefix());
185             $name->Suffix($user->suffix());
186             $optionalfields->NameInformation($name);
187         }
188
189         # Next, check for user address information.
190         if (grep {$_ eq 'User Address Information'} @$elements) {
191             my $addresses = [];
192
193             # See if the user has any valid, physcial addresses.
194             foreach my $addr (@{$user->addresses()}) {
195                 next if ($U->is_true($addr->pending()));
196                 my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>$addr->address_type()});
197                 my $physical = NCIP::StructuredAddress->new();
198                 $physical->Line1($addr->street1());
199                 $physical->Line2($addr->street2());
200                 $physical->Locality($addr->city());
201                 $physical->Region($addr->state());
202                 $physical->PostalCode($addr->post_code());
203                 $physical->Country($addr->country());
204                 $address->PhysicalAddress($physical);
205                 push @$addresses, $address;
206             }
207
208             # Right now, we're only sharing email address if the user
209             # has it. We don't share phone numbers.
210             if ($user->email()) {
211                 my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>'Email Address'});
212                 $address->ElectronicAddress(
213                     NCIP::ElectronicAddress->new({
214                         Type=>'Email Address',
215                         Data=>$user->email()
216                     })
217                 );
218                 push @$addresses, $address;
219             }
220
221             $optionalfields->UserAddressInformation($addresses);
222         }
223
224         # Check for User Privilege.
225         if (grep {$_ eq 'User Privilege'} @$elements) {
226             # Get the user's group:
227             my $pgt = $U->simplereq(
228                 'open-ils.pcrud',
229                 'open-ils.pcrud.retrieve.pgt',
230                 $self->{session}->{authtoken},
231                 $user->profile()
232             );
233             if ($pgt) {
234                 my $privilege = NCIP::User::Privilege->new();
235                 $privilege->AgencyId($user->home_ou->shortname());
236                 $privilege->AgencyUserPrivilegeType($pgt->name());
237                 $privilege->ValidToDate($user->expire_date());
238                 $privilege->ValidFromDate($user->create_date());
239
240                 my $status = 'Active';
241                 if (_expired($user)) {
242                     $status = 'Expired';
243                 } elsif ($U->is_true($user->barred())) {
244                     $status = 'Barred';
245                 } elsif (!$U->is_true($user->active())) {
246                     $status = 'Inactive';
247                 }
248                 if ($status) {
249                     $privilege->UserPrivilegeStatus(
250                         NCIP::User::PrivilegeStatus->new({
251                             UserPrivilegeStatusType => $status
252                         })
253                     );
254                 }
255
256                 $optionalfields->UserPrivilege([$privilege]);
257             }
258         }
259
260         # Check for Block Or Trap.
261         if (grep {$_ eq 'Block Or Trap'} @$elements) {
262             my $blocks = [];
263
264             # First, let's check if the profile is blocked from ILL.
265             if (grep {$_->id() == $user->profile()} @{$self->{blocked_profiles}}) {
266                 my $block = NCIP::User::BlockOrTrap->new();
267                 $block->AgencyId($user->home_ou->shortname());
268                 $block->BlockOrTrapType('Block Interlibrary Loan');
269                 push @$blocks, $block;
270             }
271
272             # Next, we loop through the user's standing penalties
273             # looking for blocks on CIRC, HOLD, and RENEW.
274             my ($have_circ, $have_renew, $have_hold) = (0,0,0);
275             foreach my $penalty (@{$user->standing_penalties()}) {
276                 next unless($penalty->standing_penalty->block_list());
277                 my @block_list = split(/\|/, $penalty->standing_penalty->block_list());
278                 my $ou = $U->simplereq(
279                     'open-ils.pcrud',
280                     'open-ils.pcrud.retrieve.aou',
281                     $self->{session}->{authtoken},
282                     $penalty->org_unit()
283                 );
284
285                 # Block checkout.
286                 if (!$have_circ && grep {$_ eq 'CIRC'} @block_list) {
287                     my $bot = NCIP::User::BlockOrTrap->new();
288                     $bot->AgencyId($ou->shortname());
289                     $bot->BlockOrTrapType('Block Checkout');
290                     push @$blocks, $bot;
291                     $have_circ = 1;
292                 }
293
294                 # Block holds.
295                 if (!$have_hold && grep {$_ eq 'HOLD' || $_ eq 'FULFILL'} @block_list) {
296                     my $bot = NCIP::User::BlockOrTrap->new();
297                     $bot->AgencyId($ou->shortname());
298                     $bot->BlockOrTrapType('Block Holds');
299                     push @$blocks, $bot;
300                     $have_hold = 1;
301                 }
302
303                 # Block renewals.
304                 if (!$have_renew && grep {$_ eq 'RENEW'} @block_list) {
305                     my $bot = NCIP::User::BlockOrTrap->new();
306                     $bot->AgencyId($ou->shortname());
307                     $bot->BlockOrTrapType('Block Renewals');
308                     push @$blocks, $bot;
309                     $have_renew = 1;
310                 }
311
312                 # Stop after we report one of each, even if more
313                 # blocks remain.
314                 last if ($have_circ && $have_renew && $have_hold);
315             }
316
317             $optionalfields->BlockOrTrap($blocks);
318         }
319
320         $userdata->UserOptionalFields($optionalfields);
321     }
322
323     $response->data($userdata);
324
325     return $response;
326 }
327
328 =head2 acceptitem
329
330     $ils->acceptitem($request);
331
332 Processes an AcceptItem request.
333
334 =cut
335
336 sub acceptitem {
337     my $self = shift;
338     my $request = shift;
339
340     # Check our session and login if necessary.
341     $self->login() unless ($self->checkauth());
342
343     # Common preparation.
344     my $message = $self->parse_request_type($request);
345     my $response = NCIP::Response->new({type => $message . 'Response'});
346     $response->header($self->make_header($request));
347
348     # We only accept holds for the time being.
349     if ($request->{$message}->{RequestedActionType} !~ /^hold\w/i) {
350         # We need the item id or we can't do anything at all.
351         my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
352         if (ref($item_barcode) eq 'NCIP::Problem') {
353             $response->problem($item_barcode);
354             return $response;
355         }
356
357         # We need to find a patron barcode or we can't look anyone up
358         # to place a hold.
359         my ($user_barcode, $user_idfield) = $self->find_user_barcode($request, 'UserIdentifierValue');
360         if (ref($user_barcode) eq 'NCIP::Problem') {
361             $response->problem($user_barcode);
362             return $response;
363         }
364         # Look up our patron by barcode:
365         my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
366         if (ref($user) eq 'NCIP::Problem') {
367             $response->problem($user);
368             return $response;
369         }
370         # We're doing patron checks before looking for bibliographic
371         # information and creating the item because problems with the
372         # patron are more likely to occur.
373         my $problem = $self->check_user_for_problems($user, 'HOLD');
374         if ($problem) {
375             $response->problem($problem);
376             return $response;
377         }
378
379         # Check if the item barcode already exists:
380         my $item = $self->retrieve_copy_by_barcode($item_barcode);
381         if ($item) {
382             # What to do here was not defined in the
383             # specification. Since the copies that we create this way
384             # should get deleted when checked in, it would be an error
385             # if we try to create another one. It means that something
386             # has gone wrong somewhere.
387             $response->problem(
388                 NCIP::Problem->new(
389                     {
390                         ProblemType => 'Duplicate Item',
391                         ProblemDetail => "Item with barcode $item_barcode already exists.",
392                         ProblemElement => $item_idfield,
393                         ProblemValue => $item_barcode
394                     }
395                 )
396             );
397             return $response;
398         }
399
400         # Now, we have to create our new copy and/or bib and call number.
401
402         # First, we have to gather the necessary information from the
403         # request.  Store in a hashref for convenience. We may write a
404         # method to get this information in the future if we find we
405         # need it in other handlers. Such a function would be a
406         # candidate to go into our parent, NCIP::ILS.
407         my $item_info = {
408             barcode => $item_barcode,
409             call_number => $request->{$message}->{ItemOptionalFields}->{ItemDescription}->{CallNumber},
410             title => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{Author},
411             author => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{Title},
412             publisher => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{Publisher},
413             publication_date => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{PublicationDate},
414             medium => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{MediumType},
415             electronic => $request->{$message}->{ItemOptionalFields}->{BibliographicDescription}->{ElectronicResource}
416         };
417
418         if ($self->{config}->{items}->{use_precats}) {
419             # We only need to create a precat copy.
420             $item = $self->create_precat_copy($item_info);
421         } else {
422             # We have to create a "partial" bib record, a call number and a copy.
423             $item = $self->create_fuller_copy($item_info);
424         }
425
426         # If we failed to create the copy, report a problem.
427         unless ($item) {
428             $response->problem(
429                 {
430                     ProblemType => 'Temporary Processing Failure',
431                     ProblemDetail => 'Failed to create the item in the system',
432                     ProblemElement => $item_idfield,
433                     ProblemValue => $item_barcode
434                 }
435             );
436             return $response;
437         }
438
439         # We try to find the pickup location in our database. It's OK
440         # if it does not exist, the user's home library will be used
441         # instead.
442         my $location = $request->{$message}->{PickupLocation};
443         if ($location) {
444             $location = $self->retrieve_org_unit_by_shortname($location);
445         }
446
447         # Now, we place the hold on the newly created copy on behalf
448         # of the patron retrieved above.
449         my $hold = $self->place_hold($item, $user, $location);
450         if (ref($hold) eq 'NCIP::Problem') {
451             $response->problem($hold);
452             return $response;
453         }
454
455         # We return the RequestId and optionally, the ItemID. We'll
456         # just return what was sent to us, since we ignored all of it
457         # but the barcode.
458         my $data = {};
459         $data->{RequestId} = NCIP::RequestId->new(
460             {
461                 AgencyId => $request->{$message}->{RequestId}->{AgencyId},
462                 RequestIdentifierType => $request->{$message}->{RequestId}->{RequestIdentifierType},
463                 RequestIdentifierValue => $request->{$message}->{RequestId}->{RequestIdentifierValue}
464             }
465         );
466         $data->{ItemId} = NCIP::Item::Id->new(
467             {
468                 AgencyId => $request->{$message}->{ItemId}->{AgencyId},
469                 ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
470                 ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
471             }
472         );
473         $response->data($data);
474
475     } else {
476         my $problem = NCIP::Problem->new();
477         $problem->ProblemType('Unauthorized Combination Of Element Values For System');
478         $problem->ProblemDetail('We only support Hold For Pickup');
479         $problem->ProblemElement('RequestedActionType');
480         $problem->ProblemValue($request->{$message}->{RequestedActionType});
481         $response->problem($problem);
482     }
483
484     return $response;
485 }
486
487 =head1 METHODS USEFUL to SUBCLASSES
488
489 =head2 login
490
491     $ils->login();
492
493 Login to Evergreen via OpenSRF. It uses internal state from the
494 configuration file to login.
495
496 =cut
497
498 # Login via OpenSRF to Evergreen.
499 sub login {
500     my $self = shift;
501
502     # Get the authentication seed.
503     my $seed = $U->simplereq(
504         'open-ils.auth',
505         'open-ils.auth.authenticate.init',
506         $self->{config}->{credentials}->{username}
507     );
508
509     # Actually login.
510     if ($seed) {
511         my $response = $U->simplereq(
512             'open-ils.auth',
513             'open-ils.auth.authenticate.complete',
514             {
515                 username => $self->{config}->{credentials}->{username},
516                 password => md5_hex(
517                     $seed . md5_hex($self->{config}->{credentials}->{password})
518                 ),
519                 type => 'staff',
520                 workstation => $self->{config}->{credentials}->{workstation}
521             }
522         );
523         if ($response) {
524             $self->{session}->{authtoken} = $response->{payload}->{authtoken};
525             $self->{session}->{authtime} = $response->{payload}->{authtime};
526
527             # Set/reset the work_ou and user data in case something changed.
528
529             # Retrieve the work_ou as an object.
530             $self->{session}->{work_ou} = $U->simplereq(
531                 'open-ils.pcrud',
532                 'open-ils.pcrud.search.aou',
533                 $self->{session}->{authtoken},
534                 {shortname => $self->{config}->{credentials}->{work_ou}}
535             );
536
537             # We need the user information in order to do some things.
538             $self->{session}->{user} = $U->check_user_session($self->{session}->{authtoken});
539
540         }
541     }
542 }
543
544 =head2 checkauth
545
546     $valid = $ils->checkauth();
547
548 Returns 1 if the object a 'valid' authtoken, 0 if not.
549
550 =cut
551
552 sub checkauth {
553     my $self = shift;
554
555     # We use AppUtils to do the heavy lifting.
556     if (defined($self->{session})) {
557         if ($U->check_user_session($self->{session}->{authtoken})) {
558             return 1;
559         } else {
560             return 0;
561         }
562     }
563
564     # If we reach here, we don't have a session, so we are definitely
565     # not logged in.
566     return 0;
567 }
568
569 =head2 retrieve_user_by_barcode
570
571     $user = $ils->retrieve_user_by_barcode($user_barcode, $user_idfield);
572
573 Do a fleshed retrieve of a patron by barcode. Return the patron if
574 found and valid. Return a NCIP::Problem of 'Unknown User' otherwise.
575
576 The id field argument is used for the ProblemElement field in the
577 NCIP::Problem object.
578
579 An invalid patron is one where the barcode is not found in the
580 database, the patron is deleted, or the barcode used to retrieve the
581 patron is not active. The problem element is also returned if an error
582 occurs during the retrieval.
583
584 =cut
585
586 sub retrieve_user_by_barcode {
587     my ($self, $barcode, $idfield) = @_;
588     my $result = $U->simplereq(
589         'open-ils.actor',
590         'open-ils.actor.user.fleshed.retrieve_by_barcode',
591         $self->{session}->{authtoken},
592         $barcode,
593         1
594     );
595
596     # Check for a failure, or a deleted, inactive, or expired user,
597     # and if so, return empty userdata.
598     if (!$result || $U->event_code($result) || $U->is_true($result->deleted())
599             || !grep {$_->barcode() eq $barcode && $U->is_true($_->active())} @{$result->cards()}) {
600
601         my $problem = NCIP::Problem->new();
602         $problem->ProblemType('Unknown User');
603         $problem->ProblemDetail("User with barcode $barcode unknown");
604         $problem->ProblemElement($idfield);
605         $problem->ProblemValue($barcode);
606         $result = $problem;
607     }
608
609     return $result;
610 }
611
612 =head2 check_user_for_problems
613
614     $problem = $ils>check_user_for_problems($user, 'HOLD, 'CIRC', 'RENEW');
615
616 This function checks if a user has a blocked profile or any from a
617 list of provided blocks. If it does, then a NCIP::Problem object is
618 returned, otherwise an undefined value is returned.
619
620 The list of blocks appears as additional arguments after the user. You
621 can provide any value(s) that might appear in a standing penalty block
622 lit in Evergreen. The example above checks for HOLD, CIRC, and
623 RENEW. Any number of such values can be provided. If none are
624 provided, the function only checks if the patron's profiles appears in
625 the object's blocked profiles list.
626
627 It stops on the first matching block, if any.
628
629 =cut
630
631 sub check_user_for_problems {
632     my $self = shift;
633     my $user = shift;
634     my @blocks = @_;
635
636     # Fill this in if we have a problem, otherwise just return it.
637     my $problem;
638
639     # First, check the user's profile.
640     if (grep {$_->id() == $user->profile()} @{$self->{blocked_profiles}}) {
641         $problem = NCIP::Problem->new(
642             {
643                 ProblemType => 'User Blocked',
644                 ProblemDetail => 'User blocked from inter-library loan',
645                 ProblemElement => 'NULL',
646                 ProblemValue => 'NULL'
647             }
648         );
649     }
650
651     # Next, check if the patron has one of the indicated blocks.
652     unless ($problem) {
653         foreach my $block (@blocks) {
654             if (grep {$_->standing_penalty->block_list() =~ /$block/} @{$user->standing_penalties()}) {
655                 $problem = NCIP::Problem->new(
656                     {
657                         ProblemType => 'User Blocked',
658                         ProblemDetail => 'User blocked from ' .
659                             ($block eq 'HOLD') ? 'holds' : (($block eq 'RENEW') ? 'renewals' :
660                                                                 (($block eq 'CIRC') ? 'checkout' : lc($block))),
661                         ProblemElement => 'NULL',
662                         ProblemValue => 'NULL'
663                     }
664                 );
665                 last;
666             }
667         }
668     }
669
670     return $problem;
671 }
672
673 =head2 retrieve_copy_by_barcode
674
675     $copy = $ils->retrieve_copy_by_barcode($copy_barcode);
676
677 Look up and retrieve some copy details by the copy barcode. This
678 method returns either a valid copy object or undefined if no copy
679 exists with that barcode or if some error occurs.
680
681 This method differs from C<retrieve_user_by_barcode> in that a copy
682 cannot be invalid if it exists and it is not always an error if no
683 copy exists. In some cases, when handling AcceptItem, we might prefer
684 there to be no copy.
685
686 =cut
687
688 sub retrieve_copy_by_barcode {
689     my $self = shift;
690     my $barcode = shift;
691
692     my $copy = $U->simplereq(
693         'open-ils.circ',
694         'open-ils.circ.copy_details.retrieve.barcode',
695         $self->{session}->{authtoken},
696         $barcode
697     );
698
699     # If $copy is an event, return undefined.
700     if ($copy && $U->event_code($copy)) {
701         undef($copy);
702     }
703
704     return $copy;
705 }
706
707 =head2 retrieve_org_unit_by_shortname
708
709     $org_unit = $ils->retrieve_org_unit_by_shortname($shortname);
710
711 Retrieves an org. unit from the database by shortname. Returns the
712 org. unit as a Fieldmapper object or undefined.
713
714 =cut
715
716 sub retrieve_org_unit_by_shortname {
717     my $self = shift;
718     my $shortname = shift;
719
720     my $aou = $U->simplereq(
721         'open-ils.pcrud',
722         'open-ils.pcrud.search.aou',
723         $self->{session}->{authtoken},
724         {shortname => {'=' => {transform => 'lower', value => ['lower', $shortname]}}}
725     );
726
727     return $aou;
728 }
729
730 =head2 create_precat_copy
731
732     $item_info->{
733         barcode => '312340123456789',
734         author => 'Public, John Q.',
735         title => 'Magnum Opus',
736         call_number => '005.82',
737         publisher => 'Brick House',
738         publication_date => '2014'
739     };
740
741     $item = $ils->create_precat_copy($item_info);
742
743
744 Create a "precat" copy to use for the incoming item using a hashref of
745 item information. At a minimum, the barcode, author and title fields
746 need to be filled in. The other fields are ignored if provided.
747
748 This method is called by the AcceptItem handler if the C<use_precats>
749 configuration option is turned on.
750
751 =cut
752
753 sub create_precat_copy {
754     my $self = shift;
755     my $item_info = shift;
756
757     my $item = Fieldmapper::asset::copy->new();
758     $item->barcode($item_info->{barcode});
759     $item->call_number(OILS_PRECAT_CALL_NUMBER);
760     $item->dummy_title($item_info->{title});
761     $item->dummy_author($item_info->{author});
762     $item->circ_lib($self->{session}->{work_ou}->id());
763     $item->circulate('t');
764     $item->holdable('t');
765     $item->opac_visible('f');
766     $item->deleted('f');
767     $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
768     $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
769     $item->location(1);
770     $item->status(0);
771     $item->editor($self->{session}->{user}->id());
772     $item->creator($self->{session}->{user}->id());
773     $item->isnew(1);
774
775     # Actually create it:
776     my $xact;
777     my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
778     $ses->connect();
779     eval {
780         $xact = $ses->request(
781             'open-ils.pcrud.transaction.begin',
782             $self->{session}->{authtoken}
783         )->gather(1);
784         $item = $ses->request(
785             'open-ils.pcrud.create.acp',
786             $self->{session}->{authtoken},
787             $item
788         )->gather(1);
789         $xact = $ses->request(
790             'open-ils.pcrud.transaction.commit',
791             $self->{session}->{authtoken}
792         )->gather(1);
793     };
794     if ($@) {
795         undef($item);
796         if ($xact) {
797             eval {
798                 $ses->request(
799                     'open-ils.pcrud.transaction.rollback',
800                     $self->{session}->{authtoken}
801                 )->gather(1);
802             };
803         }
804     }
805     $ses->disconnect();
806
807     return $item;
808 }
809
810 =head2 create_fuller_copy
811
812     $item_info->{
813         barcode => '31234003456789',
814         author => 'Public, John Q.',
815         title => 'Magnum Opus',
816         call_number => '005.82',
817         publisher => 'Brick House',
818         publication_date => '2014'
819     };
820
821     $item = $ils->create_fuller_copy($item_info);
822
823 Creates a skeletal bibliographic record, call number, and copy for the
824 incoming item using a hashref with item information in it. At a
825 minimum, the barcode, author, title, and call_number fields must be
826 filled in.
827
828 This method is used by the AcceptItem handler if the C<use_precats>
829 configuration option is NOT set.
830
831 =cut
832
833 sub create_fuller_copy {
834     my $self = shift;
835     my $item_info = shift;
836
837     my $item;
838
839     # We do everything in one transaction, because it should be atomic.
840     my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
841     $ses->connect();
842     my $xact;
843     eval {
844         $xact = $ses->request(
845             'open-ils.pcrud.transaction.begin',
846             $self->{session}->{authtoken}
847         )->gather(1);
848     };
849     if ($@) {
850         undef($xact);
851     }
852
853     # The rest depends on there being a transaction.
854     if ($xact) {
855
856         # Create the MARC record.
857         my $record = MARC::Record->new();
858         $record->encoding('UTF-8');
859         $record->leader('00881nam a2200193   4500');
860         my $datespec = strftime("%Y%m%d%H%M%S.0", localtime);
861         my @fields = ();
862         push(@fields, MARC::Field->new('005', $datespec));
863         push(@fields, MARC::Field->new('082', '0', '4', 'a' => $item_info->{call_number}));
864         push(@fields, MARC::Field->new('245', '0', '0', 'a' => $item_info->{title}));
865         # Publisher is a little trickier:
866         if ($item_info->{publisher}) {
867             my $pub = MARC::Field->new('260', ' ', ' ', 'a' => '[S.l.]', 'b' => $item_info->{publisher});
868             $pub->add_subfields('c' => $item_info->{publication_date}) if ($item_info->{publication_date});
869             push(@fields, $pub);
870         }
871         # We have no idea if the author is personal corporate or something else, so we use a 720.
872         push(@fields, MARC::Field->new('720', ' ', ' ', 'a' => $item_info->{author}, '4' => 'aut'));
873         $record->append_fields(@fields);
874         my $marc = clean_marc($record);
875
876         # Create the bib object.
877         my $bib = Fieldmapper::biblio::record_entry->new();
878         $bib->creator($self->{session}->{user}->id());
879         $bib->editor($self->{session}->{user}->id());
880         $bib->source($self->{bib_source}->id());
881         $bib->active('t');
882         $bib->deleted('f');
883         $bib->marc($marc);
884         $bib->isnew(1);
885
886         eval {
887             $bib = $ses->request(
888                 'open-ils.pcrud.create.bre',
889                 $self->{session}->{authtoken},
890                 $bib
891             )->gather(1);
892         };
893         if ($@) {
894             undef($bib);
895             eval {
896                 $ses->request(
897                     'open-ils.pcrud.transaction.rollback',
898                     $self->{session}->{authtoken}
899                 )->gather(1);
900             };
901         }
902
903         # Create the call number
904         my $acn;
905         if ($bib) {
906             $acn = Fieldmapper::asset::call_number->new();
907             $acn->creator($self->{session}->{user}->id());
908             $acn->editor($self->{session}->{user}->id());
909             $acn->label($item_info->{call_number});
910             $acn->record($bib->id());
911             $acn->owning_lib($self->{session}->{work_ou}->id());
912             $acn->deleted('f');
913             $acn->isnew(1);
914
915             eval {
916                 $acn = $ses->request(
917                     'open-ils.pcrud.create.acn',
918                     $self->{session}->{authtoken},
919                     $acn
920                 )->gather(1);
921             };
922             if ($@) {
923                 undef($acn);
924                 eval {
925                     $ses->request(
926                         'open-ils.pcrud.transaction.rollback',
927                         $self->{session}->{authtoken}
928                     )->gather(1);
929                 };
930             }
931         }
932
933         # create the copy
934         if ($acn) {
935             $item = Fieldmapper::asset::copy->new();
936             $item->barcode($item_info->{barcode});
937             $item->call_number($acn->id());
938             $item->circ_lib($self->{session}->{work_ou}->id);
939             $item->circulate('t');
940             $item->holdable('t');
941             $item->opac_visible('f');
942             $item->deleted('f');
943             $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
944             $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
945             $item->location(1);
946             $item->status(0);
947             $item->editor($self->{session}->{user}->id);
948             $item->creator($self->{session}->{user}->id);
949             $item->isnew(1);
950
951             eval {
952                 $item = $ses->request(
953                     'open-ils.pcrud.create.acp',
954                     $self->{session}->{authtoken},
955                     $item
956                 )->gather(1);
957
958                 # Cross our fingers and commit the work.
959                 $xact = $ses->request(
960                     'open-ils.pcrud.transaction.commit',
961                     $self->{session}->{authtoken}
962                 )->gather(1);
963             };
964             if ($@) {
965                 undef($item);
966                 eval {
967                     $ses->request(
968                         'open-ils.pcrud.transaction.rollback',
969                         $self->{session}->{authtoken}
970                     )->gather(1) if ($xact);
971                 };
972             }
973         }
974     }
975
976     # We need to disconnect our session.
977     $ses->disconnect();
978
979     # Now, we handle our asset stat_cat entries.
980     if ($item) {
981         # It would be nice to do these in the above transaction, but
982         # pcrud does not support the ascecm object, yet.
983         foreach my $entry (@{$self->{stat_cat_entries}}) {
984             my $map = Fieldmapper::asset::stat_cat_entry_copy_map->new();
985             $map->isnew(1);
986             $map->stat_cat($entry->stat_cat());
987             $map->stat_cat_entry($entry->id());
988             $map->owning_copy($item->id());
989             # We don't really worry if it succeeds or not.
990             $U->simplereq(
991                 'open-ils.circ',
992                 'open-ils.circ.stat_cat.asset.copy_map.create',
993                 $self->{session}->{authtoken},
994                 $map
995             );
996         }
997     }
998
999     return $item;
1000 }
1001
1002 =head2 place_hold
1003
1004     $hold = $ils->place_hold($item, $user, $location);
1005
1006 This function places a hold on $item for $user for pickup at
1007 $location. If location is not provided or undefined, the user's home
1008 library is used as a fallback.
1009
1010 $item can be a copy (asset::copy), volume (asset::call_number), or bib
1011 (biblio::record_entry). The appropriate hold type will be placed
1012 depending on the object.
1013
1014 On success, the method returns the object representing the hold. On
1015 failure, a NCIP::Problem object, describing the failure, is returned.
1016
1017 =cut
1018
1019 sub place_hold {
1020     my $self = shift;
1021     my $item = shift;
1022     my $user = shift;
1023     my $location = shift;
1024
1025     # If $location is undefined, use the user's home_ou, which should
1026     # have been fleshed when the user was retrieved.
1027     $location = $user->home_ou() unless ($location);
1028
1029     # $hold is the hold. $params is for the is_possible check.
1030     my ($hold, $params);
1031
1032     # Prep the hold with fields common to all hold types:
1033     $hold = Fieldmapper::action::hold_request->new();
1034     $hold->isnew(1); # Just to make sure.
1035     $hold->target($item->id());
1036     $hold->usr($user->id());
1037     $hold->pickup_lib($location->id());
1038     if (!$user->email()) {
1039         $hold->email_notify('f');
1040         $hold->phone_notify($user->day_phone()) if ($user->day_phone());
1041     } else {
1042         $hold->email_notify('t');
1043     }
1044
1045     # Ditto the params:
1046     $params = { pickup_lib => $location->id(), patronid => $user->id() };
1047
1048     if (ref($item) eq 'Fieldmapper::asset::copy') {
1049         $hold->hold_type('C');
1050         $hold->current_copy($item->id());
1051         $params->{hold_type} = 'C';
1052         $params->{copy_id} = $item->id();
1053     } elsif (ref($item) eq 'Fieldmapper::asset::call_number') {
1054         $hold->hold_type('V');
1055         $params->{hold_type} = 'V';
1056         $params->{volume_id} = $item->id();
1057     } elsif (ref($item) eq 'Fieldmapper::biblio::record_entry') {
1058         $hold->hold_type('T');
1059         $params->{hold_type} = 'T';
1060         $params->{titleid} = $item->id();
1061     }
1062
1063     # Check if the hold is possible:
1064     my $r = $U->simplereq(
1065         'open-ils.circ',
1066         'open-ils.circ.title_hold.is_possible',
1067         $self->{session}->{authtoken},
1068         $params
1069     );
1070
1071     if ($r->{success}) {
1072         $hold = $U->simplereq(
1073             'open-ils.circ',
1074             'open-ils.circ.holds.create.override',
1075             $self->{session}->{authtoken},
1076             $hold
1077         );
1078         if (ref($hold) eq 'HASH') {
1079             $hold = _problem_from_event('Request Not Possible', $hold);
1080         }
1081     } elsif ($r->{last_event}) {
1082         $hold = _problem_from_event('Request Not Possible', $r->{last_event});
1083     } elsif ($r->{text_code}) {
1084         $hold = _problem_from_event('Request Not Possible', $r);
1085     } else {
1086         $hold = _problem_from_event('Request Not Possible');
1087     }
1088
1089     return $hold;
1090 }
1091
1092 =head1 OVERRIDDEN PARENT METHODS
1093
1094 =head2 find_user_barcode
1095
1096 We dangerously override our parent's C<find_user_barcode> to return
1097 either the $barcode or a Problem object. In list context the barcode
1098 or problem will be the first argument and the id field, if any, will
1099 be the second. We also add a second, optional, argument to indicate a
1100 default value for the id field in the event of a failure to find
1101 anything at all. (Perl lets us get away with this.)
1102
1103 =cut
1104
1105 sub find_user_barcode {
1106     my $self = shift;
1107     my $request = shift;
1108     my $default = shift;
1109
1110     unless ($default) {
1111         my $message = $self->parse_request_type($request);
1112         if ($message eq 'LookupUser') {
1113             $default = 'AuthenticationInputData';
1114         } else {
1115             $default = 'UserIdentifierValue';
1116         }
1117     }
1118
1119     my ($value, $idfield) = $self->SUPER::find_user_barcode($request);
1120
1121     unless ($value) {
1122         $idfield = $default unless ($idfield);
1123         $value = NCIP::Problem->new();
1124         $value->ProblemType('Needed Data Missing');
1125         $value->ProblemDetail('Cannot find user barcode in message.');
1126         $value->ProblemElement($idfield);
1127         $value->ProblemValue('NULL');
1128     }
1129
1130     return (wantarray) ? ($value, $idfield) : $value;
1131 }
1132
1133 =head2 find_item_barcode
1134
1135 We do pretty much the same thing as with C<find_user_barcode> for
1136 C<find_item_barcode>.
1137
1138 =cut
1139
1140 sub find_item_barcode {
1141     my $self = shift;
1142     my $request = shift;
1143     my $default = shift || 'ItemIdentifierValue';
1144
1145     my ($value, $idfield) = $self->SUPER::find_item_barcode($request);
1146
1147     unless ($value) {
1148         $idfield = $default unless ($idfield);
1149         $value = NCIP::Problem->new();
1150         $value->ProblemType('Needed Data Missing');
1151         $value->ProblemDetail('Cannot find item barcode in message.');
1152         $value->ProblemElement($idfield);
1153         $value->ProblemValue('NULL');
1154     }
1155
1156     return (wantarray) ? ($value, $idfield) : $value;
1157 }
1158
1159 # private subroutines not meant to be used directly by subclasses.
1160 # Most have to do with setup and/or state checking of implementation
1161 # components.
1162
1163 # Find, load, and parse our configuration file:
1164 sub _configure {
1165     my $self = shift;
1166
1167     # Find the configuration file via variables:
1168     my $file = OILS_NCIP_CONFIG_DEFAULT;
1169     $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});
1170
1171     $self->{config} = XMLin($file, NormaliseSpace => 2,
1172                             ForceArray => ['block_profile', 'stat_cat_entry']);
1173 }
1174
1175 # Bootstrap OpenSRF::System and load the IDL.
1176 sub _bootstrap {
1177     my $self = shift;
1178
1179     my $bootstrap_config = $self->{config}->{bootstrap};
1180     OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
1181
1182     my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
1183     Fieldmapper->import(IDL => $idl);
1184 }
1185
1186 # Login and then initialize some object data based on the
1187 # configuration.
1188 sub _init {
1189     my $self = shift;
1190
1191     # Login to Evergreen.
1192     $self->login();
1193
1194     # Load the barred groups as pgt objects into a blocked_profiles
1195     # list.
1196     $self->{blocked_profiles} = [];
1197     foreach (@{$self->{config}->{patrons}->{block_profile}}) {
1198         my $pgt;
1199         if (ref $_) {
1200             $pgt = $U->simplereq(
1201                 'open-ils.pcrud',
1202                 'open-ils.pcrud.retrieve.pgt',
1203                 $self->{session}->{authtoken},
1204                 $_->{grp}
1205             );
1206         } else {
1207             $pgt = $U->simplereq(
1208                 'open-ils.pcrud',
1209                 'open-ils.pcrud.search.pgt',
1210                 $self->{session}->{authtoken},
1211                 {name => $_}
1212             );
1213         }
1214         push(@{$self->{blocked_profiles}}, $pgt) if ($pgt);
1215     }
1216
1217     # Load the bib source if we're not using precats.
1218     unless ($self->{config}->{items}->{use_precats}) {
1219         # Retrieve the default
1220         $self->{bib_source} = $U->simplereq(
1221             'open-ils.pcrud',
1222             'open-ils.pcrud.retrieve.cbs',
1223             $self->{session}->{authtoken},
1224             BIB_SOURCE_DEFAULT);
1225         my $data = $self->{config}->{items}->{bib_source};
1226         if ($data) {
1227             $data = $data->[0] if (ref($data) eq 'ARRAY');
1228             my $result;
1229             if (ref $data) {
1230                 $result = $U->simplereq(
1231                     'open-ils.pcrud',
1232                     'open-ils.pcrud.retrieve.cbs',
1233                     $self->{session}->{authtoken},
1234                     $data->{cbs}
1235                 );
1236             } else {
1237                 $result = $U->simplereq(
1238                     'open-ils.pcrud',
1239                     'open-ils.pcrud.search.cbs',
1240                     $self->{session}->{authtoken},
1241                     {source => $data}
1242                 );
1243             }
1244             $self->{bib_source} = $result if ($result);
1245         }
1246     }
1247
1248     # Load the required asset.stat_cat_entries:
1249     $self->{stat_cat_entries} = [];
1250     # First, make a regex for our ou and ancestors:
1251     my $ancestors = join("|", @{$U->get_org_ancestors($self->{session}->{work_ou}->id())});
1252     my $re = qr/(?:$ancestors)/;
1253     # Get the uniq stat_cat ids from the configuration:
1254     my @cats = uniq map {$_->{stat_cat}} @{$self->{config}->{items}->{stat_cat_entry}};
1255     # Retrieve all of the fleshed stat_cats and entries for the above.
1256     my $stat_cats = $U->simplereq(
1257         'open-ils.circ',
1258         'open-ils.circ.stat_cat.asset.retrieve.batch',
1259         $self->{session}->{authtoken},
1260         @cats
1261     );
1262     foreach my $entry (@{$self->{config}->{items}->{stat_cat_entry}}) {
1263         # Must have the stat_cat attr and the name, so we must have a
1264         # reference.
1265         next unless(ref $entry);
1266         my ($stat) = grep {$_->id() == $entry->{stat_cat}} @$stat_cats;
1267         push(@{$self->{stat_cat_entries}}, grep {$_->owner() =~ $re && $_->value() eq $entry->{content}} @{$stat->entries()});
1268     }
1269 }
1270
1271 # Standalone, "helper" functions.  These do not take an object or
1272 # class reference.
1273
1274 # Check if a user is past their expiration date.
1275 sub _expired {
1276     my $user = shift;
1277     my $expired = 0;
1278
1279     # Users might not expire.  If so, they have no expire_date.
1280     if ($user->expire_date()) {
1281         my $expires = DateTime::Format::ISO8601->parse_datetime(
1282             cleanse_ISO8601($user->expire_date())
1283         )->epoch();
1284         my $now = DateTime->now()->epoch();
1285         $expired = $now > $expires;
1286     }
1287
1288     return $expired;
1289 }
1290
1291 # Creates a NCIP Problem from an event. Takes a string for the problem
1292 # type, the event hashref, and optional arguments for the
1293 # ProblemElement and ProblemValue fields.
1294 sub _problem_from_event {
1295     my ($type, $evt, $element, $value) = @_;
1296
1297     my $detail;
1298
1299     # This block will likely need to get smarter in the near future.
1300     if ($evt) {
1301         if ($evt->{text_code} eq 'PERM_FAILURE') {
1302             $detail = 'Permission Failure: ' . $evt->{ilsperm};
1303             $detail =~ s/\.override$//;
1304         } else {
1305             $detail = 'ILS returned ' . $evt->{text_code} . ' error.';
1306         }
1307     } else {
1308         $detail = 'Detail not available.';
1309     }
1310
1311     return NCIP::Problem->new(
1312         {
1313             ProblemType => $type,
1314             ProblemDetail => $detail,
1315             ProblemElement => ($element) ? $element : 'NULL',
1316             ProblemValue => ($value) ? $value : 'NULL'
1317         }
1318     );
1319 }
1320
1321 1;