73eec43ccb425858f4d2d771f78f95eb018f3df2
[koha.git] / Koha / Hold.pm
1 package Koha::Hold;
2
3 # Copyright ByWater Solutions 2014
4 # Copyright 2017 Koha Development team
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22
23 use List::MoreUtils qw( any );
24
25 use C4::Context qw(preference);
26 use C4::Letters qw( GetPreparedLetter EnqueueLetter );
27 use C4::Log qw( logaction );
28 use C4::Reserves;
29
30 use Koha::AuthorisedValues;
31 use Koha::DateUtils qw( dt_from_string );
32 use Koha::Patrons;
33 use Koha::Biblios;
34 use Koha::Hold::CancellationRequests;
35 use Koha::Items;
36 use Koha::Libraries;
37 use Koha::Old::Holds;
38 use Koha::Calendar;
39 use Koha::Plugins;
40
41 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
42
43 use Koha::Exceptions;
44 use Koha::Exceptions::Hold;
45
46 use base qw(Koha::Object);
47
48 =head1 NAME
49
50 Koha::Hold - Koha Hold object class
51
52 =head1 API
53
54 =head2 Class methods
55
56 =cut
57
58 =head3 age
59
60 returns the number of days since a hold was placed, optionally
61 using the calendar
62
63 my $age = $hold->age( $use_calendar );
64
65 =cut
66
67 sub age {
68     my ( $self, $use_calendar ) = @_;
69
70     my $today = dt_from_string;
71     my $age;
72
73     if ( $use_calendar ) {
74         my $calendar = Koha::Calendar->new( branchcode => $self->branchcode );
75         $age = $calendar->days_between( dt_from_string( $self->reservedate ), $today );
76     }
77     else {
78         $age = $today->delta_days( dt_from_string( $self->reservedate ) );
79     }
80
81     $age = $age->in_units( 'days' );
82
83     return $age;
84 }
85
86 =head3 suspend_hold
87
88 my $hold = $hold->suspend_hold( $suspend_until_dt );
89
90 =cut
91
92 sub suspend_hold {
93     my ( $self, $dt ) = @_;
94
95     my $date = $dt ? $dt->clone()->truncate( to => 'day' )->datetime : undef;
96
97     if ( $self->is_found ) {    # We can't suspend found holds
98         if ( $self->is_waiting ) {
99             Koha::Exceptions::Hold::CannotSuspendFound->throw( status => 'W' );
100         }
101         elsif ( $self->is_in_transit ) {
102             Koha::Exceptions::Hold::CannotSuspendFound->throw( status => 'T' );
103         }
104         elsif ( $self->is_in_processing ) {
105             Koha::Exceptions::Hold::CannotSuspendFound->throw( status => 'P' );
106         }
107         else {
108             Koha::Exceptions::Hold::CannotSuspendFound->throw(
109                       'Unhandled data exception on found hold (id='
110                     . $self->id
111                     . ', found='
112                     . $self->found
113                     . ')' );
114         }
115     }
116
117     $self->suspend(1);
118     $self->suspend_until($date);
119     $self->store();
120
121     Koha::Plugins->call(
122         'after_hold_action',
123         {
124             action  => 'suspend',
125             payload => { hold => $self->get_from_storage }
126         }
127     );
128
129     logaction( 'HOLDS', 'SUSPEND', $self->reserve_id, $self )
130         if C4::Context->preference('HoldsLog');
131
132     Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
133         {
134             biblio_ids => [ $self->biblionumber ]
135         }
136     ) if C4::Context->preference('RealTimeHoldsQueue');
137
138     return $self;
139 }
140
141 =head3 resume
142
143 my $hold = $hold->resume();
144
145 =cut
146
147 sub resume {
148     my ( $self ) = @_;
149
150     $self->suspend(0);
151     $self->suspend_until( undef );
152
153     $self->store();
154
155     Koha::Plugins->call(
156         'after_hold_action',
157         {
158             action  => 'resume',
159             payload => { hold => $self->get_from_storage }
160         }
161     );
162
163     logaction( 'HOLDS', 'RESUME', $self->reserve_id, $self )
164         if C4::Context->preference('HoldsLog');
165
166     Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
167         {
168             biblio_ids => [ $self->biblionumber ]
169         }
170     ) if C4::Context->preference('RealTimeHoldsQueue');
171
172     return $self;
173 }
174
175 =head3 delete
176
177 $hold->delete();
178
179 =cut
180
181 sub delete {
182     my ( $self ) = @_;
183
184     my $deleted = $self->SUPER::delete($self);
185
186     logaction( 'HOLDS', 'DELETE', $self->reserve_id, $self )
187         if C4::Context->preference('HoldsLog');
188
189     return $deleted;
190 }
191
192 =head3 set_transfer
193
194 =cut
195
196 sub set_transfer {
197     my ( $self ) = @_;
198
199     $self->priority(0);
200     $self->found('T');
201     $self->store();
202
203     return $self;
204 }
205
206 =head3 set_waiting
207
208 =cut
209
210 sub set_waiting {
211     my ( $self, $desk_id ) = @_;
212
213     $self->priority(0);
214
215     my $today = dt_from_string();
216
217     my $values = {
218         found => 'W',
219         ( !$self->waitingdate ? ( waitingdate => $today->ymd ) : () ),
220         desk_id => $desk_id,
221     };
222
223     my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
224     my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
225
226     my $new_expiration_date = $today->clone->add(days => $max_pickup_delay);
227
228     if ( C4::Context->preference("ExcludeHolidaysFromMaxPickUpDelay") ) {
229         my $itemtype = $self->item ? $self->item->effective_itemtype : $self->biblio->itemtype;
230         my $daysmode = Koha::CirculationRules->get_effective_daysmode(
231             {
232                 categorycode => $self->borrower->categorycode,
233                 itemtype     => $itemtype,
234                 branchcode   => $self->branchcode,
235             }
236         );
237         my $calendar = Koha::Calendar->new( branchcode => $self->branchcode, days_mode => $daysmode );
238
239         $new_expiration_date = $calendar->days_forward( dt_from_string(), $max_pickup_delay );
240     }
241
242     # If patron's requested expiration date is prior to the
243     # calculated one, we keep the patron's one.
244     if ( $self->patron_expiration_date ) {
245         my $requested_expiration = dt_from_string( $self->patron_expiration_date );
246
247         my $cmp =
248           $requested_expiration
249           ? DateTime->compare( $requested_expiration, $new_expiration_date )
250           : 0;
251
252         $new_expiration_date =
253           $cmp == -1 ? $requested_expiration : $new_expiration_date;
254     }
255
256     $values->{expirationdate} = $new_expiration_date->ymd;
257
258     $self->set($values)->store();
259
260     return $self;
261 }
262
263 =head3 is_pickup_location_valid
264
265     if ($hold->is_pickup_location_valid({ library_id => $library->id }) ) {
266         ...
267     }
268
269 Returns a I<boolean> representing if the passed pickup location is valid for the hold.
270 It throws a I<Koha::Exceptions::_MissingParameter> if the library_id parameter is not
271 passed.
272
273 =cut
274
275 sub is_pickup_location_valid {
276     my ( $self, $params ) = @_;
277
278     Koha::Exceptions::MissingParameter->throw('The library_id parameter is mandatory')
279         unless $params->{library_id};
280
281     my $pickup_locations;
282
283     if ( $self->itemnumber ) { # item-level
284         $pickup_locations = $self->item->pickup_locations({ patron => $self->patron });
285     }
286     else { # biblio-level
287         $pickup_locations = $self->biblio->pickup_locations({ patron => $self->patron });
288     }
289
290     return any { $_->branchcode eq $params->{library_id} } $pickup_locations->as_list;
291 }
292
293 =head3 set_pickup_location
294
295     $hold->set_pickup_location(
296         {
297             library_id => $library->id,
298           [ force   => 0|1 ]
299         }
300     );
301
302 Updates the hold pickup location. It throws a I<Koha::Exceptions::Hold::InvalidPickupLocation> if
303 the passed pickup location is not valid.
304
305 Note: It is up to the caller to verify if I<AllowHoldPolicyOverride> is set when setting the
306 B<force> parameter.
307
308 =cut
309
310 sub set_pickup_location {
311     my ( $self, $params ) = @_;
312
313     Koha::Exceptions::MissingParameter->throw('The library_id parameter is mandatory')
314         unless $params->{library_id};
315
316     if (
317         $params->{force}
318         || $self->is_pickup_location_valid(
319             { library_id => $params->{library_id} }
320         )
321       )
322     {
323         # all good, set the new pickup location
324         $self->branchcode( $params->{library_id} )->store;
325     }
326     else {
327         Koha::Exceptions::Hold::InvalidPickupLocation->throw;
328     }
329
330     return $self;
331 }
332
333 =head3 set_processing
334
335 $hold->set_processing;
336
337 Mark the hold as in processing.
338
339 =cut
340
341 sub set_processing {
342     my ( $self ) = @_;
343
344     $self->priority(0);
345     $self->found('P');
346     $self->store();
347
348     return $self;
349 }
350
351 =head3 is_found
352
353 Returns true if hold is waiting, in transit or in processing
354
355 =cut
356
357 sub is_found {
358     my ($self) = @_;
359
360     return 0 unless $self->found();
361     return 1 if $self->found() eq 'W';
362     return 1 if $self->found() eq 'T';
363     return 1 if $self->found() eq 'P';
364 }
365
366 =head3 is_waiting
367
368 Returns true if hold is a waiting hold
369
370 =cut
371
372 sub is_waiting {
373     my ($self) = @_;
374
375     my $found = $self->found;
376     return $found && $found eq 'W';
377 }
378
379 =head3 is_in_transit
380
381 Returns true if hold is a in_transit hold
382
383 =cut
384
385 sub is_in_transit {
386     my ($self) = @_;
387
388     return 0 unless $self->found();
389     return $self->found() eq 'T';
390 }
391
392 =head3 is_in_processing
393
394 Returns true if hold is a in_processing hold
395
396 =cut
397
398 sub is_in_processing {
399     my ($self) = @_;
400
401     return 0 unless $self->found();
402     return $self->found() eq 'P';
403 }
404
405 =head3 is_cancelable_from_opac
406
407 Returns true if hold is a cancelable hold
408
409 Holds may be only canceled if they are not found.
410
411 This is used from the OPAC.
412
413 =cut
414
415 sub is_cancelable_from_opac {
416     my ($self) = @_;
417
418     return 1 unless $self->is_found();
419     return 0; # if ->is_in_transit or if ->is_waiting or ->is_in_processing
420 }
421
422 =head3 cancellation_requestable_from_opac
423
424     if ( $hold->cancellation_requestable_from_opac ) { ... }
425
426 Returns a I<boolean> representing if a cancellation request can be placed on the hold
427 from the OPAC. It targets holds that cannot be cancelled from the OPAC (see the
428 B<is_cancelable_from_opac> method above), but for which circulation rules allow
429 requesting cancellation.
430
431 Throws a B<Koha::Exceptions::InvalidStatus> exception with the following I<invalid_status>
432 values:
433
434 =over 4
435
436 =item B<'hold_not_waiting'>: the hold is expected to be waiting and it is not.
437
438 =item B<'no_item_linked'>: the waiting hold doesn't have an item properly linked.
439
440 =back
441
442 =cut
443
444 sub cancellation_requestable_from_opac {
445     my ( $self ) = @_;
446
447     Koha::Exceptions::InvalidStatus->throw( invalid_status => 'hold_not_waiting' )
448       unless $self->is_waiting;
449
450     my $item = $self->item;
451
452     Koha::Exceptions::InvalidStatus->throw( invalid_status => 'no_item_linked' )
453       unless $item;
454
455     my $patron = $self->patron;
456
457     my $controlbranch = $patron->branchcode;
458
459     if ( C4::Context->preference('ReservesControlBranch') eq 'ItemHomeLibrary' ) {
460         $controlbranch = $item->homebranch;
461     }
462
463     return Koha::CirculationRules->get_effective_rule_value(
464         {
465             categorycode => $patron->categorycode,
466             itemtype     => $item->itype,
467             branchcode   => $controlbranch,
468             rule_name    => 'waiting_hold_cancellation',
469         }
470     ) ? 1 : 0;
471 }
472
473 =head3 is_at_destination
474
475 Returns true if hold is waiting
476 and the hold's pickup branch matches
477 the hold item's holding branch
478
479 =cut
480
481 sub is_at_destination {
482     my ($self) = @_;
483
484     return $self->is_waiting() && ( $self->branchcode() eq $self->item()->holdingbranch() );
485 }
486
487 =head3 biblio
488
489 Returns the related Koha::Biblio object for this hold
490
491 =cut
492
493 sub biblio {
494     my ($self) = @_;
495
496     $self->{_biblio} ||= Koha::Biblios->find( $self->biblionumber() );
497
498     return $self->{_biblio};
499 }
500
501 =head3 patron
502
503 Returns the related Koha::Patron object for this hold
504
505 =cut
506
507 sub patron {
508     my ($self) = @_;
509
510     my $patron_rs = $self->_result->patron;
511     return Koha::Patron->_new_from_dbic($patron_rs);
512 }
513
514 =head3 item
515
516 Returns the related Koha::Item object for this Hold
517
518 =cut
519
520 sub item {
521     my ($self) = @_;
522
523     $self->{_item} ||= Koha::Items->find( $self->itemnumber() );
524
525     return $self->{_item};
526 }
527
528 =head3 branch
529
530 Returns the related Koha::Library object for this Hold
531
532 =cut
533
534 sub branch {
535     my ($self) = @_;
536
537     $self->{_branch} ||= Koha::Libraries->find( $self->branchcode() );
538
539     return $self->{_branch};
540 }
541
542 =head3 desk
543
544 Returns the related Koha::Desk object for this Hold
545
546 =cut
547
548 sub desk {
549     my $self = shift;
550     my $desk_rs = $self->_result->desk;
551     return unless $desk_rs;
552     return Koha::Desk->_new_from_dbic($desk_rs);
553 }
554
555 =head3 borrower
556
557 Returns the related Koha::Patron object for this Hold
558
559 =cut
560
561 # FIXME Should be renamed with ->patron
562 sub borrower {
563     my ($self) = @_;
564
565     $self->{_borrower} ||= Koha::Patrons->find( $self->borrowernumber() );
566
567     return $self->{_borrower};
568 }
569
570 =head3 is_suspended
571
572 my $bool = $hold->is_suspended();
573
574 =cut
575
576 sub is_suspended {
577     my ( $self ) = @_;
578
579     return $self->suspend();
580 }
581
582 =head3 add_cancellation_request
583
584     my $cancellation_request = $hold->add_cancellation_request({ [ creation_date => $creation_date ] });
585
586 Adds a cancellation request to the hold. Returns the generated
587 I<Koha::Hold::CancellationRequest> object.
588
589 =cut
590
591 sub add_cancellation_request {
592     my ( $self, $params ) = @_;
593
594     my $request = Koha::Hold::CancellationRequest->new(
595         {   hold_id      => $self->id,
596             ( $params->{creation_date} ? ( creation_date => $params->{creation_date} ) : () ),
597         }
598     )->store;
599
600     $request->discard_changes;
601
602     return $request;
603 }
604
605 =head3 cancellation_requests
606
607     my $cancellation_requests = $hold->cancellation_requests;
608
609 Returns related a I<Koha::Hold::CancellationRequests> resultset.
610
611 =cut
612
613 sub cancellation_requests {
614     my ($self) = @_;
615
616     return Koha::Hold::CancellationRequests->search( { hold_id => $self->id } );
617 }
618
619 =head3 cancel
620
621 my $cancel_hold = $hold->cancel(
622     {
623         [ charge_cancel_fee   => 1||0, ]
624         [ cancellation_reason => $cancellation_reason, ]
625         [ skip_holds_queue    => 1||0 ]
626     }
627 );
628
629 Cancel a hold:
630 - The hold will be moved to the old_reserves table with a priority=0
631 - The priority of other holds will be updated
632 - The patron will be charge (see ExpireReservesMaxPickUpDelayCharge) if the charge_cancel_fee parameter is set
633 - The canceled hold will have the cancellation reason added to old_reserves.cancellation_reason if one is passed in
634 - a CANCEL HOLDS log will be done if the pref HoldsLog is on
635
636 =cut
637
638 sub cancel {
639     my ( $self, $params ) = @_;
640
641     my $autofill_next = $params->{autofill} && $self->itemnumber && $self->found && $self->found eq 'W';
642
643     $self->_result->result_source->schema->txn_do(
644         sub {
645             my $patron = $self->patron;
646
647             $self->cancellationdate( dt_from_string->strftime( '%Y-%m-%d %H:%M:%S' ) );
648             $self->priority(0);
649             $self->cancellation_reason( $params->{cancellation_reason} );
650             $self->store();
651
652             if ( $params->{cancellation_reason} ) {
653                 my $letter = C4::Letters::GetPreparedLetter(
654                     module                 => 'reserves',
655                     letter_code            => 'HOLD_CANCELLATION',
656                     message_transport_type => 'email',
657                     branchcode             => $self->borrower->branchcode,
658                     lang                   => $self->borrower->lang,
659                     tables => {
660                         branches    => $self->borrower->branchcode,
661                         borrowers   => $self->borrowernumber,
662                         items       => $self->itemnumber,
663                         biblio      => $self->biblionumber,
664                         biblioitems => $self->biblionumber,
665                         reserves    => $self->unblessed,
666                     }
667                 );
668
669                 if ($letter) {
670                     C4::Letters::EnqueueLetter(
671                         {
672                             letter                   => $letter,
673                             borrowernumber         => $self->borrowernumber,
674                             message_transport_type => 'email',
675                         }
676                     );
677                 }
678             }
679
680             my $old_me = $self->_move_to_old;
681
682             Koha::Plugins->call(
683                 'after_hold_action',
684                 {
685                     action  => 'cancel',
686                     payload => { hold => $old_me->get_from_storage }
687                 }
688             );
689
690             # anonymize if required
691             $old_me->anonymize
692                 if $patron->privacy == 2;
693
694             $self->SUPER::delete(); # Do not add a DELETE log
695             # now fix the priority on the others....
696             C4::Reserves::_FixPriority({ biblionumber => $self->biblionumber });
697
698             # and, if desired, charge a cancel fee
699             my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
700             if ( $charge && $params->{'charge_cancel_fee'} ) {
701                 my $account =
702                   Koha::Account->new( { patron_id => $self->borrowernumber } );
703                 $account->add_debit(
704                     {
705                         amount     => $charge,
706                         user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
707                         interface  => C4::Context->interface,
708                         library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
709                         type       => 'RESERVE_EXPIRED',
710                         item_id    => $self->itemnumber
711                     }
712                 );
713             }
714
715             C4::Log::logaction( 'HOLDS', 'CANCEL', $self->reserve_id, $self )
716                 if C4::Context->preference('HoldsLog');
717
718             Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
719                 {
720                     biblio_ids => [ $old_me->biblionumber ]
721                 }
722             ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
723         }
724     );
725
726     if ($autofill_next) {
727         my ( undef, $next_hold ) = C4::Reserves::CheckReserves( $self->itemnumber );
728         if ($next_hold) {
729             my $is_transfer = $self->branchcode ne $next_hold->{branchcode};
730
731             C4::Reserves::ModReserveAffect( $self->itemnumber, $self->borrowernumber, $is_transfer, $next_hold->{reserve_id}, $self->desk_id, $autofill_next );
732             C4::Items::ModItemTransfer( $self->itemnumber, $self->branchcode, $next_hold->{branchcode}, "Reserve" ) if $is_transfer;
733         }
734     }
735
736     return $self;
737 }
738
739 =head3 fill
740
741     $hold->fill;
742
743 This method marks the hold as filled. It effectively moves it to old_reserves.
744
745 =cut
746
747 sub fill {
748     my ( $self ) = @_;
749     $self->_result->result_source->schema->txn_do(
750         sub {
751             my $patron = $self->patron;
752
753             $self->set(
754                 {
755                     found    => 'F',
756                     priority => 0,
757                 }
758             );
759
760             my $old_me = $self->_move_to_old;
761
762             Koha::Plugins->call(
763                 'after_hold_action',
764                 {
765                     action  => 'fill',
766                     payload => { hold => $old_me->get_from_storage }
767                 }
768             );
769
770             # anonymize if required
771             $old_me->anonymize
772                 if $patron->privacy == 2;
773
774             $self->SUPER::delete(); # Do not add a DELETE log
775
776             # now fix the priority on the others....
777             C4::Reserves::_FixPriority({ biblionumber => $self->biblionumber });
778
779             if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
780                 my $fee = $patron->category->reservefee // 0;
781                 if ( $fee > 0 ) {
782                     $patron->account->add_debit(
783                         {
784                             amount       => $fee,
785                             description  => $self->biblio->title,
786                             user_id      => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
787                             library_id   => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
788                             interface    => C4::Context->interface,
789                             type         => 'RESERVE',
790                             item_id      => $self->itemnumber
791                         }
792                     );
793                 }
794             }
795
796             C4::Log::logaction( 'HOLDS', 'FILL', $self->id, $self )
797                 if C4::Context->preference('HoldsLog');
798
799             Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
800                 {
801                     biblio_ids => [ $old_me->biblionumber ]
802                 }
803             ) if C4::Context->preference('RealTimeHoldsQueue');
804         }
805     );
806     return $self;
807 }
808
809 =head3 store
810
811 Override base store method to set default
812 expirationdate for holds.
813
814 =cut
815
816 sub store {
817     my ($self) = @_;
818
819     Koha::Exceptions::Hold::MissingPickupLocation->throw() unless $self->branchcode;
820
821     if ( !$self->in_storage ) {
822         if ( ! $self->expirationdate && $self->patron_expiration_date ) {
823             $self->expirationdate($self->patron_expiration_date);
824         }
825
826         if (
827             C4::Context->preference('DefaultHoldExpirationdate')
828                 && !$self->expirationdate
829           )
830         {
831             $self->_set_default_expirationdate;
832         }
833     }
834     else {
835
836         my %updated_columns = $self->_result->get_dirty_columns;
837         return $self->SUPER::store unless %updated_columns;
838
839         if ( exists $updated_columns{reservedate} ) {
840             if (
841                 C4::Context->preference('DefaultHoldExpirationdate')
842                 && ! exists $updated_columns{expirationdate}
843               )
844             {
845                 $self->_set_default_expirationdate;
846             }
847         }
848     }
849
850     $self = $self->SUPER::store;
851 }
852
853 sub _set_default_expirationdate {
854     my $self = shift;
855
856     my $period = C4::Context->preference('DefaultHoldExpirationdatePeriod') || 0;
857     my $timeunit =
858       C4::Context->preference('DefaultHoldExpirationdateUnitOfTime') || 'days';
859
860     $self->expirationdate(
861         dt_from_string( $self->reservedate )->add( $timeunit => $period ) );
862 }
863
864 =head3 _move_to_old
865
866 my $is_moved = $hold->_move_to_old;
867
868 Move a hold to the old_reserve table following the same pattern as Koha::Patron->move_to_deleted
869
870 =cut
871
872 sub _move_to_old {
873     my ($self) = @_;
874     my $hold_infos = $self->unblessed;
875     return Koha::Old::Hold->new( $hold_infos )->store;
876 }
877
878 =head3 to_api_mapping
879
880 This method returns the mapping for representing a Koha::Hold object
881 on the API.
882
883 =cut
884
885 sub to_api_mapping {
886     return {
887         reserve_id       => 'hold_id',
888         borrowernumber   => 'patron_id',
889         reservedate      => 'hold_date',
890         biblionumber     => 'biblio_id',
891         branchcode       => 'pickup_library_id',
892         notificationdate => undef,
893         reminderdate     => undef,
894         cancellationdate => 'cancellation_date',
895         reservenotes     => 'notes',
896         found            => 'status',
897         itemnumber       => 'item_id',
898         waitingdate      => 'waiting_date',
899         expirationdate   => 'expiration_date',
900         patron_expiration_date => undef,
901         lowestPriority   => 'lowest_priority',
902         suspend          => 'suspended',
903         suspend_until    => 'suspended_until',
904         itemtype         => 'item_type',
905         item_level_hold  => 'item_level',
906     };
907 }
908
909 =head2 Internal methods
910
911 =head3 _type
912
913 =cut
914
915 sub _type {
916     return 'Reserve';
917 }
918
919 =head1 AUTHORS
920
921 Kyle M Hall <kyle@bywatersolutions.com>
922 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
923 Martin Renvoize <martin.renvoize@ptfs-europe.com>
924
925 =cut
926
927 1;