Bug 14364: Allow automatically canceled expired waiting holds to fill the next hold
[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(
464         {
465             categorycode => $patron->categorycode,
466             itemtype     => $item->itype,
467             branchcode   => $controlbranch,
468             rule_name    => 'waiting_hold_cancellation',
469         }
470     )->rule_value ? 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 <<<<<<< HEAD
641 =======
642
643     my $autofill_next = $params->{autofill} && $self->itemnumber && $self->found && $self->found eq 'W';
644
645 >>>>>>> Bug 14364: Allow automatically canceled expired waiting holds to fill the next hold
646     $self->_result->result_source->schema->txn_do(
647         sub {
648             my $patron = $self->patron;
649
650             $self->cancellationdate( dt_from_string->strftime( '%Y-%m-%d %H:%M:%S' ) );
651             $self->priority(0);
652             $self->cancellation_reason( $params->{cancellation_reason} );
653             $self->store();
654
655             if ( $params->{cancellation_reason} ) {
656                 my $letter = C4::Letters::GetPreparedLetter(
657                     module                 => 'reserves',
658                     letter_code            => 'HOLD_CANCELLATION',
659                     message_transport_type => 'email',
660                     branchcode             => $self->borrower->branchcode,
661                     lang                   => $self->borrower->lang,
662                     tables => {
663                         branches    => $self->borrower->branchcode,
664                         borrowers   => $self->borrowernumber,
665                         items       => $self->itemnumber,
666                         biblio      => $self->biblionumber,
667                         biblioitems => $self->biblionumber,
668                         reserves    => $self->unblessed,
669                     }
670                 );
671
672                 if ($letter) {
673                     C4::Letters::EnqueueLetter(
674                         {
675                             letter                   => $letter,
676                             borrowernumber         => $self->borrowernumber,
677                             message_transport_type => 'email',
678                         }
679                     );
680                 }
681             }
682
683             my $old_me = $self->_move_to_old;
684
685             Koha::Plugins->call(
686                 'after_hold_action',
687                 {
688                     action  => 'cancel',
689                     payload => { hold => $old_me->get_from_storage }
690                 }
691             );
692
693             # anonymize if required
694             $old_me->anonymize
695                 if $patron->privacy == 2;
696
697             $self->SUPER::delete(); # Do not add a DELETE log
698             # now fix the priority on the others....
699             C4::Reserves::_FixPriority({ biblionumber => $self->biblionumber });
700
701             # and, if desired, charge a cancel fee
702             my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
703             if ( $charge && $params->{'charge_cancel_fee'} ) {
704                 my $account =
705                   Koha::Account->new( { patron_id => $self->borrowernumber } );
706                 $account->add_debit(
707                     {
708                         amount     => $charge,
709                         user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
710                         interface  => C4::Context->interface,
711                         library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
712                         type       => 'RESERVE_EXPIRED',
713                         item_id    => $self->itemnumber
714                     }
715                 );
716             }
717
718             C4::Log::logaction( 'HOLDS', 'CANCEL', $self->reserve_id, $self )
719                 if C4::Context->preference('HoldsLog');
720
721             Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
722                 {
723                     biblio_ids => [ $old_me->biblionumber ]
724                 }
725             ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
726         }
727     );
728
729     if ($autofill_next) {
730         my ( undef, $next_hold ) = C4::Reserves::CheckReserves( $self->itemnumber );
731         if ($next_hold) {
732             my $is_transfer = $self->branchcode ne $next_hold->{branchcode};
733
734             C4::Reserves::ModReserveAffect( $self->itemnumber, $self->borrowernumber, $is_transfer, $next_hold->{reserve_id}, $self->desk_id, $autofill_next );
735             C4::Reserves::ModItemTransfer( $self->itemnumber, $self->branchcode, $next_hold->{branchcode}, "Reserve" ) if $is_transfer;
736         }
737     }
738
739     return $self;
740 }
741
742 =head3 fill
743
744     $hold->fill;
745
746 This method marks the hold as filled. It effectively moves it to old_reserves.
747
748 =cut
749
750 sub fill {
751     my ( $self ) = @_;
752     $self->_result->result_source->schema->txn_do(
753         sub {
754             my $patron = $self->patron;
755
756             $self->set(
757                 {
758                     found    => 'F',
759                     priority => 0,
760                 }
761             );
762
763             my $old_me = $self->_move_to_old;
764
765             Koha::Plugins->call(
766                 'after_hold_action',
767                 {
768                     action  => 'fill',
769                     payload => { hold => $old_me->get_from_storage }
770                 }
771             );
772
773             # anonymize if required
774             $old_me->anonymize
775                 if $patron->privacy == 2;
776
777             $self->SUPER::delete(); # Do not add a DELETE log
778
779             # now fix the priority on the others....
780             C4::Reserves::_FixPriority({ biblionumber => $self->biblionumber });
781
782             if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
783                 my $fee = $patron->category->reservefee // 0;
784                 if ( $fee > 0 ) {
785                     $patron->account->add_debit(
786                         {
787                             amount       => $fee,
788                             description  => $self->biblio->title,
789                             user_id      => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
790                             library_id   => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
791                             interface    => C4::Context->interface,
792                             type         => 'RESERVE',
793                             item_id      => $self->itemnumber
794                         }
795                     );
796                 }
797             }
798
799             C4::Log::logaction( 'HOLDS', 'FILL', $self->id, $self )
800                 if C4::Context->preference('HoldsLog');
801
802             Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
803                 {
804                     biblio_ids => [ $old_me->biblionumber ]
805                 }
806             ) if C4::Context->preference('RealTimeHoldsQueue');
807         }
808     );
809     return $self;
810 }
811
812 =head3 store
813
814 Override base store method to set default
815 expirationdate for holds.
816
817 =cut
818
819 sub store {
820     my ($self) = @_;
821
822     Koha::Exceptions::Hold::MissingPickupLocation->throw() unless $self->branchcode;
823
824     if ( !$self->in_storage ) {
825         if ( ! $self->expirationdate && $self->patron_expiration_date ) {
826             $self->expirationdate($self->patron_expiration_date);
827         }
828
829         if (
830             C4::Context->preference('DefaultHoldExpirationdate')
831                 && !$self->expirationdate
832           )
833         {
834             $self->_set_default_expirationdate;
835         }
836     }
837     else {
838
839         my %updated_columns = $self->_result->get_dirty_columns;
840         return $self->SUPER::store unless %updated_columns;
841
842         if ( exists $updated_columns{reservedate} ) {
843             if (
844                 C4::Context->preference('DefaultHoldExpirationdate')
845                 && ! exists $updated_columns{expirationdate}
846               )
847             {
848                 $self->_set_default_expirationdate;
849             }
850         }
851     }
852
853     $self = $self->SUPER::store;
854 }
855
856 sub _set_default_expirationdate {
857     my $self = shift;
858
859     my $period = C4::Context->preference('DefaultHoldExpirationdatePeriod') || 0;
860     my $timeunit =
861       C4::Context->preference('DefaultHoldExpirationdateUnitOfTime') || 'days';
862
863     $self->expirationdate(
864         dt_from_string( $self->reservedate )->add( $timeunit => $period ) );
865 }
866
867 =head3 _move_to_old
868
869 my $is_moved = $hold->_move_to_old;
870
871 Move a hold to the old_reserve table following the same pattern as Koha::Patron->move_to_deleted
872
873 =cut
874
875 sub _move_to_old {
876     my ($self) = @_;
877     my $hold_infos = $self->unblessed;
878     return Koha::Old::Hold->new( $hold_infos )->store;
879 }
880
881 =head3 to_api_mapping
882
883 This method returns the mapping for representing a Koha::Hold object
884 on the API.
885
886 =cut
887
888 sub to_api_mapping {
889     return {
890         reserve_id       => 'hold_id',
891         borrowernumber   => 'patron_id',
892         reservedate      => 'hold_date',
893         biblionumber     => 'biblio_id',
894         branchcode       => 'pickup_library_id',
895         notificationdate => undef,
896         reminderdate     => undef,
897         cancellationdate => 'cancellation_date',
898         reservenotes     => 'notes',
899         found            => 'status',
900         itemnumber       => 'item_id',
901         waitingdate      => 'waiting_date',
902         expirationdate   => 'expiration_date',
903         patron_expiration_date => undef,
904         lowestPriority   => 'lowest_priority',
905         suspend          => 'suspended',
906         suspend_until    => 'suspended_until',
907         itemtype         => 'item_type',
908         item_level_hold  => 'item_level',
909     };
910 }
911
912 =head2 Internal methods
913
914 =head3 _type
915
916 =cut
917
918 sub _type {
919     return 'Reserve';
920 }
921
922 =head1 AUTHORS
923
924 Kyle M Hall <kyle@bywatersolutions.com>
925 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
926 Martin Renvoize <martin.renvoize@ptfs-europe.com>
927
928 =cut
929
930 1;