]> git.evergreen-ils.org Git - working/NCIPServer.git/blob - lib/NCIP/ILS/Evergreen.pm
Add CheckInItem support to NCIP::ILS::Evergreen.
[working/NCIPServer.git] / lib / NCIP / ILS / Evergreen.pm
1 # ---------------------------------------------------------------
2 # Copyright © 2014 Jason J.A. Stephenson <jason@sigio.com>
3 #
4 # This file is part of NCIPServer.
5 #
6 # NCIPServer is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # NCIPServer is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with NCIPServer.  If not, see <http://www.gnu.org/licenses/>.
18 # ---------------------------------------------------------------
19 package NCIP::ILS::Evergreen;
20
21 use Modern::Perl;
22 use XML::LibXML::Simple qw(XMLin);
23 use DateTime;
24 use DateTime::Format::ISO8601;
25 use Digest::MD5 qw/md5_hex/;
26 use OpenSRF::System;
27 use OpenSRF::AppSession;
28 use OpenSRF::Utils qw/:datetime/;
29 use OpenSRF::Utils::SettingsClient;
30 use OpenILS::Utils::Fieldmapper;
31 use OpenILS::Utils::Normalize qw(clean_marc);
32 use OpenILS::Application::AppUtils;
33 use OpenILS::Const qw/:const/;
34 use MARC::Record;
35 use MARC::Field;
36 use MARC::File::XML;
37 use List::MoreUtils qw/uniq/;
38 use POSIX qw/strftime/;
39
40 # We need a bunch of NCIP::* objects.
41 use NCIP::Response;
42 use NCIP::Problem;
43 use NCIP::User;
44 use NCIP::User::OptionalFields;
45 use NCIP::User::AddressInformation;
46 use NCIP::User::Id;
47 use NCIP::User::BlockOrTrap;
48 use NCIP::User::Privilege;
49 use NCIP::User::PrivilegeStatus;
50 use NCIP::StructuredPersonalUserName;
51 use NCIP::StructuredAddress;
52 use NCIP::ElectronicAddress;
53 use NCIP::RequestId;
54 use NCIP::Item::Id;
55
56 # Inherit from NCIP::ILS.
57 use parent qw(NCIP::ILS);
58
59 =head1 NAME
60
61 Evergreen - Evergreen driver for NCIPServer
62
63 =head1 SYNOPSIS
64
65     my $ils = NCIP::ILS::Evergreen->new(name => $config->{NCIP.ils.value});
66
67 =head1 DESCRIPTION
68
69 NCIP::ILS::Evergreen is the default driver for Evergreen and
70 NCIPServer. It was initially developed to work with Auto-Graphics'
71 SHAREit software using a subset of an unspecified ILL/DCB profile.
72
73 =cut
74
75 # Default values we define for things that might be missing in our
76 # runtime environment or configuration file that absolutely must have
77 # values.
78 #
79 # OILS_NCIP_CONFIG_DEFAULT is the default location to find our
80 # driver's configuration file.  This location can be overridden by
81 # setting the path in the OILS_NCIP_CONFIG environment variable.
82 #
83 # BIB_SOURCE_DEFAULT is the config.bib_source.id to use when creating
84 # "short" bibs.  It is used only if no entry is supplied in the
85 # configuration file.  The provided default is 2, the id of the
86 # "System Local" source that comes with a default Evergreen
87 # installation.
88 use constant {
89     OILS_NCIP_CONFIG_DEFAULT => '/openils/conf/oils_ncip.xml',
90     BIB_SOURCE_DEFAULT => 2
91 };
92
93 # A common Evergreen code shortcut to use AppUtils:
94 my $U = 'OpenILS::Application::AppUtils';
95
96 # The usual constructor:
97 sub new {
98     my $class = shift;
99     $class = ref($class) if (ref $class);
100
101     # Instantiate our parent with the rest of the arguments.  It
102     # creates a blessed hashref.
103     my $self = $class->SUPER::new(@_);
104
105     # Look for our configuration file, load, and parse it:
106     $self->_configure();
107
108     # Bootstrap OpenSRF and prepare some OpenILS components.
109     $self->_bootstrap();
110
111     # Initialize the rest of our internal state.
112     $self->_init();
113
114     return $self;
115 }
116
117 =head1 HANDLER METHODS
118
119 =head2 lookupuser
120
121     $ils->lookupuser($request);
122
123 Processes a LookupUser request.
124
125 =cut
126
127 sub lookupuser {
128     my $self = shift;
129     my $request = shift;
130
131     # Check our session and login if necessary.
132     $self->login() unless ($self->checkauth());
133
134     my $message_type = $self->parse_request_type($request);
135
136     # Let's go ahead and create our response object. We need this even
137     # if there is a problem.
138     my $response = NCIP::Response->new({type => $message_type . "Response"});
139     $response->header($self->make_header($request));
140
141     # Need to parse the request object to get the user barcode.
142     my ($barcode, $idfield) = $self->find_user_barcode($request);
143
144     # If we did not find a barcode, then report the problem.
145     if (ref($barcode) eq 'NCIP::Problem') {
146         $response->problem($barcode);
147         return $response;
148     }
149
150     # Look up our patron by barcode:
151     my $user = $self->retrieve_user_by_barcode($barcode, $idfield);
152     if (ref($user) eq 'NCIP::Problem') {
153         $response->problem($user);
154         return $response;
155     }
156
157     # We got the information, so lets fill in our userdata.
158     my $userdata = NCIP::User->new();
159
160     # Make an array of the user's active barcodes.
161     my $ids = [];
162     foreach my $card (@{$user->cards()}) {
163         if ($U->is_true($card->active())) {
164             my $id = NCIP::User::Id->new({
165                 UserIdentifierType => 'Barcode',
166                 UserIdentifierValue => $card->barcode()
167             });
168             push(@$ids, $id);
169         }
170     }
171     $userdata->UserId($ids);
172
173     # Check if they requested any optional fields and return those.
174     my $elements = $request->{$message_type}->{UserElementType};
175     if ($elements) {
176         $elements = [$elements] unless (ref $elements eq 'ARRAY');
177         my $optionalfields = NCIP::User::OptionalFields->new();
178
179         # First, we'll look for name information.
180         if (grep {$_ eq 'Name Information'} @$elements) {
181             my $name = NCIP::StructuredPersonalUserName->new();
182             $name->Surname($user->family_name());
183             $name->GivenName($user->first_given_name());
184             $name->Prefix($user->prefix());
185             $name->Suffix($user->suffix());
186             $optionalfields->NameInformation($name);
187         }
188
189         # Next, check for user address information.
190         if (grep {$_ eq 'User Address Information'} @$elements) {
191             my $addresses = [];
192
193             # See if the user has any valid, physcial addresses.
194             foreach my $addr (@{$user->addresses()}) {
195                 next if ($U->is_true($addr->pending()));
196                 my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>$addr->address_type()});
197                 my $physical = NCIP::StructuredAddress->new();
198                 $physical->Line1($addr->street1());
199                 $physical->Line2($addr->street2());
200                 $physical->Locality($addr->city());
201                 $physical->Region($addr->state());
202                 $physical->PostalCode($addr->post_code());
203                 $physical->Country($addr->country());
204                 $address->PhysicalAddress($physical);
205                 push @$addresses, $address;
206             }
207
208             # Right now, we're only sharing email address if the user
209             # has it. We don't share phone numbers.
210             if ($user->email()) {
211                 my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>'Email Address'});
212                 $address->ElectronicAddress(
213                     NCIP::ElectronicAddress->new({
214                         Type=>'Email Address',
215                         Data=>$user->email()
216                     })
217                 );
218                 push @$addresses, $address;
219             }
220
221             $optionalfields->UserAddressInformation($addresses);
222         }
223
224         # Check for User Privilege.
225         if (grep {$_ eq 'User Privilege'} @$elements) {
226             # Get the user's group:
227             my $pgt = $U->simplereq(
228                 'open-ils.pcrud',
229                 'open-ils.pcrud.retrieve.pgt',
230                 $self->{session}->{authtoken},
231                 $user->profile()
232             );
233             if ($pgt) {
234                 my $privilege = NCIP::User::Privilege->new();
235                 $privilege->AgencyId($user->home_ou->shortname());
236                 $privilege->AgencyUserPrivilegeType($pgt->name());
237                 $privilege->ValidToDate($user->expire_date());
238                 $privilege->ValidFromDate($user->create_date());
239
240                 my $status = 'Active';
241                 if (_expired($user)) {
242                     $status = 'Expired';
243                 } elsif ($U->is_true($user->barred())) {
244                     $status = 'Barred';
245                 } elsif (!$U->is_true($user->active())) {
246                     $status = 'Inactive';
247                 }
248                 if ($status) {
249                     $privilege->UserPrivilegeStatus(
250                         NCIP::User::PrivilegeStatus->new({
251                             UserPrivilegeStatusType => $status
252                         })
253                     );
254                 }
255
256                 $optionalfields->UserPrivilege([$privilege]);
257             }
258         }
259
260         # Check for Block Or Trap.
261         if (grep {$_ eq 'Block Or Trap'} @$elements) {
262             my $blocks = [];
263
264             # First, let's check if the profile is blocked from ILL.
265             if (grep {$_->id() == $user->profile()} @{$self->{blocked_profiles}}) {
266                 my $block = NCIP::User::BlockOrTrap->new();
267                 $block->AgencyId($user->home_ou->shortname());
268                 $block->BlockOrTrapType('Block Interlibrary Loan');
269                 push @$blocks, $block;
270             }
271
272             # Next, we loop through the user's standing penalties
273             # looking for blocks on CIRC, HOLD, and RENEW.
274             my ($have_circ, $have_renew, $have_hold) = (0,0,0);
275             foreach my $penalty (@{$user->standing_penalties()}) {
276                 next unless($penalty->standing_penalty->block_list());
277                 my @block_list = split(/\|/, $penalty->standing_penalty->block_list());
278                 my $ou = $U->simplereq(
279                     'open-ils.pcrud',
280                     'open-ils.pcrud.retrieve.aou',
281                     $self->{session}->{authtoken},
282                     $penalty->org_unit()
283                 );
284
285                 # Block checkout.
286                 if (!$have_circ && grep {$_ eq 'CIRC'} @block_list) {
287                     my $bot = NCIP::User::BlockOrTrap->new();
288                     $bot->AgencyId($ou->shortname());
289                     $bot->BlockOrTrapType('Block Checkout');
290                     push @$blocks, $bot;
291                     $have_circ = 1;
292                 }
293
294                 # Block holds.
295                 if (!$have_hold && grep {$_ eq 'HOLD' || $_ eq 'FULFILL'} @block_list) {
296                     my $bot = NCIP::User::BlockOrTrap->new();
297                     $bot->AgencyId($ou->shortname());
298                     $bot->BlockOrTrapType('Block Holds');
299                     push @$blocks, $bot;
300                     $have_hold = 1;
301                 }
302
303                 # Block renewals.
304                 if (!$have_renew && grep {$_ eq 'RENEW'} @block_list) {
305                     my $bot = NCIP::User::BlockOrTrap->new();
306                     $bot->AgencyId($ou->shortname());
307                     $bot->BlockOrTrapType('Block Renewals');
308                     push @$blocks, $bot;
309                     $have_renew = 1;
310                 }
311
312                 # Stop after we report one of each, even if more
313                 # blocks remain.
314                 last if ($have_circ && $have_renew && $have_hold);
315             }
316
317             $optionalfields->BlockOrTrap($blocks);
318         }
319
320         $userdata->UserOptionalFields($optionalfields);
321     }
322
323     $response->data($userdata);
324
325     return $response;
326 }
327
328 =head2 acceptitem
329
330     $ils->acceptitem($request);
331
332 Processes an AcceptItem request.
333
334 =cut
335
336 sub acceptitem {
337     my $self = shift;
338     my $request = shift;
339
340     # Check our session and login if necessary.
341     $self->login() unless ($self->checkauth());
342
343     # Common preparation.
344     my $message = $self->parse_request_type($request);
345     my $response = NCIP::Response->new({type => $message . 'Response'});
346     $response->header($self->make_header($request));
347
348     # We only accept holds for the time being.
349     if ($request->{$message}->{RequestedActionType} !~ /^hold\w/i) {
350         # We need the item id or we can't do anything at all.
351         my ($item_barcode, $item_idfield) = $self->find_item_barcode($request);
352         if (ref($item_barcode) eq 'NCIP::Problem') {
353             $response->problem($item_barcode);
354             return $response;
355         }
356
357         # We need to find a patron barcode or we can't look anyone up
358         # to place a hold.
359         my ($user_barcode, $user_idfield) = $self->find_user_barcode($request, 'UserIdentifierValue');
360         if (ref($user_barcode) eq 'NCIP::Problem') {
361             $response->problem($user_barcode);
362             return $response;
363         }
364         # Look up our patron by barcode:
365         my $user = $self->retrieve_user_by_barcode($user_barcode, $user_idfield);
366         if (ref($user) eq 'NCIP::Problem') {
367             $response->problem($user);
368             return $response;
369         }
370         # We're doing patron checks before looking for bibliographic
371         # information and creating the item because problems with the
372         # patron are more likely to occur.
373         my $problem = $self->check_user_for_problems($user, 'HOLD');
374         if ($problem) {
375             $response->problem($problem);
376             return $response;
377         }
378
379         # Check if the item barcode already exists:
380         my $item = $self->retrieve_copy_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     # Look for a circulation and examine its information:
538     my $circ = $details->{circ};
539     if (!$circ || $circ->checkin_time()) {
540         # Item isn't checked out.
541         $response->problem(
542             NCIP::Problem->new(
543                 {
544                     ProblemType => 'Item Not Checked Out',
545                     ProblemDetail => "Item with barcode $barcode not checkout out.",
546                     ProblemElement => $item_idfield,
547                     ProblemValue => $item_barcode
548                 }
549             )
550         );
551     } else {
552         # Isolate the copy.
553         my $copy = $details->{copy};
554
555         # Get data on the patron who has it checked out.
556         my $user = $self->retrieve_user_by_id($details->{circ}->usr());
557
558         # At some point in the future, we should probably check if the
559         # request contains a user barcode. We would then look that
560         # user up, too, and make sure it is the same user that has the
561         # item checked out. If not, we would report a
562         # problem. However, the user id is optional in the CheckInItem
563         # message, and it doesn't look like our target system sends
564         # it.
565
566         # Checkin parameters. We want to skip hold targeting or making
567         # transits, to force the checkin despite the copy status, as
568         # well as void overdues.
569         my $params = {
570             barcode => $copy->barcode(),
571             force => 1,
572             noop => 1,
573             void_overdues => 1
574         };
575         my $result = $U->simplereq(
576             'open-ils.circ',
577             'open-ils.circ.checkin.override',
578             $self->{session}->{authtoken},
579             $params
580         );
581
582         # We should check for errors here, but I'll leave that for
583         # later.
584
585         my $data = {
586             ItemId => NCIP::Item::Id->new(
587                 {
588                     AgencyId => $request->{$message}->{ItemId}->{AgencyId},
589                     ItemIdentifierType => $request->{$message}->{ItemId}->{ItemIdentifierType},
590                     ItemIdentifierValue => $request->{$message}->{ItemId}->{ItemIdentifierValue}
591                 }
592             ),
593             UserId => NCIP::User::Id->new(
594                 {
595                     UserIdentifierType => 'Barcode Id',
596                     UserIdentifierValue => $user->card->barcode()
597                 }
598             )
599         };
600
601         $response->data($data);
602
603         # At some point in the future, we should probably check if
604         # they requested optional user or item elements and return
605         # those. For the time being, we ignore those at the risk of
606         # being considered non-compliant.
607     }
608
609     return $response
610 }
611
612 =head1 METHODS USEFUL to SUBCLASSES
613
614 =head2 login
615
616     $ils->login();
617
618 Login to Evergreen via OpenSRF. It uses internal state from the
619 configuration file to login.
620
621 =cut
622
623 # Login via OpenSRF to Evergreen.
624 sub login {
625     my $self = shift;
626
627     # Get the authentication seed.
628     my $seed = $U->simplereq(
629         'open-ils.auth',
630         'open-ils.auth.authenticate.init',
631         $self->{config}->{credentials}->{username}
632     );
633
634     # Actually login.
635     if ($seed) {
636         my $response = $U->simplereq(
637             'open-ils.auth',
638             'open-ils.auth.authenticate.complete',
639             {
640                 username => $self->{config}->{credentials}->{username},
641                 password => md5_hex(
642                     $seed . md5_hex($self->{config}->{credentials}->{password})
643                 ),
644                 type => 'staff',
645                 workstation => $self->{config}->{credentials}->{workstation}
646             }
647         );
648         if ($response) {
649             $self->{session}->{authtoken} = $response->{payload}->{authtoken};
650             $self->{session}->{authtime} = $response->{payload}->{authtime};
651
652             # Set/reset the work_ou and user data in case something changed.
653
654             # Retrieve the work_ou as an object.
655             $self->{session}->{work_ou} = $U->simplereq(
656                 'open-ils.pcrud',
657                 'open-ils.pcrud.search.aou',
658                 $self->{session}->{authtoken},
659                 {shortname => $self->{config}->{credentials}->{work_ou}}
660             );
661
662             # We need the user information in order to do some things.
663             $self->{session}->{user} = $U->check_user_session($self->{session}->{authtoken});
664
665         }
666     }
667 }
668
669 =head2 checkauth
670
671     $valid = $ils->checkauth();
672
673 Returns 1 if the object a 'valid' authtoken, 0 if not.
674
675 =cut
676
677 sub checkauth {
678     my $self = shift;
679
680     # We use AppUtils to do the heavy lifting.
681     if (defined($self->{session})) {
682         if ($U->check_user_session($self->{session}->{authtoken})) {
683             return 1;
684         } else {
685             return 0;
686         }
687     }
688
689     # If we reach here, we don't have a session, so we are definitely
690     # not logged in.
691     return 0;
692 }
693
694 =head2 retrieve_user_by_barcode
695
696     $user = $ils->retrieve_user_by_barcode($user_barcode, $user_idfield);
697
698 Do a fleshed retrieve of a patron by barcode. Return the patron if
699 found and valid. Return a NCIP::Problem of 'Unknown User' otherwise.
700
701 The id field argument is used for the ProblemElement field in the
702 NCIP::Problem object.
703
704 An invalid patron is one where the barcode is not found in the
705 database, the patron is deleted, or the barcode used to retrieve the
706 patron is not active. The problem element is also returned if an error
707 occurs during the retrieval.
708
709 =cut
710
711 sub retrieve_user_by_barcode {
712     my ($self, $barcode, $idfield) = @_;
713     my $result = $U->simplereq(
714         'open-ils.actor',
715         'open-ils.actor.user.fleshed.retrieve_by_barcode',
716         $self->{session}->{authtoken},
717         $barcode,
718         1
719     );
720
721     # Check for a failure, or a deleted, inactive, or expired user,
722     # and if so, return empty userdata.
723     if (!$result || $U->event_code($result) || $U->is_true($result->deleted())
724             || !grep {$_->barcode() eq $barcode && $U->is_true($_->active())} @{$result->cards()}) {
725
726         my $problem = NCIP::Problem->new();
727         $problem->ProblemType('Unknown User');
728         $problem->ProblemDetail("User with barcode $barcode unknown");
729         $problem->ProblemElement($idfield);
730         $problem->ProblemValue($barcode);
731         $result = $problem;
732     }
733
734     return $result;
735 }
736
737 =head2 retrieve_user_by_id
738
739     $user = $ils->retrieve_user_by_id($id);
740
741 Similar to C<retrieve_user_by_barcode> but takes the user's database
742 id rather than barcode. This is useful when you have a circulation or
743 hold and need to get information about the user's involved in the hold
744 or circulaiton.
745
746 It returns a fleshed user on success or undef on failure.
747
748 =cut
749
750 sub retrieve_user_by_id {
751     my ($self, $id) = @_;
752
753     # Do a fleshed retrieve of the patron, and flesh the fields that
754     # we would normally use.
755     my $result = $U->simplereq(
756         'open-ils.actor',
757         'open-ils.actor.user.fleshed.retrieve',
758         $self->{session}->{authtoken},
759         $id,
760         [ 'card', 'cards', 'standing_penalties', 'addresses', 'home_ou' ]
761     );
762     # Check for an error.
763     undef($result) if ($result && $U->event_code($result));
764
765     return $result;
766 }
767
768 =head2 check_user_for_problems
769
770     $problem = $ils>check_user_for_problems($user, 'HOLD, 'CIRC', 'RENEW');
771
772 This function checks if a user has a blocked profile or any from a
773 list of provided blocks. If it does, then a NCIP::Problem object is
774 returned, otherwise an undefined value is returned.
775
776 The list of blocks appears as additional arguments after the user. You
777 can provide any value(s) that might appear in a standing penalty block
778 lit in Evergreen. The example above checks for HOLD, CIRC, and
779 RENEW. Any number of such values can be provided. If none are
780 provided, the function only checks if the patron's profiles appears in
781 the object's blocked profiles list.
782
783 It stops on the first matching block, if any.
784
785 =cut
786
787 sub check_user_for_problems {
788     my $self = shift;
789     my $user = shift;
790     my @blocks = @_;
791
792     # Fill this in if we have a problem, otherwise just return it.
793     my $problem;
794
795     # First, check the user's profile.
796     if (grep {$_->id() == $user->profile()} @{$self->{blocked_profiles}}) {
797         $problem = NCIP::Problem->new(
798             {
799                 ProblemType => 'User Blocked',
800                 ProblemDetail => 'User blocked from inter-library loan',
801                 ProblemElement => 'NULL',
802                 ProblemValue => 'NULL'
803             }
804         );
805     }
806
807     # Next, check if the patron has one of the indicated blocks.
808     unless ($problem) {
809         foreach my $block (@blocks) {
810             if (grep {$_->standing_penalty->block_list() =~ /$block/} @{$user->standing_penalties()}) {
811                 $problem = NCIP::Problem->new(
812                     {
813                         ProblemType => 'User Blocked',
814                         ProblemDetail => 'User blocked from ' .
815                             ($block eq 'HOLD') ? 'holds' : (($block eq 'RENEW') ? 'renewals' :
816                                                                 (($block eq 'CIRC') ? 'checkout' : lc($block))),
817                         ProblemElement => 'NULL',
818                         ProblemValue => 'NULL'
819                     }
820                 );
821                 last;
822             }
823         }
824     }
825
826     return $problem;
827 }
828
829 =head2 retrieve_copy_details_by_barcode
830
831     $copy = $ils->retrieve_copy_details_by_barcode($copy_barcode);
832
833 Look up and retrieve some copy details by the copy barcode. This
834 method returns either a hashref with the copy details or undefined if
835 no copy exists with that barcode or if some error occurs.
836
837 The hashref has the fields copy, hold, transit, circ, volume, and mvr.
838
839 This method differs from C<retrieve_user_by_barcode> in that a copy
840 cannot be invalid if it exists and it is not always an error if no
841 copy exists. In some cases, when handling AcceptItem, we might prefer
842 there to be no copy.
843
844 =cut
845
846 sub retrieve_copy_details_by_barcode {
847     my $self = shift;
848     my $barcode = shift;
849
850     my $copy = $U->simplereq(
851         'open-ils.circ',
852         'open-ils.circ.copy_details.retrieve.barcode',
853         $self->{session}->{authtoken},
854         $barcode
855     );
856
857     # If $copy is an event, return undefined.
858     if ($copy && $U->event_code($copy)) {
859         undef($copy);
860     }
861
862     return $copy;
863 }
864
865 =head2 retrieve_org_unit_by_shortname
866
867     $org_unit = $ils->retrieve_org_unit_by_shortname($shortname);
868
869 Retrieves an org. unit from the database by shortname. Returns the
870 org. unit as a Fieldmapper object or undefined.
871
872 =cut
873
874 sub retrieve_org_unit_by_shortname {
875     my $self = shift;
876     my $shortname = shift;
877
878     my $aou = $U->simplereq(
879         'open-ils.pcrud',
880         'open-ils.pcrud.search.aou',
881         $self->{session}->{authtoken},
882         {shortname => {'=' => {transform => 'lower', value => ['lower', $shortname]}}}
883     );
884
885     return $aou;
886 }
887
888 =head2 create_precat_copy
889
890     $item_info->{
891         barcode => '312340123456789',
892         author => 'Public, John Q.',
893         title => 'Magnum Opus',
894         call_number => '005.82',
895         publisher => 'Brick House',
896         publication_date => '2014'
897     };
898
899     $item = $ils->create_precat_copy($item_info);
900
901
902 Create a "precat" copy to use for the incoming item using a hashref of
903 item information. At a minimum, the barcode, author and title fields
904 need to be filled in. The other fields are ignored if provided.
905
906 This method is called by the AcceptItem handler if the C<use_precats>
907 configuration option is turned on.
908
909 =cut
910
911 sub create_precat_copy {
912     my $self = shift;
913     my $item_info = shift;
914
915     my $item = Fieldmapper::asset::copy->new();
916     $item->barcode($item_info->{barcode});
917     $item->call_number(OILS_PRECAT_CALL_NUMBER);
918     $item->dummy_title($item_info->{title});
919     $item->dummy_author($item_info->{author});
920     $item->circ_lib($self->{session}->{work_ou}->id());
921     $item->circulate('t');
922     $item->holdable('t');
923     $item->opac_visible('f');
924     $item->deleted('f');
925     $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
926     $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
927     $item->location(1);
928     $item->status(0);
929     $item->editor($self->{session}->{user}->id());
930     $item->creator($self->{session}->{user}->id());
931     $item->isnew(1);
932
933     # Actually create it:
934     my $xact;
935     my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
936     $ses->connect();
937     eval {
938         $xact = $ses->request(
939             'open-ils.pcrud.transaction.begin',
940             $self->{session}->{authtoken}
941         )->gather(1);
942         $item = $ses->request(
943             'open-ils.pcrud.create.acp',
944             $self->{session}->{authtoken},
945             $item
946         )->gather(1);
947         $xact = $ses->request(
948             'open-ils.pcrud.transaction.commit',
949             $self->{session}->{authtoken}
950         )->gather(1);
951     };
952     if ($@) {
953         undef($item);
954         if ($xact) {
955             eval {
956                 $ses->request(
957                     'open-ils.pcrud.transaction.rollback',
958                     $self->{session}->{authtoken}
959                 )->gather(1);
960             };
961         }
962     }
963     $ses->disconnect();
964
965     return $item;
966 }
967
968 =head2 create_fuller_copy
969
970     $item_info->{
971         barcode => '31234003456789',
972         author => 'Public, John Q.',
973         title => 'Magnum Opus',
974         call_number => '005.82',
975         publisher => 'Brick House',
976         publication_date => '2014'
977     };
978
979     $item = $ils->create_fuller_copy($item_info);
980
981 Creates a skeletal bibliographic record, call number, and copy for the
982 incoming item using a hashref with item information in it. At a
983 minimum, the barcode, author, title, and call_number fields must be
984 filled in.
985
986 This method is used by the AcceptItem handler if the C<use_precats>
987 configuration option is NOT set.
988
989 =cut
990
991 sub create_fuller_copy {
992     my $self = shift;
993     my $item_info = shift;
994
995     my $item;
996
997     # We do everything in one transaction, because it should be atomic.
998     my $ses = OpenSRF::AppSession->create('open-ils.pcrud');
999     $ses->connect();
1000     my $xact;
1001     eval {
1002         $xact = $ses->request(
1003             'open-ils.pcrud.transaction.begin',
1004             $self->{session}->{authtoken}
1005         )->gather(1);
1006     };
1007     if ($@) {
1008         undef($xact);
1009     }
1010
1011     # The rest depends on there being a transaction.
1012     if ($xact) {
1013
1014         # Create the MARC record.
1015         my $record = MARC::Record->new();
1016         $record->encoding('UTF-8');
1017         $record->leader('00881nam a2200193   4500');
1018         my $datespec = strftime("%Y%m%d%H%M%S.0", localtime);
1019         my @fields = ();
1020         push(@fields, MARC::Field->new('005', $datespec));
1021         push(@fields, MARC::Field->new('082', '0', '4', 'a' => $item_info->{call_number}));
1022         push(@fields, MARC::Field->new('245', '0', '0', 'a' => $item_info->{title}));
1023         # Publisher is a little trickier:
1024         if ($item_info->{publisher}) {
1025             my $pub = MARC::Field->new('260', ' ', ' ', 'a' => '[S.l.]', 'b' => $item_info->{publisher});
1026             $pub->add_subfields('c' => $item_info->{publication_date}) if ($item_info->{publication_date});
1027             push(@fields, $pub);
1028         }
1029         # We have no idea if the author is personal corporate or something else, so we use a 720.
1030         push(@fields, MARC::Field->new('720', ' ', ' ', 'a' => $item_info->{author}, '4' => 'aut'));
1031         $record->append_fields(@fields);
1032         my $marc = clean_marc($record);
1033
1034         # Create the bib object.
1035         my $bib = Fieldmapper::biblio::record_entry->new();
1036         $bib->creator($self->{session}->{user}->id());
1037         $bib->editor($self->{session}->{user}->id());
1038         $bib->source($self->{bib_source}->id());
1039         $bib->active('t');
1040         $bib->deleted('f');
1041         $bib->marc($marc);
1042         $bib->isnew(1);
1043
1044         eval {
1045             $bib = $ses->request(
1046                 'open-ils.pcrud.create.bre',
1047                 $self->{session}->{authtoken},
1048                 $bib
1049             )->gather(1);
1050         };
1051         if ($@) {
1052             undef($bib);
1053             eval {
1054                 $ses->request(
1055                     'open-ils.pcrud.transaction.rollback',
1056                     $self->{session}->{authtoken}
1057                 )->gather(1);
1058             };
1059         }
1060
1061         # Create the call number
1062         my $acn;
1063         if ($bib) {
1064             $acn = Fieldmapper::asset::call_number->new();
1065             $acn->creator($self->{session}->{user}->id());
1066             $acn->editor($self->{session}->{user}->id());
1067             $acn->label($item_info->{call_number});
1068             $acn->record($bib->id());
1069             $acn->owning_lib($self->{session}->{work_ou}->id());
1070             $acn->deleted('f');
1071             $acn->isnew(1);
1072
1073             eval {
1074                 $acn = $ses->request(
1075                     'open-ils.pcrud.create.acn',
1076                     $self->{session}->{authtoken},
1077                     $acn
1078                 )->gather(1);
1079             };
1080             if ($@) {
1081                 undef($acn);
1082                 eval {
1083                     $ses->request(
1084                         'open-ils.pcrud.transaction.rollback',
1085                         $self->{session}->{authtoken}
1086                     )->gather(1);
1087                 };
1088             }
1089         }
1090
1091         # create the copy
1092         if ($acn) {
1093             $item = Fieldmapper::asset::copy->new();
1094             $item->barcode($item_info->{barcode});
1095             $item->call_number($acn->id());
1096             $item->circ_lib($self->{session}->{work_ou}->id);
1097             $item->circulate('t');
1098             $item->holdable('t');
1099             $item->opac_visible('f');
1100             $item->deleted('f');
1101             $item->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1102             $item->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1103             $item->location(1);
1104             $item->status(0);
1105             $item->editor($self->{session}->{user}->id);
1106             $item->creator($self->{session}->{user}->id);
1107             $item->isnew(1);
1108
1109             eval {
1110                 $item = $ses->request(
1111                     'open-ils.pcrud.create.acp',
1112                     $self->{session}->{authtoken},
1113                     $item
1114                 )->gather(1);
1115
1116                 # Cross our fingers and commit the work.
1117                 $xact = $ses->request(
1118                     'open-ils.pcrud.transaction.commit',
1119                     $self->{session}->{authtoken}
1120                 )->gather(1);
1121             };
1122             if ($@) {
1123                 undef($item);
1124                 eval {
1125                     $ses->request(
1126                         'open-ils.pcrud.transaction.rollback',
1127                         $self->{session}->{authtoken}
1128                     )->gather(1) if ($xact);
1129                 };
1130             }
1131         }
1132     }
1133
1134     # We need to disconnect our session.
1135     $ses->disconnect();
1136
1137     # Now, we handle our asset stat_cat entries.
1138     if ($item) {
1139         # It would be nice to do these in the above transaction, but
1140         # pcrud does not support the ascecm object, yet.
1141         foreach my $entry (@{$self->{stat_cat_entries}}) {
1142             my $map = Fieldmapper::asset::stat_cat_entry_copy_map->new();
1143             $map->isnew(1);
1144             $map->stat_cat($entry->stat_cat());
1145             $map->stat_cat_entry($entry->id());
1146             $map->owning_copy($item->id());
1147             # We don't really worry if it succeeds or not.
1148             $U->simplereq(
1149                 'open-ils.circ',
1150                 'open-ils.circ.stat_cat.asset.copy_map.create',
1151                 $self->{session}->{authtoken},
1152                 $map
1153             );
1154         }
1155     }
1156
1157     return $item;
1158 }
1159
1160 =head2 place_hold
1161
1162     $hold = $ils->place_hold($item, $user, $location);
1163
1164 This function places a hold on $item for $user for pickup at
1165 $location. If location is not provided or undefined, the user's home
1166 library is used as a fallback.
1167
1168 $item can be a copy (asset::copy), volume (asset::call_number), or bib
1169 (biblio::record_entry). The appropriate hold type will be placed
1170 depending on the object.
1171
1172 On success, the method returns the object representing the hold. On
1173 failure, a NCIP::Problem object, describing the failure, is returned.
1174
1175 =cut
1176
1177 sub place_hold {
1178     my $self = shift;
1179     my $item = shift;
1180     my $user = shift;
1181     my $location = shift;
1182
1183     # If $location is undefined, use the user's home_ou, which should
1184     # have been fleshed when the user was retrieved.
1185     $location = $user->home_ou() unless ($location);
1186
1187     # $hold is the hold. $params is for the is_possible check.
1188     my ($hold, $params);
1189
1190     # Prep the hold with fields common to all hold types:
1191     $hold = Fieldmapper::action::hold_request->new();
1192     $hold->isnew(1); # Just to make sure.
1193     $hold->target($item->id());
1194     $hold->usr($user->id());
1195     $hold->pickup_lib($location->id());
1196     if (!$user->email()) {
1197         $hold->email_notify('f');
1198         $hold->phone_notify($user->day_phone()) if ($user->day_phone());
1199     } else {
1200         $hold->email_notify('t');
1201     }
1202
1203     # Ditto the params:
1204     $params = { pickup_lib => $location->id(), patronid => $user->id() };
1205
1206     if (ref($item) eq 'Fieldmapper::asset::copy') {
1207         $hold->hold_type('C');
1208         $hold->current_copy($item->id());
1209         $params->{hold_type} = 'C';
1210         $params->{copy_id} = $item->id();
1211     } elsif (ref($item) eq 'Fieldmapper::asset::call_number') {
1212         $hold->hold_type('V');
1213         $params->{hold_type} = 'V';
1214         $params->{volume_id} = $item->id();
1215     } elsif (ref($item) eq 'Fieldmapper::biblio::record_entry') {
1216         $hold->hold_type('T');
1217         $params->{hold_type} = 'T';
1218         $params->{titleid} = $item->id();
1219     }
1220
1221     # Check if the hold is possible:
1222     my $r = $U->simplereq(
1223         'open-ils.circ',
1224         'open-ils.circ.title_hold.is_possible',
1225         $self->{session}->{authtoken},
1226         $params
1227     );
1228
1229     if ($r->{success}) {
1230         $hold = $U->simplereq(
1231             'open-ils.circ',
1232             'open-ils.circ.holds.create.override',
1233             $self->{session}->{authtoken},
1234             $hold
1235         );
1236         if (ref($hold) eq 'HASH') {
1237             $hold = _problem_from_event('Request Not Possible', $hold);
1238         }
1239     } elsif ($r->{last_event}) {
1240         $hold = _problem_from_event('Request Not Possible', $r->{last_event});
1241     } elsif ($r->{text_code}) {
1242         $hold = _problem_from_event('Request Not Possible', $r);
1243     } else {
1244         $hold = _problem_from_event('Request Not Possible');
1245     }
1246
1247     return $hold;
1248 }
1249
1250 =head1 OVERRIDDEN PARENT METHODS
1251
1252 =head2 find_user_barcode
1253
1254 We dangerously override our parent's C<find_user_barcode> to return
1255 either the $barcode or a Problem object. In list context the barcode
1256 or problem will be the first argument and the id field, if any, will
1257 be the second. We also add a second, optional, argument to indicate a
1258 default value for the id field in the event of a failure to find
1259 anything at all. (Perl lets us get away with this.)
1260
1261 =cut
1262
1263 sub find_user_barcode {
1264     my $self = shift;
1265     my $request = shift;
1266     my $default = shift;
1267
1268     unless ($default) {
1269         my $message = $self->parse_request_type($request);
1270         if ($message eq 'LookupUser') {
1271             $default = 'AuthenticationInputData';
1272         } else {
1273             $default = 'UserIdentifierValue';
1274         }
1275     }
1276
1277     my ($value, $idfield) = $self->SUPER::find_user_barcode($request);
1278
1279     unless ($value) {
1280         $idfield = $default unless ($idfield);
1281         $value = NCIP::Problem->new();
1282         $value->ProblemType('Needed Data Missing');
1283         $value->ProblemDetail('Cannot find user barcode in message.');
1284         $value->ProblemElement($idfield);
1285         $value->ProblemValue('NULL');
1286     }
1287
1288     return (wantarray) ? ($value, $idfield) : $value;
1289 }
1290
1291 =head2 find_item_barcode
1292
1293 We do pretty much the same thing as with C<find_user_barcode> for
1294 C<find_item_barcode>.
1295
1296 =cut
1297
1298 sub find_item_barcode {
1299     my $self = shift;
1300     my $request = shift;
1301     my $default = shift || 'ItemIdentifierValue';
1302
1303     my ($value, $idfield) = $self->SUPER::find_item_barcode($request);
1304
1305     unless ($value) {
1306         $idfield = $default unless ($idfield);
1307         $value = NCIP::Problem->new();
1308         $value->ProblemType('Needed Data Missing');
1309         $value->ProblemDetail('Cannot find item barcode in message.');
1310         $value->ProblemElement($idfield);
1311         $value->ProblemValue('NULL');
1312     }
1313
1314     return (wantarray) ? ($value, $idfield) : $value;
1315 }
1316
1317 # private subroutines not meant to be used directly by subclasses.
1318 # Most have to do with setup and/or state checking of implementation
1319 # components.
1320
1321 # Find, load, and parse our configuration file:
1322 sub _configure {
1323     my $self = shift;
1324
1325     # Find the configuration file via variables:
1326     my $file = OILS_NCIP_CONFIG_DEFAULT;
1327     $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});
1328
1329     $self->{config} = XMLin($file, NormaliseSpace => 2,
1330                             ForceArray => ['block_profile', 'stat_cat_entry']);
1331 }
1332
1333 # Bootstrap OpenSRF::System and load the IDL.
1334 sub _bootstrap {
1335     my $self = shift;
1336
1337     my $bootstrap_config = $self->{config}->{bootstrap};
1338     OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
1339
1340     my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
1341     Fieldmapper->import(IDL => $idl);
1342 }
1343
1344 # Login and then initialize some object data based on the
1345 # configuration.
1346 sub _init {
1347     my $self = shift;
1348
1349     # Login to Evergreen.
1350     $self->login();
1351
1352     # Load the barred groups as pgt objects into a blocked_profiles
1353     # list.
1354     $self->{blocked_profiles} = [];
1355     foreach (@{$self->{config}->{patrons}->{block_profile}}) {
1356         my $pgt;
1357         if (ref $_) {
1358             $pgt = $U->simplereq(
1359                 'open-ils.pcrud',
1360                 'open-ils.pcrud.retrieve.pgt',
1361                 $self->{session}->{authtoken},
1362                 $_->{grp}
1363             );
1364         } else {
1365             $pgt = $U->simplereq(
1366                 'open-ils.pcrud',
1367                 'open-ils.pcrud.search.pgt',
1368                 $self->{session}->{authtoken},
1369                 {name => $_}
1370             );
1371         }
1372         push(@{$self->{blocked_profiles}}, $pgt) if ($pgt);
1373     }
1374
1375     # Load the bib source if we're not using precats.
1376     unless ($self->{config}->{items}->{use_precats}) {
1377         # Retrieve the default
1378         $self->{bib_source} = $U->simplereq(
1379             'open-ils.pcrud',
1380             'open-ils.pcrud.retrieve.cbs',
1381             $self->{session}->{authtoken},
1382             BIB_SOURCE_DEFAULT);
1383         my $data = $self->{config}->{items}->{bib_source};
1384         if ($data) {
1385             $data = $data->[0] if (ref($data) eq 'ARRAY');
1386             my $result;
1387             if (ref $data) {
1388                 $result = $U->simplereq(
1389                     'open-ils.pcrud',
1390                     'open-ils.pcrud.retrieve.cbs',
1391                     $self->{session}->{authtoken},
1392                     $data->{cbs}
1393                 );
1394             } else {
1395                 $result = $U->simplereq(
1396                     'open-ils.pcrud',
1397                     'open-ils.pcrud.search.cbs',
1398                     $self->{session}->{authtoken},
1399                     {source => $data}
1400                 );
1401             }
1402             $self->{bib_source} = $result if ($result);
1403         }
1404     }
1405
1406     # Load the required asset.stat_cat_entries:
1407     $self->{stat_cat_entries} = [];
1408     # First, make a regex for our ou and ancestors:
1409     my $ancestors = join("|", @{$U->get_org_ancestors($self->{session}->{work_ou}->id())});
1410     my $re = qr/(?:$ancestors)/;
1411     # Get the uniq stat_cat ids from the configuration:
1412     my @cats = uniq map {$_->{stat_cat}} @{$self->{config}->{items}->{stat_cat_entry}};
1413     # Retrieve all of the fleshed stat_cats and entries for the above.
1414     my $stat_cats = $U->simplereq(
1415         'open-ils.circ',
1416         'open-ils.circ.stat_cat.asset.retrieve.batch',
1417         $self->{session}->{authtoken},
1418         @cats
1419     );
1420     foreach my $entry (@{$self->{config}->{items}->{stat_cat_entry}}) {
1421         # Must have the stat_cat attr and the name, so we must have a
1422         # reference.
1423         next unless(ref $entry);
1424         my ($stat) = grep {$_->id() == $entry->{stat_cat}} @$stat_cats;
1425         push(@{$self->{stat_cat_entries}}, grep {$_->owner() =~ $re && $_->value() eq $entry->{content}} @{$stat->entries()});
1426     }
1427 }
1428
1429 # Standalone, "helper" functions.  These do not take an object or
1430 # class reference.
1431
1432 # Check if a user is past their expiration date.
1433 sub _expired {
1434     my $user = shift;
1435     my $expired = 0;
1436
1437     # Users might not expire.  If so, they have no expire_date.
1438     if ($user->expire_date()) {
1439         my $expires = DateTime::Format::ISO8601->parse_datetime(
1440             cleanse_ISO8601($user->expire_date())
1441         )->epoch();
1442         my $now = DateTime->now()->epoch();
1443         $expired = $now > $expires;
1444     }
1445
1446     return $expired;
1447 }
1448
1449 # Creates a NCIP Problem from an event. Takes a string for the problem
1450 # type, the event hashref, and optional arguments for the
1451 # ProblemElement and ProblemValue fields.
1452 sub _problem_from_event {
1453     my ($type, $evt, $element, $value) = @_;
1454
1455     my $detail;
1456
1457     # This block will likely need to get smarter in the near future.
1458     if ($evt) {
1459         if ($evt->{text_code} eq 'PERM_FAILURE') {
1460             $detail = 'Permission Failure: ' . $evt->{ilsperm};
1461             $detail =~ s/\.override$//;
1462         } else {
1463             $detail = 'ILS returned ' . $evt->{text_code} . ' error.';
1464         }
1465     } else {
1466         $detail = 'Detail not available.';
1467     }
1468
1469     return NCIP::Problem->new(
1470         {
1471             ProblemType => $type,
1472             ProblemDetail => $detail,
1473             ProblemElement => ($element) ? $element : 'NULL',
1474             ProblemValue => ($value) ? $value : 'NULL'
1475         }
1476     );
1477 }
1478
1479 1;