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