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 => 6;
25 use C4::Circulation qw( AddIssue );
26 use C4::Reserves qw( AddReserve ModReserve ModReserveCancelAll );
27 use Koha::AuthorisedValueCategory;
32 use t::lib::TestBuilder;
34 my $schema = Koha::Database->new->schema;
35 $schema->storage->txn_begin;
37 my $builder = t::lib::TestBuilder->new;
39 subtest 'DB constraints' => sub {
42 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
43 my $item = $builder->build_sample_item;
45 branchcode => $patron->branchcode,
46 borrowernumber => $patron->borrowernumber,
47 biblionumber => $item->biblionumber,
49 title => "title for fee",
50 itemnumber => $item->itemnumber,
53 my $reserve_id = C4::Reserves::AddReserve($hold_info);
54 my $hold = Koha::Holds->find( $reserve_id );
57 eval { $hold->priority(undef)->store }
59 qr{.*DBD::mysql::st execute failed: Column 'priority' cannot be null.*},
60 'DBD should have raised an error about priority that cannot be null';
63 subtest 'cancel' => sub {
65 my $biblioitem = $builder->build_object( { class => 'Koha::Biblioitems' } );
66 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
67 my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes', value => { rentalcharge => 0 } } );
69 biblionumber => $biblioitem->biblionumber,
70 biblioitemnumber => $biblioitem->biblioitemnumber,
71 homebranch => $library->branchcode,
72 holdingbranch => $library->branchcode,
73 itype => $itemtype->itemtype,
75 my $item = $builder->build_object( { class => 'Koha::Items', value => $item_info } );
76 my $manager = $builder->build_object({ class => "Koha::Patrons" });
77 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
79 my ( @patrons, @holds );
80 for my $i ( 0 .. 2 ) {
81 my $priority = $i + 1;
82 my $patron = $builder->build_object(
84 class => 'Koha::Patrons',
85 value => { branchcode => $library->branchcode, }
88 my $reserve_id = C4::Reserves::AddReserve(
90 branchcode => $library->branchcode,
91 borrowernumber => $patron->borrowernumber,
92 biblionumber => $item->biblionumber,
93 priority => $priority,
94 title => "title for fee",
95 itemnumber => $item->itemnumber,
98 my $hold = Koha::Holds->find($reserve_id);
99 push @patrons, $patron;
103 # There are 3 holds on this records
105 Koha::Holds->search( { biblionumber => $item->biblionumber } )->count;
107 'There should have 3 holds placed on this biblio record' );
108 my $first_hold = $holds[0];
109 my $second_hold = $holds[1];
110 my $third_hold = $holds[2];
111 is( ref($second_hold), 'Koha::Hold',
112 'We should play with Koha::Hold objects' );
113 is( $second_hold->priority, 2,
114 'Second hold should have a priority set to 3' );
116 # Remove the second hold, only 2 should still exist in DB and priorities must have been updated
117 my $is_cancelled = $second_hold->cancel;
118 is( ref($is_cancelled), 'Koha::Hold',
119 'Koha::Hold->cancel should return the Koha::Hold (?)' )
120 ; # This is can reconsidered
121 is( $second_hold->in_storage, 0,
122 'The hold has been cancelled and does not longer exist in DB' );
124 Koha::Holds->search( { biblionumber => $item->biblionumber } )->count;
126 'a hold has been cancelled, there should have only 2 holds placed on this biblio record'
129 # discard_changes to refetch
130 is( $first_hold->discard_changes->priority, 1, 'First hold should still be first' );
131 is( $third_hold->discard_changes->priority, 2, 'Third hold should now be second' );
133 subtest 'charge_cancel_fee parameter' => sub {
135 my $patron_category = $builder->build_object({ class => 'Koha::Patron::Categories', value => { reservefee => 0 } } );
136 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $patron_category->categorycode } });
137 is( $patron->account->balance, 0, 'A new patron does not have any charges' );
140 branchcode => $library->branchcode,
141 borrowernumber => $patron->borrowernumber,
142 biblionumber => $item->biblionumber,
144 title => "title for fee",
145 itemnumber => $item->itemnumber,
148 # First, test cancelling a reserve when there's no charge configured.
149 t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelayCharge', 0);
150 my $reserve_id = C4::Reserves::AddReserve( $hold_info );
151 Koha::Holds->find( $reserve_id )->cancel( { charge_cancel_fee => 1 } );
152 is( $patron->account->balance, 0, 'ExpireReservesMaxPickUpDelayCharge=0 - The patron should not have been charged' );
154 # Then, test cancelling a reserve when there's no charge desired.
155 t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelayCharge', 42);
156 $reserve_id = C4::Reserves::AddReserve( $hold_info );
157 Koha::Holds->find( $reserve_id )->cancel(); # charge_cancel_fee => 0
158 is( $patron->account->balance, 0, 'ExpireReservesMaxPickUpDelayCharge=42, but charge_cancel_fee => 0, The patron should not have been charged' );
161 # Finally, test cancelling a reserve when there's a charge desired and configured.
162 t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelayCharge', 42);
163 $reserve_id = C4::Reserves::AddReserve( $hold_info );
164 Koha::Holds->find( $reserve_id )->cancel( { charge_cancel_fee => 1 } );
165 is( int($patron->account->balance), 42, 'ExpireReservesMaxPickUpDelayCharge=42 and charge_cancel_fee => 1, The patron should have been charged!' );
168 subtest 'waiting hold' => sub {
170 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
171 my $reserve_id = C4::Reserves::AddReserve(
173 branchcode => $library->branchcode,
174 borrowernumber => $patron->borrowernumber,
175 biblionumber => $item->biblionumber,
177 title => "title for fee",
178 itemnumber => $item->itemnumber,
182 Koha::Holds->find( $reserve_id )->cancel;
183 my $hold_old = Koha::Old::Holds->find( $reserve_id );
184 is( $hold_old->found, 'W', 'The found column should have been kept and a hold is cancelled' );
187 subtest 'HoldsLog' => sub {
189 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
191 branchcode => $library->branchcode,
192 borrowernumber => $patron->borrowernumber,
193 biblionumber => $item->biblionumber,
195 title => "title for fee",
196 itemnumber => $item->itemnumber,
199 t::lib::Mocks::mock_preference('HoldsLog', 0);
200 my $reserve_id = C4::Reserves::AddReserve($hold_info);
201 Koha::Holds->find( $reserve_id )->cancel;
202 my $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'HOLDS', action => 'CANCEL', object => $reserve_id } )->count;
203 is( $number_of_logs, 0, 'Without HoldsLog, Koha::Hold->cancel should not have logged' );
205 t::lib::Mocks::mock_preference('HoldsLog', 1);
206 $reserve_id = C4::Reserves::AddReserve($hold_info);
207 Koha::Holds->find( $reserve_id )->cancel;
208 $number_of_logs = $schema->resultset('ActionLog')->search( { module => 'HOLDS', action => 'CANCEL', object => $reserve_id } )->count;
209 is( $number_of_logs, 1, 'With HoldsLog, Koha::Hold->cancel should have logged' );
212 subtest 'rollback' => sub {
214 my $patron_category = $builder->build_object(
216 class => 'Koha::Patron::Categories',
217 value => { reservefee => 0 }
220 my $patron = $builder->build_object(
222 class => 'Koha::Patrons',
223 value => { categorycode => $patron_category->categorycode }
227 branchcode => $library->branchcode,
228 borrowernumber => $patron->borrowernumber,
229 biblionumber => $item->biblionumber,
231 title => "title for fee",
232 itemnumber => $item->itemnumber,
235 t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge',42 );
236 my $reserve_id = C4::Reserves::AddReserve($hold_info);
237 my $hold = Koha::Holds->find($reserve_id);
239 # Add a row with the same id to make the cancel fails
240 Koha::Old::Hold->new( $hold->unblessed )->store;
243 eval { $hold->cancel( { charge_cancel_fee => 1 } ) };
245 qr{.*DBD::mysql::st execute failed: Duplicate entry.*},
246 'DBD should have raised an error about dup primary key';
248 $hold = Koha::Holds->find($reserve_id);
249 is( ref($hold), 'Koha::Hold', 'The hold should not have been deleted' );
250 is( $patron->account->balance, 0,
251 'If the hold has not been cancelled, the patron should not have been charged'
257 subtest 'cancel with reason' => sub {
259 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
260 my $item = $builder->build_sample_item({ library => $library->branchcode });
261 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
262 t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
264 my $patron = $builder->build_object(
266 class => 'Koha::Patrons',
267 value => { branchcode => $library->branchcode, }
271 my $reserve_id = C4::Reserves::AddReserve(
273 branchcode => $library->branchcode,
274 borrowernumber => $patron->borrowernumber,
275 biblionumber => $item->biblionumber,
277 itemnumber => $item->itemnumber,
281 my $hold = Koha::Holds->find($reserve_id);
283 ok($reserve_id, "Hold created");
284 ok($hold, "Hold found");
286 my $av = Koha::AuthorisedValue->new( { category => 'HOLD_CANCELLATION', authorised_value => 'TEST_REASON' } )->store;
287 Koha::Notice::Templates->search({ code => 'HOLD_CANCELLATION'})->delete();
288 my $notice = Koha::Notice::Template->new({
289 name => 'Hold cancellation',
290 module => 'reserves',
291 code => 'HOLD_CANCELLATION',
292 title => 'Hold cancelled',
293 content => 'Your hold was cancelled.',
294 message_transport_type => 'email',
298 $hold->cancel({cancellation_reason => 'TEST_REASON'});
300 $hold = Koha::Holds->find($reserve_id);
301 is( $hold, undef, 'Hold is not in the reserves table');
302 $hold = Koha::Old::Holds->find($reserve_id);
303 ok( $hold, 'Hold was found in the old reserves table');
305 my $message = Koha::Notice::Messages->find({ borrowernumber => $patron->id, letter_code => 'HOLD_CANCELLATION'});
306 ok( $message, 'Found hold cancellation message');
307 is( $message->subject, 'Hold cancelled', 'Message has correct title' );
308 is( $message->content, 'Your hold was cancelled.', 'Message has correct content');
315 subtest 'cancel all with reason' => sub {
317 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
318 my $item = $builder->build_sample_item({ library => $library->branchcode });
319 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
320 t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
322 my $patron = $builder->build_object(
324 class => 'Koha::Patrons',
325 value => { branchcode => $library->branchcode, }
329 my $reserve_id = C4::Reserves::AddReserve(
331 branchcode => $library->branchcode,
332 borrowernumber => $patron->borrowernumber,
333 biblionumber => $item->biblionumber,
335 itemnumber => $item->itemnumber,
339 my $hold = Koha::Holds->find($reserve_id);
341 ok($reserve_id, "Hold created");
342 ok($hold, "Hold found");
344 my $av = Koha::AuthorisedValue->new( { category => 'HOLD_CANCELLATION', authorised_value => 'TEST_REASON' } )->store;
345 Koha::Notice::Templates->search({ code => 'HOLD_CANCELLATION'})->delete();
346 my $notice = Koha::Notice::Template->new({
347 name => 'Hold cancellation',
348 module => 'reserves',
349 code => 'HOLD_CANCELLATION',
350 title => 'Hold cancelled',
351 content => 'Your hold was cancelled.',
352 message_transport_type => 'email',
356 ModReserveCancelAll($item->id, $patron->id, 'TEST_REASON');
358 $hold = Koha::Holds->find($reserve_id);
359 is( $hold, undef, 'Hold is not in the reserves table');
360 $hold = Koha::Old::Holds->find($reserve_id);
361 ok( $hold, 'Hold was found in the old reserves table');
363 my $message = Koha::Notice::Messages->find({ borrowernumber => $patron->id, letter_code => 'HOLD_CANCELLATION'});
364 ok( $message, 'Found hold cancellation message');
365 is( $message->subject, 'Hold cancelled', 'Message has correct title' );
366 is( $message->content, 'Your hold was cancelled.', 'Message has correct content');
372 subtest 'Desks' => sub {
374 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
376 my $desk = Koha::Desk->new({
377 desk_name => 'my_desk_name_for_test',
378 branchcode => $library->branchcode ,
380 ok($desk, "Desk created");
381 my $item = $builder->build_sample_item({ library => $library->branchcode });
382 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
383 t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
385 my $patron = $builder->build_object(
387 class => 'Koha::Patrons',
388 value => { branchcode => $library->branchcode, }
392 my $reserve_id = C4::Reserves::AddReserve(
394 branchcode => $library->branchcode,
395 borrowernumber => $patron->borrowernumber,
396 biblionumber => $item->biblionumber,
398 itemnumber => $item->itemnumber,
402 my $hold = Koha::Holds->find($reserve_id);
404 ok($reserve_id, "Hold created");
405 ok($hold, "Hold found");
406 $hold->set_waiting($desk->desk_id);
407 is($hold->found, 'W', 'Hold is waiting with correct status set');
408 is($hold->desk_id, $desk->desk_id, 'Hold is attach to its desk');
412 subtest 'get_items_that_can_fill' => sub {
415 my $biblio = $builder->build_sample_biblio;
416 my $itype_1 = $builder->build_object({ class => 'Koha::ItemTypes' }); # For 1, 2, 3, 4
417 my $itype_2 = $builder->build_object({ class => 'Koha::ItemTypes' });
418 my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } );
420 my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } );
421 my $item_3 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } )
423 my $item_4 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_1->itemtype } )
425 my $item_5 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $itype_2->itemtype } );
426 my $lost = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 1 } );
427 my $withdrawn = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 1 } );
428 my $notforloan = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
430 my $patron_1 = $builder->build_object( { class => 'Koha::Patrons' } );
431 my $patron_2 = $builder->build_object( { class => 'Koha::Patrons' } );
432 my $patron_3 = $builder->build_object( { class => 'Koha::Patrons' } );
434 t::lib::Mocks::mock_userenv( { patron => $patron_1 } );
436 my $reserve_id_1 = C4::Reserves::AddReserve(
438 borrowernumber => $patron_1->borrowernumber,
439 biblionumber => $biblio->biblionumber,
441 itemnumber => $item_1->itemnumber,
445 my $reserve_id_2 = C4::Reserves::AddReserve(
447 borrowernumber => $patron_2->borrowernumber,
448 biblionumber => $biblio->biblionumber,
450 itemnumber => $item_1->itemnumber,
454 my $waiting_reserve_id = C4::Reserves::AddReserve(
456 borrowernumber => $patron_2->borrowernumber,
457 biblionumber => $biblio->biblionumber,
460 itemnumber => $item_1->itemnumber,
465 AddIssue( $patron_3->unblessed, $item_3->barcode );
467 # item 4 is in transfer
468 my $from = $builder->build_object( { class => 'Koha::Libraries' } );
469 my $to = $builder->build_object( { class => 'Koha::Libraries' } );
470 Koha::Item::Transfer->new(
472 itemnumber => $item_4->itemnumber,
473 datearrived => undef,
474 frombranch => $from->branchcode,
475 tobranch => $to->branchcode
479 my $holds = Koha::Holds->search(
481 reserve_id => [ $reserve_id_1, $reserve_id_2, $waiting_reserve_id, ]
485 my $items = $holds->get_items_that_can_fill;
486 is_deeply( [ map { $_->itemnumber } $items->as_list ],
487 [ $item_2->itemnumber, $item_5->itemnumber ], 'Only item 2 and 5 are available for filling the hold' );
489 # Marking item_5 is no hold allowed
490 Koha::CirculationRule->new(
492 rule_name => 'holdallowed',
493 rule_value => 'not_allowed',
494 itemtype => $item_5->itype
497 $items = $holds->get_items_that_can_fill;
498 is_deeply( [ map { $_->itemnumber } $items->as_list ],
499 [ $item_2->itemnumber ], 'Only item 1 is available for filling the hold' );
503 $schema->storage->txn_rollback;