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