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