Bug 35086: Add chunk_size option to elasticsearch configuration
[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 => 10;
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     subtest 'test rules that can be blank' => sub {
538         plan tests => 1;
539         foreach my $blank_rule ( ('overduefinescap') ) {
540             Koha::CirculationRules->set_rule(
541                 {
542                     branchcode   => $branchcode,
543                     categorycode => '*',
544                     itemtype     => '*',
545                     rule_name    => $blank_rule,
546                     rule_value   => '',
547                 }
548             );
549
550             $rule = Koha::CirculationRules->get_effective_rule(
551                 {
552                     branchcode   => $branchcode,
553                     categorycode => undef,
554                     itemtype     => undef,
555                     rule_name    => $blank_rule,
556                 }
557             );
558             is( $rule->rule_value, '', "$blank_rule allowed to be set to blank" );
559         }
560     };
561
562     subtest 'test rule matching with different combinations of rule scopes' => sub {
563         my ( $tests, $order ) = _prepare_tests_for_rule_scope_combinations(
564             {
565                 branchcode   => $branchcode,
566                 categorycode => $categorycode,
567                 itemtype     => $itemtype,
568             },
569             'maxissueqty'
570         );
571
572         plan tests => 2**scalar @$order;
573
574         foreach my $test (@$tests) {
575             my $rule_params = {%$test};
576             $rule_params->{rule_name} = $rule_name;
577             my $rule_value = $rule_params->{rule_value} = int( rand(10) );
578
579             Koha::CirculationRules->set_rule($rule_params);
580
581             my $rule = Koha::CirculationRules->get_effective_rule(
582                 {
583                     branchcode   => $branchcode,
584                     categorycode => $categorycode,
585                     itemtype     => $itemtype,
586                     rule_name    => $rule_name,
587                 }
588             );
589
590             my $scope_output = '';
591             foreach my $key ( values @$order ) {
592                 $scope_output .= " $key" if $test->{$key} ne '*';
593             }
594
595             is( $rule->rule_value, $rule_value,
596                 'Explicitly scoped'
597                   . ( $scope_output ? $scope_output : ' nothing' ) );
598         }
599     };
600
601     my $our_branch_rules = Koha::CirculationRules->search({branchcode => $branchcode});
602     is( $our_branch_rules->count, 5, "We added 9 rules");
603     $our_branch_rules->delete;
604     is( $our_branch_rules->count, 0, "We deleted 9 rules");
605
606     $schema->storage->txn_rollback;
607 };
608
609 subtest 'get_onshelfholds_policy() tests' => sub {
610
611     plan tests => 2;
612
613     $schema->storage->txn_begin;
614
615     my $item = $builder->build_sample_item();
616
617     my $circ_rules = Koha::CirculationRules->new;
618     # Cleanup
619     $circ_rules->search({ rule_name => 'onshelfholds' })->delete;
620
621     $circ_rules->set_rule(
622         {
623             branchcode   => '*',
624             categorycode => '*',
625             itemtype     => '*',
626             rule_name    => 'onshelfholds',
627             rule_value   => 1,
628         }
629     );
630
631     is( $circ_rules->get_onshelfholds_policy({ item => $item }), 1, 'If rule_value is set on a matching rule, return it' );
632     # Delete the rule (i.e. get_effective_rule returns undef)
633     $circ_rules->delete;
634     is( $circ_rules->get_onshelfholds_policy({ item => $item }), 0, 'If no matching rule, fallback to 0' );
635
636     $schema->storage->txn_rollback;
637 };
638
639 subtest 'get_effective_daysmode' => sub {
640     plan tests => 4;
641
642     $schema->storage->txn_begin;
643
644     my $item_1 = $builder->build_sample_item();
645     my $item_2 = $builder->build_sample_item();
646
647     my $circ_rules =
648       Koha::CirculationRules->search( { rule_name => 'daysmode' } )->delete;
649
650     # Default value 'Datedue' at pref level
651     t::lib::Mocks::mock_preference( 'useDaysMode', 'Datedue' );
652
653     is(
654         Koha::CirculationRules->get_effective_daysmode(
655             {
656                 categorycode => undef,
657                 itemtype     => $item_1->effective_itemtype,
658                 branchcode   => undef
659             }
660         ),
661         'Datedue',
662         'daysmode default to pref value if the rule does not exist'
663     );
664
665     Koha::CirculationRules->set_rule(
666         {
667             branchcode   => '*',
668             categorycode => '*',
669             itemtype     => '*',
670             rule_name    => 'daysmode',
671             rule_value   => 'Calendar',
672         }
673     );
674     Koha::CirculationRules->set_rule(
675         {
676             branchcode   => '*',
677             categorycode => '*',
678             itemtype     => $item_1->effective_itemtype,
679             rule_name    => 'daysmode',
680             rule_value   => 'Days',
681         }
682     );
683
684     is(
685         Koha::CirculationRules->get_effective_daysmode(
686             {
687                 categorycode => undef,
688                 itemtype     => $item_1->effective_itemtype,
689                 branchcode   => undef
690             }
691         ),
692         'Days',
693         "daysmode for item_1 is the specific rule"
694     );
695     is(
696         Koha::CirculationRules->get_effective_daysmode(
697             {
698                 categorycode => undef,
699                 itemtype     => $item_2->effective_itemtype,
700                 branchcode   => undef
701             }
702         ),
703         'Calendar',
704         "daysmode for item_2 is the one defined for the default circ rule"
705     );
706
707     Koha::CirculationRules->set_rule(
708         {
709             branchcode   => '*',
710             categorycode => '*',
711             itemtype     => $item_2->effective_itemtype,
712             rule_name    => 'daysmode',
713             rule_value   => '',
714         }
715     );
716
717     is(
718         Koha::CirculationRules->get_effective_daysmode(
719             {
720                 categorycode => undef,
721                 itemtype     => $item_2->effective_itemtype,
722                 branchcode   => undef
723             }
724         ),
725         'Datedue',
726         'daysmode default to pref value if the rule exists but set to""'
727     );
728
729     $schema->storage->txn_rollback;
730 };
731
732 subtest 'get_lostreturn_policy() tests' => sub {
733     plan tests => 7;
734
735     $schema->storage->txn_begin;
736
737     $schema->resultset('CirculationRule')->search()->delete;
738
739     my $default_proc_rule_charge = $builder->build(
740         {
741             source => 'CirculationRule',
742             value  => {
743                 branchcode   => undef,
744                 categorycode => undef,
745                 itemtype     => undef,
746                 rule_name    => 'processingreturn',
747                 rule_value   => 'charge'
748             }
749         }
750     );
751     my $default_lost_rule_charge = $builder->build(
752         {
753             source => 'CirculationRule',
754             value  => {
755                 branchcode   => undef,
756                 categorycode => undef,
757                 itemtype     => undef,
758                 rule_name    => 'lostreturn',
759                 rule_value   => 'charge'
760             }
761         }
762     );
763     my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
764     my $specific_lost_rule_false = $builder->build(
765         {
766             source => 'CirculationRule',
767             value  => {
768                 branchcode   => $branchcode,
769                 categorycode => undef,
770                 itemtype     => undef,
771                 rule_name    => 'lostreturn',
772                 rule_value   => 0
773             }
774         }
775     );
776     my $specific_proc_rule_false = $builder->build(
777         {
778             source => 'CirculationRule',
779             value  => {
780                 branchcode   => $branchcode,
781                 categorycode => undef,
782                 itemtype     => undef,
783                 rule_name    => 'processingreturn',
784                 rule_value   => 0
785             }
786         }
787     );
788     my $branchcode2 = $builder->build( { source => 'Branch' } )->{branchcode};
789     my $specific_lost_rule_refund = $builder->build(
790         {
791             source => 'CirculationRule',
792             value  => {
793                 branchcode   => $branchcode2,
794                 categorycode => undef,
795                 itemtype     => undef,
796                 rule_name    => 'lostreturn',
797                 rule_value   => 'refund'
798             }
799         }
800     );
801     my $specific_proc_rule_refund = $builder->build(
802         {
803             source => 'CirculationRule',
804             value  => {
805                 branchcode   => $branchcode2,
806                 categorycode => undef,
807                 itemtype     => undef,
808                 rule_name    => 'processingreturn',
809                 rule_value   => 'refund'
810             }
811         }
812     );
813     my $branchcode3 = $builder->build( { source => 'Branch' } )->{branchcode};
814     my $specific_lost_rule_restore = $builder->build(
815         {
816             source => 'CirculationRule',
817             value  => {
818                 branchcode   => $branchcode3,
819                 categorycode => undef,
820                 itemtype     => undef,
821                 rule_name    => 'lostreturn',
822                 rule_value   => 'restore'
823             }
824         }
825     );
826     my $specific_proc_rule_restore = $builder->build(
827         {
828             source => 'CirculationRule',
829             value  => {
830                 branchcode   => $branchcode3,
831                 categorycode => undef,
832                 itemtype     => undef,
833                 rule_name    => 'processingreturn',
834                 rule_value   => 'restore'
835             }
836         }
837     );
838
839     # Make sure we have an unused branchcode
840     my $branch_without_rule = $builder->build( { source => 'Branch' } )->{branchcode};
841
842     my $item = $builder->build_sample_item(
843         {
844             homebranch    => $specific_lost_rule_restore->{branchcode},
845             holdingbranch => $specific_lost_rule_false->{branchcode}
846         }
847     );
848     my $params = {
849         return_branch => $specific_lost_rule_refund->{ branchcode },
850         item          => $item
851     };
852
853     # Specific rules
854     t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'CheckinLibrary' );
855     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
856         { lostreturn => 'refund', processingreturn => 'refund' },'Specific rule for checkin branch is applied (refund)');
857
858     t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'ItemHomeBranch' );
859     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
860          { lostreturn => 'restore', processingreturn => 'restore' },'Specific rule for home branch is applied (restore)');
861
862     t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'ItemHoldingBranch' );
863     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
864          { lostreturn => 0, processingreturn => 0 },'Specific rule for holding branch is applied (false)');
865
866     # Default rule check
867     t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'CheckinLibrary' );
868     $params->{return_branch} = $branch_without_rule;
869     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
870          { lostreturn => 'charge', processingreturn => 'charge' },'No rule for branch, global rule applied (charge)');
871
872     # Change the default value just to try
873     Koha::CirculationRules->search({ branchcode => undef, rule_name => 'lostreturn' })->next->rule_value(0)->store;
874     Koha::CirculationRules->search({ branchcode => undef, rule_name => 'processingreturn' })->next->rule_value(0)->store;
875     my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
876     $memory_cache->flush();
877     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
878          { lostreturn => 0, processingreturn => 0 },'No rule for branch, global rule applied (false)');
879
880     # No default rule defined check
881     Koha::CirculationRules
882         ->search(
883             {
884                 branchcode   => undef,
885                 categorycode => undef,
886                 itemtype     => undef,
887                 rule_name    => 'lostreturn'
888             }
889           )
890         ->next
891         ->delete;
892     # No default rule defined check
893     Koha::CirculationRules
894         ->search(
895             {
896                 branchcode   => undef,
897                 categorycode => undef,
898                 itemtype     => undef,
899                 rule_name    => 'processingreturn'
900             }
901           )
902         ->next
903         ->delete;
904     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
905          { lostreturn => 'refund', processingreturn => 'refund' },'No rule for branch, no default rule, fallback default (refund)');
906
907     # Fallback to ItemHoldBranch if CheckinLibrary is undefined
908     $params->{return_branch} = undef;
909     is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
910          { lostreturn => 'restore', processingreturn => 'restore' },'return_branch undefined, fallback to ItemHomeBranch rule (restore)');
911
912     $schema->storage->txn_rollback;
913 };
914
915 sub _is_row_match {
916     my ( $rule, $expected, $message ) = @_;
917
918     ok( $rule, $message ) ?
919         cmp_methods( $rule, [ %$expected ], $message ) :
920         fail( $message );
921 }
922
923 sub _prepare_tests_for_rule_scope_combinations {
924     my ( $scope, $rule_name ) = @_;
925
926     # Here we create a combinations of 1s and 0s the following way
927     #
928     # 000...
929     # 001...
930     # 010...
931     # 011...
932     # 100...
933     # 101...
934     # 110...
935     # 111...
936     #
937     # (the number of columns equals to the amount of rule scopes)
938     # The ... symbolizes possible future scopes.
939     #
940     # - 0 equals to circulation rule scope with any value (aka. *)
941     # - 1 equals to circulation rule scope exact value, e.g.
942     #     "CPL" (for branchcode).
943     #
944     # The order is the same as the weight of scopes when sorting circulation
945     # rules. So the first column of numbers is the scope with most weight.
946     # This is defined by C<$order> which will be assigned next.
947     #
948     # We must maintain the order in order to keep the test valid. This should be
949     # equal to Koha/CirculationRules.pm "order_by" of C<get_effective_rule> sub.
950     # Let's explicitly define the order and fail test if we are missing a scope:
951     my $order = [ 'branchcode', 'categorycode', 'itemtype' ];
952     is( join(", ", sort keys %$scope),
953        join(", ", sort @$order), 'Missing a scope!' ) if keys %$scope ne scalar @$order;
954
955     my @tests = ();
956     foreach my $value ( glob( "{0,1}" x keys %$scope || 1 ) ) {
957         my $test = { %$scope };
958         for ( my $i=0; $i < keys %$scope; $i++ ) {
959             $test->{$order->[$i]} = '*' unless substr( $value, $i, 1 );
960         }
961         push @tests, $test;
962     }
963
964     return \@tests, $order;
965 }