Bug 33028: Unit tests
[koha.git] / t / db_dependent / Koha / CirculationRules.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 Benchmark;
23 use Test::More tests => 7;
24 use Test::Deep qw( cmp_methods );
25 use Test::Exception;
26
27 use Koha::CirculationRules;
28 use Koha::Database;
29
30 use t::lib::Mocks;
31 use t::lib::TestBuilder;
32 use Koha::Cache::Memory::Lite;
33
34 my $schema = Koha::Database->new->schema;
35 my $builder = t::lib::TestBuilder->new;
36
37 subtest 'get_effective_issuing_rule' => sub {
38     plan tests => 2;
39
40     $schema->storage->txn_begin;
41
42     my $categorycode = $builder->build({ source => 'Category' })->{'categorycode'};
43     my $itemtype     = $builder->build({ source => 'Itemtype' })->{'itemtype'};
44     my $branchcode   = $builder->build({ source => 'Branch' })->{'branchcode'};
45
46     subtest 'Call with undefined values' => sub {
47         plan tests => 5;
48
49         my $rule;
50         Koha::CirculationRules->delete;
51
52         is(Koha::CirculationRules->search->count, 0, 'There are no issuing rules.');
53         # undef, undef, undef => 1
54         $rule = Koha::CirculationRules->get_effective_rule({
55             branchcode   => undef,
56             categorycode => undef,
57             itemtype     => undef,
58             rule_name    => 'fine',
59             rule_value   => 1,
60         });
61         is($rule, undef, 'When I attempt to get effective issuing rule by'
62            .' providing undefined values, then undef is returned.');
63
64        # undef, undef, undef => 2
65         ok(
66             Koha::CirculationRule->new(
67                 {
68                     branchcode   => undef,
69                     categorycode => undef,
70                     itemtype     => undef,
71                     rule_name    => 'fine',
72                     rule_value   => 2,
73                 }
74               )->store,
75             'Given I added an issuing rule branchcode => undef,'
76            .' categorycode => undef, itemtype => undef,');
77         $rule = Koha::CirculationRules->get_effective_rule({
78             branchcode   => undef,
79             categorycode => undef,
80             itemtype     => undef,
81             rule_name    => 'fine',
82         });
83         _is_row_match(
84             $rule,
85             {
86                 branchcode   => undef,
87                 categorycode => undef,
88                 itemtype     => undef,
89                 rule_name    => 'fine',
90                 rule_value   => 2,
91             },
92             'When I attempt to get effective'
93            .' issuing rule by providing undefined values, then the above one is'
94            .' returned.'
95         );
96     };
97
98     subtest 'Performance' => sub {
99         plan tests => 4;
100
101         my $worst_case = timethis(500,
102                     sub { Koha::CirculationRules->get_effective_rule({
103                             branchcode   => 'nonexistent',
104                             categorycode => 'nonexistent',
105                             itemtype     => 'nonexistent',
106                             rule_name    => 'nonexistent',
107                         });
108                     }
109                 );
110         my $mid_case = timethis(500,
111                     sub { Koha::CirculationRules->get_effective_rule({
112                             branchcode   => $branchcode,
113                             categorycode => 'nonexistent',
114                             itemtype     => 'nonexistent',
115                             rule_name    => 'nonexistent',
116                         });
117                     }
118                 );
119         my $sec_best_case = timethis(500,
120                     sub { Koha::CirculationRules->get_effective_rule({
121                             branchcode   => $branchcode,
122                             categorycode => $categorycode,
123                             itemtype     => 'nonexistent',
124                             rule_name    => 'nonexistent',
125                         });
126                     }
127                 );
128         my $best_case = timethis(500,
129                     sub { Koha::CirculationRules->get_effective_rule({
130                             branchcode   => $branchcode,
131                             categorycode => $categorycode,
132                             itemtype     => $itemtype,
133                             rule_name    => 'nonexistent',
134                         });
135                     }
136                 );
137         ok($worst_case, 'In worst case, get_effective_issuing_rule finds matching'
138            .' rule '.sprintf('%.2f', $worst_case->iters/$worst_case->cpu_a)
139            .' times per second.');
140         ok($mid_case, 'In mid case, get_effective_issuing_rule finds matching'
141            .' rule '.sprintf('%.2f', $mid_case->iters/$mid_case->cpu_a)
142            .' times per second.');
143         ok($sec_best_case, 'In second best case, get_effective_issuing_rule finds matching'
144            .' rule '.sprintf('%.2f', $sec_best_case->iters/$sec_best_case->cpu_a)
145            .' times per second.');
146         ok($best_case, 'In best case, get_effective_issuing_rule finds matching'
147            .' rule '.sprintf('%.2f', $best_case->iters/$best_case->cpu_a)
148            .' times per second.');
149     };
150
151     $schema->storage->txn_rollback;
152
153 };
154
155 subtest 'set_rule' => sub {
156     plan tests => 4;
157
158     $schema->storage->txn_begin;
159
160     my $branchcode   = $builder->build({ source => 'Branch' })->{'branchcode'};
161     my $categorycode = $builder->build({ source => 'Category' })->{'categorycode'};
162     my $itemtype     = $builder->build({ source => 'Itemtype' })->{'itemtype'};
163
164     subtest 'Correct call' => sub {
165         plan tests => 5;
166
167         Koha::CirculationRules->delete;
168
169         lives_ok( sub {
170             Koha::CirculationRules->set_rule( {
171                 branchcode => $branchcode,
172                 rule_name => 'lostreturn',
173                 rule_value => '',
174             } );
175         }, 'setting lostreturn with branch' );
176
177         lives_ok( sub {
178             Koha::CirculationRules->set_rule( {
179                 branchcode => $branchcode,
180                 rule_name => 'processingreturn',
181                 rule_value => '',
182             } );
183         }, 'setting processingreturn with branch' );
184
185         lives_ok( sub {
186             Koha::CirculationRules->set_rule( {
187                 branchcode => $branchcode,
188                 categorycode => $categorycode,
189                 rule_name => 'patron_maxissueqty',
190                 rule_value => '',
191             } );
192         }, 'setting patron_maxissueqty with branch/category succeeds' );
193
194         lives_ok( sub {
195             Koha::CirculationRules->set_rule( {
196                 branchcode => $branchcode,
197                 itemtype => $itemtype,
198                 rule_name => 'holdallowed',
199                 rule_value => '',
200             } );
201         }, 'setting holdallowed with branch/itemtype succeeds' );
202
203         lives_ok( sub {
204             Koha::CirculationRules->set_rule( {
205                 branchcode => $branchcode,
206                 categorycode => $categorycode,
207                 itemtype => $itemtype,
208                 rule_name => 'article_requests',
209                 rule_value => '',
210             } );
211         }, 'setting fine with branch/category/itemtype succeeds' );
212     };
213
214     subtest 'Call with missing params' => sub {
215         plan tests => 5;
216
217         Koha::CirculationRules->delete;
218
219         throws_ok( sub {
220             Koha::CirculationRules->set_rule( {
221                 rule_name => 'lostreturn',
222                 rule_value => '',
223             } );
224         }, qr/branchcode/, 'setting lostreturn without branch fails' );
225
226         throws_ok( sub {
227             Koha::CirculationRules->set_rule( {
228                 rule_name => 'processingreturn',
229                 rule_value => '',
230             } );
231         }, qr/branchcode/, 'setting processingreturn without branch fails' );
232
233         throws_ok( sub {
234             Koha::CirculationRules->set_rule( {
235                 branchcode => $branchcode,
236                 rule_name => 'patron_maxissueqty',
237                 rule_value => '',
238             } );
239         }, qr/categorycode/, 'setting patron_maxissueqty without categorycode fails' );
240
241         throws_ok( sub {
242             Koha::CirculationRules->set_rule( {
243                 branchcode => $branchcode,
244                 rule_name => 'holdallowed',
245                 rule_value => '',
246             } );
247         }, qr/itemtype/, 'setting holdallowed without itemtype fails' );
248
249         throws_ok( sub {
250             Koha::CirculationRules->set_rule( {
251                 branchcode => $branchcode,
252                 categorycode => $categorycode,
253                 rule_name => 'fine',
254                 rule_value => '',
255             } );
256         }, qr/itemtype/, 'setting fine without itemtype fails' );
257     };
258
259     subtest 'Call with extra params' => sub {
260         plan tests => 4;
261
262         Koha::CirculationRules->delete;
263
264         throws_ok( sub {
265             Koha::CirculationRules->set_rule( {
266                 branchcode => $branchcode,
267                 categorycode => $categorycode,
268                 rule_name => 'lostreturn',
269                 rule_value => '',
270             } );
271         }, qr/categorycode/, 'setting lostreturn with categorycode fails' );
272
273         throws_ok( sub {
274             Koha::CirculationRules->set_rule( {
275                 branchcode => $branchcode,
276                 categorycode => $categorycode,
277                 rule_name => 'processingreturn',
278                 rule_value => '',
279             } );
280         }, qr/categorycode/, 'setting processingreturn with categorycode fails' );
281
282         throws_ok( sub {
283             Koha::CirculationRules->set_rule( {
284                 branchcode => $branchcode,
285                 categorycode => $categorycode,
286                 itemtype => $itemtype,
287                 rule_name => 'patron_maxissueqty',
288                 rule_value => '',
289             } );
290         }, qr/itemtype/, 'setting patron_maxissueqty with itemtype fails' );
291
292         throws_ok( sub {
293             Koha::CirculationRules->set_rule( {
294                 branchcode => $branchcode,
295                 rule_name => 'holdallowed',
296                 categorycode => $categorycode,
297                 itemtype => $itemtype,
298                 rule_value => '',
299             } );
300         }, qr/categorycode/, 'setting holdallowed with categorycode fails' );
301     };
302
303     subtest 'Call with badly formatted params' => sub {
304         plan tests => 4;
305
306         Koha::CirculationRules->delete;
307
308         foreach my $monetary_rule ( ( 'article_request_fee', 'fine', 'overduefinescap', 'recall_overdue_fine' ) ) {
309             throws_ok(
310                 sub {
311                     Koha::CirculationRules->set_rule(
312                         {
313                             categorycode => '*',
314                             branchcode   => '*',
315                             ( $monetary_rule ne 'article_request_fee' ? ( itemtype => '*' ) : () ),
316                             rule_name  => $monetary_rule,
317                             rule_value => '10,00',
318                         }
319                     );
320                 },
321                 qr/decimal/,
322                 "setting $monetary_rule fails when passed value is not decimal"
323             );
324         }
325     };
326
327     $schema->storage->txn_rollback;
328 };
329
330 subtest 'clone' => sub {
331     plan tests => 2;
332
333     $schema->storage->txn_begin;
334
335     my $branchcode   = $builder->build({ source => 'Branch' })->{'branchcode'};
336     my $categorycode = $builder->build({ source => 'Category' })->{'categorycode'};
337     my $itemtype     = $builder->build({ source => 'Itemtype' })->{'itemtype'};
338
339     subtest 'Clone multiple rules' => sub {
340         plan tests => 4;
341
342         Koha::CirculationRules->delete;
343
344         Koha::CirculationRule->new({
345             branchcode   => undef,
346             categorycode => $categorycode,
347             itemtype     => $itemtype,
348             rule_name    => 'fine',
349             rule_value   => 5,
350         })->store;
351
352         Koha::CirculationRule->new({
353             branchcode   => undef,
354             categorycode => $categorycode,
355             itemtype     => $itemtype,
356             rule_name    => 'lengthunit',
357             rule_value   => 'days',
358         })->store;
359
360         Koha::CirculationRules->search({ branchcode => undef })->clone($branchcode);
361
362         my $rule_fine = Koha::CirculationRules->get_effective_rule({
363             branchcode   => $branchcode,
364             categorycode => $categorycode,
365             itemtype     => $itemtype,
366             rule_name    => 'fine',
367         });
368         my $rule_lengthunit = Koha::CirculationRules->get_effective_rule({
369             branchcode   => $branchcode,
370             categorycode => $categorycode,
371             itemtype     => $itemtype,
372             rule_name    => 'lengthunit',
373         });
374
375         _is_row_match(
376             $rule_fine,
377             {
378                 branchcode   => $branchcode,
379                 categorycode => $categorycode,
380                 itemtype     => $itemtype,
381                 rule_name    => 'fine',
382                 rule_value   => 5,
383             },
384             'When I attempt to get cloned fine rule,'
385            .' then the above one is returned.'
386         );
387         _is_row_match(
388             $rule_lengthunit,
389             {
390                 branchcode   => $branchcode,
391                 categorycode => $categorycode,
392                 itemtype     => $itemtype,
393                 rule_name    => 'lengthunit',
394                 rule_value   => 'days',
395             },
396             'When I attempt to get cloned lengthunit rule,'
397            .' then the above one is returned.'
398         );
399
400     };
401
402     subtest 'Clone one rule' => sub {
403         plan tests => 2;
404
405         Koha::CirculationRules->delete;
406
407         Koha::CirculationRule->new({
408             branchcode   => undef,
409             categorycode => $categorycode,
410             itemtype     => $itemtype,
411             rule_name    => 'fine',
412             rule_value   => 5,
413         })->store;
414
415         my $rule = Koha::CirculationRules->search({ branchcode => undef })->next;
416         $rule->clone($branchcode);
417
418         my $cloned_rule = Koha::CirculationRules->get_effective_rule({
419             branchcode   => $branchcode,
420             categorycode => $categorycode,
421             itemtype     => $itemtype,
422             rule_name    => 'fine',
423         });
424
425         _is_row_match(
426             $cloned_rule,
427             {
428                 branchcode   => $branchcode,
429                 categorycode => $categorycode,
430                 itemtype     => $itemtype,
431                 rule_name    => 'fine',
432                 rule_value   => '5',
433             },
434             'When I attempt to get cloned fine rule,'
435            .' then the above one is returned.'
436         );
437
438     };
439
440     $schema->storage->txn_rollback;
441 };
442
443 subtest 'set_rule + get_effective_rule' => sub {
444     plan tests => 9;
445
446     $schema->storage->txn_begin;
447
448     my $categorycode = $builder->build_object( { class => 'Koha::Patron::Categories' } )->categorycode;
449     my $itemtype     = $builder->build_object( { class => 'Koha::ItemTypes' } )->itemtype;
450     my $branchcode   = $builder->build_object( { class => 'Koha::Libraries' } )->branchcode;
451     my $branchcode_2 = $builder->build_object( { class => 'Koha::Libraries' } )->branchcode;
452     my $rule_name    = 'maxissueqty';
453     my $default_rule_value = 1;
454
455     my $rule;
456     Koha::CirculationRules->delete;
457
458     throws_ok { Koha::CirculationRules->get_effective_rule }
459     'Koha::Exceptions::MissingParameter',
460     "Exception should be raised if get_effective_rule is called without rule_name parameter";
461
462     $rule = Koha::CirculationRules->get_effective_rule(
463         {
464             branchcode   => $branchcode,
465             categorycode => $categorycode,
466             itemtype     => $itemtype,
467             rule_name    => $rule_name,
468         }
469     );
470     is( $rule, undef, 'Undef should be returned if no rule exist' );
471
472     Koha::CirculationRules->set_rule(
473         {
474             branchcode   => '*',
475             categorycode => '*',
476             itemtype     => '*',
477             rule_name    => $rule_name,
478             rule_value   => $default_rule_value,
479         }
480     );
481
482     $rule = Koha::CirculationRules->get_effective_rule(
483         {
484             branchcode   => undef,
485             categorycode => undef,
486             itemtype     => undef,
487             rule_name    => $rule_name,
488         }
489     );
490     is( $rule->rule_value, $default_rule_value, 'undef means default' );
491     $rule = Koha::CirculationRules->get_effective_rule(
492         {
493             branchcode   => '*',
494             categorycode => '*',
495             itemtype     => '*',
496             rule_name    => $rule_name,
497         }
498     );
499
500     is( $rule->rule_value, $default_rule_value, '* means default' );
501
502     $rule = Koha::CirculationRules->get_effective_rule(
503         {
504             branchcode   => $branchcode_2,
505             categorycode => '*',
506             itemtype     => '*',
507             rule_name    => $rule_name,
508         }
509     );
510     is( $rule->rule_value, 1,
511         'Default rule is returned if there is no rule for this branchcode' );
512
513     subtest 'test rules that cannot be blank' => sub {
514         plan tests => 3;
515         foreach my $no_blank_rule ( ('holdallowed','hold_fulfillment_policy','returnbranch') ){
516             Koha::CirculationRules->set_rule(
517                 {
518                     branchcode   => $branchcode,
519                     itemtype     => '*',
520                     rule_name    => $no_blank_rule,
521                     rule_value   => '',
522                 }
523             );
524
525             $rule = Koha::CirculationRules->get_effective_rule(
526                 {
527                     branchcode   => $branchcode,
528                     categorycode => undef,
529                     itemtype     => undef,
530                     rule_name    => $no_blank_rule,
531                 }
532             );
533             is( $rule, undef, 'Rules that cannot be blank are not set when passed blank string' );
534         }
535     };
536
537
538     subtest 'test rule matching with different combinations of rule scopes' => sub {
539         my ( $tests, $order ) = _prepare_tests_for_rule_scope_combinations(
540             {
541                 branchcode   => $branchcode,
542                 categorycode => $categorycode,
543                 itemtype     => $itemtype,
544             },
545             'maxissueqty'
546         );
547
548         plan tests => 2**scalar @$order;
549
550         foreach my $test (@$tests) {
551             my $rule_params = {%$test};
552             $rule_params->{rule_name} = $rule_name;
553             my $rule_value = $rule_params->{rule_value} = int( rand(10) );
554
555             Koha::CirculationRules->set_rule($rule_params);
556
557             my $rule = Koha::CirculationRules->get_effective_rule(
558                 {
559                     branchcode   => $branchcode,
560                     categorycode => $categorycode,
561                     itemtype     => $itemtype,
562                     rule_name    => $rule_name,
563                 }
564             );
565
566             my $scope_output = '';
567             foreach my $key ( values @$order ) {
568                 $scope_output .= " $key" if $test->{$key} ne '*';
569             }
570
571             is( $rule->rule_value, $rule_value,
572                 'Explicitly scoped'
573                   . ( $scope_output ? $scope_output : ' nothing' ) );
574         }
575     };
576
577     my $our_branch_rules = Koha::CirculationRules->search({branchcode => $branchcode});
578     is( $our_branch_rules->count, 4, "We added 8 rules");
579     $our_branch_rules->delete;
580     is( $our_branch_rules->count, 0, "We deleted 8 rules");
581
582     $schema->storage->txn_rollback;
583 };
584
585 subtest 'get_onshelfholds_policy() tests' => sub {
586
587     plan tests => 2;
588
589     $schema->storage->txn_begin;
590
591     my $item = $builder->build_sample_item();
592
593     my $circ_rules = Koha::CirculationRules->new;
594     # Cleanup
595     $circ_rules->search({ rule_name => 'onshelfholds' })->delete;
596
597     $circ_rules->set_rule(
598         {
599             branchcode   => '*',
600             categorycode => '*',
601             itemtype     => '*',
602             rule_name    => 'onshelfholds',
603             rule_value   => 1,
604         }
605     );
606
607     is( $circ_rules->get_onshelfholds_policy({ item => $item }), 1, 'If rule_value is set on a matching rule, return it' );
608     # Delete the rule (i.e. get_effective_rule returns undef)
609     $circ_rules->delete;
610     is( $circ_rules->get_onshelfholds_policy({ item => $item }), 0, 'If no matching rule, fallback to 0' );
611
612     $schema->storage->txn_rollback;
613 };
614
615 subtest 'get_effective_daysmode' => sub {
616     plan tests => 4;
617
618     $schema->storage->txn_begin;
619
620     my $item_1 = $builder->build_sample_item();
621     my $item_2 = $builder->build_sample_item();
622
623     my $circ_rules =
624       Koha::CirculationRules->search( { rule_name => 'daysmode' } )->delete;
625
626     # Default value 'Datedue' at pref level
627     t::lib::Mocks::mock_preference( 'useDaysMode', 'Datedue' );
628
629     is(
630         Koha::CirculationRules->get_effective_daysmode(
631             {
632                 categorycode => undef,
633                 itemtype     => $item_1->effective_itemtype,
634                 branchcode   => undef
635             }
636         ),
637         'Datedue',
638         'daysmode default to pref value if the rule does not exist'
639     );
640
641     Koha::CirculationRules->set_rule(
642         {
643             branchcode   => '*',
644             categorycode => '*',
645             itemtype     => '*',
646             rule_name    => 'daysmode',
647             rule_value   => 'Calendar',
648         }
649     );
650     Koha::CirculationRules->set_rule(
651         {
652             branchcode   => '*',
653             categorycode => '*',
654             itemtype     => $item_1->effective_itemtype,
655             rule_name    => 'daysmode',
656             rule_value   => 'Days',
657         }
658     );
659
660     is(
661         Koha::CirculationRules->get_effective_daysmode(
662             {
663                 categorycode => undef,
664                 itemtype     => $item_1->effective_itemtype,
665                 branchcode   => undef
666             }
667         ),
668         'Days',
669         "daysmode for item_1 is the specific rule"
670     );
671     is(
672         Koha::CirculationRules->get_effective_daysmode(
673             {
674                 categorycode => undef,
675                 itemtype     => $item_2->effective_itemtype,
676                 branchcode   => undef
677             }
678         ),
679         'Calendar',
680         "daysmode for item_2 is the one defined for the default circ rule"
681     );
682
683     Koha::CirculationRules->set_rule(
684         {
685             branchcode   => '*',
686             categorycode => '*',
687             itemtype     => $item_2->effective_itemtype,
688             rule_name    => 'daysmode',
689             rule_value   => '',
690         }
691     );
692
693     is(
694         Koha::CirculationRules->get_effective_daysmode(
695             {
696                 categorycode => undef,
697                 itemtype     => $item_2->effective_itemtype,
698                 branchcode   => undef
699             }
700         ),
701         'Datedue',
702         'daysmode default to pref value if the rule exists but set to""'
703     );
704
705     $schema->storage->txn_rollback;
706 };
707
708 subtest 'get_lostreturn_policy() tests' => sub {
709     plan tests => 7;
710
711     $schema->storage->txn_begin;
712
713     $schema->resultset('CirculationRule')->search()->delete;
714
715     my $default_proc_rule_charge = $builder->build(
716         {
717             source => 'CirculationRule',
718             value  => {
719                 branchcode   => undef,
720                 categorycode => undef,
721                 itemtype     => undef,
722                 rule_name    => 'processingreturn',
723                 rule_value   => 'charge'
724             }
725         }
726     );
727     my $default_lost_rule_charge = $builder->build(
728         {
729             source => 'CirculationRule',
730             value  => {
731                 branchcode   => undef,
732                 categorycode => undef,
733                 itemtype     => undef,
734                 rule_name    => 'lostreturn',
735                 rule_value   => 'charge'
736             }
737         }
738     );
739     my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
740     my $specific_lost_rule_false = $builder->build(
741         {
742             source => 'CirculationRule',
743             value  => {
744                 branchcode   => $branchcode,
745                 categorycode => undef,
746                 itemtype     => undef,
747                 rule_name    => 'lostreturn',
748                 rule_value   => 0
749             }
750         }
751     );
752     my $specific_proc_rule_false = $builder->build(
753         {
754             source => 'CirculationRule',
755             value  => {
756                 branchcode   => $branchcode,
757                 categorycode => undef,
758                 itemtype     => undef,
759                 rule_name    => 'processingreturn',
760                 rule_value   => 0
761             }
762         }
763     );
764     my $branchcode2 = $builder->build( { source => 'Branch' } )->{branchcode};
765     my $specific_lost_rule_refund = $builder->build(
766         {
767             source => 'CirculationRule',
768             value  => {
769                 branchcode   => $branchcode2,
770                 categorycode => undef,
771                 itemtype     => undef,
772                 rule_name    => 'lostreturn',
773                 rule_value   => 'refund'
774             }
775         }
776     );
777     my $specific_proc_rule_refund = $builder->build(
778         {
779             source => 'CirculationRule',
780             value  => {
781                 branchcode   => $branchcode2,
782                 categorycode => undef,
783                 itemtype     => undef,
784                 rule_name    => 'processingreturn',
785                 rule_value   => 'refund'
786             }
787         }
788     );
789     my $branchcode3 = $builder->build( { source => 'Branch' } )->{branchcode};
790     my $specific_lost_rule_restore = $builder->build(
791         {
792             source => 'CirculationRule',
793             value  => {
794                 branchcode   => $branchcode3,
795                 categorycode => undef,
796                 itemtype     => undef,
797                 rule_name    => 'lostreturn',
798                 rule_value   => 'restore'
799             }
800         }
801     );
802     my $specific_proc_rule_restore = $builder->build(
803         {
804             source => 'CirculationRule',
805             value  => {
806                 branchcode   => $branchcode3,
807                 categorycode => undef,
808                 itemtype     => undef,
809                 rule_name    => 'processingreturn',
810                 rule_value   => 'restore'
811             }
812         }
813     );
814
815     # Make sure we have an unused branchcode
816     my $branch_without_rule = $builder->build( { source => 'Branch' } )->{branchcode};
817
818     my $item = $builder->build_sample_item(
819         {
820             homebranch    => $specific_lost_rule_restore->{branchcode},
821             holdingbranch => $specific_lost_rule_false->{branchcode}
822         }
823     );
824     my $params = {
825         return_branch => $specific_lost_rule_refund->{ branchcode },
826         item          => $item
827     };
828
829     # Specific rules
830     t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'CheckinLibrary' );
831     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
832         { lostreturn => 'refund', processingreturn => 'refund' },'Specific rule for checkin branch is applied (refund)');
833
834     t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'ItemHomeBranch' );
835     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
836          { lostreturn => 'restore', processingreturn => 'restore' },'Specific rule for home branch is applied (restore)');
837
838     t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'ItemHoldingBranch' );
839     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
840          { lostreturn => 0, processingreturn => 0 },'Specific rule for holding branch is applied (false)');
841
842     # Default rule check
843     t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'CheckinLibrary' );
844     $params->{return_branch} = $branch_without_rule;
845     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
846          { lostreturn => 'charge', processingreturn => 'charge' },'No rule for branch, global rule applied (charge)');
847
848     # Change the default value just to try
849     Koha::CirculationRules->search({ branchcode => undef, rule_name => 'lostreturn' })->next->rule_value(0)->store;
850     Koha::CirculationRules->search({ branchcode => undef, rule_name => 'processingreturn' })->next->rule_value(0)->store;
851     my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
852     $memory_cache->flush();
853     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
854          { lostreturn => 0, processingreturn => 0 },'No rule for branch, global rule applied (false)');
855
856     # No default rule defined check
857     Koha::CirculationRules
858         ->search(
859             {
860                 branchcode   => undef,
861                 categorycode => undef,
862                 itemtype     => undef,
863                 rule_name    => 'lostreturn'
864             }
865           )
866         ->next
867         ->delete;
868     # No default rule defined check
869     Koha::CirculationRules
870         ->search(
871             {
872                 branchcode   => undef,
873                 categorycode => undef,
874                 itemtype     => undef,
875                 rule_name    => 'processingreturn'
876             }
877           )
878         ->next
879         ->delete;
880     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
881          { lostreturn => 'refund', processingreturn => 'refund' },'No rule for branch, no default rule, fallback default (refund)');
882
883     # Fallback to ItemHoldBranch if CheckinLibrary is undefined
884     $params->{return_branch} = undef;
885     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
886          { lostreturn => 'restore', processingreturn => 'restore' },'return_branch undefined, fallback to ItemHomeBranch rule (restore)');
887
888     $schema->storage->txn_rollback;
889 };
890
891 sub _is_row_match {
892     my ( $rule, $expected, $message ) = @_;
893
894     ok( $rule, $message ) ?
895         cmp_methods( $rule, [ %$expected ], $message ) :
896         fail( $message );
897 }
898
899 sub _prepare_tests_for_rule_scope_combinations {
900     my ( $scope, $rule_name ) = @_;
901
902     # Here we create a combinations of 1s and 0s the following way
903     #
904     # 000...
905     # 001...
906     # 010...
907     # 011...
908     # 100...
909     # 101...
910     # 110...
911     # 111...
912     #
913     # (the number of columns equals to the amount of rule scopes)
914     # The ... symbolizes possible future scopes.
915     #
916     # - 0 equals to circulation rule scope with any value (aka. *)
917     # - 1 equals to circulation rule scope exact value, e.g.
918     #     "CPL" (for branchcode).
919     #
920     # The order is the same as the weight of scopes when sorting circulation
921     # rules. So the first column of numbers is the scope with most weight.
922     # This is defined by C<$order> which will be assigned next.
923     #
924     # We must maintain the order in order to keep the test valid. This should be
925     # equal to Koha/CirculationRules.pm "order_by" of C<get_effective_rule> sub.
926     # Let's explicitly define the order and fail test if we are missing a scope:
927     my $order = [ 'branchcode', 'categorycode', 'itemtype' ];
928     is( join(", ", sort keys %$scope),
929        join(", ", sort @$order), 'Missing a scope!' ) if keys %$scope ne scalar @$order;
930
931     my @tests = ();
932     foreach my $value ( glob( "{0,1}" x keys %$scope || 1 ) ) {
933         my $test = { %$scope };
934         for ( my $i=0; $i < keys %$scope; $i++ ) {
935             $test->{$order->[$i]} = '*' unless substr( $value, $i, 1 );
936         }
937         push @tests, $test;
938     }
939
940     return \@tests, $order;
941 }