3 # Copyright 2020 Koha Development team
5 # This file is part of Koha
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use Test::More tests => 7;
25 use C4::Circulation qw( AddIssue );
26 use C4::Reserves qw( AddReserve ModReserve ModReserveCancelAll );
27 use Koha::AuthorisedValueCategory;
29 use Koha::DateUtils qw( dt_from_string );
33 use t::lib::TestBuilder;
35 my $schema = Koha::Database->new->schema;
36 $schema->storage->txn_begin;
38 my $builder = t::lib::TestBuilder->new;
40 subtest 'DB constraints' => sub {
43 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
44 my $item = $builder->build_sample_item;
46 branchcode => $patron->branchcode,
47 borrowernumber => $patron->borrowernumber,
48 biblionumber => $item->biblionumber,
50 title => "title for fee",
51 itemnumber => $item->itemnumber,
54 my $reserve_id = C4::Reserves::AddReserve($hold_info);
55 my $hold = Koha::Holds->find( $reserve_id );
58 eval { $hold->priority(undef)->store }
60 qr{.*DBD::mysql::st execute failed: Column 'priority' cannot be null.*},
61 'DBD should have raised an error about priority that cannot be null';
64 subtest 'cancel' => sub {
66 my $biblioitem = $builder->build_object( { class => 'Koha::Biblioitems' } );
67 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
68 my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes', value => { rentalcharge => 0 } } );
70 biblionumber => $biblioitem->biblionumber,
71 biblioitemnumber => $biblioitem->biblioitemnumber,
72 homebranch => $library->branchcode,
73 holdingbranch => $library->branchcode,
74 itype => $itemtype->itemtype,
76 my $item = $builder->build_object( { class => 'Koha::Items', value => $item_info } );
77 my $manager = $builder->build_object({ class => "Koha::Patrons" });
78 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
80 my ( @patrons, @holds );
81 for my $i ( 0 .. 2 ) {
82 my $priority = $i + 1;
83 my $patron = $builder->build_object(
85 class => 'Koha::Patrons',
86 value => { branchcode => $library->branchcode, }
89 my $reserve_id = C4::Reserves::AddReserve(
91 branchcode => $library->branchcode,
92 borrowernumber => $patron->borrowernumber,
93 biblionumber => $item->biblionumber,
94 priority => $priority,
95 title => "title for fee",
96 itemnumber => $item->itemnumber,
99 my $hold = Koha::Holds->find($reserve_id);
100 push @patrons, $patron;
104 # There are 3 holds on this records
106 Koha::Holds->search( { biblionumber => $item->biblionumber } )->count;
108 'There should have 3 holds placed on this biblio record' );
109 my $first_hold = $holds[0];
110 my $second_hold = $holds[1];
111 my $third_hold = $holds[2];
112 is( ref($second_hold), 'Koha::Hold',
113 'We should play with Koha::Hold objects' );
114 is( $second_hold->priority, 2,
115 'Second hold should have a priority set to 3' );
117 # Remove the second hold, only 2 should still exist in DB and priorities must have been updated
118 my $is_cancelled = $second_hold->cancel;
119 is( ref($is_cancelled), 'Koha::Hold',
120 'Koha::Hold->cancel should return the Koha::Hold (?)' )
121 ; # This is can reconsidered
122 is( $second_hold->in_storage, 0,
123 'The hold has been cancelled and does not longer exist in DB' );
125 Koha::Holds->search( { biblionumber => $item->biblionumber } )->count;
127 'a hold has been cancelled, there should have only 2 holds placed on this biblio record'
130 # discard_changes to refetch
131 is( $first_hold->discard_changes->priority, 1, 'First hold should still be first' );
132 is( $third_hold->discard_changes->priority, 2, 'Third hold should now be second' );
134 subtest 'charge_cancel_fee parameter' => sub {
136 my $patron_category = $builder->build_object({ class => 'Koha::Patron::Categories', value => { reservefee => 0 } } );
137 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $patron_category->categorycode } });
138 is( $patron->account->balance, 0, 'A new patron does not have any charges' );
141 branchcode => $library->branchcode,
142 borrowernumber => $patron->borrowernumber,
143 biblionumber => $item->biblionumber,
145 title => "title for fee",
146 itemnumber => $item->itemnumber,
149 # First, test cancelling a reserve when there's no charge configured.
150 t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelayCharge', 0);
151 my $reserve_id = C4::Reserves::AddReserve( $hold_info );
152 Koha::Holds->find( $reserve_id )->cancel( { charge_cancel_fee => 1 } );
153 is( $patron->account->balance, 0, 'ExpireReservesMaxPickUpDelayCharge=0 - The patron should not have been charged' );
155 # Then, test cancelling a reserve when there's no charge desired.
156 t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelayCharge', 42);
157 $reserve_id = C4::Reserves::AddReserve( $hold_info );
158 Koha::Holds->find( $reserve_id )->cancel(); # charge_cancel_fee => 0
159 is( $patron->account->balance, 0, 'ExpireReservesMaxPickUpDelayCharge=42, but charge_cancel_fee => 0, The patron should not have been charged' );
162 # Finally, test cancelling a reserve when there's a charge desired and configured.
163 t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelayCharge', 42);
164 $reserve_id = C4::Reserves::AddReserve( $hold_info );
165 Koha::Holds->find( $reserve_id )->cancel( { charge_cancel_fee => 1 } );
166 is( int($patron->account->balance), 42, 'ExpireReservesMaxPickUpDelayCharge=42 and charge_cancel_fee => 1, The patron should have been charged!' );
169 subtest 'waiting hold' => sub {
171 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
172 my $reserve_id = C4::Reserves::AddReserve(
174 branchcode => $library->branchcode,
175 borrowernumber => $patron->borrowernumber,
176 biblionumber => $item->biblionumber,
178 title => "title for fee",
179 itemnumber => $item->itemnumber,
183 Koha::Holds->find( $reserve_id )->cancel;
184 my $hold_old = Koha::Old::Holds->find( $reserve_id );
185 is( $hold_old->found, 'W', 'The found column should have been kept and a hold is cancelled' );
188 subtest 'HoldsLog' => sub {
190 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
192 branchcode => $library->branchcode,
193 borrowernumber => $patron->borrowernumber,
194 biblionumber => $item->biblionumber,
196 title => "title for fee",
197 itemnumber => $item->itemnumber,
200 t::lib::Mocks::mock_preference('HoldsLog', 0);
201 my $reserve_id = C4::Reserves::AddReserve($hold_info);
202 Koha::Holds->find( $reserve_id )->cancel;
203 my $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'HOLDS', action => 'CANCEL', object => $reserve_id } )->count;
204 is( $number_of_logs, 0, 'Without HoldsLog, Koha::Hold->cancel should not have logged' );
206 t::lib::Mocks::mock_preference('HoldsLog', 1);
207 $reserve_id = C4::Reserves::AddReserve($hold_info);
208 Koha::Holds->find( $reserve_id )->cancel;
209 $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'HOLDS', action => 'CANCEL', object => $reserve_id } )->count;
210 is( $number_of_logs, 1, 'With HoldsLog, Koha::Hold->cancel should have logged' );
213 subtest 'rollback' => sub {
215 my $patron_category = $builder->build_object(
217 class => 'Koha::Patron::Categories',
218 value => { reservefee => 0 }
221 my $patron = $builder->build_object(
223 class => 'Koha::Patrons',
224 value => { categorycode => $patron_category->categorycode }
228 branchcode => $library->branchcode,
229 borrowernumber => $patron->borrowernumber,
230 biblionumber => $item->biblionumber,
232 title => "title for fee",
233 itemnumber => $item->itemnumber,
236 t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge',42 );
237 my $reserve_id = C4::Reserves::AddReserve($hold_info);
238 my $hold = Koha::Holds->find($reserve_id);
240 # Add a row with the same id to make the cancel fails
241 Koha::Old::Hold->new( $hold->unblessed )->store;
244 eval { $hold->cancel( { charge_cancel_fee => 1 } ) };
246 qr{.*DBD::mysql::st execute failed: Duplicate entry.*},
247 'DBD should have raised an error about dup primary key';
249 $hold = Koha::Holds->find($reserve_id);
250 is( ref($hold), 'Koha::Hold', 'The hold should not have been deleted' );
251 is( $patron->account->balance, 0,
252 'If the hold has not been cancelled, the patron should not have been charged'
258 subtest 'cancel with reason' => sub {
260 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
261 my $item = $builder->build_sample_item({ library => $library->branchcode });
262 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
263 t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
265 my $patron = $builder->build_object(
267 class => 'Koha::Patrons',
268 value => { branchcode => $library->branchcode, }
272 my $reserve_id = C4::Reserves::AddReserve(
274 branchcode => $library->branchcode,
275 borrowernumber => $patron->borrowernumber,
276 biblionumber => $item->biblionumber,
278 itemnumber => $item->itemnumber,
282 my $hold = Koha::Holds->find($reserve_id);
284 ok($reserve_id, "Hold created");
285 ok($hold, "Hold found");
287 my $av = Koha::AuthorisedValue->new( { category => 'HOLD_CANCELLATION', authorised_value => 'TEST_REASON' } )->store;
288 Koha::Notice::Templates->search({ code => 'HOLD_CANCELLATION'})->delete();
289 my $notice = Koha::Notice::Template->new({
290 name => 'Hold cancellation',
291 module => 'reserves',
292 code => 'HOLD_CANCELLATION',
293 title => 'Hold cancelled',
294 content => 'Your hold was cancelled.',
295 message_transport_type => 'email',
299 $hold->cancel({cancellation_reason => 'TEST_REASON'});
301 $hold = Koha::Holds->find($reserve_id);
302 is( $hold, undef, 'Hold is not in the reserves table');
303 $hold = Koha::Old::Holds->find($reserve_id);
304 ok( $hold, 'Hold was found in the old reserves table');
306 my $message = Koha::Notice::Messages->find({ borrowernumber => $patron->id, letter_code => 'HOLD_CANCELLATION'});
307 ok( $message, 'Found hold cancellation message');
308 is( $message->subject, 'Hold cancelled', 'Message has correct title' );
309 is( $message->content, 'Your hold was cancelled.', 'Message has correct content');
316 subtest 'cancel all with reason' => sub {
318 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
319 my $item = $builder->build_sample_item({ library => $library->branchcode });
320 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
321 t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
323 my $patron = $builder->build_object(
325 class => 'Koha::Patrons',
326 value => { branchcode => $library->branchcode, }
330 my $reserve_id = C4::Reserves::AddReserve(
332 branchcode => $library->branchcode,
333 borrowernumber => $patron->borrowernumber,
334 biblionumber => $item->biblionumber,
336 itemnumber => $item->itemnumber,
340 my $hold = Koha::Holds->find($reserve_id);
342 ok($reserve_id, "Hold created");
343 ok($hold, "Hold found");
345 my $av = Koha::AuthorisedValue->new( { category => 'HOLD_CANCELLATION', authorised_value => 'TEST_REASON' } )->store;
346 Koha::Notice::Templates->search({ code => 'HOLD_CANCELLATION'})->delete();
347 my $notice = Koha::Notice::Template->new({
348 name => 'Hold cancellation',
349 module => 'reserves',
350 code => 'HOLD_CANCELLATION',
351 title => 'Hold cancelled',
352 content => 'Your hold was cancelled.',
353 message_transport_type => 'email',
357 ModReserveCancelAll($item->id, $patron->id, 'TEST_REASON');
359 $hold = Koha::Holds->find($reserve_id);
360 is( $hold, undef, 'Hold is not in the reserves table');
361 $hold = Koha::Old::Holds->find($reserve_id);
362 ok( $hold, 'Hold was found in the old reserves table');
364 my $message = Koha::Notice::Messages->find({ borrowernumber => $patron->id, letter_code => 'HOLD_CANCELLATION'});
365 ok( $message, 'Found hold cancellation message');
366 is( $message->subject, 'Hold cancelled', 'Message has correct title' );
367 is( $message->content, 'Your hold was cancelled.', 'Message has correct content');
373 subtest 'Desks' => sub {
375 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
377 my $desk = Koha::Desk->new({
378 desk_name => 'my_desk_name_for_test',
379 branchcode => $library->branchcode ,
381 ok($desk, "Desk created");
382 my $item = $builder->build_sample_item({ library => $library->branchcode });
383 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
384 t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
386 my $patron = $builder->build_object(
388 class => 'Koha::Patrons',
389 value => { branchcode => $library->branchcode, }
393 my $reserve_id = C4::Reserves::AddReserve(
395 branchcode => $library->branchcode,
396 borrowernumber => $patron->borrowernumber,
397 biblionumber => $item->biblionumber,
399 itemnumber => $item->itemnumber,
403 my $hold = Koha::Holds->find($reserve_id);
405 ok($reserve_id, "Hold created");
406 ok($hold, "Hold found");
407 $hold->set_waiting($desk->desk_id);
408 is($hold->found, 'W', 'Hold is waiting with correct status set');
409 is($hold->desk_id, $desk->desk_id, 'Hold is attach to its desk');
413 subtest 'get_items_that_can_fill' => sub {
416 my $biblio = $builder->build_sample_biblio;
417 my $itype_1 = $builder->build_object({ class => 'Koha::ItemTypes' }); # For 1, 2, 3, 4
418 my $itype_2 = $builder->build_object({ class => 'Koha::ItemTypes' });
419 my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } );
421 my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } );
422 my $item_3 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } )
424 my $item_4 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } )
426 my $item_5 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_2->itemtype } );
427 my $lost = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 1 } );
428 my $withdrawn = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 1 } );
429 my $notforloan = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
431 my $patron_1 = $builder->build_object( { class => 'Koha::Patrons' } );
432 my $patron_2 = $builder->build_object( { class => 'Koha::Patrons' } );
433 my $patron_3 = $builder->build_object( { class => 'Koha::Patrons' } );
435 t::lib::Mocks::mock_userenv( { patron => $patron_1 } );
437 my $reserve_id_1 = C4::Reserves::AddReserve(
439 borrowernumber => $patron_1->borrowernumber,
440 biblionumber => $biblio->biblionumber,
442 itemnumber => $item_1->itemnumber,
446 my $holds = Koha::Holds->search({ reserve_id => $reserve_id_1 });
447 my $items = $holds->get_items_that_can_fill;
448 is_deeply( [ map { $_->itemnumber } $items->as_list ], [ $item_1->itemnumber ], 'Item level hold can only be filled by the specific item');
450 my $reserve_id_2 = C4::Reserves::AddReserve(
452 borrowernumber => $patron_2->borrowernumber,
453 biblionumber => $biblio->biblionumber,
455 branchcode => $item_1->homebranch,
459 my $waiting_reserve_id = C4::Reserves::AddReserve(
461 borrowernumber => $patron_2->borrowernumber,
462 biblionumber => $biblio->biblionumber,
465 itemnumber => $item_1->itemnumber,
470 AddIssue( $patron_3->unblessed, $item_3->barcode );
472 # item 4 is in transfer
473 my $from = $builder->build_object( { class => 'Koha::Libraries' } );
474 my $to = $builder->build_object( { class => 'Koha::Libraries' } );
475 Koha::Item::Transfer->new(
477 itemnumber => $item_4->itemnumber,
478 datearrived => undef,
479 frombranch => $from->branchcode,
480 tobranch => $to->branchcode
484 $holds = Koha::Holds->search(
486 reserve_id => [ $reserve_id_1, $reserve_id_2, $waiting_reserve_id, ]
490 $items = $holds->get_items_that_can_fill;
491 is_deeply( [ map { $_->itemnumber } $items->as_list ],
492 [ $item_2->itemnumber, $item_5->itemnumber ], 'Only item 2 and 5 are available for filling the hold' );
494 # Marking item_5 is no hold allowed
495 Koha::CirculationRule->new(
497 rule_name => 'holdallowed',
498 rule_value => 'not_allowed',
499 itemtype => $item_5->itype
502 $items = $holds->get_items_that_can_fill;
503 is_deeply( [ map { $_->itemnumber } $items->as_list ],
504 [ $item_2->itemnumber ], 'Only item 2 is available for filling the hold' );
508 subtest 'set_waiting+patron_expiration_date' => sub {
510 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
513 $builder->build_sample_item( { library => $library->branchcode } );
514 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
515 t::lib::Mocks::mock_userenv(
516 { patron => $manager, branchcode => $manager->branchcode } );
518 my $patron = $builder->build_object(
520 class => 'Koha::Patrons',
521 value => { branchcode => $library->branchcode, }
525 subtest 'patron_expiration_date < expiration_date' => sub {
527 t::lib::Mocks::mock_preference( 'ReservesMaxPickUpDelay', 5 );
528 my $patron_expiration_date = dt_from_string->add( days => 3 )->ymd;
529 my $reserve_id = C4::Reserves::AddReserve(
531 branchcode => $library->branchcode,
532 borrowernumber => $patron->borrowernumber,
533 biblionumber => $item->biblionumber,
535 itemnumber => $item->itemnumber,
536 expiration_date => $patron_expiration_date,
540 my $hold = Koha::Holds->find($reserve_id);
543 $hold->expirationdate,
544 $patron_expiration_date,
545 'expiration date set to patron expiration date'
548 $hold->patron_expiration_date, $patron_expiration_date,
549 'patron expiration date correctly set'
554 $hold = $hold->get_from_storage;
555 is( $hold->expirationdate, $patron_expiration_date );
556 is( $hold->patron_expiration_date, $patron_expiration_date );
558 C4::Reserves::RevertWaitingStatus(
559 { itemnumber => $item->itemnumber }
562 $hold = $hold->get_from_storage;
563 is( $hold->expirationdate, $patron_expiration_date );
564 is( $hold->patron_expiration_date, $patron_expiration_date );
567 subtest 'patron_expiration_date > expiration_date' => sub {
569 t::lib::Mocks::mock_preference( 'ReservesMaxPickUpDelay', 5 );
570 my $new_expiration_date = dt_from_string->add( days => 5 )->ymd;
571 my $patron_expiration_date = dt_from_string->add( days => 6 )->ymd;
572 my $reserve_id = C4::Reserves::AddReserve(
574 branchcode => $library->branchcode,
575 borrowernumber => $patron->borrowernumber,
576 biblionumber => $item->biblionumber,
578 itemnumber => $item->itemnumber,
579 expiration_date => $patron_expiration_date,
583 my $hold = Koha::Holds->find($reserve_id);
586 $hold->expirationdate,
587 $patron_expiration_date,
588 'expiration date set to patron expiration date'
591 $hold->patron_expiration_date, $patron_expiration_date,
592 'patron expiration date correctly set'
597 $hold = $hold->get_from_storage;
598 is( $hold->expirationdate, $new_expiration_date );
599 is( $hold->patron_expiration_date, $patron_expiration_date );
601 C4::Reserves::RevertWaitingStatus(
602 { itemnumber => $item->itemnumber }
605 $hold = $hold->get_from_storage;
606 is( $hold->expirationdate, $patron_expiration_date );
607 is( $hold->patron_expiration_date, $patron_expiration_date );
612 $schema->storage->txn_rollback;