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