Bug 22456: Add cancellation request methods to Koha::Hold
[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 => 9;
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::Holds;
33 use Koha::Libraries;
34 use Koha::Old::Holds;
35
36 my $schema  = Koha::Database->new->schema;
37 my $builder = t::lib::TestBuilder->new;
38
39 subtest 'store() tests' => sub {
40     plan tests => 2;
41
42     $schema->storage->txn_begin;
43
44     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
45     my $item   = $builder->build_sample_item;
46     throws_ok {
47         Koha::Hold->new(
48             {
49                 borrowernumber => $patron->borrowernumber,
50                 biblionumber   => $item->biblionumber,
51                 priority       => 1,
52                 itemnumber     => $item->itemnumber,
53             }
54         )->store
55     }
56     'Koha::Exceptions::Hold::MissingPickupLocation',
57       'Exception thrown because branchcode was not passed';
58
59     my $hold = $builder->build_object( { class => 'Koha::Holds' } );
60     throws_ok {
61         $hold->branchcode(undef)->store;
62     }
63     'Koha::Exceptions::Hold::MissingPickupLocation',
64       'Exception thrown if one tries to set branchcode to null';
65
66     $schema->storage->txn_rollback;
67 };
68
69 subtest 'fill() tests' => sub {
70
71     plan tests => 13;
72
73     $schema->storage->txn_begin;
74
75     my $fee = 15;
76
77     my $category = $builder->build_object(
78         {
79             class => 'Koha::Patron::Categories',
80             value => { reservefee => $fee }
81         }
82     );
83     my $patron = $builder->build_object(
84         {
85             class => 'Koha::Patrons',
86             value => { categorycode => $category->id }
87         }
88     );
89     my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
90
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(
95         {
96             class => 'Koha::Holds',
97             value => {
98                 biblionumber   => $biblio->id,
99                 borrowernumber => $patron->id,
100                 itemnumber     => $item->id,
101                 priority       => 10,
102             }
103         }
104     );
105
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 } );
110
111     my $interface = 'api';
112     C4::Context->interface($interface);
113
114     my $ret = $hold->fill;
115
116     is( ref($ret), 'Koha::Hold', '->fill returns the object type' );
117     is( $ret->id, $hold->id, '->fill returns the object' );
118
119     is( Koha::Holds->find($hold->id), undef, 'Hold no longer current' );
120     my $old_hold = Koha::Old::Holds->find( $hold->id );
121
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' );
125
126     subtest 'fee applied tests' => sub {
127
128         plan tests => 9;
129
130         my $account = $patron->account;
131         is( $account->balance, $fee, 'Charge applied correctly' );
132
133         my $debits = $account->outstanding_debits;
134         is( $debits->count, 1, 'Only one fee charged' );
135
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' );
150     };
151
152     my $logs = Koha::ActionLogs->search(
153         {
154             action => 'FILL',
155             module => 'HOLDS',
156             object => $hold->id
157         }
158     );
159
160     is( $logs->count, 1, '1 log line added' );
161
162     # Set HoldFeeMode to something other than any_time_is_collected
163     t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
164     # Disable logging
165     t::lib::Mocks::mock_preference( 'HoldsLog',    0 );
166
167     $hold = $builder->build_object(
168         {
169             class => 'Koha::Holds',
170             value => {
171                 biblionumber   => $biblio->id,
172                 borrowernumber => $patron->id,
173                 itemnumber     => $item->id,
174                 priority       => 10,
175             }
176         }
177     );
178
179     $hold->fill;
180
181     my $account = $patron->account;
182     is( $account->balance, $fee, 'No new charge applied' );
183
184     my $debits = $account->outstanding_debits;
185     is( $debits->count, 1, 'Only one fee charged, because of HoldFeeMode' );
186
187     $logs = Koha::ActionLogs->search(
188         {
189             action => 'FILL',
190             module => 'HOLDS',
191             object => $hold->id
192         }
193     );
194
195     is( $logs->count, 0, 'HoldsLog disabled, no logs added' );
196
197     subtest 'anonymization behavior tests' => sub {
198
199         plan tests => 5;
200
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 );
206
207         # 0 == keep forever
208         $patron->privacy(0)->store;
209         my $hold = $builder->build_object(
210             {
211                 class => 'Koha::Holds',
212                 value => { borrowernumber => $patron->id, found => undef }
213             }
214         );
215         $hold->fill();
216         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
217             $patron->borrowernumber, 'Patron link is kept' );
218
219         # 1 == "default", meaning it is not protected from removal
220         $patron->privacy(1)->store;
221         $hold = $builder->build_object(
222             {
223                 class => 'Koha::Holds',
224                 value => { borrowernumber => $patron->id, found => undef }
225             }
226         );
227         $hold->fill();
228         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
229             $patron->borrowernumber, 'Patron link is kept' );
230
231         # 2 == delete immediately
232         $patron->privacy(2)->store;
233         $hold = $builder->build_object(
234             {
235                 class => 'Koha::Holds',
236                 value => { borrowernumber => $patron->id, found => undef }
237             }
238         );
239
240         throws_ok
241             { $hold->fill(); }
242             'Koha::Exception',
243             'AnonymousPatron not set, exception thrown';
244
245         $hold->discard_changes; # refresh from DB
246
247         ok( !$hold->is_found, 'Hold is not filled' );
248
249         my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
250         t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
251
252         $hold = $builder->build_object(
253             {
254                 class => 'Koha::Holds',
255                 value => { borrowernumber => $patron->id, found => undef }
256             }
257         );
258         $hold->fill();
259         is(
260             Koha::Old::Holds->find( $hold->id )->borrowernumber,
261             $anonymous_patron->id,
262             'Patron link is set to the configured anonymous patron immediately'
263         );
264     };
265
266     subtest 'holds_queue update tests' => sub {
267
268         plan tests => 1;
269
270         my $biblio = $builder->build_sample_biblio;
271
272         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
273         $mock->mock( 'enqueue', sub {
274             my ( $self, $args ) = @_;
275             is_deeply(
276                 $args->{biblio_ids},
277                 [ $biblio->id ],
278                 '->fill triggers a holds queue update for the related biblio'
279             );
280         } );
281
282         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
283
284         $builder->build_object(
285             {
286                 class => 'Koha::Holds',
287                 value => {
288                     biblionumber   => $biblio->id,
289                 }
290             }
291         )->fill;
292
293         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
294         # this call shouldn't add a new test
295         $builder->build_object(
296             {
297                 class => 'Koha::Holds',
298                 value => {
299                     biblionumber   => $biblio->id,
300                 }
301             }
302         )->fill;
303     };
304
305     $schema->storage->txn_rollback;
306 };
307
308 subtest 'patron() tests' => sub {
309
310     plan tests => 2;
311
312     $schema->storage->txn_begin;
313
314     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
315     my $hold   = $builder->build_object(
316         {
317             class => 'Koha::Holds',
318             value => {
319                 borrowernumber => $patron->borrowernumber
320             }
321         }
322     );
323
324     my $hold_patron = $hold->patron;
325     is( ref($hold_patron), 'Koha::Patron', 'Right type' );
326     is( $hold_patron->id, $patron->id, 'Right object' );
327
328     $schema->storage->txn_rollback;
329 };
330
331 subtest 'set_pickup_location() tests' => sub {
332
333     plan tests => 11;
334
335     $schema->storage->txn_begin;
336
337     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
338     my $mock_item   = Test::MockModule->new('Koha::Item');
339
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' });
343
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 ] } );
347     });
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 ] } );
351     });
352
353     my $biblio = $builder->build_sample_biblio;
354     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
355
356     # Test biblio-level holds
357     my $biblio_hold = $builder->build_object(
358         {
359             class => "Koha::Holds",
360             value => {
361                 biblionumber => $biblio->biblionumber,
362                 branchcode   => $library_3->branchcode,
363                 itemnumber   => undef,
364             }
365         }
366     );
367
368     throws_ok
369         { $biblio_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
370         'Koha::Exceptions::Hold::InvalidPickupLocation',
371         'Exception thrown on invalid pickup location';
372
373     $biblio_hold->discard_changes;
374     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
375
376     my $ret = $biblio_hold->set_pickup_location({ library_id => $library_2->id });
377     is( ref($ret), 'Koha::Hold', 'self is returned' );
378
379     $biblio_hold->discard_changes;
380     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
381
382     # Test item-level holds
383     my $item_hold = $builder->build_object(
384         {
385             class => "Koha::Holds",
386             value => {
387                 biblionumber => $biblio->biblionumber,
388                 branchcode   => $library_3->branchcode,
389                 itemnumber   => $item->itemnumber,
390             }
391         }
392     );
393
394     throws_ok
395         { $item_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
396         'Koha::Exceptions::Hold::InvalidPickupLocation',
397         'Exception thrown on invalid pickup location';
398
399     $item_hold->discard_changes;
400     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
401
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\'' );
405
406     $ret = $item_hold->set_pickup_location({ library_id => $library_2->id });
407     is( ref($ret), 'Koha::Hold', 'self is returned' );
408
409     $item_hold->discard_changes;
410     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
411
412     throws_ok
413         { $item_hold->set_pickup_location({ library_id => undef }); }
414         'Koha::Exceptions::MissingParameter',
415         'Exception thrown if missing parameter';
416
417     like( "$@", qr/The library_id parameter is mandatory/, 'Exception message is clear' );
418
419     $schema->storage->txn_rollback;
420 };
421
422 subtest 'is_pickup_location_valid() tests' => sub {
423
424     plan tests => 5;
425
426     $schema->storage->txn_begin;
427
428     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
429     my $mock_item   = Test::MockModule->new('Koha::Item');
430
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' });
434
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 ] } );
438     });
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 ] } );
442     });
443
444     my $biblio = $builder->build_sample_biblio;
445     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
446
447     # Test biblio-level holds
448     my $biblio_hold = $builder->build_object(
449         {
450             class => "Koha::Holds",
451             value => {
452                 biblionumber => $biblio->biblionumber,
453                 branchcode   => $library_3->branchcode,
454                 itemnumber   => undef,
455             }
456         }
457     );
458
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');
461
462     # Test item-level holds
463     my $item_hold = $builder->build_object(
464         {
465             class => "Koha::Holds",
466             value => {
467                 biblionumber => $biblio->biblionumber,
468                 branchcode   => $library_3->branchcode,
469                 itemnumber   => $item->itemnumber,
470             }
471         }
472     );
473
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' );
476
477     subtest 'pickup_locations() returning ->empty' => sub {
478
479         plan tests => 2;
480
481         $schema->storage->txn_begin;
482
483         my $library = $builder->build_object({ class => 'Koha::Libraries' });
484
485         my $mock_item = Test::MockModule->new('Koha::Item');
486         $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
487
488         my $mock_biblio = Test::MockModule->new('Koha::Biblio');
489         $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
490
491         my $item   = $builder->build_sample_item();
492         my $biblio = $item->biblio;
493
494         # Test biblio-level holds
495         my $biblio_hold = $builder->build_object(
496             {
497                 class => "Koha::Holds",
498                 value => {
499                     biblionumber => $biblio->biblionumber,
500                     itemnumber   => undef,
501                 }
502             }
503         );
504
505         ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
506
507         # Test item-level holds
508         my $item_hold = $builder->build_object(
509             {
510                 class => "Koha::Holds",
511                 value => {
512                     biblionumber => $biblio->biblionumber,
513                     itemnumber   => $item->itemnumber,
514                 }
515             }
516         );
517
518         ok( !$item_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
519
520         $schema->storage->txn_rollback;
521     };
522
523     $schema->storage->txn_rollback;
524 };
525
526 subtest 'cancel() tests' => sub {
527
528     plan tests => 6;
529
530     $schema->storage->txn_begin;
531
532     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
533
534     # reduce the tests noise
535     t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
536     t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge',
537         undef );
538
539     t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
540
541     # 0 == keep forever
542     $patron->privacy(0)->store;
543     my $hold = $builder->build_object(
544         {
545             class => 'Koha::Holds',
546             value => { borrowernumber => $patron->id, found => undef }
547         }
548     );
549     $hold->cancel();
550     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
551         $patron->borrowernumber, 'Patron link is kept' );
552
553     # 1 == "default", meaning it is not protected from removal
554     $patron->privacy(1)->store;
555     $hold = $builder->build_object(
556         {
557             class => 'Koha::Holds',
558             value => { borrowernumber => $patron->id, found => undef }
559         }
560     );
561     $hold->cancel();
562     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
563         $patron->borrowernumber, 'Patron link is kept' );
564
565     # 2 == delete immediately
566     $patron->privacy(2)->store;
567     $hold = $builder->build_object(
568         {
569             class => 'Koha::Holds',
570             value => { borrowernumber => $patron->id, found => undef }
571         }
572     );
573     throws_ok
574         { $hold->cancel(); }
575         'Koha::Exception',
576         'AnonymousPatron not set, exception thrown';
577
578     $hold->discard_changes;
579
580     ok( !$hold->is_found, 'Hold is not cancelled' );
581
582     my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
583     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
584
585     $hold = $builder->build_object(
586         {
587             class => 'Koha::Holds',
588             value => { borrowernumber => $patron->id, found => undef }
589         }
590     );
591     $hold->cancel();
592     is(
593         Koha::Old::Holds->find( $hold->id )->borrowernumber,
594         $anonymous_patron->id,
595         'Patron link is set to the configured anonymous patron immediately'
596     );
597
598     subtest 'holds_queue update tests' => sub {
599
600         plan tests => 1;
601
602         my $biblio = $builder->build_sample_biblio;
603
604         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
605
606         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
607         $mock->mock( 'enqueue', sub {
608             my ( $self, $args ) = @_;
609             is_deeply(
610                 $args->{biblio_ids},
611                 [ $biblio->id ],
612                 '->cancel triggers a holds queue update for the related biblio'
613             );
614         } );
615
616         $builder->build_object(
617             {
618                 class => 'Koha::Holds',
619                 value => {
620                     biblionumber   => $biblio->id,
621                 }
622             }
623         )->cancel;
624
625         # If the skip_holds_queue param is not honoured, then test count will fail.
626         $builder->build_object(
627             {
628                 class => 'Koha::Holds',
629                 value => {
630                     biblionumber   => $biblio->id,
631                 }
632             }
633         )->cancel({ skip_holds_queue => 1 });
634
635         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
636
637         $builder->build_object(
638             {
639                 class => 'Koha::Holds',
640                 value => {
641                     biblionumber   => $biblio->id,
642                 }
643             }
644         )->cancel({ skip_holds_queue => 0 });
645     };
646
647     $schema->storage->txn_rollback;
648 };
649
650 subtest 'suspend_hold() and resume() tests' => sub {
651
652     plan tests => 2;
653
654     $schema->storage->txn_begin;
655
656     my $biblio = $builder->build_sample_biblio;
657     my $action;
658
659     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
660
661     my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
662     $mock->mock( 'enqueue', sub {
663         my ( $self, $args ) = @_;
664         is_deeply(
665             $args->{biblio_ids},
666             [ $biblio->id ],
667             "->$action triggers a holds queue update for the related biblio"
668         );
669     } );
670
671     my $hold = $builder->build_object(
672         {
673             class => 'Koha::Holds',
674             value => {
675                 biblionumber => $biblio->id,
676                 found        => undef,
677             }
678         }
679     );
680
681     $action = 'suspend_hold';
682     $hold->suspend_hold;
683
684     $action = 'resume';
685     $hold->resume;
686
687     $schema->storage->txn_rollback;
688 };
689
690 subtest 'cancellation_requests() and add_cancellation_request() tests' => sub {
691
692     plan tests => 4;
693
694     $schema->storage->txn_begin;
695
696     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
697
698     my $hold = $builder->build_object( { class => 'Koha::Holds', } );
699
700     is( $hold->cancellation_requests->count, 0 );
701
702     # Add two cancellation requests
703     my $request_1 = $hold->add_cancellation_request;
704     isnt( $request_1->creation_date, undef, 'creation_date is set' );
705
706     my $requester     = $builder->build_object( { class => 'Koha::Patrons' } );
707     my $creation_date = '2021-06-25 14:05:35';
708
709     my $request_2 = $hold->add_cancellation_request(
710         {
711             creation_date => $creation_date,
712         }
713     );
714
715     is( $request_2->creation_date, $creation_date, 'Passed creation_date set' );
716
717     is( $hold->cancellation_requests->count, 2 );
718
719     $schema->storage->txn_rollback;
720 };
721
722 subtest 'cancellation_requestable_from_opac() tests' => sub {
723
724     plan tests => 5;
725
726     $schema->storage->txn_begin;
727
728     my $category =
729       $builder->build_object( { class => 'Koha::Patron::Categories' } );
730     my $item_home_library =
731       $builder->build_object( { class => 'Koha::Libraries' } );
732     my $patron_home_library =
733       $builder->build_object( { class => 'Koha::Libraries' } );
734
735     my $item =
736       $builder->build_sample_item( { library => $item_home_library->id } );
737     my $patron = $builder->build_object(
738         {
739             class => 'Koha::Patrons',
740             value => { branchcode => $patron_home_library->id }
741         }
742     );
743
744     subtest 'Exception cases' => sub {
745
746         plan tests => 4;
747
748         my $hold = $builder->build_object(
749             {
750                 class => 'Koha::Holds',
751                 value => {
752                     itemnumber     => undef,
753                     found          => undef,
754                     borrowernumber => $patron->id
755                 }
756             }
757         );
758
759         throws_ok { $hold->cancellation_requestable_from_opac; }
760         'Koha::Exceptions::InvalidStatus',
761           'Exception thrown because hold is not waiting';
762
763         is( $@->invalid_status, 'hold_not_waiting' );
764
765         $hold = $builder->build_object(
766             {
767                 class => 'Koha::Holds',
768                 value => {
769                     itemnumber     => undef,
770                     found          => 'W',
771                     borrowernumber => $patron->id
772                 }
773             }
774         );
775
776         throws_ok { $hold->cancellation_requestable_from_opac; }
777         'Koha::Exceptions::InvalidStatus',
778           'Exception thrown because waiting hold has no item linked';
779
780         is( $@->invalid_status, 'no_item_linked' );
781     };
782
783     # set default rule to enabled
784     Koha::CirculationRules->set_rule(
785         {
786             categorycode => '*',
787             itemtype     => '*',
788             branchcode   => '*',
789             rule_name    => 'waiting_hold_cancellation',
790             rule_value   => 1,
791         }
792     );
793
794     my $hold = $builder->build_object(
795         {
796             class => 'Koha::Holds',
797             value => {
798                 itemnumber     => $item->id,
799                 found          => 'W',
800                 borrowernumber => $patron->id
801             }
802         }
803     );
804
805     t::lib::Mocks::mock_preference( 'ReservesControlBranch',
806         'ItemHomeLibrary' );
807
808     Koha::CirculationRules->set_rule(
809         {
810             categorycode => $patron->categorycode,
811             itemtype     => $item->itype,
812             branchcode   => $item->homebranch,
813             rule_name    => 'waiting_hold_cancellation',
814             rule_value   => 0,
815         }
816     );
817
818     ok( !$hold->cancellation_requestable_from_opac );
819
820     Koha::CirculationRules->set_rule(
821         {
822             categorycode => $patron->categorycode,
823             itemtype     => $item->itype,
824             branchcode   => $item->homebranch,
825             rule_name    => 'waiting_hold_cancellation',
826             rule_value   => 1,
827         }
828     );
829
830     ok(
831         $hold->cancellation_requestable_from_opac,
832         'Make sure it is picking the right circulation rule'
833     );
834
835     t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
836
837     Koha::CirculationRules->set_rule(
838         {
839             categorycode => $patron->categorycode,
840             itemtype     => $item->itype,
841             branchcode   => $patron->branchcode,
842             rule_name    => 'waiting_hold_cancellation',
843             rule_value   => 0,
844         }
845     );
846
847     ok( !$hold->cancellation_requestable_from_opac );
848
849     Koha::CirculationRules->set_rule(
850         {
851             categorycode => $patron->categorycode,
852             itemtype     => $item->itype,
853             branchcode   => $patron->branchcode,
854             rule_name    => 'waiting_hold_cancellation',
855             rule_value   => 1,
856         }
857     );
858
859     ok(
860         $hold->cancellation_requestable_from_opac,
861         'Make sure it is picking the right circulation rule'
862     );
863
864     $schema->storage->txn_rollback;
865 };