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