]> git.evergreen-ils.org Git - working/NCIPServer.git/blob - lib/NCIP/ILS/Evergreen.pm
Add check for other volumes before delete a bib.
[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_details_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 =head2 checkinitem
488
489     $response = $ils->checkinitem($request);
490
491 Checks the item in if we can find the barcode in the message. It
492 returns problems if it cannot find the item in the system or if the
493 item is not checked out.
494
495 It could definitely use some more brains at some point as it does not
496 fully support everything that the standard allows. It also does not
497 really check if the checkin succeeded or not.
498
499 =cut
500
501 sub checkinitem {
502     my $self = shift;
503     my $request = shift;
504
505     # Check our session and login if necessary:
506     $self->login() unless ($self->checkauth());
507
508     # Common stuff:
509     my $message = $self->parse_request_type($request);
510     my $response = NCIP::Response->new({type => $message . 'Response'});
511     $response->header($self->make_header($request));
512
513     # We need the copy barcode from the message.
514     my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
515     if (ref($item_barcode) eq 'NCIP::Problem') {
516         $response->problem($item_barcode);
517         return $response;
518     }
519
520     # Retrieve the copy details.
521     my $details = $self->retrieve_copy_details_by_barcode($item_barcode);
522     unless ($details) {
523         # Return an Unkown Item problem unless we find the copy.
524         $response->problem(
525             NCIP::Problem->new(
526                 {
527                     ProblemType => 'Unknown Item',
528                     ProblemDetail => "Item with barcode $item_barcode is not known.",
529                     ProblemElement => $item_idfield,
530                     ProblemValue => $item_barcode
531                 }
532             )
533         );
534         return $response;
535     }
536
537     # Isolate the copy.
538     my $copy = $details->{copy};
539
540     # Look for a circulation and examine its information:
541     my $circ = $details->{circ};
542
543     # Shortcut for the next check.
544     my $ou_id = $self->{session}->{work_ou}->id();
545     # We need to make sure that the copy is checked out, and it was
546     # either created by the NCIP user or checked out at the NCIP
547     # org. unit.
548     if (!$circ || $circ->checkin_time() || ($circ->circ_lib() != $ou_id && $copy->circ_lib() != $ou_id)) {
549         # Item isn't checked out.
550         $response->problem(
551             NCIP::Problem->new(
552                 {
553                     ProblemType => 'Item Not Checked Out',
554                     ProblemDetail => "Item with barcode $item_barcode not checkout out.",
555                     ProblemElement => $item_idfield,
556                     ProblemValue => $item_barcode
557                 }
558             )
559         );
560     } else {
561         # Get data on the patron who has it checked out.
562         my $circ_user = $self->retrieve_user_by_id($circ->usr());
563
564         # Check if an optional UserId was provided. If so, make sure
565         # the copy was checked out to that user. We record the id
566         # field to report it as the problem value if the copy is
567         # checked out to someone else.
568         my ($user_barcode, $user_idfield) = $self->find_user_barcode($request);
569         if (ref($user_barcode) ne 'NCIP::Problem') {
570             my $user = $self->retrieve_user_by_bacode($user_barcode);
571             if ($user->id() != $circ_user->id()) {
572                 $response->problem(
573                     NCIP::Problem->new(
574                         {
575                             ProblemType => 'Item Not Checked Out To This User',
576                             ProblemDetail => "Item with barcode $item_barcode not checkout out to user with barcode $user_barcode.",
577                             ProblemElement => $user_idfield,
578                             ProblemValue => $user_barcode
579                         }
580                     )
581                 );
582                 return $response; # Short circuit
583             }
584         }
585
586         # Checkin parameters. We want to skip hold targeting or making
587         # transits, to force the checkin despite the copy status, as
588         # well as void overdues.
589         my $params = {
590             barcode => $copy->barcode(),
591             force => 1,
592             noop => 1,
593             void_overdues => 1
594         };
595         my $result = $U->simplereq(
596             'open-ils.circ',
597             'open-ils.circ.checkin.override',
598             $self->{session}->{authtoken},
599             $params
600         );
601         if ($result->{textcode} eq 'SUCCESS') {
602             # Delete the copy. Since delete_copy checks ownership
603             # before attempting to delete the copy, we don't bother
604             # checking who owns it.
605             $self->delete_copy($copy);
606         }
607
608         # We should check for errors here, but I'll leave that for
609         # later.
610
611         my $data = {
612             ItemId => NCIP::Item::Id->new(
613                 {
614                     AgencyId => $request->{$message}->{ItemId}->{AgencyId},
615                     ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
616                     ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
617                 }
618             ),
619             UserId => NCIP::User::Id->new(
620                 {
621                     UserIdentifierType => 'Barcode Id',
622                     UserIdentifierValue => $circ_user->card->barcode()
623                 }
624             )
625         };
626
627         $response->data($data);
628
629         # At some point in the future, we should probably check if
630         # they requested optional user or item elements and return
631         # those. For the time being, we ignore those at the risk of
632         # being considered non-compliant.
633     }
634
635     return $response
636 }
637
638 =head1 METHODS USEFUL to SUBCLASSES
639
640 =head2 login
641
642     $ils->login();
643
644 Login to Evergreen via OpenSRF. It uses internal state from the
645 configuration file to login.
646
647 =cut
648
649 # Login via OpenSRF to Evergreen.
650 sub login {
651     my $self = shift;
652
653     # Get the authentication seed.
654     my $seed = $U->simplereq(
655         'open-ils.auth',
656         'open-ils.auth.authenticate.init',
657         $self->{config}->{credentials}->{username}
658     );
659
660     # Actually login.
661     if ($seed) {
662         my $response = $U->simplereq(
663             'open-ils.auth',
664             'open-ils.auth.authenticate.complete',
665             {
666                 username => $self->{config}->{credentials}->{username},
667                 password => md5_hex(
668                     $seed . md5_hex($self->{config}->{credentials}->{password})
669                 ),
670                 type => 'staff',
671                 workstation => $self->{config}->{credentials}->{workstation}
672             }
673         );
674         if ($response) {
675             $self->{session}->{authtoken} = $response->{payload}->{authtoken};
676             $self->{session}->{authtime} = $response->{payload}->{authtime};
677
678             # Set/reset the work_ou and user data in case something changed.
679
680             # Retrieve the work_ou as an object.
681             $self->{session}->{work_ou} = $U->simplereq(
682                 'open-ils.pcrud',
683                 'open-ils.pcrud.search.aou',
684                 $self->{session}->{authtoken},
685                 {shortname => $self->{config}->{credentials}->{work_ou}}
686             );
687
688             # We need the user information in order to do some things.
689             $self->{session}->{user} = $U->check_user_session($self->{session}->{authtoken});
690
691         }
692     }
693 }
694
695 =head2 checkauth
696
697     $valid = $ils->checkauth();
698
699 Returns 1 if the object a 'valid' authtoken, 0 if not.
700
701 =cut
702
703 sub checkauth {
704     my $self = shift;
705
706     # We use AppUtils to do the heavy lifting.
707     if (defined($self->{session})) {
708         if ($U->check_user_session($self->{session}->{authtoken})) {
709             return 1;
710         } else {
711             return 0;
712         }
713     }
714
715     # If we reach here, we don't have a session, so we are definitely
716     # not logged in.
717     return 0;
718 }
719
720 =head2 retrieve_user_by_barcode
721
722     $user = $ils->retrieve_user_by_barcode($user_barcode, $user_idfield);
723
724 Do a fleshed retrieve of a patron by barcode. Return the patron if
725 found and valid. Return a NCIP::Problem of 'Unknown User' otherwise.
726
727 The id field argument is used for the ProblemElement field in the
728 NCIP::Problem object.
729
730 An invalid patron is one where the barcode is not found in the
731 database, the patron is deleted, or the barcode used to retrieve the
732 patron is not active. The problem element is also returned if an error
733 occurs during the retrieval.
734
735 =cut
736
737 sub retrieve_user_by_barcode {
738     my ($self, $barcode, $idfield) = @_;
739     my $result = $U->simplereq(
740         'open-ils.actor',
741         'open-ils.actor.user.fleshed.retrieve_by_barcode',
742         $self->{session}->{authtoken},
743         $barcode,
744         1
745     );
746
747     # Check for a failure, or a deleted, inactive, or expired user,
748     # and if so, return empty userdata.
749     if (!$result || $U->event_code($result) || $U->is_true($result->deleted())
750             || !grep {$_->barcode() eq $barcode && $U->is_true($_->active())} @{$result->cards()}) {
751
752         my $problem = NCIP::Problem->new();
753         $problem->ProblemType('Unknown User');
754         $problem->ProblemDetail("User with barcode $barcode unknown");
755         $problem->ProblemElement($idfield);
756         $problem->ProblemValue($barcode);
757         $result = $problem;
758     }
759
760     return $result;
761 }
762
763 =head2 retrieve_user_by_id
764
765     $user = $ils->retrieve_user_by_id($id);
766
767 Similar to C<retrieve_user_by_barcode> but takes the user's database
768 id rather than barcode. This is useful when you have a circulation or
769 hold and need to get information about the user's involved in the hold
770 or circulaiton.
771
772 It returns a fleshed user on success or undef on failure.
773
774 =cut
775
776 sub retrieve_user_by_id {
777     my ($self, $id) = @_;
778
779     # Do a fleshed retrieve of the patron, and flesh the fields that
780     # we would normally use.
781     my $result = $U->simplereq(
782         'open-ils.actor',
783         'open-ils.actor.user.fleshed.retrieve',
784         $self->{session}->{authtoken},
785         $id,
786         [ 'card', 'cards', 'standing_penalties', 'addresses', 'home_ou' ]
787     );
788     # Check for an error.
789     undef($result) if ($result && $U->event_code($result));
790
791     return $result;
792 }
793
794 =head2 check_user_for_problems
795
796     $problem = $ils>check_user_for_problems($user, 'HOLD, 'CIRC', 'RENEW');
797
798 This function checks if a user has a blocked profile or any from a
799 list of provided blocks. If it does, then a NCIP::Problem object is
800 returned, otherwise an undefined value is returned.
801
802 The list of blocks appears as additional arguments after the user. You
803 can provide any value(s) that might appear in a standing penalty block
804 lit in Evergreen. The example above checks for HOLD, CIRC, and
805 RENEW. Any number of such values can be provided. If none are
806 provided, the function only checks if the patron's profiles appears in
807 the object's blocked profiles list.
808
809 It stops on the first matching block, if any.
810
811 =cut
812
813 sub check_user_for_problems {
814     my $self = shift;
815     my $user = shift;
816     my @blocks = @_;
817
818     # Fill this in if we have a problem, otherwise just return it.
819     my $problem;
820
821     # First, check the user's profile.
822     if (grep {$_->id() == $user->profile()} @{$self->{blocked_profiles}}) {
823         $problem = NCIP::Problem->new(
824             {
825                 ProblemType => 'User Blocked',
826                 ProblemDetail => 'User blocked from inter-library loan',
827                 ProblemElement => 'NULL',
828                 ProblemValue => 'NULL'
829             }
830         );
831     }
832
833     # Next, check if the patron has one of the indicated blocks.
834     unless ($problem) {
835         foreach my $block (@blocks) {
836             if (grep {$_->standing_penalty->block_list() =~ /$block/} @{$user->standing_penalties()}) {
837                 $problem = NCIP::Problem->new(
838                     {
839                         ProblemType => 'User Blocked',
840                         ProblemDetail => 'User blocked from ' .
841                             ($block eq 'HOLD') ? 'holds' : (($block eq 'RENEW') ? 'renewals' :
842                                                                 (($block eq 'CIRC') ? 'checkout' : lc($block))),
843                         ProblemElement => 'NULL',
844                         ProblemValue => 'NULL'
845                     }
846                 );
847                 last;
848             }
849         }
850     }
851
852     return $problem;
853 }
854
855 =head2 retrieve_copy_details_by_barcode
856
857     $copy = $ils->retrieve_copy_details_by_barcode($copy_barcode);
858
859 Look up and retrieve some copy details by the copy barcode. This
860 method returns either a hashref with the copy details or undefined if
861 no copy exists with that barcode or if some error occurs.
862
863 The hashref has the fields copy, hold, transit, circ, volume, and mvr.
864
865 This method differs from C<retrieve_user_by_barcode> in that a copy
866 cannot be invalid if it exists and it is not always an error if no
867 copy exists. In some cases, when handling AcceptItem, we might prefer
868 there to be no copy.
869
870 =cut
871
872 sub retrieve_copy_details_by_barcode {
873     my $self = shift;
874     my $barcode = shift;
875
876     my $copy = $U->simplereq(
877         'open-ils.circ',
878         'open-ils.circ.copy_details.retrieve.barcode',
879         $self->{session}->{authtoken},
880         $barcode
881     );
882
883     # If $copy is an event, return undefined.
884     if ($copy && $U->event_code($copy)) {
885         undef($copy);
886     }
887
888     return $copy;
889 }
890
891 =head2 retrieve_org_unit_by_shortname
892
893     $org_unit = $ils->retrieve_org_unit_by_shortname($shortname);
894
895 Retrieves an org. unit from the database by shortname. Returns the
896 org. unit as a Fieldmapper object or undefined.
897
898 =cut
899
900 sub retrieve_org_unit_by_shortname {
901     my $self = shift;
902     my $shortname = shift;
903
904     my $aou = $U->simplereq(
905         'open-ils.pcrud',
906         'open-ils.pcrud.search.aou',
907         $self->{session}->{authtoken},
908         {shortname => {'=' => {transform => 'lower', value => ['lower', $shortname]}}}
909     );
910
911     return $aou;
912 }
913
914 =head2 create_precat_copy
915
916     $item_info->{
917         barcode => '312340123456789',
918         author => 'Public, John Q.',
919         title => 'Magnum Opus',
920         call_number => '005.82',
921         publisher => 'Brick House',
922         publication_date => '2014'
923     };
924
925     $item = $ils->create_precat_copy($item_info);
926
927
928 Create a "precat" copy to use for the incoming item using a hashref of
929 item information. At a minimum, the barcode, author and title fields
930 need to be filled in. The other fields are ignored if provided.
931
932 This method is called by the AcceptItem handler if the C<use_precats>
933 configuration option is turned on.
934
935 =cut
936
937 sub create_precat_copy {
938     my $self = shift;
939     my $item_info = shift;
940
941     my $item = Fieldmapper::asset::copy->new();
942     $item->barcode($item_info->{barcode});
943     $item->call_number(OILS_PRECAT_CALL_NUMBER);
944     $item->dummy_title($item_info->{title});
945     $item->dummy_author($item_info->{author});
946     $item->circ_lib($self->{session}->{work_ou}->id());
947     $item->circulate('t');
948     $item->holdable('t');
949     $item->opac_visible('f');
950     $item->deleted('f');
951     $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
952     $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
953     $item->location(1);
954     $item->status(0);
955     $item->editor($self->{session}->{user}->id());
956     $item->creator($self->{session}->{user}->id());
957     $item->isnew(1);
958
959     # Actually create it:
960     my $xact;
961     my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
962     $ses->connect();
963     eval {
964         $xact = $ses->request(
965             'open-ils.pcrud.transaction.begin',
966             $self->{session}->{authtoken}
967         )->gather(1);
968         $item = $ses->request(
969             'open-ils.pcrud.create.acp',
970             $self->{session}->{authtoken},
971             $item
972         )->gather(1);
973         $xact = $ses->request(
974             'open-ils.pcrud.transaction.commit',
975             $self->{session}->{authtoken}
976         )->gather(1);
977     };
978     if ($@) {
979         undef($item);
980         if ($xact) {
981             eval {
982                 $ses->request(
983                     'open-ils.pcrud.transaction.rollback',
984                     $self->{session}->{authtoken}
985                 )->gather(1);
986             };
987         }
988     }
989     $ses->disconnect();
990
991     return $item;
992 }
993
994 =head2 create_fuller_copy
995
996     $item_info->{
997         barcode => '31234003456789',
998         author => 'Public, John Q.',
999         title => 'Magnum Opus',
1000         call_number => '005.82',
1001         publisher => 'Brick House',
1002         publication_date => '2014'
1003     };
1004
1005     $item = $ils->create_fuller_copy($item_info);
1006
1007 Creates a skeletal bibliographic record, call number, and copy for the
1008 incoming item using a hashref with item information in it. At a
1009 minimum, the barcode, author, title, and call_number fields must be
1010 filled in.
1011
1012 This method is used by the AcceptItem handler if the C<use_precats>
1013 configuration option is NOT set.
1014
1015 =cut
1016
1017 sub create_fuller_copy {
1018     my $self = shift;
1019     my $item_info = shift;
1020
1021     my $item;
1022
1023     # We do everything in one transaction, because it should be atomic.
1024     my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
1025     $ses->connect();
1026     my $xact;
1027     eval {
1028         $xact = $ses->request(
1029             'open-ils.pcrud.transaction.begin',
1030             $self->{session}->{authtoken}
1031         )->gather(1);
1032     };
1033     if ($@) {
1034         undef($xact);
1035     }
1036
1037     # The rest depends on there being a transaction.
1038     if ($xact) {
1039
1040         # Create the MARC record.
1041         my $record = MARC::Record->new();
1042         $record->encoding('UTF-8');
1043         $record->leader('00881nam a2200193   4500');
1044         my $datespec = strftime("%Y%m%d%H%M%S.0", localtime);
1045         my @fields = ();
1046         push(@fields, MARC::Field->new('005', $datespec));
1047         push(@fields, MARC::Field->new('082', '0', '4', 'a' => $item_info->{call_number}));
1048         push(@fields, MARC::Field->new('245', '0', '0', 'a' => $item_info->{title}));
1049         # Publisher is a little trickier:
1050         if ($item_info->{publisher}) {
1051             my $pub = MARC::Field->new('260', ' ', ' ', 'a' => '[S.l.]', 'b' => $item_info->{publisher});
1052             $pub->add_subfields('c' => $item_info->{publication_date}) if ($item_info->{publication_date});
1053             push(@fields, $pub);
1054         }
1055         # We have no idea if the author is personal corporate or something else, so we use a 720.
1056         push(@fields, MARC::Field->new('720', ' ', ' ', 'a' => $item_info->{author}, '4' => 'aut'));
1057         $record->append_fields(@fields);
1058         my $marc = clean_marc($record);
1059
1060         # Create the bib object.
1061         my $bib = Fieldmapper::biblio::record_entry->new();
1062         $bib->creator($self->{session}->{user}->id());
1063         $bib->editor($self->{session}->{user}->id());
1064         $bib->source($self->{bib_source}->id());
1065         $bib->active('t');
1066         $bib->deleted('f');
1067         $bib->marc($marc);
1068         $bib->isnew(1);
1069
1070         eval {
1071             $bib = $ses->request(
1072                 'open-ils.pcrud.create.bre',
1073                 $self->{session}->{authtoken},
1074                 $bib
1075             )->gather(1);
1076         };
1077         if ($@) {
1078             undef($bib);
1079             eval {
1080                 $ses->request(
1081                     'open-ils.pcrud.transaction.rollback',
1082                     $self->{session}->{authtoken}
1083                 )->gather(1);
1084             };
1085         }
1086
1087         # Create the call number
1088         my $acn;
1089         if ($bib) {
1090             $acn = Fieldmapper::asset::call_number->new();
1091             $acn->creator($self->{session}->{user}->id());
1092             $acn->editor($self->{session}->{user}->id());
1093             $acn->label($item_info->{call_number});
1094             $acn->record($bib->id());
1095             $acn->owning_lib($self->{session}->{work_ou}->id());
1096             $acn->deleted('f');
1097             $acn->isnew(1);
1098
1099             eval {
1100                 $acn = $ses->request(
1101                     'open-ils.pcrud.create.acn',
1102                     $self->{session}->{authtoken},
1103                     $acn
1104                 )->gather(1);
1105             };
1106             if ($@) {
1107                 undef($acn);
1108                 eval {
1109                     $ses->request(
1110                         'open-ils.pcrud.transaction.rollback',
1111                         $self->{session}->{authtoken}
1112                     )->gather(1);
1113                 };
1114             }
1115         }
1116
1117         # create the copy
1118         if ($acn) {
1119             $item = Fieldmapper::asset::copy->new();
1120             $item->barcode($item_info->{barcode});
1121             $item->call_number($acn->id());
1122             $item->circ_lib($self->{session}->{work_ou}->id);
1123             $item->circulate('t');
1124             if ($self->{config}->{items}->{use_force_holds}) {
1125                 $item->holdable('f');
1126             } else {
1127                 $item->holdable('t');
1128             }
1129             $item->opac_visible('f');
1130             $item->deleted('f');
1131             $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1132             $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1133             $item->location(1);
1134             $item->status(0);
1135             $item->editor($self->{session}->{user}->id);
1136             $item->creator($self->{session}->{user}->id);
1137             $item->isnew(1);
1138
1139             eval {
1140                 $item = $ses->request(
1141                     'open-ils.pcrud.create.acp',
1142                     $self->{session}->{authtoken},
1143                     $item
1144                 )->gather(1);
1145
1146                 # Cross our fingers and commit the work.
1147                 $xact = $ses->request(
1148                     'open-ils.pcrud.transaction.commit',
1149                     $self->{session}->{authtoken}
1150                 )->gather(1);
1151             };
1152             if ($@) {
1153                 undef($item);
1154                 eval {
1155                     $ses->request(
1156                         'open-ils.pcrud.transaction.rollback',
1157                         $self->{session}->{authtoken}
1158                     )->gather(1) if ($xact);
1159                 };
1160             }
1161         }
1162     }
1163
1164     # We need to disconnect our session.
1165     $ses->disconnect();
1166
1167     # Now, we handle our asset stat_cat entries.
1168     if ($item) {
1169         # It would be nice to do these in the above transaction, but
1170         # pcrud does not support the ascecm object, yet.
1171         foreach my $entry (@{$self->{stat_cat_entries}}) {
1172             my $map = Fieldmapper::asset::stat_cat_entry_copy_map->new();
1173             $map->isnew(1);
1174             $map->stat_cat($entry->stat_cat());
1175             $map->stat_cat_entry($entry->id());
1176             $map->owning_copy($item->id());
1177             # We don't really worry if it succeeds or not.
1178             $U->simplereq(
1179                 'open-ils.circ',
1180                 'open-ils.circ.stat_cat.asset.copy_map.create',
1181                 $self->{session}->{authtoken},
1182                 $map
1183             );
1184         }
1185     }
1186
1187     return $item;
1188 }
1189
1190 =head2 place_hold
1191
1192     $hold = $ils->place_hold($item, $user, $location);
1193
1194 This function places a hold on $item for $user for pickup at
1195 $location. If location is not provided or undefined, the user's home
1196 library is used as a fallback.
1197
1198 $item can be a copy (asset::copy), volume (asset::call_number), or bib
1199 (biblio::record_entry). The appropriate hold type will be placed
1200 depending on the object.
1201
1202 On success, the method returns the object representing the hold. On
1203 failure, a NCIP::Problem object, describing the failure, is returned.
1204
1205 =cut
1206
1207 sub place_hold {
1208     my $self = shift;
1209     my $item = shift;
1210     my $user = shift;
1211     my $location = shift;
1212
1213     # If $location is undefined, use the user's home_ou, which should
1214     # have been fleshed when the user was retrieved.
1215     $location = $user->home_ou() unless ($location);
1216
1217     # $hold is the hold. $params is for the is_possible check.
1218     my ($hold, $params);
1219
1220     # Prep the hold with fields common to all hold types:
1221     $hold = Fieldmapper::action::hold_request->new();
1222     $hold->isnew(1); # Just to make sure.
1223     $hold->target($item->id());
1224     $hold->usr($user->id());
1225     $hold->pickup_lib($location->id());
1226     if (!$user->email()) {
1227         $hold->email_notify('f');
1228         $hold->phone_notify($user->day_phone()) if ($user->day_phone());
1229     } else {
1230         $hold->email_notify('t');
1231     }
1232
1233     # Ditto the params:
1234     $params = { pickup_lib => $location->id(), patronid => $user->id() };
1235
1236     if (ref($item) eq 'Fieldmapper::asset::copy') {
1237         my $type = ($self->{config}->{items}->{use_force_holds}) ? 'F' : 'C';
1238         $hold->hold_type($type);
1239         $hold->current_copy($item->id());
1240         $params->{hold_type} = $type;
1241         $params->{copy_id} = $item->id();
1242     } elsif (ref($item) eq 'Fieldmapper::asset::call_number') {
1243         $hold->hold_type('V');
1244         $params->{hold_type} = 'V';
1245         $params->{volume_id} = $item->id();
1246     } elsif (ref($item) eq 'Fieldmapper::biblio::record_entry') {
1247         $hold->hold_type('T');
1248         $params->{hold_type} = 'T';
1249         $params->{titleid} = $item->id();
1250     }
1251
1252     # Check if the hold is possible:
1253     my $r = $U->simplereq(
1254         'open-ils.circ',
1255         'open-ils.circ.title_hold.is_possible',
1256         $self->{session}->{authtoken},
1257         $params
1258     );
1259
1260     if ($r->{success}) {
1261         $hold = $U->simplereq(
1262             'open-ils.circ',
1263             'open-ils.circ.holds.create.override',
1264             $self->{session}->{authtoken},
1265             $hold
1266         );
1267         if (ref($hold) eq 'HASH') {
1268             $hold = _problem_from_event('Request Not Possible', $hold);
1269         }
1270     } elsif ($r->{last_event}) {
1271         $hold = _problem_from_event('Request Not Possible', $r->{last_event});
1272     } elsif ($r->{text_code}) {
1273         $hold = _problem_from_event('Request Not Possible', $r);
1274     } else {
1275         $hold = _problem_from_event('Request Not Possible');
1276     }
1277
1278     return $hold;
1279 }
1280
1281 =head2 delete_copy
1282
1283     $ils->delete_copy($copy);
1284
1285 Deletes the copy, and if it is owned by our work_ou and not a precat,
1286 we also delete the volume and bib on which the copy depends.
1287
1288 =cut
1289
1290 sub delete_copy {
1291     my $self = shift;
1292     my $copy = shift;
1293
1294     # Shortcut for ownership checks below.
1295     my $ou_id = $self->{session}->{work_ou}->id();
1296
1297     # First, make sure the copy is not already deleted and we own it.
1298     return undef if ($U->is_true($copy->deleted()) || $copy->circ_lib() != $ou_id);
1299
1300     # We need a transaction & connected session.
1301     my $xact;
1302     my $session = OpenSRF::AppSession->create('open-ils.pcrud');
1303     $session->connect();
1304     eval {
1305         $xact = $session->request(
1306             'open-ils.pcrud.transaction.begin',
1307             $self->{session}->{authtoken}
1308         )->gather(1);
1309     };
1310     if ($@) {
1311         undef($xact);
1312     }
1313
1314     if ($xact) {
1315         # Do the rest in one eval block.
1316         eval {
1317             # Delete the copy.
1318             my $r = $session->request(
1319                 'open-ils.pcrud.delete.acp',
1320                 $self->{session}->{authtoken},
1321                 $copy
1322             )->gather(1);
1323             # Check for volume.
1324             if ($copy->call_number() != -1) {
1325                 # Retrieve the acn object and flesh the bib.
1326                 my $acn = $session->request(
1327                     'open-ils.pcrud.retrieve.acn',
1328                     $self->{session}->{authtoken},
1329                     $copy->call_number(),
1330                     {flesh => 2, flesh_fields => {acn => ['record'], bre => ['call_numbers']}}
1331                 )->gather(1);
1332                 if ($acn) {
1333                     # Get the bib and deflesh the acn.
1334                     my $bib = $acn->record();
1335                     $acn->record($bib->id());
1336                     # Check if we own the call_number.
1337                     if ($acn->owning_lib() == $ou_id) {
1338                         $r = $session->request(
1339                             'open-ils.pcrud.delete.acn',
1340                             $self->{session}->{authtoken},
1341                             $acn
1342                         )->gather(1);
1343                         if ($r) {
1344                             # Check if we created the bib.
1345                             if ($bib->creator() == $self->{session}->{user}->id()) {
1346                                 # Check for other call numbers on the bib:
1347                                 my @vols = map {$_->id() != $acn->id() && !$U->is_true($_->deleted())} @{$bib->call_numbers()};
1348                                 unless (@vols) {
1349                                     $r = $session->request(
1350                                         'open-ils.pcrud.delete.bre',
1351                                         $self->{session}->{authtoken},
1352                                         $bib
1353                                     )->gather(1);
1354                                 }
1355                             }
1356                         }
1357                     }
1358                 }
1359             }
1360             $r = $session->request(
1361                 'open-ils.pcrud.transaction.commit',
1362                 $self->{session}->{authtoken}
1363             )->gather(1);
1364         };
1365         if ($@) {
1366             eval {
1367                 my $r = $session->request(
1368                     'open-ils.pcrud.transaction.rollback',
1369                     $self->{session}->{authtoken}
1370                 )->gather(1);
1371             }
1372         }
1373     }
1374
1375     $session->disconnect();
1376
1377     return undef;
1378 }
1379
1380 =head1 OVERRIDDEN PARENT METHODS
1381
1382 =head2 find_user_barcode
1383
1384 We dangerously override our parent's C<find_user_barcode> to return
1385 either the $barcode or a Problem object. In list context the barcode
1386 or problem will be the first argument and the id field, if any, will
1387 be the second. We also add a second, optional, argument to indicate a
1388 default value for the id field in the event of a failure to find
1389 anything at all. (Perl lets us get away with this.)
1390
1391 =cut
1392
1393 sub find_user_barcode {
1394     my $self = shift;
1395     my $request = shift;
1396     my $default = shift;
1397
1398     unless ($default) {
1399         my $message = $self->parse_request_type($request);
1400         if ($message eq 'LookupUser') {
1401             $default = 'AuthenticationInputData';
1402         } else {
1403             $default = 'UserIdentifierValue';
1404         }
1405     }
1406
1407     my ($value, $idfield) = $self->SUPER::find_user_barcode($request);
1408
1409     unless ($value) {
1410         $idfield = $default unless ($idfield);
1411         $value = NCIP::Problem->new();
1412         $value->ProblemType('Needed Data Missing');
1413         $value->ProblemDetail('Cannot find user barcode in message.');
1414         $value->ProblemElement($idfield);
1415         $value->ProblemValue('NULL');
1416     }
1417
1418     return (wantarray) ? ($value, $idfield) : $value;
1419 }
1420
1421 =head2 find_item_barcode
1422
1423 We do pretty much the same thing as with C<find_user_barcode> for
1424 C<find_item_barcode>.
1425
1426 =cut
1427
1428 sub find_item_barcode {
1429     my $self = shift;
1430     my $request = shift;
1431     my $default = shift || 'ItemIdentifierValue';
1432
1433     my ($value, $idfield) = $self->SUPER::find_item_barcode($request);
1434
1435     unless ($value) {
1436         $idfield = $default unless ($idfield);
1437         $value = NCIP::Problem->new();
1438         $value->ProblemType('Needed Data Missing');
1439         $value->ProblemDetail('Cannot find item barcode in message.');
1440         $value->ProblemElement($idfield);
1441         $value->ProblemValue('NULL');
1442     }
1443
1444     return (wantarray) ? ($value, $idfield) : $value;
1445 }
1446
1447 # private subroutines not meant to be used directly by subclasses.
1448 # Most have to do with setup and/or state checking of implementation
1449 # components.
1450
1451 # Find, load, and parse our configuration file:
1452 sub _configure {
1453     my $self = shift;
1454
1455     # Find the configuration file via variables:
1456     my $file = OILS_NCIP_CONFIG_DEFAULT;
1457     $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});
1458
1459     $self->{config} = XMLin($file, NormaliseSpace => 2,
1460                             ForceArray => ['block_profile', 'stat_cat_entry']);
1461 }
1462
1463 # Bootstrap OpenSRF::System and load the IDL.
1464 sub _bootstrap {
1465     my $self = shift;
1466
1467     my $bootstrap_config = $self->{config}->{bootstrap};
1468     OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
1469
1470     my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
1471     Fieldmapper->import(IDL => $idl);
1472 }
1473
1474 # Login and then initialize some object data based on the
1475 # configuration.
1476 sub _init {
1477     my $self = shift;
1478
1479     # Login to Evergreen.
1480     $self->login();
1481
1482     # Load the barred groups as pgt objects into a blocked_profiles
1483     # list.
1484     $self->{blocked_profiles} = [];
1485     foreach (@{$self->{config}->{patrons}->{block_profile}}) {
1486         my $pgt;
1487         if (ref $_) {
1488             $pgt = $U->simplereq(
1489                 'open-ils.pcrud',
1490                 'open-ils.pcrud.retrieve.pgt',
1491                 $self->{session}->{authtoken},
1492                 $_->{grp}
1493             );
1494         } else {
1495             $pgt = $U->simplereq(
1496                 'open-ils.pcrud',
1497                 'open-ils.pcrud.search.pgt',
1498                 $self->{session}->{authtoken},
1499                 {name => $_}
1500             );
1501         }
1502         push(@{$self->{blocked_profiles}}, $pgt) if ($pgt);
1503     }
1504
1505     # Load the bib source if we're not using precats.
1506     unless ($self->{config}->{items}->{use_precats}) {
1507         # Retrieve the default
1508         $self->{bib_source} = $U->simplereq(
1509             'open-ils.pcrud',
1510             'open-ils.pcrud.retrieve.cbs',
1511             $self->{session}->{authtoken},
1512             BIB_SOURCE_DEFAULT);
1513         my $data = $self->{config}->{items}->{bib_source};
1514         if ($data) {
1515             $data = $data->[0] if (ref($data) eq 'ARRAY');
1516             my $result;
1517             if (ref $data) {
1518                 $result = $U->simplereq(
1519                     'open-ils.pcrud',
1520                     'open-ils.pcrud.retrieve.cbs',
1521                     $self->{session}->{authtoken},
1522                     $data->{cbs}
1523                 );
1524             } else {
1525                 $result = $U->simplereq(
1526                     'open-ils.pcrud',
1527                     'open-ils.pcrud.search.cbs',
1528                     $self->{session}->{authtoken},
1529                     {source => $data}
1530                 );
1531             }
1532             $self->{bib_source} = $result if ($result);
1533         }
1534     }
1535
1536     # Load the required asset.stat_cat_entries:
1537     $self->{stat_cat_entries} = [];
1538     # First, make a regex for our ou and ancestors:
1539     my $ancestors = join("|", @{$U->get_org_ancestors($self->{session}->{work_ou}->id())});
1540     my $re = qr/(?:$ancestors)/;
1541     # Get the uniq stat_cat ids from the configuration:
1542     my @cats = uniq map {$_->{stat_cat}} @{$self->{config}->{items}->{stat_cat_entry}};
1543     # Retrieve all of the fleshed stat_cats and entries for the above.
1544     my $stat_cats = $U->simplereq(
1545         'open-ils.circ',
1546         'open-ils.circ.stat_cat.asset.retrieve.batch',
1547         $self->{session}->{authtoken},
1548         @cats
1549     );
1550     foreach my $entry (@{$self->{config}->{items}->{stat_cat_entry}}) {
1551         # Must have the stat_cat attr and the name, so we must have a
1552         # reference.
1553         next unless(ref $entry);
1554         my ($stat) = grep {$_->id() == $entry->{stat_cat}} @$stat_cats;
1555         push(@{$self->{stat_cat_entries}}, grep {$_->owner() =~ $re && $_->value() eq $entry->{content}} @{$stat->entries()});
1556     }
1557 }
1558
1559 # Standalone, "helper" functions.  These do not take an object or
1560 # class reference.
1561
1562 # Check if a user is past their expiration date.
1563 sub _expired {
1564     my $user = shift;
1565     my $expired = 0;
1566
1567     # Users might not expire.  If so, they have no expire_date.
1568     if ($user->expire_date()) {
1569         my $expires = DateTime::Format::ISO8601->parse_datetime(
1570             cleanse_ISO8601($user->expire_date())
1571         )->epoch();
1572         my $now = DateTime->now()->epoch();
1573         $expired = $now > $expires;
1574     }
1575
1576     return $expired;
1577 }
1578
1579 # Creates a NCIP Problem from an event. Takes a string for the problem
1580 # type, the event hashref, and optional arguments for the
1581 # ProblemElement and ProblemValue fields.
1582 sub _problem_from_event {
1583     my ($type, $evt, $element, $value) = @_;
1584
1585     my $detail;
1586
1587     # This block will likely need to get smarter in the near future.
1588     if ($evt) {
1589         if ($evt->{text_code} eq 'PERM_FAILURE') {
1590             $detail = 'Permission Failure: ' . $evt->{ilsperm};
1591             $detail =~ s/\.override$//;
1592         } else {
1593             $detail = 'ILS returned ' . $evt->{text_code} . ' error.';
1594         }
1595     } else {
1596         $detail = 'Detail not available.';
1597     }
1598
1599     return NCIP::Problem->new(
1600         {
1601             ProblemType => $type,
1602             ProblemDetail => $detail,
1603             ProblemElement => ($element) ? $element : 'NULL',
1604             ProblemValue => ($value) ? $value : 'NULL'
1605         }
1606     );
1607 }
1608
1609 1;