Bug 29346: Hold actions triggers
[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 => 6;
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 'fill() tests' => sub {
40
41     plan tests => 13;
42
43     $schema->storage->txn_begin;
44
45     my $fee = 15;
46
47     my $category = $builder->build_object(
48         {
49             class => 'Koha::Patron::Categories',
50             value => { reservefee => $fee }
51         }
52     );
53     my $patron = $builder->build_object(
54         {
55             class => 'Koha::Patrons',
56             value => { categorycode => $category->id }
57         }
58     );
59     my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
60
61     my $title  = 'Do what you want';
62     my $biblio = $builder->build_sample_biblio( { title => $title } );
63     my $item   = $builder->build_sample_item( { biblionumber => $biblio->id } );
64     my $hold   = $builder->build_object(
65         {
66             class => 'Koha::Holds',
67             value => {
68                 biblionumber   => $biblio->id,
69                 borrowernumber => $patron->id,
70                 itemnumber     => $item->id,
71                 priority       => 10,
72             }
73         }
74     );
75
76     t::lib::Mocks::mock_preference( 'HoldFeeMode', 'any_time_is_collected' );
77     t::lib::Mocks::mock_preference( 'HoldsLog',    1 );
78     t::lib::Mocks::mock_userenv(
79         { patron => $manager, branchcode => $manager->branchcode } );
80
81     my $interface = 'api';
82     C4::Context->interface($interface);
83
84     my $ret = $hold->fill;
85
86     is( ref($ret), 'Koha::Hold', '->fill returns the object type' );
87     is( $ret->id, $hold->id, '->fill returns the object' );
88
89     is( Koha::Holds->find($hold->id), undef, 'Hold no longer current' );
90     my $old_hold = Koha::Old::Holds->find( $hold->id );
91
92     is( $old_hold->id, $hold->id, 'reserve_id retained' );
93     is( $old_hold->priority, 0, 'priority set to 0' );
94     is( $old_hold->found, 'F', 'found set to F' );
95
96     subtest 'fee applied tests' => sub {
97
98         plan tests => 9;
99
100         my $account = $patron->account;
101         is( $account->balance, $fee, 'Charge applied correctly' );
102
103         my $debits = $account->outstanding_debits;
104         is( $debits->count, 1, 'Only one fee charged' );
105
106         my $fee_debit = $debits->next;
107         is( $fee_debit->amount * 1, $fee, 'Fee amount stored correctly' );
108         is( $fee_debit->description, $title,
109             'Fee description stored correctly' );
110         is( $fee_debit->manager_id, $manager->id,
111             'Fee manager_id stored correctly' );
112         is( $fee_debit->branchcode, $manager->branchcode,
113             'Fee branchcode stored correctly' );
114         is( $fee_debit->interface, $interface,
115             'Fee interface stored correctly' );
116         is( $fee_debit->debit_type_code,
117             'RESERVE', 'Fee debit_type_code stored correctly' );
118         is( $fee_debit->itemnumber, $item->id,
119             'Fee itemnumber stored correctly' );
120     };
121
122     my $logs = Koha::ActionLogs->search(
123         {
124             action => 'FILL',
125             module => 'HOLDS',
126             object => $hold->id
127         }
128     );
129
130     is( $logs->count, 1, '1 log line added' );
131
132     # Set HoldFeeMode to something other than any_time_is_collected
133     t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
134     # Disable logging
135     t::lib::Mocks::mock_preference( 'HoldsLog',    0 );
136
137     $hold = $builder->build_object(
138         {
139             class => 'Koha::Holds',
140             value => {
141                 biblionumber   => $biblio->id,
142                 borrowernumber => $patron->id,
143                 itemnumber     => $item->id,
144                 priority       => 10,
145             }
146         }
147     );
148
149     $hold->fill;
150
151     my $account = $patron->account;
152     is( $account->balance, $fee, 'No new charge applied' );
153
154     my $debits = $account->outstanding_debits;
155     is( $debits->count, 1, 'Only one fee charged, because of HoldFeeMode' );
156
157     $logs = Koha::ActionLogs->search(
158         {
159             action => 'FILL',
160             module => 'HOLDS',
161             object => $hold->id
162         }
163     );
164
165     is( $logs->count, 0, 'HoldsLog disabled, no logs added' );
166
167     subtest 'anonymization behavior tests' => sub {
168
169         plan tests => 5;
170
171         # reduce the tests noise
172         t::lib::Mocks::mock_preference( 'HoldsLog',    0 );
173         t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
174         # unset AnonymousPatron
175         t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
176
177         # 0 == keep forever
178         $patron->privacy(0)->store;
179         my $hold = $builder->build_object(
180             {
181                 class => 'Koha::Holds',
182                 value => { borrowernumber => $patron->id, found => undef }
183             }
184         );
185         $hold->fill();
186         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
187             $patron->borrowernumber, 'Patron link is kept' );
188
189         # 1 == "default", meaning it is not protected from removal
190         $patron->privacy(1)->store;
191         $hold = $builder->build_object(
192             {
193                 class => 'Koha::Holds',
194                 value => { borrowernumber => $patron->id, found => undef }
195             }
196         );
197         $hold->fill();
198         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
199             $patron->borrowernumber, 'Patron link is kept' );
200
201         # 2 == delete immediately
202         $patron->privacy(2)->store;
203         $hold = $builder->build_object(
204             {
205                 class => 'Koha::Holds',
206                 value => { borrowernumber => $patron->id, found => undef }
207             }
208         );
209
210         throws_ok
211             { $hold->fill(); }
212             'Koha::Exception',
213             'AnonymousPatron not set, exception thrown';
214
215         $hold->discard_changes; # refresh from DB
216
217         ok( !$hold->is_found, 'Hold is not filled' );
218
219         my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
220         t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
221
222         $hold = $builder->build_object(
223             {
224                 class => 'Koha::Holds',
225                 value => { borrowernumber => $patron->id, found => undef }
226             }
227         );
228         $hold->fill();
229         is(
230             Koha::Old::Holds->find( $hold->id )->borrowernumber,
231             $anonymous_patron->id,
232             'Patron link is set to the configured anonymous patron immediately'
233         );
234     };
235
236     subtest 'holds_queue update tests' => sub {
237
238         plan tests => 1;
239
240         my $biblio = $builder->build_sample_biblio;
241
242         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
243         $mock->mock( 'enqueue', sub {
244             my ( $self, $args ) = @_;
245             is_deeply(
246                 $args->{biblio_ids},
247                 [ $biblio->id ],
248                 '->fill triggers a holds queue update for the related biblio'
249             );
250         } );
251
252         $builder->build_object(
253             {
254                 class => 'Koha::Holds',
255                 value => {
256                     biblionumber   => $biblio->id,
257                 }
258             }
259         )->fill;
260     };
261
262     $schema->storage->txn_rollback;
263 };
264
265 subtest 'patron() tests' => sub {
266
267     plan tests => 2;
268
269     $schema->storage->txn_begin;
270
271     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
272     my $hold   = $builder->build_object(
273         {
274             class => 'Koha::Holds',
275             value => {
276                 borrowernumber => $patron->borrowernumber
277             }
278         }
279     );
280
281     my $hold_patron = $hold->patron;
282     is( ref($hold_patron), 'Koha::Patron', 'Right type' );
283     is( $hold_patron->id, $patron->id, 'Right object' );
284
285     $schema->storage->txn_rollback;
286 };
287
288 subtest 'set_pickup_location() tests' => sub {
289
290     plan tests => 11;
291
292     $schema->storage->txn_begin;
293
294     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
295     my $mock_item   = Test::MockModule->new('Koha::Item');
296
297     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
298     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
299     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
300
301     # let's control what Koha::Biblio->pickup_locations returns, for testing
302     $mock_biblio->mock( 'pickup_locations', sub {
303         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
304     });
305     # let's mock what Koha::Item->pickup_locations returns, for testing
306     $mock_item->mock( 'pickup_locations', sub {
307         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
308     });
309
310     my $biblio = $builder->build_sample_biblio;
311     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
312
313     # Test biblio-level holds
314     my $biblio_hold = $builder->build_object(
315         {
316             class => "Koha::Holds",
317             value => {
318                 biblionumber => $biblio->biblionumber,
319                 branchcode   => $library_3->branchcode,
320                 itemnumber   => undef,
321             }
322         }
323     );
324
325     throws_ok
326         { $biblio_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
327         'Koha::Exceptions::Hold::InvalidPickupLocation',
328         'Exception thrown on invalid pickup location';
329
330     $biblio_hold->discard_changes;
331     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
332
333     my $ret = $biblio_hold->set_pickup_location({ library_id => $library_2->id });
334     is( ref($ret), 'Koha::Hold', 'self is returned' );
335
336     $biblio_hold->discard_changes;
337     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
338
339     # Test item-level holds
340     my $item_hold = $builder->build_object(
341         {
342             class => "Koha::Holds",
343             value => {
344                 biblionumber => $biblio->biblionumber,
345                 branchcode   => $library_3->branchcode,
346                 itemnumber   => $item->itemnumber,
347             }
348         }
349     );
350
351     throws_ok
352         { $item_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
353         'Koha::Exceptions::Hold::InvalidPickupLocation',
354         'Exception thrown on invalid pickup location';
355
356     $item_hold->discard_changes;
357     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
358
359     $item_hold->set_pickup_location({ library_id => $library_1->branchcode, force => 1 });
360     $item_hold->discard_changes;
361     is( $item_hold->branchcode, $library_1->branchcode, 'branchcode changed because of \'force\'' );
362
363     $ret = $item_hold->set_pickup_location({ library_id => $library_2->id });
364     is( ref($ret), 'Koha::Hold', 'self is returned' );
365
366     $item_hold->discard_changes;
367     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
368
369     throws_ok
370         { $item_hold->set_pickup_location({ library_id => undef }); }
371         'Koha::Exceptions::MissingParameter',
372         'Exception thrown if missing parameter';
373
374     like( "$@", qr/The library_id parameter is mandatory/, 'Exception message is clear' );
375
376     $schema->storage->txn_rollback;
377 };
378
379 subtest 'is_pickup_location_valid() tests' => sub {
380
381     plan tests => 5;
382
383     $schema->storage->txn_begin;
384
385     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
386     my $mock_item   = Test::MockModule->new('Koha::Item');
387
388     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
389     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
390     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
391
392     # let's control what Koha::Biblio->pickup_locations returns, for testing
393     $mock_biblio->mock( 'pickup_locations', sub {
394         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
395     });
396     # let's mock what Koha::Item->pickup_locations returns, for testing
397     $mock_item->mock( 'pickup_locations', sub {
398         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
399     });
400
401     my $biblio = $builder->build_sample_biblio;
402     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
403
404     # Test biblio-level holds
405     my $biblio_hold = $builder->build_object(
406         {
407             class => "Koha::Holds",
408             value => {
409                 biblionumber => $biblio->biblionumber,
410                 branchcode   => $library_3->branchcode,
411                 itemnumber   => undef,
412             }
413         }
414     );
415
416     ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
417     ok( $biblio_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid');
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     ok( !$item_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
432     ok( $item_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid' );
433
434     subtest 'pickup_locations() returning ->empty' => sub {
435
436         plan tests => 2;
437
438         $schema->storage->txn_begin;
439
440         my $library = $builder->build_object({ class => 'Koha::Libraries' });
441
442         my $mock_item = Test::MockModule->new('Koha::Item');
443         $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
444
445         my $mock_biblio = Test::MockModule->new('Koha::Biblio');
446         $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
447
448         my $item   = $builder->build_sample_item();
449         my $biblio = $item->biblio;
450
451         # Test biblio-level holds
452         my $biblio_hold = $builder->build_object(
453             {
454                 class => "Koha::Holds",
455                 value => {
456                     biblionumber => $biblio->biblionumber,
457                     itemnumber   => undef,
458                 }
459             }
460         );
461
462         ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
463
464         # Test item-level holds
465         my $item_hold = $builder->build_object(
466             {
467                 class => "Koha::Holds",
468                 value => {
469                     biblionumber => $biblio->biblionumber,
470                     itemnumber   => $item->itemnumber,
471                 }
472             }
473         );
474
475         ok( !$item_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
476
477         $schema->storage->txn_rollback;
478     };
479
480     $schema->storage->txn_rollback;
481 };
482
483 subtest 'cancel() tests' => sub {
484
485     plan tests => 6;
486
487     $schema->storage->txn_begin;
488
489     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
490
491     # reduce the tests noise
492     t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
493     t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge',
494         undef );
495
496     t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
497
498     # 0 == keep forever
499     $patron->privacy(0)->store;
500     my $hold = $builder->build_object(
501         {
502             class => 'Koha::Holds',
503             value => { borrowernumber => $patron->id, found => undef }
504         }
505     );
506     $hold->cancel();
507     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
508         $patron->borrowernumber, 'Patron link is kept' );
509
510     # 1 == "default", meaning it is not protected from removal
511     $patron->privacy(1)->store;
512     $hold = $builder->build_object(
513         {
514             class => 'Koha::Holds',
515             value => { borrowernumber => $patron->id, found => undef }
516         }
517     );
518     $hold->cancel();
519     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
520         $patron->borrowernumber, 'Patron link is kept' );
521
522     # 2 == delete immediately
523     $patron->privacy(2)->store;
524     $hold = $builder->build_object(
525         {
526             class => 'Koha::Holds',
527             value => { borrowernumber => $patron->id, found => undef }
528         }
529     );
530     throws_ok
531         { $hold->cancel(); }
532         'Koha::Exception',
533         'AnonymousPatron not set, exception thrown';
534
535     $hold->discard_changes;
536
537     ok( !$hold->is_found, 'Hold is not cancelled' );
538
539     my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
540     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
541
542     $hold = $builder->build_object(
543         {
544             class => 'Koha::Holds',
545             value => { borrowernumber => $patron->id, found => undef }
546         }
547     );
548     $hold->cancel();
549     is(
550         Koha::Old::Holds->find( $hold->id )->borrowernumber,
551         $anonymous_patron->id,
552         'Patron link is set to the configured anonymous patron immediately'
553     );
554
555     subtest 'holds_queue update tests' => sub {
556
557         plan tests => 1;
558
559         my $biblio = $builder->build_sample_biblio;
560
561         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
562         $mock->mock( 'enqueue', sub {
563             my ( $self, $args ) = @_;
564             is_deeply(
565                 $args->{biblio_ids},
566                 [ $biblio->id ],
567                 '->cancel triggers a holds queue update for the related biblio'
568             );
569         } );
570
571         $builder->build_object(
572             {
573                 class => 'Koha::Holds',
574                 value => {
575                     biblionumber   => $biblio->id,
576                 }
577             }
578         )->cancel;
579
580         # If the skip_holds_queue param is not honoured, then test count will fail.
581         $builder->build_object(
582             {
583                 class => 'Koha::Holds',
584                 value => {
585                     biblionumber   => $biblio->id,
586                 }
587             }
588         )->cancel({ skip_holds_queue => 1 });
589     };
590
591     $schema->storage->txn_rollback;
592 };
593
594 subtest 'suspend_hold() and resume() tests' => sub {
595
596     plan tests => 2;
597
598     $schema->storage->txn_begin;
599
600     my $biblio = $builder->build_sample_biblio;
601     my $action;
602
603     my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
604     $mock->mock( 'enqueue', sub {
605         my ( $self, $args ) = @_;
606         is_deeply(
607             $args->{biblio_ids},
608             [ $biblio->id ],
609             "->$action triggers a holds queue update for the related biblio"
610         );
611     } );
612
613     my $hold = $builder->build_object(
614         {
615             class => 'Koha::Holds',
616             value => {
617                 biblionumber => $biblio->id,
618             }
619         }
620     );
621
622     $action = 'suspend_hold';
623     $hold->suspend_hold;
624
625     $action = 'resume';
626     $hold->resume;
627
628     $schema->storage->txn_rollback;
629 };