Bug 35353: Add REST API endpoint to retrieve old holds
[koha.git] / t / db_dependent / Koha / Hold.t
1 #!/usr/bin/perl
2
3 # Copyright 2020 Koha Development team
4 #
5 # This file is part of Koha
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21
22 use Test::More tests => 14;
23
24 use Test::Exception;
25 use Test::MockModule;
26
27 use t::lib::Mocks;
28 use t::lib::TestBuilder;
29 use t::lib::Mocks;
30
31 use Koha::ActionLogs;
32 use Koha::DateUtils qw(dt_from_string);
33 use Koha::Holds;
34 use Koha::Libraries;
35 use Koha::Old::Holds;
36
37 my $schema  = Koha::Database->new->schema;
38 my $builder = t::lib::TestBuilder->new;
39
40 subtest 'store() tests' => sub {
41     plan tests => 2;
42
43     $schema->storage->txn_begin;
44
45     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
46     my $item   = $builder->build_sample_item;
47     throws_ok {
48         Koha::Hold->new(
49             {
50                 borrowernumber => $patron->borrowernumber,
51                 biblionumber   => $item->biblionumber,
52                 priority       => 1,
53                 itemnumber     => $item->itemnumber,
54             }
55         )->store
56     }
57     'Koha::Exceptions::Hold::MissingPickupLocation',
58       'Exception thrown because branchcode was not passed';
59
60     my $hold = $builder->build_object( { class => 'Koha::Holds' } );
61     throws_ok {
62         $hold->branchcode(undef)->store;
63     }
64     'Koha::Exceptions::Hold::MissingPickupLocation',
65       'Exception thrown if one tries to set branchcode to null';
66
67     $schema->storage->txn_rollback;
68 };
69
70 subtest 'biblio() tests' => sub {
71
72     plan tests => 1;
73
74     $schema->storage->txn_begin;
75
76     my $hold = $builder->build_object(
77         {
78             class => 'Koha::Holds',
79         }
80     );
81
82     local $SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /cannot be null/ };
83     throws_ok { $hold->biblionumber(undef)->store; }
84     'DBIx::Class::Exception',
85         'reserves.biblionumber cannot be null, exception thrown';
86
87     $schema->storage->txn_rollback;
88 };
89
90 subtest 'pickup_library/branch tests' => sub {
91
92     plan tests => 1;
93
94     $schema->storage->txn_begin;
95
96     my $hold = $builder->build_object(
97         {
98             class => 'Koha::Holds',
99         }
100     );
101
102     is( ref( $hold->pickup_library ), 'Koha::Library', '->pickup_library should return a Koha::Library object' );
103
104     $schema->storage->txn_rollback;
105 };
106
107 subtest 'fill() tests' => sub {
108
109     plan tests => 14;
110
111     $schema->storage->txn_begin;
112
113     my $fee = 15;
114
115     my $category = $builder->build_object(
116         {
117             class => 'Koha::Patron::Categories',
118             value => { reservefee => $fee }
119         }
120     );
121     my $patron = $builder->build_object(
122         {
123             class => 'Koha::Patrons',
124             value => { categorycode => $category->id }
125         }
126     );
127     my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
128
129     my $title  = 'Do what you want';
130     my $biblio = $builder->build_sample_biblio( { title => $title } );
131     my $item   = $builder->build_sample_item( { biblionumber => $biblio->id } );
132     my $hold   = $builder->build_object(
133         {
134             class => 'Koha::Holds',
135             value => {
136                 biblionumber   => $biblio->id,
137                 borrowernumber => $patron->id,
138                 itemnumber     => $item->id,
139                 priority       => 10,
140             }
141         }
142     );
143
144     t::lib::Mocks::mock_preference( 'HoldFeeMode', 'any_time_is_collected' );
145     t::lib::Mocks::mock_preference( 'HoldsLog',    1 );
146     t::lib::Mocks::mock_userenv(
147         { patron => $manager, branchcode => $manager->branchcode } );
148
149     my $interface = 'api';
150     C4::Context->interface($interface);
151
152     my $ret = $hold->fill;
153
154     is( ref($ret), 'Koha::Hold', '->fill returns the object type' );
155     is( $ret->id, $hold->id, '->fill returns the object' );
156
157     is( Koha::Holds->find($hold->id), undef, 'Hold no longer current' );
158     my $old_hold = Koha::Old::Holds->find( $hold->id );
159
160     is( $old_hold->id, $hold->id, 'reserve_id retained' );
161     is( $old_hold->priority, 0, 'priority set to 0' );
162     is( $old_hold->found, 'F', 'found set to F' );
163
164     subtest 'item_id parameter' => sub {
165         plan tests => 1;
166         $category->reservefee(0)->store; # do not disturb later accounts
167         $hold = $builder->build_object({ class => 'Koha::Holds', value => { biblionumber => $biblio->id, borrowernumber => $patron->id, itemnumber => undef, priority => 1 } });
168         # Simulating checkout without confirming hold
169         $hold->fill({ item_id => $item->id });
170         $old_hold = Koha::Old::Holds->find($hold->id);
171         is( $old_hold->itemnumber, $item->itemnumber, 'The itemnumber has been saved in old_reserves by fill' );
172         $old_hold->delete;
173         $category->reservefee($fee)->store; # restore
174     };
175
176     subtest 'fee applied tests' => sub {
177
178         plan tests => 9;
179
180         my $account = $patron->account;
181         is( $account->balance, $fee, 'Charge applied correctly' );
182
183         my $debits = $account->outstanding_debits;
184         is( $debits->count, 1, 'Only one fee charged' );
185
186         my $fee_debit = $debits->next;
187         is( $fee_debit->amount * 1, $fee, 'Fee amount stored correctly' );
188         is( $fee_debit->description, $title,
189             'Fee description stored correctly' );
190         is( $fee_debit->manager_id, $manager->id,
191             'Fee manager_id stored correctly' );
192         is( $fee_debit->branchcode, $manager->branchcode,
193             'Fee branchcode stored correctly' );
194         is( $fee_debit->interface, $interface,
195             'Fee interface stored correctly' );
196         is( $fee_debit->debit_type_code,
197             'RESERVE', 'Fee debit_type_code stored correctly' );
198         is( $fee_debit->itemnumber, $item->id,
199             'Fee itemnumber stored correctly' );
200     };
201
202     my $logs = Koha::ActionLogs->search(
203         {
204             action => 'FILL',
205             module => 'HOLDS',
206             object => $hold->id
207         }
208     );
209
210     is( $logs->count, 1, '1 log line added' );
211
212     # Set HoldFeeMode to something other than any_time_is_collected
213     t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
214     # Disable logging
215     t::lib::Mocks::mock_preference( 'HoldsLog',    0 );
216
217     $hold = $builder->build_object(
218         {
219             class => 'Koha::Holds',
220             value => {
221                 biblionumber   => $biblio->id,
222                 borrowernumber => $patron->id,
223                 itemnumber     => $item->id,
224                 priority       => 10,
225             }
226         }
227     );
228
229     $hold->fill;
230
231     my $account = $patron->account;
232     is( $account->balance, $fee, 'No new charge applied' );
233
234     my $debits = $account->outstanding_debits;
235     is( $debits->count, 1, 'Only one fee charged, because of HoldFeeMode' );
236
237     $logs = Koha::ActionLogs->search(
238         {
239             action => 'FILL',
240             module => 'HOLDS',
241             object => $hold->id
242         }
243     );
244
245     is( $logs->count, 0, 'HoldsLog disabled, no logs added' );
246
247     subtest 'anonymization behavior tests' => sub {
248
249         plan tests => 5;
250
251         # reduce the tests noise
252         t::lib::Mocks::mock_preference( 'HoldsLog',    0 );
253         t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
254         # unset AnonymousPatron
255         t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
256
257         # 0 == keep forever
258         $patron->privacy(0)->store;
259         my $hold = $builder->build_object(
260             {
261                 class => 'Koha::Holds',
262                 value => { borrowernumber => $patron->id, found => undef }
263             }
264         );
265         $hold->fill();
266         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
267             $patron->borrowernumber, 'Patron link is kept' );
268
269         # 1 == "default", meaning it is not protected from removal
270         $patron->privacy(1)->store;
271         $hold = $builder->build_object(
272             {
273                 class => 'Koha::Holds',
274                 value => { borrowernumber => $patron->id, found => undef }
275             }
276         );
277         $hold->fill();
278         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
279             $patron->borrowernumber, 'Patron link is kept' );
280
281         my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
282         t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
283         # We need anonymous patron set to change patron privacy to never
284         # (2 == delete immediately)
285         # then we can undef for further tests
286         $patron->privacy(2)->store;
287         t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
288         $hold = $builder->build_object(
289             {
290                 class => 'Koha::Holds',
291                 value => { borrowernumber => $patron->id, found => undef }
292             }
293         );
294
295         throws_ok
296             { $hold->fill(); }
297             'Koha::Exception',
298             'AnonymousPatron not set, exception thrown';
299
300         $hold->discard_changes; # refresh from DB
301
302         ok( !$hold->is_found, 'Hold is not filled' );
303
304         t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
305
306         $hold = $builder->build_object(
307             {
308                 class => 'Koha::Holds',
309                 value => { borrowernumber => $patron->id, found => undef }
310             }
311         );
312         $hold->fill();
313         is(
314             Koha::Old::Holds->find( $hold->id )->borrowernumber,
315             $anonymous_patron->id,
316             'Patron link is set to the configured anonymous patron immediately'
317         );
318     };
319
320     subtest 'holds_queue update tests' => sub {
321
322         plan tests => 1;
323
324         my $biblio = $builder->build_sample_biblio;
325
326         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
327         $mock->mock( 'enqueue', sub {
328             my ( $self, $args ) = @_;
329             is_deeply(
330                 $args->{biblio_ids},
331                 [ $biblio->id ],
332                 '->fill triggers a holds queue update for the related biblio'
333             );
334         } );
335
336         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
337
338         $builder->build_object(
339             {
340                 class => 'Koha::Holds',
341                 value => {
342                     biblionumber   => $biblio->id,
343                 }
344             }
345         )->fill;
346
347         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
348         # this call shouldn't add a new test
349         $builder->build_object(
350             {
351                 class => 'Koha::Holds',
352                 value => {
353                     biblionumber   => $biblio->id,
354                 }
355             }
356         )->fill;
357     };
358
359     $schema->storage->txn_rollback;
360 };
361
362 subtest 'patron() tests' => sub {
363
364     plan tests => 2;
365
366     $schema->storage->txn_begin;
367
368     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
369     my $hold   = $builder->build_object(
370         {
371             class => 'Koha::Holds',
372             value => {
373                 borrowernumber => $patron->borrowernumber
374             }
375         }
376     );
377
378     my $hold_patron = $hold->patron;
379     is( ref($hold_patron), 'Koha::Patron', 'Right type' );
380     is( $hold_patron->id, $patron->id, 'Right object' );
381
382     $schema->storage->txn_rollback;
383 };
384
385 subtest 'set_pickup_location() tests' => sub {
386
387     plan tests => 11;
388
389     $schema->storage->txn_begin;
390
391     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
392     my $mock_item   = Test::MockModule->new('Koha::Item');
393
394     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
395     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
396     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
397
398     # let's control what Koha::Biblio->pickup_locations returns, for testing
399     $mock_biblio->mock( 'pickup_locations', sub {
400         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
401     });
402     # let's mock what Koha::Item->pickup_locations returns, for testing
403     $mock_item->mock( 'pickup_locations', sub {
404         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
405     });
406
407     my $biblio = $builder->build_sample_biblio;
408     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
409
410     # Test biblio-level holds
411     my $biblio_hold = $builder->build_object(
412         {
413             class => "Koha::Holds",
414             value => {
415                 biblionumber => $biblio->biblionumber,
416                 branchcode   => $library_3->branchcode,
417                 itemnumber   => undef,
418             }
419         }
420     );
421
422     throws_ok
423         { $biblio_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
424         'Koha::Exceptions::Hold::InvalidPickupLocation',
425         'Exception thrown on invalid pickup location';
426
427     $biblio_hold->discard_changes;
428     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
429
430     my $ret = $biblio_hold->set_pickup_location({ library_id => $library_2->id });
431     is( ref($ret), 'Koha::Hold', 'self is returned' );
432
433     $biblio_hold->discard_changes;
434     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
435
436     # Test item-level holds
437     my $item_hold = $builder->build_object(
438         {
439             class => "Koha::Holds",
440             value => {
441                 biblionumber => $biblio->biblionumber,
442                 branchcode   => $library_3->branchcode,
443                 itemnumber   => $item->itemnumber,
444             }
445         }
446     );
447
448     throws_ok
449         { $item_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
450         'Koha::Exceptions::Hold::InvalidPickupLocation',
451         'Exception thrown on invalid pickup location';
452
453     $item_hold->discard_changes;
454     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
455
456     $item_hold->set_pickup_location({ library_id => $library_1->branchcode, force => 1 });
457     $item_hold->discard_changes;
458     is( $item_hold->branchcode, $library_1->branchcode, 'branchcode changed because of \'force\'' );
459
460     $ret = $item_hold->set_pickup_location({ library_id => $library_2->id });
461     is( ref($ret), 'Koha::Hold', 'self is returned' );
462
463     $item_hold->discard_changes;
464     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
465
466     throws_ok
467         { $item_hold->set_pickup_location({ library_id => undef }); }
468         'Koha::Exceptions::MissingParameter',
469         'Exception thrown if missing parameter';
470
471     like( "$@", qr/The library_id parameter is mandatory/, 'Exception message is clear' );
472
473     $schema->storage->txn_rollback;
474 };
475
476 subtest 'is_pickup_location_valid() tests' => sub {
477
478     plan tests => 5;
479
480     $schema->storage->txn_begin;
481
482     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
483     my $mock_item   = Test::MockModule->new('Koha::Item');
484
485     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
486     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
487     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
488
489     # let's control what Koha::Biblio->pickup_locations returns, for testing
490     $mock_biblio->mock( 'pickup_locations', sub {
491         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
492     });
493     # let's mock what Koha::Item->pickup_locations returns, for testing
494     $mock_item->mock( 'pickup_locations', sub {
495         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
496     });
497
498     my $biblio = $builder->build_sample_biblio;
499     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
500
501     # Test biblio-level holds
502     my $biblio_hold = $builder->build_object(
503         {
504             class => "Koha::Holds",
505             value => {
506                 biblionumber => $biblio->biblionumber,
507                 branchcode   => $library_3->branchcode,
508                 itemnumber   => undef,
509             }
510         }
511     );
512
513     ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
514     ok( $biblio_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid');
515
516     # Test item-level holds
517     my $item_hold = $builder->build_object(
518         {
519             class => "Koha::Holds",
520             value => {
521                 biblionumber => $biblio->biblionumber,
522                 branchcode   => $library_3->branchcode,
523                 itemnumber   => $item->itemnumber,
524             }
525         }
526     );
527
528     ok( !$item_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
529     ok( $item_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid' );
530
531     subtest 'pickup_locations() returning ->empty' => sub {
532
533         plan tests => 2;
534
535         $schema->storage->txn_begin;
536
537         my $library = $builder->build_object({ class => 'Koha::Libraries' });
538
539         my $mock_item = Test::MockModule->new('Koha::Item');
540         $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
541
542         my $mock_biblio = Test::MockModule->new('Koha::Biblio');
543         $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
544
545         my $item   = $builder->build_sample_item();
546         my $biblio = $item->biblio;
547
548         # Test biblio-level holds
549         my $biblio_hold = $builder->build_object(
550             {
551                 class => "Koha::Holds",
552                 value => {
553                     biblionumber => $biblio->biblionumber,
554                     itemnumber   => undef,
555                 }
556             }
557         );
558
559         ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
560
561         # Test item-level holds
562         my $item_hold = $builder->build_object(
563             {
564                 class => "Koha::Holds",
565                 value => {
566                     biblionumber => $biblio->biblionumber,
567                     itemnumber   => $item->itemnumber,
568                 }
569             }
570         );
571
572         ok( !$item_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
573
574         $schema->storage->txn_rollback;
575     };
576
577     $schema->storage->txn_rollback;
578 };
579
580 subtest 'cancel() tests' => sub {
581
582     plan tests => 6;
583
584     $schema->storage->txn_begin;
585
586     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
587
588     # reduce the tests noise
589     t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
590     t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge',
591         undef );
592
593     t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
594
595     # 0 == keep forever
596     $patron->privacy(0)->store;
597     my $hold = $builder->build_object(
598         {
599             class => 'Koha::Holds',
600             value => { borrowernumber => $patron->id, found => undef }
601         }
602     );
603     $hold->cancel();
604     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
605         $patron->borrowernumber, 'Patron link is kept' );
606
607     # 1 == "default", meaning it is not protected from removal
608     $patron->privacy(1)->store;
609     $hold = $builder->build_object(
610         {
611             class => 'Koha::Holds',
612             value => { borrowernumber => $patron->id, found => undef }
613         }
614     );
615     $hold->cancel();
616     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
617         $patron->borrowernumber, 'Patron link is kept' );
618
619     my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
620     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
621     # We need anonymous patron set to change patron privacy to never
622     # (2 == delete immediately)
623     # then we can undef for further tests
624     $patron->privacy(2)->store;
625     t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
626     $hold = $builder->build_object(
627         {
628             class => 'Koha::Holds',
629             value => { borrowernumber => $patron->id, found => undef }
630         }
631     );
632     throws_ok
633         { $hold->cancel(); }
634         'Koha::Exception',
635         'AnonymousPatron not set, exception thrown';
636
637     $hold->discard_changes;
638
639     ok( !$hold->is_found, 'Hold is not cancelled' );
640
641     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
642
643     $hold = $builder->build_object(
644         {
645             class => 'Koha::Holds',
646             value => { borrowernumber => $patron->id, found => undef }
647         }
648     );
649     $hold->cancel();
650     is(
651         Koha::Old::Holds->find( $hold->id )->borrowernumber,
652         $anonymous_patron->id,
653         'Patron link is set to the configured anonymous patron immediately'
654     );
655
656     subtest 'holds_queue update tests' => sub {
657
658         plan tests => 1;
659
660         my $biblio = $builder->build_sample_biblio;
661
662         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
663
664         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
665         $mock->mock( 'enqueue', sub {
666             my ( $self, $args ) = @_;
667             is_deeply(
668                 $args->{biblio_ids},
669                 [ $biblio->id ],
670                 '->cancel triggers a holds queue update for the related biblio'
671             );
672         } );
673
674         $builder->build_object(
675             {
676                 class => 'Koha::Holds',
677                 value => {
678                     biblionumber   => $biblio->id,
679                 }
680             }
681         )->cancel;
682
683         # If the skip_holds_queue param is not honoured, then test count will fail.
684         $builder->build_object(
685             {
686                 class => 'Koha::Holds',
687                 value => {
688                     biblionumber   => $biblio->id,
689                 }
690             }
691         )->cancel({ skip_holds_queue => 1 });
692
693         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
694
695         $builder->build_object(
696             {
697                 class => 'Koha::Holds',
698                 value => {
699                     biblionumber   => $biblio->id,
700                 }
701             }
702         )->cancel({ skip_holds_queue => 0 });
703     };
704
705     $schema->storage->txn_rollback;
706 };
707
708 subtest 'suspend_hold() and resume() tests' => sub {
709
710     plan tests => 2;
711
712     $schema->storage->txn_begin;
713
714     my $biblio = $builder->build_sample_biblio;
715     my $action;
716
717     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
718
719     my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
720     $mock->mock( 'enqueue', sub {
721         my ( $self, $args ) = @_;
722         is_deeply(
723             $args->{biblio_ids},
724             [ $biblio->id ],
725             "->$action triggers a holds queue update for the related biblio"
726         );
727     } );
728
729     my $hold = $builder->build_object(
730         {
731             class => 'Koha::Holds',
732             value => {
733                 biblionumber => $biblio->id,
734                 found        => undef,
735             }
736         }
737     );
738
739     $action = 'suspend_hold';
740     $hold->suspend_hold;
741
742     $action = 'resume';
743     $hold->resume;
744
745     $schema->storage->txn_rollback;
746 };
747
748 subtest 'cancellation_requests(), add_cancellation_request() and cancellation_requested() tests' => sub {
749
750     plan tests => 6;
751
752     $schema->storage->txn_begin;
753
754     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
755
756     my $hold = $builder->build_object( { class => 'Koha::Holds', } );
757
758     is( $hold->cancellation_requests->count, 0 );
759     ok( !$hold->cancellation_requested );
760
761     # Add two cancellation requests
762     my $request_1 = $hold->add_cancellation_request;
763     isnt( $request_1->creation_date, undef, 'creation_date is set' );
764
765     my $requester     = $builder->build_object( { class => 'Koha::Patrons' } );
766     my $creation_date = '2021-06-25 14:05:35';
767
768     my $request_2 = $hold->add_cancellation_request(
769         {
770             creation_date => $creation_date,
771         }
772     );
773
774     is( $request_2->creation_date, $creation_date, 'Passed creation_date set' );
775
776     is( $hold->cancellation_requests->count, 2 );
777     ok( $hold->cancellation_requested );
778
779     $schema->storage->txn_rollback;
780 };
781
782 subtest 'cancellation_requestable_from_opac() tests' => sub {
783
784     plan tests => 5;
785
786     $schema->storage->txn_begin;
787
788     my $category =
789       $builder->build_object( { class => 'Koha::Patron::Categories' } );
790     my $item_home_library =
791       $builder->build_object( { class => 'Koha::Libraries' } );
792     my $patron_home_library =
793       $builder->build_object( { class => 'Koha::Libraries' } );
794
795     my $item =
796       $builder->build_sample_item( { library => $item_home_library->id } );
797     my $patron = $builder->build_object(
798         {
799             class => 'Koha::Patrons',
800             value => { branchcode => $patron_home_library->id }
801         }
802     );
803
804     subtest 'Exception cases' => sub {
805
806         plan tests => 4;
807
808         my $hold = $builder->build_object(
809             {
810                 class => 'Koha::Holds',
811                 value => {
812                     itemnumber     => undef,
813                     found          => undef,
814                     borrowernumber => $patron->id
815                 }
816             }
817         );
818
819         throws_ok { $hold->cancellation_requestable_from_opac; }
820         'Koha::Exceptions::InvalidStatus',
821           'Exception thrown because hold is not waiting';
822
823         is( $@->invalid_status, 'hold_not_waiting' );
824
825         $hold = $builder->build_object(
826             {
827                 class => 'Koha::Holds',
828                 value => {
829                     itemnumber     => undef,
830                     found          => 'W',
831                     borrowernumber => $patron->id
832                 }
833             }
834         );
835
836         throws_ok { $hold->cancellation_requestable_from_opac; }
837         'Koha::Exceptions::InvalidStatus',
838           'Exception thrown because waiting hold has no item linked';
839
840         is( $@->invalid_status, 'no_item_linked' );
841     };
842
843     # set default rule to enabled
844     Koha::CirculationRules->set_rule(
845         {
846             categorycode => '*',
847             itemtype     => '*',
848             branchcode   => '*',
849             rule_name    => 'waiting_hold_cancellation',
850             rule_value   => 1,
851         }
852     );
853
854     my $hold = $builder->build_object(
855         {
856             class => 'Koha::Holds',
857             value => {
858                 itemnumber     => $item->id,
859                 found          => 'W',
860                 borrowernumber => $patron->id
861             }
862         }
863     );
864
865     t::lib::Mocks::mock_preference( 'ReservesControlBranch',
866         'ItemHomeLibrary' );
867
868     Koha::CirculationRules->set_rule(
869         {
870             categorycode => $patron->categorycode,
871             itemtype     => $item->itype,
872             branchcode   => $item->homebranch,
873             rule_name    => 'waiting_hold_cancellation',
874             rule_value   => 0,
875         }
876     );
877
878     ok( !$hold->cancellation_requestable_from_opac );
879
880     Koha::CirculationRules->set_rule(
881         {
882             categorycode => $patron->categorycode,
883             itemtype     => $item->itype,
884             branchcode   => $item->homebranch,
885             rule_name    => 'waiting_hold_cancellation',
886             rule_value   => 1,
887         }
888     );
889
890     ok(
891         $hold->cancellation_requestable_from_opac,
892         'Make sure it is picking the right circulation rule'
893     );
894
895     t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
896
897     Koha::CirculationRules->set_rule(
898         {
899             categorycode => $patron->categorycode,
900             itemtype     => $item->itype,
901             branchcode   => $patron->branchcode,
902             rule_name    => 'waiting_hold_cancellation',
903             rule_value   => 0,
904         }
905     );
906
907     ok( !$hold->cancellation_requestable_from_opac );
908
909     Koha::CirculationRules->set_rule(
910         {
911             categorycode => $patron->categorycode,
912             itemtype     => $item->itype,
913             branchcode   => $patron->branchcode,
914             rule_name    => 'waiting_hold_cancellation',
915             rule_value   => 1,
916         }
917     );
918
919     ok(
920         $hold->cancellation_requestable_from_opac,
921         'Make sure it is picking the right circulation rule'
922     );
923
924     $schema->storage->txn_rollback;
925 };
926
927 subtest 'can_update_pickup_location_opac() tests' => sub {
928
929     plan tests => 8;
930
931     $schema->storage->txn_begin;
932
933     my $hold = $builder->build_object(
934         {   class => 'Koha::Holds',
935             value => { found => undef, suspend => 0, suspend_until => undef, waitingdate => undef }
936         }
937     );
938
939     t::lib::Mocks::mock_preference( 'OPACAllowUserToChangeBranch', '' );
940     $hold->found(undef);
941     is( $hold->can_update_pickup_location_opac, 0, "Pending hold pickup can't be changed (No change allowed)" );
942
943     $hold->found('T');
944     is( $hold->can_update_pickup_location_opac, 0, "In transit hold pickup can't be changed (No change allowed)" );
945
946     $hold->found('W');
947     is( $hold->can_update_pickup_location_opac, 0, "Waiting hold pickup can't be changed (No change allowed)" );
948
949     $hold->found(undef);
950     my $dt = dt_from_string();
951
952     $hold->suspend_hold( $dt );
953     is( $hold->can_update_pickup_location_opac, 0, "Suspended hold pickup can't be changed (No change allowed)" );
954     $hold->resume();
955
956     t::lib::Mocks::mock_preference( 'OPACAllowUserToChangeBranch', 'pending,intransit,suspended' );
957     $hold->found(undef);
958     is( $hold->can_update_pickup_location_opac, 1, "Pending hold pickup can be changed (pending,intransit,suspended allowed)" );
959
960     $hold->found('T');
961     is( $hold->can_update_pickup_location_opac, 1, "In transit hold pickup can be changed (pending,intransit,suspended allowed)" );
962
963     $hold->found('W');
964     is( $hold->can_update_pickup_location_opac, 0, "Waiting hold pickup can't be changed (pending,intransit,suspended allowed)" );
965
966     $hold->found(undef);
967     $dt = dt_from_string();
968     $hold->suspend_hold( $dt );
969     is( $hold->can_update_pickup_location_opac, 1, "Suspended hold pickup can be changed (pending,intransit,suspended allowed)" );
970
971     $schema->storage->txn_rollback;
972 };
973
974 subtest 'Koha::Hold::item_group tests' => sub {
975
976     plan tests => 1;
977
978     $schema->storage->txn_begin;
979
980     my $library  = $builder->build_object( { class => 'Koha::Libraries' } );
981     my $category = $builder->build_object(
982         {
983             class => 'Koha::Patron::Categories',
984             value => { exclude_from_local_holds_priority => 0 }
985         }
986     );
987     my $patron = $builder->build_object(
988         {
989             class => "Koha::Patrons",
990             value => {
991                 branchcode   => $library->branchcode,
992                 categorycode => $category->categorycode
993             }
994         }
995     );
996     my $biblio = $builder->build_sample_biblio();
997
998     my $item_group =
999       Koha::Biblio::ItemGroup->new( { biblio_id => $biblio->id } )->store();
1000
1001     my $hold = $builder->build_object(
1002         {
1003             class => "Koha::Holds",
1004             value => {
1005                 borrowernumber => $patron->borrowernumber,
1006                 biblionumber   => $biblio->biblionumber,
1007                 priority       => 1,
1008                 item_group_id  => $item_group->id,
1009             }
1010         }
1011     );
1012
1013     is( $hold->item_group->id, $item_group->id, "Got correct item group" );
1014
1015     $schema->storage->txn_rollback;
1016 };
1017
1018 subtest 'change_type() tests' => sub {
1019
1020     plan tests => 13;
1021
1022     $schema->storage->txn_begin;
1023
1024     my $item = $builder->build_object( { class => 'Koha::Items', } );
1025     my $hold = $builder->build_object(
1026         {
1027             class => 'Koha::Holds',
1028             value => {
1029                 itemnumber      => undef,
1030                 item_level_hold => 0,
1031             }
1032         }
1033     );
1034
1035     my $hold2 = $builder->build_object(
1036         {
1037             class => 'Koha::Holds',
1038             value => {
1039                 borrowernumber => $hold->borrowernumber,
1040             }
1041         }
1042     );
1043
1044     ok( $hold->change_type );
1045
1046     $hold->discard_changes;
1047
1048     is( $hold->itemnumber, undef, 'record hold to record hold, no changes' );
1049
1050     is( $hold->item_level_hold, 0, 'item_level_hold=0' );
1051
1052     ok( $hold->change_type( $item->itemnumber ) );
1053
1054     $hold->discard_changes;
1055
1056     is( $hold->itemnumber, $item->itemnumber, 'record hold to item hold' );
1057
1058     is( $hold->item_level_hold, 1, 'item_level_hold=1' );
1059
1060     ok( $hold->change_type( $item->itemnumber ) );
1061
1062     $hold->discard_changes;
1063
1064     is( $hold->itemnumber, $item->itemnumber, 'item hold to item hold, no changes' );
1065
1066     is( $hold->item_level_hold, 1, 'item_level_hold=1' );
1067
1068     ok( $hold->change_type );
1069
1070     $hold->discard_changes;
1071
1072     is( $hold->itemnumber, undef, 'item hold to record hold' );
1073
1074     is( $hold->item_level_hold, 0, 'item_level_hold=0' );
1075
1076     my $hold3 = $builder->build_object(
1077         {
1078             class => 'Koha::Holds',
1079             value => {
1080                 biblionumber   => $hold->biblionumber,
1081                 borrowernumber => $hold->borrowernumber,
1082             }
1083         }
1084     );
1085
1086     throws_ok { $hold->change_type }
1087     'Koha::Exceptions::Hold::CannotChangeHoldType',
1088         'Exception thrown because more than one hold per record';
1089
1090     $schema->storage->txn_rollback;
1091 };