Add option to use patron id instead of barcode
[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 ($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     my $patron_barcode = "zyyyy";    # XXX: CUSTOMIZATION NEEDED XXX institution/eg_as_item_agency user lookup here
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 }