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