Bug 10667: Allow authentication with cardnumber for ILS-DI
[koha-equinox.git] / C4 / ILSDI / Services.pm
1 package C4::ILSDI::Services;
2
3 # Copyright 2009 SARL Biblibre
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 use strict;
21 use warnings;
22
23 use C4::Members;
24 use C4::Items;
25 use C4::Circulation;
26 use C4::Branch;
27 use C4::Accounts;
28 use C4::Biblio;
29 use C4::Reserves qw(AddReserve CancelReserve GetReservesFromBiblionumber GetReservesFromBorrowernumber CanBookBeReserved CanItemBeReserved);
30 use C4::Context;
31 use C4::AuthoritiesMarc;
32 use C4::ILSDI::Utility;
33 use XML::Simple;
34 use HTML::Entities;
35 use CGI;
36 use DateTime;
37 use C4::Auth;
38
39 =head1 NAME
40
41 C4::ILS-DI::Services - ILS-DI Services
42
43 =head1 DESCRIPTION
44
45 Each function in this module represents an ILS-DI service.
46 They all takes a CGI instance as argument and most of them return a 
47 hashref that will be printed by XML::Simple in opac/ilsdi.pl
48
49 =head1 SYNOPSIS
50
51         use C4::ILSDI::Services;
52         use XML::Simple;
53         use CGI;
54
55         my $cgi = new CGI;
56
57         $out = LookupPatron($cgi);
58
59         print CGI::header('text/xml');
60         print XMLout($out,
61                 noattr => 1, 
62                 noescape => 1,
63                 nosort => 1,
64                 xmldecl => '<?xml version="1.0" encoding="UTF-8" ?>',
65                 RootName => 'LookupPatron', 
66                 SuppressEmpty => 1);
67
68 =cut
69
70 =head1 FUNCTIONS
71
72 =head2 GetAvailability
73
74 Given a set of biblionumbers or itemnumbers, returns a list with 
75 availability of the items associated with the identifiers.
76
77 Parameters:
78
79 =head3 id (Required)
80
81 list of either biblionumbers or itemnumbers
82
83 =head3 id_type (Required)
84
85 defines the type of record identifier being used in the request, 
86 possible values:
87
88   - bib
89   - item
90
91 =head3 return_type (Optional)
92
93 requests a particular level of detail in reporting availability, 
94 possible values:
95
96   - bib
97   - item
98
99 =head3 return_fmt (Optional)
100
101 requests a particular format or set of formats in reporting 
102 availability 
103
104 =cut
105
106 sub GetAvailability {
107     my ($cgi) = @_;
108
109     my $out = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
110     $out .= "<dlf:collection\n";
111     $out .= "  xmlns:dlf=\"http://diglib.org/ilsdi/1.1\"\n";
112     $out .= "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
113     $out .= "  xsi:schemaLocation=\"http://diglib.org/ilsdi/1.1\n";
114     $out .= "    http://diglib.org/architectures/ilsdi/schemas/1.1/dlfexpanded.xsd\">\n";
115
116     foreach my $id ( split( / /, $cgi->param('id') ) ) {
117         if ( $cgi->param('id_type') eq "item" ) {
118             my ( $biblionumber, $status, $msg, $location ) = Availability($id);
119
120             $out .= "  <dlf:record>\n";
121             $out .= "    <dlf:bibliographic id=\"" . ( $biblionumber || $id ) . "\" />\n";
122             $out .= "    <dlf:items>\n";
123             $out .= "      <dlf:item id=\"" . $id . "\">\n";
124             $out .= "        <dlf:simpleavailability>\n";
125             $out .= "          <dlf:identifier>" . $id . "</dlf:identifier>\n";
126             $out .= "          <dlf:availabilitystatus>" . $status . "</dlf:availabilitystatus>\n";
127             if ($msg)      { $out .= "          <dlf:availabilitymsg>" . $msg . "</dlf:availabilitymsg>\n"; }
128             if ($location) { $out .= "          <dlf:location>" . $location . "</dlf:location>\n"; }
129             $out .= "        </dlf:simpleavailability>\n";
130             $out .= "      </dlf:item>\n";
131             $out .= "    </dlf:items>\n";
132             $out .= "  </dlf:record>\n";
133         } else {
134             my $status;
135             my $msg;
136             my $biblioitem = ( GetBiblioItemByBiblioNumber( $id, undef ) )[0];
137             if ($biblioitem) {
138
139             } else {
140                 $status = "unknown";
141                 $msg    = "Error: could not retrieve availability for this ID";
142             }
143             $out .= "  <dlf:record>\n";
144             $out .= "    <dlf:bibliographic id=\"" . $id . "\" />\n";
145             $out .= "    <dlf:simpleavailability>\n";
146             $out .= "      <dlf:identifier>" . $id . "</dlf:identifier>\n";
147             $out .= "      <dlf:availabilitystatus>" . $status . "</dlf:availabilitystatus>\n";
148             $out .= "      <dlf:availabilitymsg>" . $msg . "</dlf:availabilitymsg>\n";
149             $out .= "    </dlf:simpleavailability>\n";
150             $out .= "  </dlf:record>\n";
151         }
152     }
153     $out .= "</dlf:collection>\n";
154
155     return $out;
156 }
157
158 =head2 GetRecords
159
160 Given a list of biblionumbers, returns a list of record objects that 
161 contain bibliographic information, as well as associated holdings and item
162 information. The caller may request a specific metadata schema for the 
163 record objects to be returned.
164
165 This function behaves similarly to HarvestBibliographicRecords and 
166 HarvestExpandedRecords in Data Aggregation, but allows quick, real time 
167 lookup by bibliographic identifier.
168
169 You can use OAI-PMH ListRecords instead of this service.
170
171 Parameters:
172
173   - id (Required)
174         list of system record identifiers
175   - id_type (Optional)
176         Defines the metadata schema in which the records are returned, 
177         possible values:
178           - MARCXML
179
180 =cut
181
182 sub GetRecords {
183     my ($cgi) = @_;
184
185     # Check if the schema is supported. For now, GetRecords only supports MARCXML
186     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
187         return { code => 'UnsupportedSchema' };
188     }
189
190     my @records;
191
192     # Loop over biblionumbers
193     foreach my $biblionumber ( split( / /, $cgi->param('id') ) ) {
194
195         # Get the biblioitem from the biblionumber
196         my $biblioitem = ( GetBiblioItemByBiblioNumber( $biblionumber, undef ) )[0];
197         if ( not $biblioitem->{'biblionumber'} ) {
198             $biblioitem->{code} = "RecordNotFound";
199         }
200
201         my $embed_items = 1;
202         my $record = GetMarcBiblio($biblionumber, $embed_items);
203         if ($record) {
204             $biblioitem->{marcxml} = $record->as_xml_record();
205         }
206
207         # We don't want MARC to be displayed
208         delete $biblioitem->{'marc'};
209
210         # Get most of the needed data
211         my $biblioitemnumber = $biblioitem->{'biblioitemnumber'};
212         my @reserves         = GetReservesFromBiblionumber( $biblionumber, undef, undef );
213         my $issues           = GetBiblioIssues($biblionumber);
214         my $items            = GetItemsByBiblioitemnumber($biblioitemnumber);
215
216         # We loop over the items to clean them
217         foreach my $item (@$items) {
218
219             # This hides additionnal XML subfields, we don't need these info
220             delete $item->{'more_subfields_xml'};
221
222             # Display branch names instead of branch codes
223             $item->{'homebranchname'}    = GetBranchName( $item->{'homebranch'} );
224             $item->{'holdingbranchname'} = GetBranchName( $item->{'holdingbranch'} );
225         }
226
227         # Hashref building...
228         $biblioitem->{'items'}->{'item'}       = $items;
229         $biblioitem->{'reserves'}->{'reserve'} = $reserves[1];
230         $biblioitem->{'issues'}->{'issue'}     = $issues;
231
232         push @records, $biblioitem;
233     }
234
235     return { record => \@records };
236 }
237
238 =head2 GetAuthorityRecords
239
240 Given a list of authority record identifiers, returns a list of record 
241 objects that contain the authority records. The function user may request 
242 a specific metadata schema for the record objects.
243
244 Parameters:
245
246   - id (Required)
247     list of authority record identifiers
248   - schema (Optional)
249     specifies the metadata schema of records to be returned, possible values:
250       - MARCXML
251
252 =cut
253
254 sub GetAuthorityRecords {
255     my ($cgi) = @_;
256
257     # If the user asks for an unsupported schema, return an error code
258     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
259         return { code => 'UnsupportedSchema' };
260     }
261
262     my @records;
263
264     # Let's loop over the authority IDs
265     foreach my $authid ( split( / /, $cgi->param('id') ) ) {
266
267         # Get the record as XML string, or error code
268         push @records, GetAuthorityXML($authid) || { code => 'RecordNotFound' };
269     }
270
271     return { record => \@records };
272 }
273
274 =head2 LookupPatron
275
276 Looks up a patron in the ILS by an identifier, and returns the borrowernumber.
277
278 Parameters:
279
280   - id (Required)
281         an identifier used to look up the patron in Koha
282   - id_type (Optional)
283         the type of the identifier, possible values:
284         - cardnumber
285         - firstname
286         - userid
287         - borrowernumber
288
289 =cut
290
291 sub LookupPatron {
292     my ($cgi) = @_;
293
294     # Get the borrower...
295     my $borrower = GetMember($cgi->param('id_type') => $cgi->param('id'));
296     if ( not $borrower->{'borrowernumber'} ) {
297         return { message => 'PatronNotFound' };
298     }
299
300     # Build the hashref
301     my $patron->{'id'} = $borrower->{'borrowernumber'};
302     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
303
304     # ...and return his ID
305     return $patron;
306 }
307
308 =head2 AuthenticatePatron
309
310 Authenticates a user's login credentials and returns the identifier for 
311 the patron.
312
313 Parameters:
314
315   - username (Required)
316     user's login identifier (userid or cardnumber)
317   - password (Required)
318     user's password
319
320 =cut
321
322 sub AuthenticatePatron {
323     my ($cgi) = @_;
324     my ($status, $cardnumber, $userid) = C4::Auth::checkpw( C4::Context->dbh, $cgi->param('username'), $cgi->param('password') );
325     if ( $status ) {
326         # Get the borrower
327         my $borrower = GetMember( cardnumber => $cardnumber );
328         my $patron->{'id'} = $borrower->{'borrowernumber'};
329         return $patron;
330     }
331     else {
332         return { code => 'PatronNotFound' };
333     }
334 }
335
336 =head2 GetPatronInfo
337
338 Returns specified information about the patron, based on options in the 
339 request. This function can optionally return patron's contact information, 
340 fine information, hold request information, and loan information.
341
342 Parameters:
343
344   - patron_id (Required)
345         the borrowernumber
346   - show_contact (Optional, default 1)
347         whether or not to return patron's contact information in the response
348   - show_fines (Optional, default 0)
349         whether or not to return fine information in the response
350   - show_holds (Optional, default 0)
351         whether or not to return hold request information in the response
352   - show_loans (Optional, default 0)
353         whether or not to return loan information request information in the response 
354
355 =cut
356
357 sub GetPatronInfo {
358     my ($cgi) = @_;
359
360     # Get Member details
361     my $borrowernumber = $cgi->param('patron_id');
362     my $borrower = GetMemberDetails( $borrowernumber );
363     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
364
365     # Cleaning the borrower hashref
366     $borrower->{'charges'}    = $borrower->{'flags'}->{'CHARGES'}->{'amount'};
367     $borrower->{'branchname'} = GetBranchName( $borrower->{'branchcode'} );
368     delete $borrower->{'flags'};
369     delete $borrower->{'userid'};
370     delete $borrower->{'password'};
371
372     # Contact fields management
373     if ( $cgi->param('show_contact') eq "0" ) {
374
375         # Define contact fields
376         my @contactfields = (
377             'email',              'emailpro',           'fax',                 'mobile',          'phone',             'phonepro',
378             'streetnumber',       'zipcode',            'city',                'streettype',      'B_address',         'B_city',
379             'B_email',            'B_phone',            'B_zipcode',           'address',         'address2',          'altcontactaddress1',
380             'altcontactaddress2', 'altcontactaddress3', 'altcontactfirstname', 'altcontactphone', 'altcontactsurname', 'altcontactzipcode'
381         );
382
383         # and delete them
384         foreach my $field (@contactfields) {
385             delete $borrower->{$field};
386         }
387     }
388
389     # Fines management
390     if ( $cgi->param('show_fines') eq "1" ) {
391         my @charges;
392         for ( my $i = 1 ; my @charge = getcharges( $borrowernumber, undef, $i ) ; $i++ ) {
393             push( @charges, @charge );
394         }
395         $borrower->{'fines'}->{'fine'} = \@charges;
396     }
397
398     # Reserves management
399     if ( $cgi->param('show_holds') eq "1" ) {
400
401         # Get borrower's reserves
402         my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
403         foreach my $reserve (@reserves) {
404
405             # Get additional informations
406             my $item = GetBiblioFromItemNumber( $reserve->{'itemnumber'}, undef );
407             my $branchname = GetBranchName( $reserve->{'branchcode'} );
408
409             # Remove unwanted fields
410             delete $item->{'marc'};
411             delete $item->{'marcxml'};
412             delete $item->{'more_subfields_xml'};
413
414             # Add additional fields
415             $reserve->{'item'}       = $item;
416             $reserve->{'branchname'} = $branchname;
417             $reserve->{'title'}      = GetBiblio( $reserve->{'biblionumber'} )->{'title'};
418         }
419         $borrower->{'holds'}->{'hold'} = \@reserves;
420     }
421
422     # Issues management
423     if ( $cgi->param('show_loans') eq "1" ) {
424         my $issues = GetPendingIssues($borrowernumber);
425         foreach my $issue ( @$issues ){
426             $issue->{'issuedate'} = $issue->{'issuedate'}->strftime('%Y-%m-%d %H:%M');
427             $issue->{'date_due'} = $issue->{'date_due'}->strftime('%Y-%m-%d %H:%M');
428         }
429         $borrower->{'loans'}->{'loan'} = $issues;
430     }
431
432     return $borrower;
433 }
434
435 =head2 GetPatronStatus
436
437 Returns a patron's status information.
438
439 Parameters:
440
441   - patron_id (Required)
442         the borrower ID
443
444 =cut
445
446 sub GetPatronStatus {
447     my ($cgi) = @_;
448
449     # Get Member details
450     my $borrowernumber = $cgi->param('patron_id');
451     my $borrower = GetMemberDetails( $borrowernumber );
452     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
453
454     # Return the results
455     return {
456         type   => $$borrower{categorycode},
457         status => 0, # TODO
458         expiry => $$borrower{dateexpiry},
459     };
460 }
461
462 =head2 GetServices
463
464 Returns information about the services available on a particular item for 
465 a particular patron.
466
467 Parameters:
468
469   - patron_id (Required)
470         a borrowernumber
471   - item_id (Required)
472         an itemnumber
473 =cut
474
475 sub GetServices {
476     my ($cgi) = @_;
477
478     # Get the member, or return an error code if not found
479     my $borrowernumber = $cgi->param('patron_id');
480     my $borrower = GetMemberDetails( $borrowernumber );
481     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
482
483     # Get the item, or return an error code if not found
484     my $itemnumber = $cgi->param('item_id');
485     my $item = GetItem( $itemnumber );
486     return { code => 'RecordNotFound' } unless $$item{itemnumber};
487
488     my @availablefor;
489
490     # Reserve level management
491     my $biblionumber = $item->{'biblionumber'};
492     my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber );
493     if ($canbookbereserved) {
494         push @availablefor, 'title level hold';
495         my $canitembereserved = IsAvailableForItemLevelRequest($itemnumber);
496         if ($canitembereserved) {
497             push @availablefor, 'item level hold';
498         }
499     }
500
501     # Reserve cancellation management
502     my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
503     my @reserveditems;
504     foreach my $reserve (@reserves) {
505         push @reserveditems, $reserve->{'itemnumber'};
506     }
507     if ( grep { $itemnumber eq $_ } @reserveditems ) {
508         push @availablefor, 'hold cancellation';
509     }
510
511     # Renewal management
512     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
513     if ( $renewal[0] ) {
514         push @availablefor, 'loan renewal';
515     }
516
517     # Issuing management
518     my $barcode = $item->{'barcode'} || '';
519     $barcode = barcodedecode($barcode) if ( $barcode && C4::Context->preference('itemBarcodeInputFilter') );
520     if ($barcode) {
521         my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower, $barcode );
522
523         # TODO push @availablefor, 'loan';
524     }
525
526     my $out;
527     $out->{'AvailableFor'} = \@availablefor;
528
529     return $out;
530 }
531
532 =head2 RenewLoan
533
534 Extends the due date for a borrower's existing issue.
535
536 Parameters:
537
538   - patron_id (Required)
539         a borrowernumber
540   - item_id (Required)
541         an itemnumber
542   - desired_due_date (Required)
543         the date the patron would like the item returned by 
544
545 =cut
546
547 sub RenewLoan {
548     my ($cgi) = @_;
549
550     # Get borrower infos or return an error code
551     my $borrowernumber = $cgi->param('patron_id');
552     my $borrower = GetMemberDetails( $borrowernumber );
553     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
554
555     # Get the item, or return an error code
556     my $itemnumber = $cgi->param('item_id');
557     my $item = GetItem( $itemnumber );
558     return { code => 'RecordNotFound' } unless $$item{itemnumber};
559
560     # Add renewal if possible
561     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
562     if ( $renewal[0] ) { AddRenewal( $borrowernumber, $itemnumber ); }
563
564     my $issue = GetItemIssue($itemnumber);
565
566     # Hashref building
567     my $out;
568     $out->{'renewals'} = $issue->{'renewals'};
569     $out->{date_due}   = $issue->{date_due}->strftime('%Y-%m-%d %H:%S');
570     $out->{'success'}  = $renewal[0];
571     $out->{'error'}    = $renewal[1];
572
573     return $out;
574 }
575
576 =head2 HoldTitle
577
578 Creates, for a borrower, a biblio-level hold reserve.
579
580 Parameters:
581
582   - patron_id (Required)
583         a borrowernumber
584   - bib_id (Required)
585         a biblionumber
586   - request_location (Required)
587         IP address where the end user request is being placed
588   - pickup_location (Optional)
589         a branch code indicating the location to which to deliver the item for pickup
590   - needed_before_date (Optional)
591         date after which hold request is no longer needed
592   - pickup_expiry_date (Optional)
593         date after which item returned to shelf if item is not picked up 
594
595 =cut
596
597 sub HoldTitle {
598     my ($cgi) = @_;
599
600     # Get the borrower or return an error code
601     my $borrowernumber = $cgi->param('patron_id');
602     my $borrower = GetMemberDetails( $borrowernumber );
603     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
604
605     # Get the biblio record, or return an error code
606     my $biblionumber = $cgi->param('bib_id');
607     my $biblio = GetBiblio( $biblionumber );
608     return { code => 'RecordNotFound' } unless $$biblio{biblionumber};
609     
610     my $title = $$biblio{title};
611
612     # Check if the biblio can be reserved
613     return { code => 'NotHoldable' } unless CanBookBeReserved( $borrowernumber, $biblionumber );
614
615     my $branch;
616
617     # Pickup branch management
618     if ( $cgi->param('pickup_location') ) {
619         $branch = $cgi->param('pickup_location');
620         my $branches = GetBranches;
621         return { code => 'LocationNotFound' } unless $$branches{$branch};
622     } else { # if the request provide no branch, use the borrower's branch
623         $branch = $$borrower{branchcode};
624     }
625
626     # Add the reserve
627     #          $branch, $borrowernumber, $biblionumber, $constraint, $bibitems,  $priority, $notes, $title, $checkitem,  $found
628     AddReserve( $branch, $borrowernumber, $biblionumber, 'a', undef, 0, undef, $title, undef, undef );
629
630     # Hashref building
631     my $out;
632     $out->{'title'}           = $title;
633     $out->{'pickup_location'} = GetBranchName($branch);
634
635     # TODO $out->{'date_available'}  = '';
636
637     return $out;
638 }
639
640 =head2 HoldItem
641
642 Creates, for a borrower, an item-level hold request on a specific item of 
643 a bibliographic record in Koha.
644
645 Parameters:
646
647   - patron_id (Required)
648         a borrowernumber
649   - bib_id (Required)
650         a biblionumber
651   - item_id (Required)
652         an itemnumber
653   - pickup_location (Optional)
654         a branch code indicating the location to which to deliver the item for pickup
655   - needed_before_date (Optional)
656         date after which hold request is no longer needed
657   - pickup_expiry_date (Optional)
658         date after which item returned to shelf if item is not picked up 
659
660 =cut
661
662 sub HoldItem {
663     my ($cgi) = @_;
664
665     # Get the borrower or return an error code
666     my $borrowernumber = $cgi->param('patron_id');
667     my $borrower = GetMemberDetails( $borrowernumber );
668     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
669
670     # Get the biblio or return an error code
671     my $biblionumber = $cgi->param('bib_id');
672     my $biblio = GetBiblio($biblionumber);
673     return { code => 'RecordNotFound' } unless $$biblio{biblionumber};
674
675     my $title = $$biblio{title};
676
677     # Get the item or return an error code
678     my $itemnumber = $cgi->param('item_id');
679     my $item = GetItem( $itemnumber );
680     return { code => 'RecordNotFound' } unless $$item{itemnumber};
681
682     # If the biblio does not match the item, return an error code
683     return { code => 'RecordNotFound' } if $$item{biblionumber} ne $$biblio{biblionumber};
684
685     # Check for item disponibility
686     my $canitembereserved = C4::Reserves::CanItemBeReserved( $borrowernumber, $itemnumber );
687     my $canbookbereserved = C4::Reserves::CanBookBeReserved( $borrowernumber, $biblionumber );
688     return { code => 'NotHoldable' } unless $canbookbereserved and $canitembereserved;
689
690     my $branch;
691
692     # Pickup branch management
693     if ( $cgi->param('pickup_location') ) {
694         $branch = $cgi->param('pickup_location');
695         my $branches = GetBranches();
696         return { code => 'LocationNotFound' } unless $$branches{$branch};
697     } else { # if the request provide no branch, use the borrower's branch
698         $branch = $$borrower{branchcode};
699     }
700
701     my $rank;
702     my $found;
703
704     # Get rank and found
705     $rank = '0' unless C4::Context->preference('ReservesNeedReturns');
706     if ( $item->{'holdingbranch'} eq $branch ) {
707         $found = 'W' unless C4::Context->preference('ReservesNeedReturns');
708     }
709
710     # Add the reserve
711     # $branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found
712     AddReserve( $branch, $borrowernumber, $biblionumber, 'a', undef, $rank, '', '', '', $title, $itemnumber, $found );
713
714     # Hashref building
715     my $out;
716     $out->{'pickup_location'} = GetBranchName($branch);
717
718     # TODO $out->{'date_available'} = '';
719
720     return $out;
721 }
722
723 =head2 CancelHold
724
725 Cancels an active reserve request for the borrower.
726
727 Parameters:
728
729   - patron_id (Required)
730         a borrowernumber
731   - item_id (Required)
732         an itemnumber 
733
734 =cut
735
736 sub CancelHold {
737     my ($cgi) = @_;
738
739     # Get the borrower or return an error code
740     my $borrowernumber = $cgi->param('patron_id');
741     my $borrower = GetMemberDetails( $borrowernumber );
742     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
743
744     # Get the item or return an error code
745     my $itemnumber = $cgi->param('item_id');
746     my $item = GetItem( $itemnumber );
747     return { code => 'RecordNotFound' } unless $$item{itemnumber};
748
749     # Get borrower's reserves
750     my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
751     my @reserveditems;
752
753     # ...and loop over it to build an array of reserved itemnumbers
754     foreach my $reserve (@reserves) {
755         push @reserveditems, $reserve->{'itemnumber'};
756     }
757
758     # if the item was not reserved by the borrower, returns an error code
759     return { code => 'NotCanceled' } unless any { $itemnumber eq $_ } @reserveditems;
760
761     # Cancel the reserve
762     CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
763
764     return { code => 'Canceled' };
765 }
766
767 1;