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;
28 use t::lib::TestBuilder;
36 my $schema = Koha::Database->new->schema;
37 my $builder = t::lib::TestBuilder->new;
39 subtest 'store() tests' => sub {
42 $schema->storage->txn_begin;
44 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
45 my $item = $builder->build_sample_item;
49 borrowernumber => $patron->borrowernumber,
50 biblionumber => $item->biblionumber,
52 itemnumber => $item->itemnumber,
56 'Koha::Exceptions::Hold::MissingPickupLocation',
57 'Exception thrown because branchcode was not passed';
59 my $hold = $builder->build_object( { class => 'Koha::Holds' } );
61 $hold->branchcode(undef)->store;
63 'Koha::Exceptions::Hold::MissingPickupLocation',
64 'Exception thrown if one tries to set branchcode to null';
66 $schema->storage->txn_rollback;
69 subtest 'fill() tests' => sub {
73 $schema->storage->txn_begin;
77 my $category = $builder->build_object(
79 class => 'Koha::Patron::Categories',
80 value => { reservefee => $fee }
83 my $patron = $builder->build_object(
85 class => 'Koha::Patrons',
86 value => { categorycode => $category->id }
89 my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
91 my $title = 'Do what you want';
92 my $biblio = $builder->build_sample_biblio( { title => $title } );
93 my $item = $builder->build_sample_item( { biblionumber => $biblio->id } );
94 my $hold = $builder->build_object(
96 class => 'Koha::Holds',
98 biblionumber => $biblio->id,
99 borrowernumber => $patron->id,
100 itemnumber => $item->id,
106 t::lib::Mocks::mock_preference( 'HoldFeeMode', 'any_time_is_collected' );
107 t::lib::Mocks::mock_preference( 'HoldsLog', 1 );
108 t::lib::Mocks::mock_userenv(
109 { patron => $manager, branchcode => $manager->branchcode } );
111 my $interface = 'api';
112 C4::Context->interface($interface);
114 my $ret = $hold->fill;
116 is( ref($ret), 'Koha::Hold', '->fill returns the object type' );
117 is( $ret->id, $hold->id, '->fill returns the object' );
119 is( Koha::Holds->find($hold->id), undef, 'Hold no longer current' );
120 my $old_hold = Koha::Old::Holds->find( $hold->id );
122 is( $old_hold->id, $hold->id, 'reserve_id retained' );
123 is( $old_hold->priority, 0, 'priority set to 0' );
124 is( $old_hold->found, 'F', 'found set to F' );
126 subtest 'fee applied tests' => sub {
130 my $account = $patron->account;
131 is( $account->balance, $fee, 'Charge applied correctly' );
133 my $debits = $account->outstanding_debits;
134 is( $debits->count, 1, 'Only one fee charged' );
136 my $fee_debit = $debits->next;
137 is( $fee_debit->amount * 1, $fee, 'Fee amount stored correctly' );
138 is( $fee_debit->description, $title,
139 'Fee description stored correctly' );
140 is( $fee_debit->manager_id, $manager->id,
141 'Fee manager_id stored correctly' );
142 is( $fee_debit->branchcode, $manager->branchcode,
143 'Fee branchcode stored correctly' );
144 is( $fee_debit->interface, $interface,
145 'Fee interface stored correctly' );
146 is( $fee_debit->debit_type_code,
147 'RESERVE', 'Fee debit_type_code stored correctly' );
148 is( $fee_debit->itemnumber, $item->id,
149 'Fee itemnumber stored correctly' );
152 my $logs = Koha::ActionLogs->search(
160 is( $logs->count, 1, '1 log line added' );
162 # Set HoldFeeMode to something other than any_time_is_collected
163 t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
165 t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
167 $hold = $builder->build_object(
169 class => 'Koha::Holds',
171 biblionumber => $biblio->id,
172 borrowernumber => $patron->id,
173 itemnumber => $item->id,
181 my $account = $patron->account;
182 is( $account->balance, $fee, 'No new charge applied' );
184 my $debits = $account->outstanding_debits;
185 is( $debits->count, 1, 'Only one fee charged, because of HoldFeeMode' );
187 $logs = Koha::ActionLogs->search(
195 is( $logs->count, 0, 'HoldsLog disabled, no logs added' );
197 subtest 'anonymization behavior tests' => sub {
201 # reduce the tests noise
202 t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
203 t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
204 # unset AnonymousPatron
205 t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
208 $patron->privacy(0)->store;
209 my $hold = $builder->build_object(
211 class => 'Koha::Holds',
212 value => { borrowernumber => $patron->id, found => undef }
216 is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
217 $patron->borrowernumber, 'Patron link is kept' );
219 # 1 == "default", meaning it is not protected from removal
220 $patron->privacy(1)->store;
221 $hold = $builder->build_object(
223 class => 'Koha::Holds',
224 value => { borrowernumber => $patron->id, found => undef }
228 is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
229 $patron->borrowernumber, 'Patron link is kept' );
231 # 2 == delete immediately
232 $patron->privacy(2)->store;
233 $hold = $builder->build_object(
235 class => 'Koha::Holds',
236 value => { borrowernumber => $patron->id, found => undef }
243 'AnonymousPatron not set, exception thrown';
245 $hold->discard_changes; # refresh from DB
247 ok( !$hold->is_found, 'Hold is not filled' );
249 my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
250 t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
252 $hold = $builder->build_object(
254 class => 'Koha::Holds',
255 value => { borrowernumber => $patron->id, found => undef }
260 Koha::Old::Holds->find( $hold->id )->borrowernumber,
261 $anonymous_patron->id,
262 'Patron link is set to the configured anonymous patron immediately'
266 subtest 'holds_queue update tests' => sub {
270 my $biblio = $builder->build_sample_biblio;
272 my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
273 $mock->mock( 'enqueue', sub {
274 my ( $self, $args ) = @_;
278 '->fill triggers a holds queue update for the related biblio'
282 t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
284 $builder->build_object(
286 class => 'Koha::Holds',
288 biblionumber => $biblio->id,
293 t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
294 # this call shouldn't add a new test
295 $builder->build_object(
297 class => 'Koha::Holds',
299 biblionumber => $biblio->id,
305 $schema->storage->txn_rollback;
308 subtest 'patron() tests' => sub {
312 $schema->storage->txn_begin;
314 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
315 my $hold = $builder->build_object(
317 class => 'Koha::Holds',
319 borrowernumber => $patron->borrowernumber
324 my $hold_patron = $hold->patron;
325 is( ref($hold_patron), 'Koha::Patron', 'Right type' );
326 is( $hold_patron->id, $patron->id, 'Right object' );
328 $schema->storage->txn_rollback;
331 subtest 'set_pickup_location() tests' => sub {
335 $schema->storage->txn_begin;
337 my $mock_biblio = Test::MockModule->new('Koha::Biblio');
338 my $mock_item = Test::MockModule->new('Koha::Item');
340 my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
341 my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
342 my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
344 # let's control what Koha::Biblio->pickup_locations returns, for testing
345 $mock_biblio->mock( 'pickup_locations', sub {
346 return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
348 # let's mock what Koha::Item->pickup_locations returns, for testing
349 $mock_item->mock( 'pickup_locations', sub {
350 return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
353 my $biblio = $builder->build_sample_biblio;
354 my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
356 # Test biblio-level holds
357 my $biblio_hold = $builder->build_object(
359 class => "Koha::Holds",
361 biblionumber => $biblio->biblionumber,
362 branchcode => $library_3->branchcode,
369 { $biblio_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
370 'Koha::Exceptions::Hold::InvalidPickupLocation',
371 'Exception thrown on invalid pickup location';
373 $biblio_hold->discard_changes;
374 is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
376 my $ret = $biblio_hold->set_pickup_location({ library_id => $library_2->id });
377 is( ref($ret), 'Koha::Hold', 'self is returned' );
379 $biblio_hold->discard_changes;
380 is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
382 # Test item-level holds
383 my $item_hold = $builder->build_object(
385 class => "Koha::Holds",
387 biblionumber => $biblio->biblionumber,
388 branchcode => $library_3->branchcode,
389 itemnumber => $item->itemnumber,
395 { $item_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
396 'Koha::Exceptions::Hold::InvalidPickupLocation',
397 'Exception thrown on invalid pickup location';
399 $item_hold->discard_changes;
400 is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
402 $item_hold->set_pickup_location({ library_id => $library_1->branchcode, force => 1 });
403 $item_hold->discard_changes;
404 is( $item_hold->branchcode, $library_1->branchcode, 'branchcode changed because of \'force\'' );
406 $ret = $item_hold->set_pickup_location({ library_id => $library_2->id });
407 is( ref($ret), 'Koha::Hold', 'self is returned' );
409 $item_hold->discard_changes;
410 is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
413 { $item_hold->set_pickup_location({ library_id => undef }); }
414 'Koha::Exceptions::MissingParameter',
415 'Exception thrown if missing parameter';
417 like( "$@", qr/The library_id parameter is mandatory/, 'Exception message is clear' );
419 $schema->storage->txn_rollback;
422 subtest 'is_pickup_location_valid() tests' => sub {
426 $schema->storage->txn_begin;
428 my $mock_biblio = Test::MockModule->new('Koha::Biblio');
429 my $mock_item = Test::MockModule->new('Koha::Item');
431 my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
432 my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
433 my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
435 # let's control what Koha::Biblio->pickup_locations returns, for testing
436 $mock_biblio->mock( 'pickup_locations', sub {
437 return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
439 # let's mock what Koha::Item->pickup_locations returns, for testing
440 $mock_item->mock( 'pickup_locations', sub {
441 return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
444 my $biblio = $builder->build_sample_biblio;
445 my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
447 # Test biblio-level holds
448 my $biblio_hold = $builder->build_object(
450 class => "Koha::Holds",
452 biblionumber => $biblio->biblionumber,
453 branchcode => $library_3->branchcode,
459 ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
460 ok( $biblio_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid');
462 # Test item-level holds
463 my $item_hold = $builder->build_object(
465 class => "Koha::Holds",
467 biblionumber => $biblio->biblionumber,
468 branchcode => $library_3->branchcode,
469 itemnumber => $item->itemnumber,
474 ok( !$item_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
475 ok( $item_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid' );
477 subtest 'pickup_locations() returning ->empty' => sub {
481 $schema->storage->txn_begin;
483 my $library = $builder->build_object({ class => 'Koha::Libraries' });
485 my $mock_item = Test::MockModule->new('Koha::Item');
486 $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
488 my $mock_biblio = Test::MockModule->new('Koha::Biblio');
489 $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
491 my $item = $builder->build_sample_item();
492 my $biblio = $item->biblio;
494 # Test biblio-level holds
495 my $biblio_hold = $builder->build_object(
497 class => "Koha::Holds",
499 biblionumber => $biblio->biblionumber,
505 ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
507 # Test item-level holds
508 my $item_hold = $builder->build_object(
510 class => "Koha::Holds",
512 biblionumber => $biblio->biblionumber,
513 itemnumber => $item->itemnumber,
518 ok( !$item_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
520 $schema->storage->txn_rollback;
523 $schema->storage->txn_rollback;
526 subtest 'cancel() tests' => sub {
530 $schema->storage->txn_begin;
532 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
534 # reduce the tests noise
535 t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
536 t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge',
539 t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
542 $patron->privacy(0)->store;
543 my $hold = $builder->build_object(
545 class => 'Koha::Holds',
546 value => { borrowernumber => $patron->id, found => undef }
550 is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
551 $patron->borrowernumber, 'Patron link is kept' );
553 # 1 == "default", meaning it is not protected from removal
554 $patron->privacy(1)->store;
555 $hold = $builder->build_object(
557 class => 'Koha::Holds',
558 value => { borrowernumber => $patron->id, found => undef }
562 is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
563 $patron->borrowernumber, 'Patron link is kept' );
565 # 2 == delete immediately
566 $patron->privacy(2)->store;
567 $hold = $builder->build_object(
569 class => 'Koha::Holds',
570 value => { borrowernumber => $patron->id, found => undef }
576 'AnonymousPatron not set, exception thrown';
578 $hold->discard_changes;
580 ok( !$hold->is_found, 'Hold is not cancelled' );
582 my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
583 t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
585 $hold = $builder->build_object(
587 class => 'Koha::Holds',
588 value => { borrowernumber => $patron->id, found => undef }
593 Koha::Old::Holds->find( $hold->id )->borrowernumber,
594 $anonymous_patron->id,
595 'Patron link is set to the configured anonymous patron immediately'
598 subtest 'holds_queue update tests' => sub {
602 my $biblio = $builder->build_sample_biblio;
604 t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
606 my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
607 $mock->mock( 'enqueue', sub {
608 my ( $self, $args ) = @_;
612 '->cancel triggers a holds queue update for the related biblio'
616 $builder->build_object(
618 class => 'Koha::Holds',
620 biblionumber => $biblio->id,
625 # If the skip_holds_queue param is not honoured, then test count will fail.
626 $builder->build_object(
628 class => 'Koha::Holds',
630 biblionumber => $biblio->id,
633 )->cancel({ skip_holds_queue => 1 });
635 t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
637 $builder->build_object(
639 class => 'Koha::Holds',
641 biblionumber => $biblio->id,
644 )->cancel({ skip_holds_queue => 0 });
647 $schema->storage->txn_rollback;
650 subtest 'suspend_hold() and resume() tests' => sub {
654 $schema->storage->txn_begin;
656 my $biblio = $builder->build_sample_biblio;
659 t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
661 my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
662 $mock->mock( 'enqueue', sub {
663 my ( $self, $args ) = @_;
667 "->$action triggers a holds queue update for the related biblio"
671 my $hold = $builder->build_object(
673 class => 'Koha::Holds',
675 biblionumber => $biblio->id,
681 $action = 'suspend_hold';
687 $schema->storage->txn_rollback;