Remove unused XML::XPath dependency
[sitka/iNCIPit.git] / iNCIPit.cgi
1 #! /usr/bin/perl 
2
3 # This file is part of iNCIPit
4 #
5 # iNCIPit is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # iNCIPit is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
13 # License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with iNCIPit. If not, see <http://www.gnu.org/licenses/>.
17
18 use warnings;
19 use strict;
20 use XML::LibXML;
21 use CGI::XMLPost;
22 use HTML::Entities;
23 use CGI::Carp;
24 use OpenSRF::System;
25 use OpenSRF::Utils::SettingsClient;
26 use Digest::MD5 qw/md5_hex/;
27 use OpenILS::Utils::Fieldmapper;
28 use OpenILS::Utils::CStoreEditor qw/:funcs/;
29 use OpenILS::Const qw/:const/;
30 use Scalar::Util qw(reftype blessed);
31 use MARC::Record;
32 use MARC::Field;
33 use MARC::File::XML;
34 use POSIX qw/strftime/;
35 use DateTime;
36 use Config::Tiny;
37
38 my $U = "OpenILS::Application::AppUtils";
39
40 my $conf = load_config( 'iNCIPit.ini' );
41
42 # Set some variables from config (or defaults)
43 my $patron_id_type;
44
45 if ($conf->{behavior}->{patron_id_as_identifier} =~ m/^yes$/i) {
46     $patron_id_type = "id";
47 } else {
48     $patron_id_type = "barcode";
49 }
50
51 # reject non-https access unless configured otherwise
52 unless ($conf->{access}->{permit_plaintext} =~ m/^yes$/i) {
53     unless (defined($ENV{HTTPS}) && $ENV{HTTPS} eq 'on') {
54         print "Content-type: text/plain\n\n";
55         print "Access denied.\n";
56         exit 0;
57     }
58 }
59
60 # TODO: support for multiple load balancer IPs
61 my $lb_ip = $conf->{access}->{load_balancer_ip};
62
63 # if we are behind a load balancer, check to see that the
64 # actual client IP is permitted
65 if ($lb_ip) {
66     my @allowed_ips = split(/ *, */, $conf->{access}->{allowed_client_ips});
67
68     my $forwarded = $ENV{HTTP_X_FORWARDED_FOR};
69     my $ok = 0;
70
71     foreach my $check_ip (@allowed_ips) {
72         $ok = 1 if ($check_ip eq $forwarded);
73     }
74
75     # if we have a load balancer IP and are relying on
76     # X-Forwarded-For, deny requests other than those
77     # from the load balancer
78     # TODO: support for chained X-Forwarded-For -- ignore all but last
79     unless ($ok && $ENV{REMOTE_ADDR} eq $lb_ip) {
80         print "Content-type: text/plain\n\n";
81         print "Access denied.\n";
82         exit 0;
83     }
84 }
85
86 my $xmlpost = CGI::XMLPost->new();
87 my $xml     = $xmlpost->data();
88
89 # log posted data
90 # XXX: posted ncip message log filename should be in config.
91 open POST_DATA, ">>post_data.txt";
92 print POST_DATA $xml;
93 close POST_DATA;
94
95 # initialize the parser
96 my $parser = new XML::LibXML;
97 my $doc = $parser->load_xml( string => $xml );
98
99 my %session = login();
100
101 if ( defined( $session{authtoken} ) ) {
102     $doc->exists('/NCIPMessage/LookupUser')           ? lookupUser()       : (
103     $doc->exists('/NCIPMessage/ItemRequested')        ? item_request()     : (
104     $doc->exists('/NCIPMessage/ItemShipped')          ? item_shipped()     : (
105     $doc->exists('/NCIPMessage/ItemCheckedOut')       ? item_checked_out() : (
106     $doc->exists('/NCIPMessage/CheckOutItem')         ? check_out_item()   : (
107     $doc->exists('/NCIPMessage/ItemCheckedIn')        ? item_checked_in()  : (
108     $doc->exists('/NCIPMessage/CheckInItem')          ? check_in_item()    : (
109     $doc->exists('/NCIPMessage/ItemReceived')         ? item_received()    : (
110     $doc->exists('/NCIPMessage/AcceptItem')           ? accept_item()      : (
111     $doc->exists('/NCIPMessage/ItemRequestCancelled') ? item_cancelled()   : (
112     $doc->exists('/NCIPMessage/ItemRenewed')          ? item_renew()       : (
113     $doc->exists('/NCIPMessage/RenewItem')            ? renew_item()       :
114     fail("UNKNOWN NCIPMessage")
115     )))))))))));
116
117     logout();
118 } else {
119     fail("Unable to perform action : Unknown Service Request");
120 }
121
122 # load and parse config file
123 sub load_config {
124     my $file = shift;
125
126     my $Config = Config::Tiny->new;
127     $Config = Config::Tiny->read( $file ) ||
128         die( "Error reading config file ", $file, ": ", Config::Tiny->errstr, "\n" );
129     return $Config;
130 }
131
132 # load and parse userpriv_map file, returning a hashref
133 sub load_userpriv_map {
134     my $filename = shift;
135     my $map = {};
136     if (open(my $fh, "<", $filename)) {
137         while (my $entry = <$fh>) {
138             chomp($entry);
139             my ($from, $to) = split(m/:/, $entry);
140             $map->{$from} = $to;
141         }
142         close $fh;
143     }
144     return $map;
145 }
146
147 sub lookup_userpriv {
148     my $input = shift;
149     my $map = shift;
150     if (defined($map->{$input})) { # if we have a mapping for this profile
151         return $map->{$input}; # return value from mapping hash
152     } else {
153         return $input; # return original value
154     }
155 }
156
157 sub logit {
158     my ( $msg, $func, $more_info ) = @_;
159     open RESP_DATA, ">>resp_data.txt";
160     print RESP_DATA $msg;
161     print RESP_DATA $more_info unless !$more_info;
162     close RESP_DATA;
163     print $msg || fail($func);
164 }
165
166 sub staff_log {
167     my ( $taiv, $faiv, $more_info ) = @_;
168     my $now = localtime();
169     open STAFF_LOG, ">>staff_data.csv";
170     print STAFF_LOG "$now, $faiv, $taiv, $more_info\n";
171     close STAFF_LOG;
172 }
173
174 sub item_renew {
175     my $faidSchemeX = $doc->findvalue('/NCIPMessage/ItemRenewed/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
176     my $faidScheme = HTML::Entities::encode($faidSchemeX);
177     my $faidValue  = $doc->find('/NCIPMessage/ItemRenewed/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
178     my $taidSchemeX = $doc->findvalue('/NCIPMessage/ItemRenewed/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
179     my $taidScheme = HTML::Entities::encode($taidSchemeX);
180     my $taidValue  = $doc->find('/NCIPMessage/ItemRenewed/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
181
182     my $pid = $doc->findvalue('/NCIPMessage/ItemRenewed/UniqueUserId/UserIdentifierValue');
183     my $visid = $doc->findvalue('/NCIPMessage/ItemRenewed/ItemOptionalFields/ItemDescription/VisibleItemId/VisibleItemIdentifier') . $faidValue;
184     my $due_date = $doc->findvalue('/NCIPMessage/ItemRenewed/DateDue');
185
186     my $r = renewal( $visid, $due_date );
187
188     my $hd = <<ITEMRENEWAL;
189 Content-type: text/xml
190
191
192 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
193 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
194     <ItemRenewedResponse>
195         <ResponseHeader>
196             <FromAgencyId>
197                 <UniqueAgencyId>
198                     <Scheme>$faidScheme</Scheme>
199                     <Value>$faidValue</Value>
200                 </UniqueAgencyId>
201             </FromAgencyId>
202             <ToAgencyId>
203                 <UniqueAgencyId>
204                     <Scheme>$taidScheme</Scheme>
205                     <Value>$taidValue</Value>
206                 </UniqueAgencyId>
207             </ToAgencyId>
208         </ResponseHeader>
209         <UniqueItemId>
210             <ItemIdentifierValue datatype="string">$visid</ItemIdentifierValue>
211         </UniqueItemId>
212     </ItemRenewedResponse>
213 </NCIPMessage> 
214
215 ITEMRENEWAL
216
217     my $more_info = <<MOREINFO;
218
219 VISID             = $visid
220 Desired Due Date     = $due_date
221
222 MOREINFO
223
224     logit( $hd, ( caller(0) )[3], $more_info );
225     staff_log( $taidValue, $faidValue,
226             "ItemRenewal -> Patronid : "
227           . $pid
228           . " | Visid : "
229           . $visid
230           . " | Due Date : "
231           . $due_date );
232 }
233
234 sub renew_item {
235     my $faidSchemeX = $doc->findvalue('/NCIPMessage/RenewItem/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
236     my $faidScheme = HTML::Entities::encode($faidSchemeX);
237     my $faidValue  = $doc->find('/NCIPMessage/RenewItem/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
238     my $taidSchemeX = $doc->findvalue('/NCIPMessage/RenewItem/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
239     my $taidScheme = HTML::Entities::encode($taidSchemeX);
240     my $taidValue  = $doc->find('/NCIPMessage/RenewItem/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
241
242     my $pid = $doc->findvalue('/NCIPMessage/RenewItem/UniqueUserId/UserIdentifierValue');
243     my $unique_item_id = $doc->findvalue('/NCIPMessage/RenewItem/UniqueItemId/ItemIdentifierValue');
244     my $due_date = $doc->findvalue('/NCIPMessage/RenewItem/DateDue');
245
246     # we are using the UniqueItemId value as a barcode here
247     my $r = renewal( $unique_item_id, $due_date );
248
249     my $hd = <<ITEMRENEWAL;
250 Content-type: text/xml
251
252
253 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
254 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
255     <RenewItemResponse>
256         <ResponseHeader>
257             <FromAgencyId>
258                 <UniqueAgencyId>
259                     <Scheme>$faidScheme</Scheme>
260                     <Value>$faidValue</Value>
261                 </UniqueAgencyId>
262             </FromAgencyId>
263             <ToAgencyId>
264                 <UniqueAgencyId>
265                     <Scheme>$taidScheme</Scheme>
266                     <Value>$taidValue</Value>
267                 </UniqueAgencyId>
268             </ToAgencyId>
269         </ResponseHeader>
270         <UniqueItemId>
271             <ItemIdentifierValue datatype="string">$unique_item_id</ItemIdentifierValue>
272         </UniqueItemId>
273     </RenewItemResponse>
274 </NCIPMessage> 
275
276 ITEMRENEWAL
277
278     my $more_info = <<MOREINFO;
279
280 UNIQUEID             = $unique_item_id
281 Desired Due Date     = $due_date
282
283 MOREINFO
284
285     logit( $hd, ( caller(0) )[3], $more_info );
286     staff_log( $taidValue, $faidValue,
287             "RenewItem -> Patronid : "
288           . $pid
289           . " | Uniqueid: : "
290           . $unique_item_id
291           . " | Due Date : "
292           . $due_date );
293 }
294
295 sub accept_item {
296     my $faidSchemeX = $doc->findvalue('/NCIPMessage/AcceptItem/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
297     my $faidScheme = HTML::Entities::encode($faidSchemeX);
298     my $faidValue  = $doc->find('/NCIPMessage/AcceptItem/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
299     my $taidSchemeX = $doc->findvalue('/NCIPMessage/AcceptItem/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
300     my $taidScheme = HTML::Entities::encode($taidSchemeX);
301     my $taidValue  = $doc->find('/NCIPMessage/AcceptItem/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
302     my $visid = $doc->findvalue('/NCIPMessage/AcceptItem/ItemOptionalFields/ItemDescription/VisibleItemId/VisibleItemIdentifier') . $faidValue;
303     my $request_id = $doc->findvalue('/NCIPMessage/AcceptItem/UniqueRequestId/RequestIdentifierValue') || "unknown";
304     my $patron = $doc->findvalue('/NCIPMessage/AcceptItem/UserOptionalFields/VisibleUserId/VisibleUserIdentifier');
305     my $copy = copy_from_barcode($visid);
306     fail( "accept_item: " . $copy->{textcode} . " $visid" ) unless ( blessed $copy);
307     my $r2 = update_copy( $copy, $conf->{status}->{hold} ); # put into INN-Reach Hold status
308
309 # TODO: this should probably fulfill the original hold, not just change the status.  Eventually we should split the hold type, as holds arriving are not the same as holds needing to be sent
310
311     my $hd = <<ACCEPTITEM;
312 Content-type: text/xml
313
314
315 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
316 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
317     <AcceptItemResponse>
318         <ResponseHeader>
319             <FromAgencyId>
320                 <UniqueAgencyId>
321                     <Scheme>$faidScheme</Scheme>
322                     <Value>$faidValue</Value>
323                 </UniqueAgencyId>
324             </FromAgencyId>
325             <ToAgencyId>
326                 <UniqueAgencyId>
327                     <Scheme>$taidScheme</Scheme>
328                     <Value>$taidValue</Value>
329                 </UniqueAgencyId>
330             </ToAgencyId>
331         </ResponseHeader>
332     <UniqueRequestId>
333             <ItemIdentifierValue datatype="string">$request_id</ItemIdentifierValue>
334         </UniqueRequestId>
335         <UniqueItemId>
336             <ItemIdentifierValue datatype="string">$visid</ItemIdentifierValue>
337         </UniqueItemId>
338     </AcceptItemResponse>
339 </NCIPMessage> 
340
341 ACCEPTITEM
342
343     logit( $hd, ( caller(0) )[3] );
344     staff_log( $taidValue, $faidValue,
345         "AcceptItem -> Request Id : " . $request_id . " | Patron Id : " . $patron . " | Visible Id :" . $visid );
346 }
347
348 sub item_received {
349     my $faidSchemeX = $doc->findvalue('/NCIPMessage/ItemReceived/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
350     my $faidScheme = HTML::Entities::encode($faidSchemeX);
351     my $faidValue = $doc->find('/NCIPMessage/ItemReceived/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
352     my $taidSchemeX = $doc->findvalue('/NCIPMessage/ItemReceived/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
353     my $taidScheme = HTML::Entities::encode($taidSchemeX);
354     my $taidValue  = $doc->find('/NCIPMessage/ItemReceived/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
355     my $visid = $doc->findvalue('/NCIPMessage/ItemReceived/ItemOptionalFields/ItemDescription/VisibleItemId/VisibleItemIdentifier') . $faidValue;
356     my $copy = copy_from_barcode($visid);
357     fail( $copy->{textcode} . " $visid" ) unless ( blessed $copy);
358     my $r1 = checkin($visid) if ( $copy->status == OILS_COPY_STATUS_CHECKED_OUT ); # checkin the item before delete if ItemCheckedIn step was skipped
359     my $r2 = delete_copy($copy);
360
361     my $hd = <<ITEMRECEIVED;
362 Content-type: text/xml
363
364
365 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
366 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
367     <ItemReceivedResponse>
368         <ResponseHeader>
369             <FromAgencyId>
370                 <UniqueAgencyId>
371                     <Scheme>$faidScheme</Scheme>
372                     <Value>$faidValue</Value>
373                 </UniqueAgencyId>
374             </FromAgencyId>
375             <ToAgencyId>
376                 <UniqueAgencyId>
377                     <Scheme>$taidScheme</Scheme>
378                     <Value>$taidValue</Value>
379                 </UniqueAgencyId>
380             </ToAgencyId>
381         </ResponseHeader>
382         <UniqueItemId>
383             <ItemIdentifierValue datatype="string">$visid</ItemIdentifierValue>
384         </UniqueItemId>
385     </ItemReceivedResponse>
386 </NCIPMessage> 
387
388 ITEMRECEIVED
389
390     logit( $hd, ( caller(0) )[3] );
391     staff_log( $taidValue, $faidValue, "ItemReceived -> Visible ID : " . $visid );
392 }
393
394 sub item_cancelled {
395     my $faidSchemeX = $doc->findvalue('/NCIPMessage/ItemRequestCancelled/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
396     my $faidScheme = HTML::Entities::encode($faidSchemeX);
397     my $faidValue  = $doc->find('/NCIPMessage/ItemRequestCancelled/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
398
399     my $taidSchemeX = $doc->findvalue('/NCIPMessage/ItemRequestCancelled/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
400     my $taidScheme = HTML::Entities::encode($taidSchemeX);
401     my $taidValue  = $doc->find('/NCIPMessage/ItemRequestCancelled/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
402     my $UniqueItemIdAgencyIdValue = $doc->findvalue('/NCIPMessage/ItemRequestCancelled/UniqueItemId/UniqueAgencyId/Value');
403
404     my $barcode = $doc->findvalue('/NCIPMessage/ItemRequestCancelled/UniqueItemId/ItemIdentifierValue');
405
406     if ( $barcode =~ /^i/ ) {    # delete copy only if barcode is an iNUMBER
407         $barcode .= $faidValue;
408         my $copy = copy_from_barcode($barcode);
409         fail( $copy->{textcode} . " $barcode" ) unless ( blessed $copy);
410         my $r = delete_copy($copy);
411     } else {
412
413         # remove hold!
414         my $copy = copy_from_barcode($barcode);
415         fail( $copy->{textcode} . " $barcode" ) unless ( blessed $copy);
416         my $r = update_copy( $copy, 0 ); # TODO: we need to actually remove the hold, not just reset to available
417     }
418
419     my $hd = <<ITEMREQUESTCANCELLED;
420 Content-type: text/xml
421
422
423 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
424 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
425     <ItemRequestCancelledResponse>
426         <ResponseHeader>
427             <FromAgencyId>
428                 <UniqueAgencyId>
429                     <Scheme>$faidScheme</Scheme>
430                     <Value>$faidValue</Value>
431                 </UniqueAgencyId>
432             </FromAgencyId>
433             <ToAgencyId>
434                 <UniqueAgencyId>
435                     <Scheme>$taidScheme</Scheme>
436                     <Value>$taidValue</Value>
437                 </UniqueAgencyId>
438             </ToAgencyId>
439         </ResponseHeader>
440         <UniqueItemId>
441             <ItemIdentifierValue datatype="string">$barcode</ItemIdentifierValue>
442         </UniqueItemId>
443     </ItemRequestCancelledResponse>
444 </NCIPMessage> 
445
446 ITEMREQUESTCANCELLED
447
448     logit( $hd, ( caller(0) )[3] );
449     staff_log( $taidValue, $faidValue,
450         "ItemRequestCancelled -> Barcode : " . $barcode );
451 }
452
453 sub item_checked_in {
454     my $faidSchemeX = $doc->findvalue('/NCIPMessage/ItemCheckedIn/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
455     my $faidScheme = HTML::Entities::encode($faidSchemeX);
456     my $faidValue  = $doc->find('/NCIPMessage/ItemCheckedIn/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
457     my $taidSchemeX = $doc->findvalue('/NCIPMessage/ItemCheckedIn/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
458     my $taidScheme = HTML::Entities::encode($taidSchemeX);
459     my $taidValue  = $doc->find('/NCIPMessage/ItemCheckedIn/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
460
461     my $visid = $doc->findvalue('/NCIPMessage/ItemCheckedIn/ItemOptionalFields/ItemDescription/VisibleItemId/VisibleItemIdentifier') . $faidValue;
462     my $r = checkin($visid);
463     my $copy = copy_from_barcode($visid);
464     fail( $copy->{textcode} . " $visid" ) unless ( blessed $copy);
465     my $r2 = update_copy( $copy, $conf->{status}->{transit_return} ); # "INN-Reach Transit Return" status
466
467     my $hd = <<ITEMCHECKEDIN;
468 Content-type: text/xml
469
470
471 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
472 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
473     <ItemCheckedInResponse>
474         <ResponseHeader>
475             <FromAgencyId>
476                 <UniqueAgencyId>
477                     <Scheme>$faidScheme</Scheme>
478                     <Value>$faidValue</Value>
479                 </UniqueAgencyId>
480             </FromAgencyId>
481             <ToAgencyId>
482                 <UniqueAgencyId>
483                     <Scheme>$taidScheme</Scheme>
484                     <Value>$taidValue</Value>
485                 </UniqueAgencyId>
486             </ToAgencyId>
487         </ResponseHeader>
488         <UniqueItemId>
489             <ItemIdentifierValue datatype="string">$visid</ItemIdentifierValue>
490         </UniqueItemId>
491     </ItemCheckedInResponse>
492 </NCIPMessage> 
493
494 ITEMCHECKEDIN
495
496     logit( $hd, ( caller(0) )[3] );
497     staff_log( $taidValue, $faidValue, "ItemCheckedIn -> Visible ID : " . $visid );
498 }
499
500 sub item_checked_out {
501     my $faidSchemeX = $doc->findvalue('/NCIPMessage/ItemCheckedOut/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
502     my $faidScheme = HTML::Entities::encode($faidSchemeX);
503     my $faidValue  = $doc->find('/NCIPMessage/ItemCheckedOut/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
504     my $taidSchemeX = $doc->findvalue('/NCIPMessage/ItemCheckedOut/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
505     my $taidScheme = HTML::Entities::encode($taidSchemeX);
506     my $taidValue  = $doc->find('/NCIPMessage/ItemCheckedOut/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
507
508     my $patron_barcode = $doc->findvalue('/NCIPMessage/ItemCheckedOut/UserOptionalFields/VisibleUserId/VisibleUserIdentifier');
509     my $due_date = $doc->findvalue('/NCIPMessage/ItemCheckedOut/DateDue');
510     my $visid = $doc->findvalue('/NCIPMessage/ItemCheckedOut/ItemOptionalFields/ItemDescription/VisibleItemId/VisibleItemIdentifier') . $faidValue;
511
512     my $copy = copy_from_barcode($visid);
513     fail( $copy->{textcode} . " $visid" ) unless ( blessed $copy);
514     my $r = update_copy( $copy, 0 ); # seemed like copy had to be available before it could be checked out, so ...
515     my $r1 = checkin($visid) if ( $copy->status == OILS_COPY_STATUS_CHECKED_OUT ); # double posted itemcheckedout messages cause error ... trying to simplify
516     my $r2 = checkout( $visid, $patron_barcode, $due_date );
517
518     my $hd = <<ITEMCHECKEDOUT;
519 Content-type: text/xml
520
521
522 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
523 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
524     <ItemCheckedOutResponse>
525         <ResponseHeader>
526             <FromAgencyId>
527                 <UniqueAgencyId>
528                     <Scheme>$faidScheme</Scheme>
529                     <Value>$faidValue</Value>
530                 </UniqueAgencyId>
531             </FromAgencyId>
532             <ToAgencyId>
533                 <UniqueAgencyId>
534                     <Scheme>$taidScheme</Scheme>
535                     <Value>$taidValue</Value>
536                 </UniqueAgencyId>
537             </ToAgencyId>
538         </ResponseHeader>
539         <UniqueItemId>
540             <ItemIdentifierValue datatype="string">$visid</ItemIdentifierValue>
541         </UniqueItemId>
542     </ItemCheckedOutResponse>
543 </NCIPMessage> 
544
545 ITEMCHECKEDOUT
546
547     logit( $hd, ( caller(0) )[3] );
548     staff_log( $taidValue, $faidValue,
549         "ItemCheckedOut -> Visible Id : " . $visid . " | Patron Barcode : " . $patron_barcode . " | Due Date : " . $due_date );
550 }
551
552 sub check_out_item {
553     my $faidSchemeX = $doc->findvalue('/NCIPMessage/CheckOutItem/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
554     my $faidScheme = HTML::Entities::encode($faidSchemeX);
555     my $faidValue  = $doc->find('/NCIPMessage/CheckOutItem/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
556     my $taidSchemeX = $doc->findvalue('/NCIPMessage/CheckOutItem/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
557     my $taidScheme = HTML::Entities::encode($taidSchemeX);
558     my $taidValue  = $doc->find('/NCIPMessage/CheckOutItem/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
559
560     my $mdate = $doc->findvalue('/NCIPMessage/CheckOutItem/MandatedAction/DateEventOccurred');
561     # TODO: look up individual accounts for agencies based on barcode prefix + agency identifier
562     my $patron_barcode = $conf->{checkout}->{institutional_patron}; # patron id if patron_id_as_identifier = yes
563
564     # For CheckOutItem and INN-REACH, this value will correspond with our local barcode
565     my $barcode = $doc->findvalue('/NCIPMessage/CheckOutItem/UniqueItemId/ItemIdentifierValue');
566
567     # TODO: watch for possible real ids here?
568     my $due_date = $doc->findvalue('/NCIPMessage/CheckOutItem/DateDue');
569
570     my $copy = copy_from_barcode($barcode);
571     fail( $copy->{textcode} . " $barcode" ) unless ( blessed $copy);
572
573     my $r2 = checkout( $barcode, $patron_barcode, $due_date );
574
575     # TODO: check for checkout exception (like OPEN_CIRCULATION_EXISTS)
576
577     my $hd = <<CHECKOUTITEM;
578 Content-type: text/xml
579
580
581 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
582 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
583     <CheckOutItemResponse>
584         <ResponseHeader>
585             <FromAgencyId>
586                 <UniqueAgencyId>
587                     <Scheme>$faidScheme</Scheme>
588                     <Value>$faidValue</Value>
589                 </UniqueAgencyId>
590             </FromAgencyId>
591             <ToAgencyId>
592                 <UniqueAgencyId>
593                     <Scheme>$taidScheme</Scheme>
594                     <Value>$taidValue</Value>
595                 </UniqueAgencyId>
596             </ToAgencyId>
597         </ResponseHeader>
598         <UniqueItemId>
599             <ItemIdentifierValue datatype="string">$barcode</ItemIdentifierValue>
600         </UniqueItemId>
601     </CheckOutItemResponse>
602 </NCIPMessage> 
603
604 CHECKOUTITEM
605
606     logit( $hd, ( caller(0) )[3] );
607     staff_log( $taidValue, $faidValue,
608         "CheckOutItem -> Barcode : " . $barcode . " | Patron Barcode : " . $patron_barcode . " | Due Date : " . $due_date );
609 }
610
611 sub check_in_item {
612     my $faidSchemeX = $doc->findvalue('/NCIPMessage/CheckInItem/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
613     my $faidScheme = HTML::Entities::encode($faidSchemeX);
614     my $faidValue  = $doc->find('/NCIPMessage/CheckInItem/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
615     my $taidSchemeX = $doc->findvalue('/NCIPMessage/CheckInItem/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
616     my $taidScheme = HTML::Entities::encode($taidSchemeX);
617     my $taidValue  = $doc->find('/NCIPMessage/CheckInItem/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
618
619     # For CheckInItem and INN-REACH, this value will correspond with our local barcode
620     my $barcode = $doc->findvalue('/NCIPMessage/CheckInItem/UniqueItemId/ItemIdentifierValue');
621     my $r = checkin($barcode);
622     fail($r) if $r =~ /^COPY_NOT_CHECKED_OUT/;
623     # TODO: do we need to do these next steps?  checkin() should handle everything, and we want this to end up in 'reshelving'.  If we are worried about transits, we should handle (abort) them, not just change the status
624     ##my $copy = copy_from_barcode($barcode);
625     ##fail($copy->{textcode}." $barcode") unless (blessed $copy);
626     ##  my $r2 = update_copy($copy,0); # Available now 
627
628     my $hd = <<CHECKINITEM;
629 Content-type: text/xml
630
631
632 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
633 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
634     <CheckInItemResponse>
635         <ResponseHeader>
636             <FromAgencyId>
637                 <UniqueAgencyId>
638                     <Scheme>$faidScheme</Scheme>
639                     <Value>$faidValue</Value>
640                 </UniqueAgencyId>
641             </FromAgencyId>
642             <ToAgencyId>
643                 <UniqueAgencyId>
644                     <Scheme>$taidScheme</Scheme>
645                     <Value>$taidValue</Value>
646                 </UniqueAgencyId>
647             </ToAgencyId>
648         </ResponseHeader>
649         <UniqueItemId>
650             <ItemIdentifierValue datatype="string">$barcode</ItemIdentifierValue>
651         </UniqueItemId>
652     </CheckInItemResponse>
653 </NCIPMessage> 
654
655 CHECKINITEM
656
657     logit( $hd, ( caller(0) )[3] );
658     staff_log( $taidValue, $faidValue, "CheckInItem -> Barcode : " . $barcode );
659 }
660
661 sub item_shipped {
662     my $faidSchemeX = $doc->findvalue('/NCIPMessage/ItemShipped/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
663     my $faidScheme = HTML::Entities::encode($faidSchemeX);
664     my $faidValue  = $doc->find('/NCIPMessage/ItemShipped/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
665     my $taidSchemeX = $doc->findvalue('/NCIPMessage/ItemShipped/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
666     my $taidScheme = HTML::Entities::encode($taidSchemeX);
667     my $taidValue  = $doc->find('/NCIPMessage/ItemShipped/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
668
669     my $visid = $doc->findvalue('/NCIPMessage/ItemShipped/ItemOptionalFields/ItemDescription/VisibleItemId/VisibleItemIdentifier') . $faidValue;
670     my $barcode = $doc->findvalue('/NCIPMessage/ItemShipped/UniqueItemId/ItemIdentifierValue') . $faidValue;
671     my $title = $doc->findvalue('/NCIPMessage/ItemShipped/ItemOptionalFields/BibliographicDescription/Title');
672     my $callnumber = $doc->findvalue('/NCIPMessage/ItemShipped/ItemOptionalFields/ItemDescription/CallNumber');
673
674     my $copy = copy_from_barcode($barcode);
675     fail( $copy->{textcode} . " $barcode" ) unless ( blessed $copy);
676     my $r = update_copy_shipped( $copy, $conf->{status}->{transit}, $visid ); # put copy into INN-Reach Transit status & modify barcode = Visid != tempIIIiNumber
677
678     my $hd = <<ITEMSHIPPED;
679 Content-type: text/xml
680
681
682 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
683 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
684     <ItemShippedResponse>
685         <ResponseHeader>
686             <FromAgencyId>
687                 <UniqueAgencyId>
688                     <Scheme>$faidScheme</Scheme>
689                     <Value>$faidValue</Value>
690                 </UniqueAgencyId>
691             </FromAgencyId>
692             <ToAgencyId>
693                 <UniqueAgencyId>
694                     <Scheme>$taidScheme</Scheme>
695                     <Value>$taidValue</Value>
696                 </UniqueAgencyId>
697             </ToAgencyId>
698         </ResponseHeader>
699         <UniqueItemId>
700             <ItemIdentifierValue datatype="string">$visid</ItemIdentifierValue>
701         </UniqueItemId>
702     </ItemShippedResponse>
703 </NCIPMessage> 
704
705 ITEMSHIPPED
706
707     logit( $hd, ( caller(0) )[3] );
708     staff_log( $taidValue, $faidValue,
709         "ItemShipped -> Visible Id : " . $visid . " | Barcode : " . $barcode . " | Title : " . $title . " | Call Number : " . $callnumber );
710 }
711
712 sub item_request {
713     my $faidSchemeX = $doc->findvalue('/NCIPMessage/ItemRequested/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
714     my $faidScheme = HTML::Entities::encode($faidSchemeX);
715     my $faidValue  = $doc->find('/NCIPMessage/ItemRequested/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
716
717     my $taidSchemeX = $doc->findvalue('/NCIPMessage/ItemRequested/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
718     my $taidScheme = HTML::Entities::encode($taidSchemeX);
719     my $taidValue  = $doc->find('/NCIPMessage/ItemRequested/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
720     my $UniqueItemIdAgencyIdValue = $doc->findvalue('/NCIPMessage/ItemRequested/UniqueItemId/UniqueAgencyId/Value');
721
722     # TODO: should we use the VisibleID for item agency variation of this method call
723
724     my $pid = $doc->findvalue('/NCIPMessage/ItemRequested/UniqueUserId/UserIdentifierValue');
725     my $barcode = $doc->findvalue('/NCIPMessage/ItemRequested/UniqueItemId/ItemIdentifierValue');
726     my $author = $doc->findvalue('/NCIPMessage/ItemRequested/ItemOptionalFields/BibliographicDescription/Author');
727     my $title = $doc->findvalue('/NCIPMessage/ItemRequested/ItemOptionalFields/BibliographicDescription/Title');
728     my $callnumber = $doc->findvalue('/NCIPMessage/ItemRequested/ItemOptionalFields/ItemDescription/CallNumber');
729     my $medium_type = $doc->find('/NCIPMessage/ItemRequested/ItemOptionalFields/BibliographicDescription/MediumType/Value');
730
731     my $r = "default error checking response";
732
733     if ( $barcode =~ /^i/ ) {    # XXX EG is User Agency # create copy only if barcode is an iNUMBER
734         my $copy_status_id = $conf->{status}->{loan_requested}; # INN-Reach Loan Requested - local configured status
735         $barcode .= $faidValue;
736         # we want our custom status to be then end result, so create the copy with status of "Available, then hold it, then update the status
737         $r = create_copy( $title, $callnumber, $barcode, 0, $medium_type );
738         my $copy = copy_from_barcode($barcode);
739         my $r2   = place_simple_hold( $copy->id, $pid );
740         my $r3   = update_copy( $copy, $copy_status_id );
741     } else {    # XXX EG is Item Agency
742         unless ( $conf->{behavior}->{no_item_agency_holds} =~ m/^y/i ) {
743             # place hold for user UniqueUserId/UniqueAgencyId/Value = institution account
744             my $copy = copy_from_barcode($barcode);
745             my $pid2 = 1013459; # XXX CUSTOMIZATION NEEDED XXX # this is the id of a user representing your DCB system, TODO: use agency information to create and link to individual accounts per agency, if needed
746             $r = place_simple_hold( $copy->id, $pid2 );
747             my $r2 = update_copy( $copy, $conf->{status}->{hold} ); # put into INN-Reach Hold status
748         }
749     }
750
751     my $hd = <<ITEMREQ;
752 Content-type: text/xml
753
754
755 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
756 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
757     <ItemRequestedResponse>
758         <ResponseHeader>
759             <FromAgencyId>
760                 <UniqueAgencyId>
761                     <Scheme>$faidScheme</Scheme>
762                     <Value>$faidValue</Value>
763                 </UniqueAgencyId>
764             </FromAgencyId>
765             <ToAgencyId>
766                 <UniqueAgencyId>
767                     <Scheme>$taidScheme</Scheme>
768                     <Value>$taidValue</Value>
769                 </UniqueAgencyId>
770             </ToAgencyId>
771         </ResponseHeader>
772         <UniqueUserId>
773             <UniqueAgencyId>
774                 <Scheme datatype="string">$taidScheme</Scheme>
775                 <Value datatype="string">$taidValue</Value>
776             </UniqueAgencyId>
777             <UserIdentifierValue datatype="string">$pid</UserIdentifierValue>
778         </UniqueUserId>
779         <UniqueItemId>
780             <ItemIdentifierValue datatype="string">$barcode</ItemIdentifierValue>
781         </UniqueItemId>
782         <ItemOptionalFields>
783             <BibliographicDescription>
784         <Author datatype="string">$author</Author>
785         <Title datatype="string">$title</Title>
786             </BibliographicDescription>
787             <ItemDescription>
788                 <CallNumber datatype="string">$callnumber</CallNumber>
789             </ItemDescription>
790        </ItemOptionalFields>
791     </ItemRequestedResponse>
792 </NCIPMessage> 
793
794 ITEMREQ
795
796     logit( $hd, ( caller(0) )[3] );
797     staff_log( $taidValue, $faidValue,
798         "ItemRequested -> Barcode : " . $barcode . " | Title : " . $title . " | Call Number : " . $callnumber . " | Patronid :" . $pid );
799 }
800
801 sub lookupUser {
802
803     my $faidScheme = $doc->findvalue('/NCIPMessage/LookupUser/InitiationHeader/FromAgencyId/UniqueAgencyId/Scheme');
804     $faidScheme = HTML::Entities::encode($faidScheme);
805     my $faidValue = $doc->find('/NCIPMessage/LookupUser/InitiationHeader/FromAgencyId/UniqueAgencyId/Value');
806     my $taidScheme = $doc->findvalue('/NCIPMessage/LookupUser/InitiationHeader/ToAgencyId/UniqueAgencyId/Scheme');
807     $taidScheme = HTML::Entities::encode($taidScheme);
808
809     my $taidValue = $doc->find('/NCIPMessage/LookupUser/InitiationHeader/ToAgencyId/UniqueAgencyId/Value');
810     my $id = $doc->findvalue('/NCIPMessage/LookupUser/VisibleUserId/VisibleUserIdentifier');
811
812     my $uidValue;
813
814     if ($patron_id_type eq 'barcode') {
815         $uidValue = user_id_from_barcode($id);
816     } else {
817         $uidValue = $id;
818     }
819
820     if ( !defined($uidValue)
821         || ( ref($uidValue) && reftype($uidValue) eq 'HASH' ) )
822     {
823         do_lookup_user_error_stanza("PATRON_NOT_FOUND : $id");
824         die;
825     }
826
827     my ( $propername, $email, $good_until, $userpriv, $block_stanza ) =
828       ( "name here", "", "good until", "", "" );    # defaults
829
830     my $patron = flesh_user($uidValue);
831
832     #if (blessed($patron)) {
833     my $patron_ok = 1;
834     my @penalties = @{ $patron->standing_penalties };
835
836     if ( $patron->deleted eq 't' ) {
837         do_lookup_user_error_stanza("PATRON_DELETED : $uidValue");
838         die;
839     } elsif ( $patron->barred eq 't' ) {
840         do_lookup_user_error_stanza("PATRON_BARRED : $uidValue");
841         die;
842     } elsif ( $patron->active eq 'f' ) {
843         do_lookup_user_error_stanza("PATRON_INACTIVE : $uidValue");
844         die;
845     }
846
847     elsif ( $#penalties > -1 ) {
848
849 #                my $penalty;
850 #                   foreach $penalty (@penalties) {
851 #                    if (defined($penalty->standing_penalty->block_list)) {
852 #                            my @block_list = split(/\|/, $penalty->standing_penalty->block_list);
853 #                            foreach my $block (@block_list) {
854 #                                foreach my $block_on (@$block_types) {
855 #                                    if ($block eq $block_on) {
856 #                                        $block_stanza .= "\n".$penalty->standing_penalty->name;
857 #                                        $patron_ok = 0;
858 #                                    }
859 #                                    last unless ($patron_ok);
860 #                            }
861 #                                last unless ($patron_ok);
862 #                          }
863 #                     }
864 #                }
865         $block_stanza = qq(
866             <BlockOrTrap>
867                 <UniqueAgencyId>
868                     <Scheme datatype="string">http://just.testing.now</Scheme>
869                     <Value datatype="string">$faidValue</Value>
870                 </UniqueAgencyId>
871                 <BlockOrTrapType>
872                     <Scheme datatype="string">http://just.testing.now</Scheme>
873                     <Value datatype="string">Block Hold</Value>
874                 </BlockOrTrapType>
875             </BlockOrTrap>);
876     }
877
878     if ( defined( $patron->email ) && $conf->{behavior}->{omit_patron_email} !~ m/^y/i ) {
879         $email = qq(
880             <UserAddressInformation>
881                 <ElectronicAddress>
882                     <ElectronicAddressType>
883                         <Scheme datatype="string">http://testing.now</Scheme>
884                         <Value datatype="string">mailto</Value>
885                     </ElectronicAddressType>
886                     <ElectronicAddressData datatype="string">)
887           . HTML::Entities::encode( $patron->email )
888           . qq(</ElectronicAddressData>
889                 </ElectronicAddress>
890             </UserAddressInformation>);
891     }
892
893     $propername = $patron->first_given_name . " " . $patron->family_name;
894     $good_until = $patron->expire_date || "unknown";
895     $userpriv = $patron->profile->name;
896
897     my $userpriv_map = load_userpriv_map( $conf->{path}->{userpriv_map} );
898
899     if ($userpriv_map) {
900         $userpriv = lookup_userpriv($userpriv, $userpriv_map);
901     }
902
903     #} else {
904     #    do_lookup_user_error_stanza("PATRON_NOT_FOUND : $id");
905     #    die;
906     #}
907     my $uniqid = $patron->id;
908     my $visid;
909     if ($patron_id_type eq 'barcode') {
910         $visid = $patron->card->barcode;
911     } else {
912         $visid = $patron->id;
913     }
914     my $hd = <<LOOKUPUSERRESPONSE;
915 Content-type: text/xml
916
917
918 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
919 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
920     <LookupUserResponse>
921         <ResponseHeader>
922             <FromAgencyId>
923                 <UniqueAgencyId>
924                     <Scheme>$taidScheme</Scheme>
925                     <Value>$taidValue</Value>
926                 </UniqueAgencyId>
927             </FromAgencyId>
928             <ToAgencyId>
929                 <UniqueAgencyId>
930                    <Scheme>$faidScheme</Scheme>
931                    <Value>$faidValue</Value>
932                 </UniqueAgencyId>
933             </ToAgencyId>
934         </ResponseHeader>
935         <UniqueUserId>
936             <UniqueAgencyId>
937                 <Scheme>$taidScheme</Scheme>
938                 <Value>$taidValue</Value>
939             </UniqueAgencyId>
940             <UserIdentifierValue>$uniqid</UserIdentifierValue>
941         </UniqueUserId>
942         <UserOptionalFields>
943             <VisibleUserId>
944                 <VisibleUserIdentifierType>
945                     <Scheme datatype="string">http://blah.com</Scheme>
946                     <Value datatype="string">Barcode</Value>
947                 </VisibleUserIdentifierType>
948                 <VisibleUserIdentifier datatype="string">$visid</VisibleUserIdentifier>
949             </VisibleUserId>
950             <NameInformation>
951                 <PersonalNameInformation>
952                     <UnstructuredPersonalUserName datatype="string">$propername</UnstructuredPersonalUserName>
953                 </PersonalNameInformation>
954             </NameInformation>
955             <UserPrivilege>
956                 <UniqueAgencyId>
957                     <Scheme datatype="string">$faidScheme</Scheme>
958                     <Value datatype="string">$faidValue</Value>
959                 </UniqueAgencyId>
960                 <AgencyUserPrivilegeType>
961                     <Scheme datatype="string">http://testing.purposes.only</Scheme>
962                     <Value datatype="string">$userpriv</Value>
963                 </AgencyUserPrivilegeType>
964                 <ValidToDate datatype="string">$good_until</ValidToDate>
965             </UserPrivilege> $email $block_stanza
966         </UserOptionalFields>
967    </LookupUserResponse>
968 </NCIPMessage>
969
970 LOOKUPUSERRESPONSE
971
972     logit( $hd, ( caller(0) )[3] );
973     staff_log( $taidValue, $faidValue,
974             "LookupUser -> Patron Barcode : "
975           . $id
976           . " | Patron Id : "
977           . $uidValue
978           . " | User Name : "
979           . $propername
980           . " | User Priv : "
981           . $userpriv );
982 }
983
984 sub fail {
985     my $error_msg =
986       shift || "THIS IS THE DEFAULT / DO NOT HANG III NCIP RESP MSG";
987     print "Content-type: text/xml\n\n";
988
989     print <<ITEMREQ;
990 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
991 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
992     <ItemRequestedResponse>
993         <ResponseHeader>
994             <FromAgencyId>
995                 <UniqueAgencyId>
996                     <Scheme>http://136.181.125.166:6601/IRCIRCD?target=get_scheme_values&amp;scheme=UniqueAgencyId</Scheme>
997                     <Value></Value>
998                 </UniqueAgencyId>
999             </FromAgencyId>
1000             <ToAgencyId>
1001                 <UniqueAgencyId>
1002                     <Scheme>http://136.181.125.166:6601/IRCIRCD?target=get_scheme_values&amp;scheme=UniqueAgencyId</Scheme>
1003                     <Value>$error_msg</Value>
1004                 </UniqueAgencyId>
1005             </ToAgencyId>
1006         </ResponseHeader>
1007     </ItemRequestedResponse>
1008 </NCIPMessage>
1009
1010 ITEMREQ
1011
1012     # XXX: we should log FromAgencyId and ToAgencyId values here, but they are not available to the code at this point
1013     staff_log( '', '',
1014         ( ( caller(0) )[3] . " -> " . $error_msg ) );
1015     die;
1016 }
1017
1018 sub do_lookup_user_error_stanza {
1019
1020     # XXX: we should include FromAgencyId and ToAgencyId values, but they are not available to the code at this point
1021     my $error = shift;
1022     my $hd    = <<LOOKUPPROB;
1023 Content-type: text/xml
1024
1025
1026 <!DOCTYPE NCIPMessage PUBLIC "-//NISO//NCIP DTD Version 1.0//EN" "http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
1027 <NCIPMessage version="http://www.niso.org/ncip/v1_0/imp1/dtd/ncip_v1_0.dtd">
1028     <LookupUserResponse>
1029         <ResponseHeader>
1030             <FromAgencyId>
1031                 <UniqueAgencyId>
1032                     <Scheme></Scheme>
1033                     <Value></Value>
1034                 </UniqueAgencyId>
1035             </FromAgencyId>
1036             <ToAgencyId>
1037                 <UniqueAgencyId>
1038                     <Scheme></Scheme>
1039                     <Value></Value>
1040                 </UniqueAgencyId>
1041             </ToAgencyId>
1042         </ResponseHeader>
1043         <Problem>
1044             <ProcessingError>
1045                 <ProcessingErrorType>
1046                     <Scheme>http://www.niso.org/ncip/v1_0/schemes/processingerrortype/lookupuserprocessingerror.scm</Scheme>
1047                     <Value>$error</Value>
1048                 </ProcessingErrorType>
1049                 <ProcessingErrorElement>
1050                     <ElementName>AuthenticationInput</ElementName>
1051                 </ProcessingErrorElement>
1052             </ProcessingError>
1053         </Problem>
1054     </LookupUserResponse>
1055 </NCIPMessage>
1056
1057 LOOKUPPROB
1058
1059     logit( $hd, ( caller(0) )[3] );
1060     # XXX: we should log FromAgencyId and ToAgencyId values here, but they are not available to the code at this point
1061     staff_log( '', '', ( ( caller(0) )[3] . " -> " . $error ) );
1062     die;
1063 }
1064
1065 # Login to the OpenSRF system/Evergreen.
1066 #
1067 # Returns a hash with the authtoken, authtime, and expiration (time in
1068 # seconds since 1/1/1970).
1069 sub login {
1070
1071  # XXX: local opensrf core conf filename should be in config.
1072  # XXX: STAFF account with ncip service related permissions should be in config.
1073     my $bootstrap = '/openils/conf/opensrf_core.xml';
1074     my $uname     = $conf->{auth}->{username};
1075     my $password  = $conf->{auth}->{password};
1076
1077     # Bootstrap the client
1078     OpenSRF::System->bootstrap_client( config_file => $bootstrap );
1079     my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
1080     Fieldmapper->import( IDL => $idl );
1081
1082     # Initialize CStoreEditor:
1083     OpenILS::Utils::CStoreEditor->init;
1084
1085     my $seed = OpenSRF::AppSession->create('open-ils.auth')
1086       ->request( 'open-ils.auth.authenticate.init', $uname )->gather(1);
1087
1088     return undef unless $seed;
1089
1090     my $response = OpenSRF::AppSession->create('open-ils.auth')->request(
1091         'open-ils.auth.authenticate.complete',
1092         {
1093             username => $uname,
1094             password => md5_hex( $seed . md5_hex($password) ),
1095             type     => 'staff'
1096         }
1097     )->gather(1);
1098
1099     return undef unless $response;
1100
1101     my %result;
1102     $result{'authtoken'}  = $response->{payload}->{authtoken};
1103     $result{'authtime'}   = $response->{payload}->{authtime};
1104     $result{'expiration'} = time() + $result{'authtime'}
1105       if ( defined( $result{'authtime'} ) );
1106     return %result;
1107 }
1108
1109 # Check the time versus the session expiration time and login again if
1110 # the session has expired, consequently resetting the session
1111 # paramters. We want to run this before doing anything that requires
1112 # us to have a current session in OpenSRF.
1113 #
1114 # Arguments
1115 # none
1116 #
1117 # Returns
1118 # Nothing
1119 sub check_session_time {
1120     if ( time() > $session{'expiration'} ) {
1121         %session = login();
1122         if ( !%session ) {
1123             die("Failed to reinitialize the session after expiration.");
1124         }
1125     }
1126 }
1127
1128 # Retrieve the logged in user.
1129 #
1130 sub get_session {
1131     my $response =
1132       OpenSRF::AppSession->create('open-ils.auth')
1133       ->request( 'open-ils.auth.session.retrieve', $session{authtoken} )
1134       ->gather(1);
1135     return $response;
1136 }
1137
1138 # Logout/destroy the OpenSRF session
1139 #
1140 # Argument is
1141 # none
1142 #
1143 # Returns
1144 # Does not return anything
1145 sub logout {
1146     if ( time() < $session{'expiration'} ) {
1147         my $response =
1148           OpenSRF::AppSession->create('open-ils.auth')
1149           ->request( 'open-ils.auth.session.delete', $session{authtoken} )
1150           ->gather(1);
1151         if ($response) {
1152
1153             # strong.silent.success
1154             exit(0);
1155         } else {
1156             fail("Logout unsuccessful. Good-bye, anyway.");
1157         }
1158     }
1159 }
1160
1161 sub update_copy {
1162     check_session_time();
1163     my ( $copy, $status_id ) = @_;
1164     my $e = new_editor( authtoken => $session{authtoken} );
1165     return $e->event->{textcode} unless ( $e->checkauth );
1166     $e->xact_begin;
1167     $copy->status($status_id);
1168     return $e->event unless $e->update_asset_copy($copy);
1169     $e->commit;
1170     return 'SUCCESS';
1171 }
1172
1173 # my paranoia re barcode on shipped items using visid for unique value
1174 sub update_copy_shipped {
1175     check_session_time();
1176     my ( $copy, $status_id, $barcode ) = @_;
1177     my $e = new_editor( authtoken => $session{authtoken} );
1178     return $e->event->{textcode} unless ( $e->checkauth );
1179     $e->xact_begin;
1180     $copy->status($status_id);
1181     $copy->barcode($barcode);
1182     return $e->event unless $e->update_asset_copy($copy);
1183     $e->commit;
1184     return 'SUCCESS';
1185 }
1186
1187 # Delete a copy
1188 #
1189 # Argument
1190 # Fieldmapper asset.copy object
1191 #
1192 # Returns
1193 # "SUCCESS" on success
1194 # Event textcode if an error occurs
1195 sub delete_copy {
1196     check_session_time();
1197     my ($copy) = @_;
1198
1199     my $e = new_editor( authtoken => $session{authtoken} );
1200     return $e->event->{textcode} unless ( $e->checkauth );
1201
1202     # Get the calnumber
1203     my $vol = $e->retrieve_asset_call_number( $copy->call_number );
1204     return $e->event->{textcode} unless ($vol);
1205
1206     # Get the biblio.record_entry
1207     my $bre = $e->retrieve_biblio_record_entry( $vol->record );
1208     return $e->event->{textcode} unless ($bre);
1209
1210     # Delete everything in a transaction and rollback if anything fails.
1211     # TODO: I think there is a utility function which handles all this
1212     $e->xact_begin;
1213     my $r;    # To hold results of editor calls
1214     $r = $e->delete_asset_copy($copy);
1215     unless ($r) {
1216         my $lval = $e->event->{textcode};
1217         $e->rollback;
1218         return $lval;
1219     }
1220     my $list =
1221       $e->search_asset_copy( { call_number => $vol->id, deleted => 'f' } );
1222     unless (@$list) {
1223         $r = $e->delete_asset_call_number($vol);
1224         unless ($r) {
1225             my $lval = $e->event->{textcode};
1226             $e->rollback;
1227             return $lval;
1228         }
1229         $list = $e->search_asset_call_number( { record => $bre->id, deleted => 'f' } );
1230         unless (@$list) {
1231             $r = $e->delete_biblio_record_entry($bre);
1232             unless ($r) {
1233                 my $lval = $e->event->{textcode};
1234                 $e->rollback;
1235                 return $lval;
1236             }
1237         }
1238     }
1239     $e->commit;
1240     return 'SUCCESS';
1241 }
1242
1243 # Get asset.copy from asset.copy.barcode.
1244 # Arguments
1245 # copy barcode
1246 #
1247 # Returns
1248 # asset.copy fieldmaper object
1249 # or hash on error
1250 sub copy_from_barcode {
1251     check_session_time();
1252     my ($barcode) = @_;
1253     my $response =
1254       OpenSRF::AppSession->create('open-ils.search')
1255       ->request( 'open-ils.search.asset.copy.find_by_barcode', $barcode )
1256       ->gather(1);
1257     return $response;
1258 }
1259
1260 sub locid_from_barcode {
1261     my ($barcode) = @_;
1262     my $response =
1263       OpenSRF::AppSession->create('open-ils.search')
1264       ->request( 'open-ils.search.biblio.find_by_barcode', $barcode )
1265       ->gather(1);
1266     return $response->{ids}[0];
1267 }
1268
1269 # Convert a MARC::Record to XML for Evergreen
1270 #
1271 # Copied from Dyrcona's issa framework which copied
1272 # it from MVLC's Safari Load program which copied it
1273 # from some code in the Open-ILS example import scripts.
1274 #
1275 # Argument
1276 # A MARC::Record object
1277 #
1278 # Returns
1279 # String with XML for the MARC::Record as Evergreen likes it
1280 sub convert2marcxml {
1281     my $input = shift;
1282     ( my $xml = $input->as_xml_record() ) =~ s/\n//sog;
1283     $xml =~ s/^<\?xml.+\?\s*>//go;
1284     $xml =~ s/>\s+</></go;
1285     $xml =~ s/\p{Cc}//go;
1286     $xml = $U->entityize($xml);
1287     $xml =~ s/[\x00-\x1f]//go;
1288     return $xml;
1289 }
1290
1291 # Create a copy and marc record
1292 #
1293 # Arguments
1294 # title
1295 # call number
1296 # copy barcode
1297 #
1298 # Returns
1299 # bib id on succes
1300 # event textcode on failure
1301 sub create_copy {
1302     check_session_time();
1303     my ( $title, $callnumber, $barcode, $copy_status_id, $medium_type ) = @_;
1304
1305     my $e = new_editor( authtoken => $session{authtoken} );
1306     return $e->event->{textcode} unless ( $e->checkauth );
1307
1308     my $r = $e->allowed( [ 'CREATE_COPY', 'CREATE_MARC', 'CREATE_VOLUME' ] );
1309     if ( ref($r) eq 'HASH' ) {
1310         return $r->{textcode} . ' ' . $r->{ilsperm};
1311     }
1312
1313     # Check if the barcode exists in asset.copy and bail if it does.
1314     my $list = $e->search_asset_copy( { deleted => 'f', barcode => $barcode } );
1315     if (@$list) {
1316 # in the future, can we update it, if it exists and only if it is an INN-Reach status item ?
1317         $e->finish;
1318         fail( 'BARCODE_EXISTS ! Barcode : ' . $barcode );
1319         die;
1320     }
1321
1322     # Create MARC record
1323     my $record = MARC::Record->new();
1324     $record->encoding('UTF-8');
1325     $record->leader('00881nam a2200193 4500');
1326     my $datespec = strftime( "%Y%m%d%H%M%S.0", localtime );
1327     my @fields = ();
1328     push( @fields, MARC::Field->new( '005', $datespec ) );
1329     push( @fields, MARC::Field->new( '082', '0', '4', 'a' => $callnumber ) );
1330     push( @fields, MARC::Field->new( '245', '0', '0', 'a' => $title ) );
1331     $record->append_fields(@fields);
1332
1333     # Convert the record to XML
1334     my $xml = convert2marcxml($record);
1335
1336     my $bre =
1337       OpenSRF::AppSession->create('open-ils.cat')
1338       ->request( 'open-ils.cat.biblio.record.xml.import',
1339         $session{authtoken}, $xml, 'System Local', 1 )->gather(1);
1340     return $bre->{textcode} if ( ref($bre) eq 'HASH' );
1341
1342     # Create volume record
1343     my $vol =
1344       OpenSRF::AppSession->create('open-ils.cat')
1345       ->request( 'open-ils.cat.call_number.find_or_create', $session{authtoken}, $callnumber, $bre->id, $conf->{volume}->{owning_lib} )
1346       ->gather(1);
1347     return $vol->{textcode} if ( $vol->{textcode} );
1348
1349     # Retrieve the user
1350     my $user = get_session;
1351
1352     # Create copy record
1353     my $copy = Fieldmapper::asset::copy->new();
1354     # XXX CUSTOMIZATION NEEDED XXX
1355     # You will need to either create a circ mod for every expected medium type,
1356     # OR you should create a single circ mod for all requests from the external
1357     # system.
1358     # Adjust these lines as needed.
1359     #    $copy->circ_modifier(qq($medium_type)); # XXX CUSTOMIZATION NEEDED XXX
1360     # OR
1361     $copy->circ_modifier($conf->{copy}->{circ_modifier});
1362     $copy->barcode($barcode);
1363     $copy->call_number( $vol->{acn_id} );
1364     $copy->circ_lib($conf->{copy}->{circ_lib});
1365     $copy->circulate('t');
1366     $copy->holdable('t');
1367     $copy->opac_visible('t');
1368     $copy->deleted('f');
1369     $copy->fine_level(2);
1370     $copy->loan_duration(2);
1371     $copy->location($conf->{copy}->{location});
1372     $copy->status($copy_status_id);
1373     $copy->editor('1');
1374     $copy->creator('1');
1375
1376     # Add the configured stat cat entries.
1377     #my @stat_cats;
1378     #my $nodes = $xpath->find("/copy/stat_cat_entry");
1379     #foreach my $node ($nodes->get_nodelist) {
1380     #    next unless ($node->isa('XML::XPath::Node::Element'));
1381     #    my $stat_cat_id = $node->getAttribute('stat_cat');
1382     #    my $value = $node->string_value();
1383     #    # Need to search for an existing asset.stat_cat_entry
1384     #        my $asce = $e->search_asset_stat_cat_entry({'stat_cat' => $stat_cat_id, 'value' => $value})->[0];
1385     #    unless ($asce) {
1386     #        # if not, create a new one and use its id.
1387     #        $asce = Fieldmapper::asset::stat_cat_entry->new();
1388     #        $asce->stat_cat($stat_cat_id);
1389     #        $asce->value($value);
1390     #        $asce->owner($ou->id);
1391     #        $e->xact_begin;
1392     #        $asce = $e->create_asset_stat_cat_entry($asce);
1393     #        $e->xact_commit;
1394     #    }
1395     #    push(@stat_cats, $asce);
1396     #}
1397
1398     $e->xact_begin;
1399     $copy = $e->create_asset_copy($copy);
1400
1401     #if (scalar @stat_cats) {
1402     #    foreach my $asce (@stat_cats) {
1403     #        my $ascecm = Fieldmapper::asset::stat_cat_entry_copy_map->new();
1404     #        $ascecm->stat_cat($asce->stat_cat);
1405     #        $ascecm->stat_cat_entry($asce->id);
1406     #        $ascecm->owning_copy($copy->id);
1407     #        $ascecm = $e->create_asset_stat_cat_entry_copy_map($ascecm);
1408     #    }
1409     #}
1410     $e->commit;
1411     return $e->event->{textcode} unless ($r);
1412     return 'SUCCESS';
1413 }
1414
1415 # Checkout a copy to a patron
1416 #
1417 # Arguments
1418 # copy barcode
1419 # patron barcode
1420 #
1421 # Returns
1422 # textcode of the OSRF response.
1423 sub checkout {
1424     check_session_time();
1425     my ( $copy_barcode, $patron_barcode, $due_date ) = @_;
1426
1427     # Check for copy:
1428     my $copy = copy_from_barcode($copy_barcode);
1429     unless ( defined($copy) && blessed($copy) ) {
1430         return 'COPY_BARCODE_NOT_FOUND : ' . $copy_barcode;
1431     }
1432
1433     # Check for user
1434     my $uid;
1435     if ($patron_id_type eq 'barcode') {
1436         $uid = user_id_from_barcode($patron_barcode);
1437     } else {
1438         $uid = $patron_barcode;
1439     }
1440     return 'PATRON_BARCODE_NOT_FOUND : ' . $patron_barcode if ( ref($uid) );
1441
1442     my $response = OpenSRF::AppSession->create('open-ils.circ')->request(
1443         'open-ils.circ.checkout.full.override',
1444         $session{authtoken},
1445         {
1446             copy_barcode => $copy_barcode,
1447             patron_id    => $uid,
1448             due_date     => $due_date
1449         }
1450     )->gather(1);
1451     return $response->{textcode};
1452 }
1453
1454 sub renewal {
1455     check_session_time();
1456     my ( $copy_barcode, $due_date ) = @_;
1457
1458     # Check for copy:
1459     my $copy = copy_from_barcode($copy_barcode);
1460     unless ( defined($copy) && blessed($copy) ) {
1461         return 'COPY_BARCODE_NOT_FOUND : ' . $copy_barcode;
1462     }
1463
1464     my $response = OpenSRF::AppSession->create('open-ils.circ')->request(
1465         'open-ils.circ.renew.override',
1466         $session{authtoken},
1467         {
1468             copy_barcode => $copy_barcode,
1469             due_date     => $due_date
1470         }
1471     )->gather(1);
1472     return $response->{textcode};
1473 }
1474
1475 # Check a copy in
1476 #
1477 # Arguments
1478 # copy barcode
1479 #
1480 # Returns
1481 # "SUCCESS" on success
1482 # textcode of a failed OSRF request
1483 # 'COPY_NOT_CHECKED_OUT' when the copy is not checked out
1484
1485 sub checkin {
1486     check_session_time();
1487     my ($barcode) = @_;
1488
1489     my $copy = copy_from_barcode($barcode);
1490     return $copy->{textcode} unless ( blessed $copy);
1491
1492     return ("COPY_NOT_CHECKED_OUT $barcode")
1493       unless ( $copy->status == OILS_COPY_STATUS_CHECKED_OUT );
1494
1495     my $e = new_editor( authtoken => $session{authtoken} );
1496     return $e->event->{textcode} unless ( $e->checkauth );
1497
1498     my $circ = $e->search_action_circulation(
1499         [ { target_copy => $copy->id, xact_finish => undef } ] )->[0];
1500     my $r =
1501       OpenSRF::AppSession->create('open-ils.circ')
1502       ->request( 'open-ils.circ.checkin.override',
1503         $session{authtoken}, { force => 1, copy_id => $copy->id } )->gather(1);
1504     return 'SUCCESS' if ( $r->{textcode} eq 'ROUTE_ITEM' );
1505     return $r->{textcode};
1506 }
1507
1508 # Get actor.usr.id from barcode.
1509 # Arguments
1510 # patron barcode
1511 #
1512 # Returns
1513 # actor.usr.id
1514 # or hash on error
1515 sub user_id_from_barcode {
1516     check_session_time();
1517     my ($barcode) = @_;
1518
1519     my $response;
1520
1521     my $e = new_editor( authtoken => $session{authtoken} );
1522     return $response unless ( $e->checkauth );
1523
1524     my $card = $e->search_actor_card( { barcode => $barcode, active => 't' } );
1525     return $e->event unless ($card);
1526
1527     $response = $card->[0]->usr if (@$card);
1528
1529     $e->finish;
1530
1531     return $response;
1532 }
1533
1534 # Place a simple hold for a patron.
1535 #
1536 # Arguments
1537 # Target object appropriate for type of hold
1538 # Patron for whom the hold is place
1539 #
1540 # Returns
1541 # "SUCCESS" on success
1542 # textcode of a failed OSRF request
1543 # "HOLD_TYPE_NOT_SUPPORTED" if the hold type is not supported
1544 # (Currently only support 'T' and 'C')
1545
1546 # simple hold should be removed and full holds sub should be used instead - pragmatic solution only
1547
1548 sub place_simple_hold {
1549     check_session_time();
1550
1551     #my ($type, $target, $patron, $pickup_ou) = @_;
1552     my ( $target, $patron_id ) = @_;
1553
1554     require $conf->{path}->{oils_header};
1555     use vars qw/ $apputils $memcache $user $authtoken $authtime /;
1556
1557     osrf_connect( $conf->{path}->{opensrf_core} );
1558     oils_login( $conf->{auth}->{username}, $conf->{auth}->{password} );
1559     my $ahr = Fieldmapper::action::hold_request->new();
1560     $ahr->hold_type('C');
1561     # The targeter doesn't like our special statuses, and changing the status after the targeter finishes is difficult because it runs asynchronously.  Our workaround is to create the hold frozen, unfreeze it, then run the targeter manually.
1562     $ahr->target($target);
1563     $ahr->usr($patron_id);
1564     $ahr->requestor($conf->{hold}->{requestor});
1565     # NOTE: When User Agency, we don't know the pickup location until ItemShipped time
1566     # TODO: When Item Agency and using holds, set this to requested copy's circ lib?
1567     $ahr->pickup_lib($conf->{hold}->{init_pickup_lib});
1568     $ahr->phone_notify(''); # TODO: set this based on usr prefs
1569     $ahr->email_notify(1); # TODO: set this based on usr prefs
1570     $ahr->frozen('t');
1571     my $resp = simplereq( CIRC(), 'open-ils.circ.holds.create', $authtoken, $ahr );
1572     my $e = new_editor( xact => 1, authtoken => $session{authtoken} );
1573     $ahr = $e->retrieve_action_hold_request($resp);    # refresh from db
1574     $ahr->frozen('f');
1575     $e->update_action_hold_request($ahr);
1576     $e->commit;
1577     $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $ahr->id );
1578
1579     #oils_event_die($resp);
1580     my $errors = "";
1581     if ( ref($resp) eq 'ARRAY' ) {
1582         ( $errors .= "error : " . $_->{textcode} ) for @$resp;
1583         return $errors;
1584     } elsif ( ref($resp) ne 'HASH' ) {
1585         return "Hold placed! hold_id = " . $resp . "\n";
1586     }
1587 }
1588
1589 # Flesh user information
1590 # Arguments
1591 # actor.usr.id
1592 #
1593 # Returns
1594 # fieldmapped, fleshed user or
1595 # event hash on error
1596 sub flesh_user {
1597     check_session_time();
1598     my ($id) = @_;
1599     my $response =
1600       OpenSRF::AppSession->create('open-ils.actor')
1601       ->request( 'open-ils.actor.user.fleshed.retrieve',
1602         $session{'authtoken'}, $id,
1603         [ 'card', 'cards', 'standing_penalties', 'home_ou', 'profile' ] )
1604       ->gather(1);
1605     return $response;
1606 }