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