3 # Copyright 2020 Koha Development team
5 # This file is part of Koha
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.
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.
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>.
23 use Test::More tests => 7;
24 use Test::Deep qw( cmp_methods );
27 use Koha::CirculationRules;
31 use t::lib::TestBuilder;
32 use Koha::Cache::Memory::Lite;
34 my $schema = Koha::Database->new->schema;
35 my $builder = t::lib::TestBuilder->new;
37 subtest 'get_effective_issuing_rule' => sub {
40 $schema->storage->txn_begin;
42 my $categorycode = $builder->build({ source => 'Category' })->{'categorycode'};
43 my $itemtype = $builder->build({ source => 'Itemtype' })->{'itemtype'};
44 my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'};
46 subtest 'Call with undefined values' => sub {
50 Koha::CirculationRules->delete;
52 is(Koha::CirculationRules->search->count, 0, 'There are no issuing rules.');
53 # undef, undef, undef => 1
54 $rule = Koha::CirculationRules->get_effective_rule({
56 categorycode => undef,
61 is($rule, undef, 'When I attempt to get effective issuing rule by'
62 .' providing undefined values, then undef is returned.');
64 # undef, undef, undef => 2
66 Koha::CirculationRule->new(
69 categorycode => undef,
75 'Given I added an issuing rule branchcode => undef,'
76 .' categorycode => undef, itemtype => undef,');
77 $rule = Koha::CirculationRules->get_effective_rule({
79 categorycode => undef,
87 categorycode => undef,
92 'When I attempt to get effective'
93 .' issuing rule by providing undefined values, then the above one is'
98 subtest 'Performance' => sub {
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',
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',
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',
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',
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.');
151 $schema->storage->txn_rollback;
155 subtest 'set_rule' => sub {
158 $schema->storage->txn_begin;
160 my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'};
161 my $categorycode = $builder->build({ source => 'Category' })->{'categorycode'};
162 my $itemtype = $builder->build({ source => 'Itemtype' })->{'itemtype'};
164 subtest 'Correct call' => sub {
167 Koha::CirculationRules->delete;
170 Koha::CirculationRules->set_rule( {
171 branchcode => $branchcode,
172 rule_name => 'lostreturn',
175 }, 'setting lostreturn with branch' );
178 Koha::CirculationRules->set_rule( {
179 branchcode => $branchcode,
180 rule_name => 'processingreturn',
183 }, 'setting processingreturn with branch' );
186 Koha::CirculationRules->set_rule( {
187 branchcode => $branchcode,
188 categorycode => $categorycode,
189 rule_name => 'patron_maxissueqty',
192 }, 'setting patron_maxissueqty with branch/category succeeds' );
195 Koha::CirculationRules->set_rule( {
196 branchcode => $branchcode,
197 itemtype => $itemtype,
198 rule_name => 'holdallowed',
201 }, 'setting holdallowed with branch/itemtype succeeds' );
204 Koha::CirculationRules->set_rule( {
205 branchcode => $branchcode,
206 categorycode => $categorycode,
207 itemtype => $itemtype,
211 }, 'setting fine with branch/category/itemtype succeeds' );
214 subtest 'Call with missing params' => sub {
217 Koha::CirculationRules->delete;
220 Koha::CirculationRules->set_rule( {
221 rule_name => 'lostreturn',
224 }, qr/branchcode/, 'setting lostreturn without branch fails' );
227 Koha::CirculationRules->set_rule( {
228 rule_name => 'processingreturn',
231 }, qr/branchcode/, 'setting processingreturn without branch fails' );
234 Koha::CirculationRules->set_rule( {
235 branchcode => $branchcode,
236 rule_name => 'patron_maxissueqty',
239 }, qr/categorycode/, 'setting patron_maxissueqty without categorycode fails' );
242 Koha::CirculationRules->set_rule( {
243 branchcode => $branchcode,
244 rule_name => 'holdallowed',
247 }, qr/itemtype/, 'setting holdallowed without itemtype fails' );
250 Koha::CirculationRules->set_rule( {
251 branchcode => $branchcode,
252 categorycode => $categorycode,
256 }, qr/itemtype/, 'setting fine without itemtype fails' );
259 subtest 'Call with extra params' => sub {
262 Koha::CirculationRules->delete;
265 Koha::CirculationRules->set_rule( {
266 branchcode => $branchcode,
267 categorycode => $categorycode,
268 rule_name => 'lostreturn',
271 }, qr/categorycode/, 'setting lostreturn with categorycode fails' );
274 Koha::CirculationRules->set_rule( {
275 branchcode => $branchcode,
276 categorycode => $categorycode,
277 rule_name => 'processingreturn',
280 }, qr/categorycode/, 'setting processingreturn with categorycode fails' );
283 Koha::CirculationRules->set_rule( {
284 branchcode => $branchcode,
285 categorycode => $categorycode,
286 itemtype => $itemtype,
287 rule_name => 'patron_maxissueqty',
290 }, qr/itemtype/, 'setting patron_maxissueqty with itemtype fails' );
293 Koha::CirculationRules->set_rule( {
294 branchcode => $branchcode,
295 rule_name => 'holdallowed',
296 categorycode => $categorycode,
297 itemtype => $itemtype,
300 }, qr/categorycode/, 'setting holdallowed with categorycode fails' );
303 $schema->storage->txn_rollback;
306 subtest 'clone' => sub {
309 $schema->storage->txn_begin;
311 my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'};
312 my $categorycode = $builder->build({ source => 'Category' })->{'categorycode'};
313 my $itemtype = $builder->build({ source => 'Itemtype' })->{'itemtype'};
315 subtest 'Clone multiple rules' => sub {
318 Koha::CirculationRules->delete;
320 Koha::CirculationRule->new({
322 categorycode => $categorycode,
323 itemtype => $itemtype,
328 Koha::CirculationRule->new({
330 categorycode => $categorycode,
331 itemtype => $itemtype,
332 rule_name => 'lengthunit',
333 rule_value => 'days',
336 Koha::CirculationRules->search({ branchcode => undef })->clone($branchcode);
338 my $rule_fine = Koha::CirculationRules->get_effective_rule({
339 branchcode => $branchcode,
340 categorycode => $categorycode,
341 itemtype => $itemtype,
344 my $rule_lengthunit = Koha::CirculationRules->get_effective_rule({
345 branchcode => $branchcode,
346 categorycode => $categorycode,
347 itemtype => $itemtype,
348 rule_name => 'lengthunit',
354 branchcode => $branchcode,
355 categorycode => $categorycode,
356 itemtype => $itemtype,
360 'When I attempt to get cloned fine rule,'
361 .' then the above one is returned.'
366 branchcode => $branchcode,
367 categorycode => $categorycode,
368 itemtype => $itemtype,
369 rule_name => 'lengthunit',
370 rule_value => 'days',
372 'When I attempt to get cloned lengthunit rule,'
373 .' then the above one is returned.'
378 subtest 'Clone one rule' => sub {
381 Koha::CirculationRules->delete;
383 Koha::CirculationRule->new({
385 categorycode => $categorycode,
386 itemtype => $itemtype,
391 my $rule = Koha::CirculationRules->search({ branchcode => undef })->next;
392 $rule->clone($branchcode);
394 my $cloned_rule = Koha::CirculationRules->get_effective_rule({
395 branchcode => $branchcode,
396 categorycode => $categorycode,
397 itemtype => $itemtype,
404 branchcode => $branchcode,
405 categorycode => $categorycode,
406 itemtype => $itemtype,
410 'When I attempt to get cloned fine rule,'
411 .' then the above one is returned.'
416 $schema->storage->txn_rollback;
419 subtest 'set_rule + get_effective_rule' => sub {
422 $schema->storage->txn_begin;
424 my $categorycode = $builder->build_object( { class => 'Koha::Patron::Categories' } )->categorycode;
425 my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } )->itemtype;
426 my $branchcode = $builder->build_object( { class => 'Koha::Libraries' } )->branchcode;
427 my $branchcode_2 = $builder->build_object( { class => 'Koha::Libraries' } )->branchcode;
428 my $rule_name = 'maxissueqty';
429 my $default_rule_value = 1;
432 Koha::CirculationRules->delete;
434 throws_ok { Koha::CirculationRules->get_effective_rule }
435 'Koha::Exceptions::MissingParameter',
436 "Exception should be raised if get_effective_rule is called without rule_name parameter";
438 $rule = Koha::CirculationRules->get_effective_rule(
440 branchcode => $branchcode,
441 categorycode => $categorycode,
442 itemtype => $itemtype,
443 rule_name => $rule_name,
446 is( $rule, undef, 'Undef should be returned if no rule exist' );
448 Koha::CirculationRules->set_rule(
453 rule_name => $rule_name,
454 rule_value => $default_rule_value,
458 $rule = Koha::CirculationRules->get_effective_rule(
461 categorycode => undef,
463 rule_name => $rule_name,
466 is( $rule->rule_value, $default_rule_value, 'undef means default' );
467 $rule = Koha::CirculationRules->get_effective_rule(
472 rule_name => $rule_name,
476 is( $rule->rule_value, $default_rule_value, '* means default' );
478 $rule = Koha::CirculationRules->get_effective_rule(
480 branchcode => $branchcode_2,
483 rule_name => $rule_name,
486 is( $rule->rule_value, 1,
487 'Default rule is returned if there is no rule for this branchcode' );
489 subtest 'test rules that cannot be blank' => sub {
491 foreach my $no_blank_rule ( ('holdallowed','hold_fulfillment_policy','returnbranch') ){
492 Koha::CirculationRules->set_rule(
494 branchcode => $branchcode,
496 rule_name => $no_blank_rule,
501 $rule = Koha::CirculationRules->get_effective_rule(
503 branchcode => $branchcode,
504 categorycode => undef,
506 rule_name => $no_blank_rule,
509 is( $rule, undef, 'Rules that cannot be blank are not set when passed blank string' );
514 subtest 'test rule matching with different combinations of rule scopes' => sub {
515 my ( $tests, $order ) = _prepare_tests_for_rule_scope_combinations(
517 branchcode => $branchcode,
518 categorycode => $categorycode,
519 itemtype => $itemtype,
524 plan tests => 2**scalar @$order;
526 foreach my $test (@$tests) {
527 my $rule_params = {%$test};
528 $rule_params->{rule_name} = $rule_name;
529 my $rule_value = $rule_params->{rule_value} = int( rand(10) );
531 Koha::CirculationRules->set_rule($rule_params);
533 my $rule = Koha::CirculationRules->get_effective_rule(
535 branchcode => $branchcode,
536 categorycode => $categorycode,
537 itemtype => $itemtype,
538 rule_name => $rule_name,
542 my $scope_output = '';
543 foreach my $key ( values @$order ) {
544 $scope_output .= " $key" if $test->{$key} ne '*';
547 is( $rule->rule_value, $rule_value,
549 . ( $scope_output ? $scope_output : ' nothing' ) );
553 my $our_branch_rules = Koha::CirculationRules->search({branchcode => $branchcode});
554 is( $our_branch_rules->count, 4, "We added 8 rules");
555 $our_branch_rules->delete;
556 is( $our_branch_rules->count, 0, "We deleted 8 rules");
558 $schema->storage->txn_rollback;
561 subtest 'get_onshelfholds_policy() tests' => sub {
565 $schema->storage->txn_begin;
567 my $item = $builder->build_sample_item();
569 my $circ_rules = Koha::CirculationRules->new;
571 $circ_rules->search({ rule_name => 'onshelfholds' })->delete;
573 $circ_rules->set_rule(
578 rule_name => 'onshelfholds',
583 is( $circ_rules->get_onshelfholds_policy({ item => $item }), 1, 'If rule_value is set on a matching rule, return it' );
584 # Delete the rule (i.e. get_effective_rule returns undef)
586 is( $circ_rules->get_onshelfholds_policy({ item => $item }), 0, 'If no matching rule, fallback to 0' );
588 $schema->storage->txn_rollback;
591 subtest 'get_effective_daysmode' => sub {
594 $schema->storage->txn_begin;
596 my $item_1 = $builder->build_sample_item();
597 my $item_2 = $builder->build_sample_item();
600 Koha::CirculationRules->search( { rule_name => 'daysmode' } )->delete;
602 # Default value 'Datedue' at pref level
603 t::lib::Mocks::mock_preference( 'useDaysMode', 'Datedue' );
606 Koha::CirculationRules->get_effective_daysmode(
608 categorycode => undef,
609 itemtype => $item_1->effective_itemtype,
614 'daysmode default to pref value if the rule does not exist'
617 Koha::CirculationRules->set_rule(
622 rule_name => 'daysmode',
623 rule_value => 'Calendar',
626 Koha::CirculationRules->set_rule(
630 itemtype => $item_1->effective_itemtype,
631 rule_name => 'daysmode',
632 rule_value => 'Days',
637 Koha::CirculationRules->get_effective_daysmode(
639 categorycode => undef,
640 itemtype => $item_1->effective_itemtype,
645 "daysmode for item_1 is the specific rule"
648 Koha::CirculationRules->get_effective_daysmode(
650 categorycode => undef,
651 itemtype => $item_2->effective_itemtype,
656 "daysmode for item_2 is the one defined for the default circ rule"
659 Koha::CirculationRules->set_rule(
663 itemtype => $item_2->effective_itemtype,
664 rule_name => 'daysmode',
670 Koha::CirculationRules->get_effective_daysmode(
672 categorycode => undef,
673 itemtype => $item_2->effective_itemtype,
678 'daysmode default to pref value if the rule exists but set to""'
681 $schema->storage->txn_rollback;
684 subtest 'get_lostreturn_policy() tests' => sub {
687 $schema->storage->txn_begin;
689 $schema->resultset('CirculationRule')->search()->delete;
691 my $default_proc_rule_charge = $builder->build(
693 source => 'CirculationRule',
696 categorycode => undef,
698 rule_name => 'processingreturn',
699 rule_value => 'charge'
703 my $default_lost_rule_charge = $builder->build(
705 source => 'CirculationRule',
708 categorycode => undef,
710 rule_name => 'lostreturn',
711 rule_value => 'charge'
715 my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
716 my $specific_lost_rule_false = $builder->build(
718 source => 'CirculationRule',
720 branchcode => $branchcode,
721 categorycode => undef,
723 rule_name => 'lostreturn',
728 my $specific_proc_rule_false = $builder->build(
730 source => 'CirculationRule',
732 branchcode => $branchcode,
733 categorycode => undef,
735 rule_name => 'processingreturn',
740 my $branchcode2 = $builder->build( { source => 'Branch' } )->{branchcode};
741 my $specific_lost_rule_refund = $builder->build(
743 source => 'CirculationRule',
745 branchcode => $branchcode2,
746 categorycode => undef,
748 rule_name => 'lostreturn',
749 rule_value => 'refund'
753 my $specific_proc_rule_refund = $builder->build(
755 source => 'CirculationRule',
757 branchcode => $branchcode2,
758 categorycode => undef,
760 rule_name => 'processingreturn',
761 rule_value => 'refund'
765 my $branchcode3 = $builder->build( { source => 'Branch' } )->{branchcode};
766 my $specific_lost_rule_restore = $builder->build(
768 source => 'CirculationRule',
770 branchcode => $branchcode3,
771 categorycode => undef,
773 rule_name => 'lostreturn',
774 rule_value => 'restore'
778 my $specific_proc_rule_restore = $builder->build(
780 source => 'CirculationRule',
782 branchcode => $branchcode3,
783 categorycode => undef,
785 rule_name => 'processingreturn',
786 rule_value => 'restore'
791 # Make sure we have an unused branchcode
792 my $branch_without_rule = $builder->build( { source => 'Branch' } )->{branchcode};
794 my $item = $builder->build_sample_item(
796 homebranch => $specific_lost_rule_restore->{branchcode},
797 holdingbranch => $specific_lost_rule_false->{branchcode}
801 return_branch => $specific_lost_rule_refund->{ branchcode },
806 t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'CheckinLibrary' );
807 is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
808 { lostreturn => 'refund', processingreturn => 'refund' },'Specific rule for checkin branch is applied (refund)');
810 t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'ItemHomeBranch' );
811 is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
812 { lostreturn => 'restore', processingreturn => 'restore' },'Specific rule for home branch is applied (restore)');
814 t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'ItemHoldingBranch' );
815 is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
816 { lostreturn => 0, processingreturn => 0 },'Specific rule for holding branch is applied (false)');
819 t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl', 'CheckinLibrary' );
820 $params->{return_branch} = $branch_without_rule;
821 is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
822 { lostreturn => 'charge', processingreturn => 'charge' },'No rule for branch, global rule applied (charge)');
824 # Change the default value just to try
825 Koha::CirculationRules->search({ branchcode => undef, rule_name => 'lostreturn' })->next->rule_value(0)->store;
826 Koha::CirculationRules->search({ branchcode => undef, rule_name => 'processingreturn' })->next->rule_value(0)->store;
827 my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
828 $memory_cache->flush();
829 is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
830 { lostreturn => 0, processingreturn => 0 },'No rule for branch, global rule applied (false)');
832 # No default rule defined check
833 Koha::CirculationRules
837 categorycode => undef,
839 rule_name => 'lostreturn'
844 # No default rule defined check
845 Koha::CirculationRules
849 categorycode => undef,
851 rule_name => 'processingreturn'
856 is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
857 { lostreturn => 'refund', processingreturn => 'refund' },'No rule for branch, no default rule, fallback default (refund)');
859 # Fallback to ItemHoldBranch if CheckinLibrary is undefined
860 $params->{return_branch} = undef;
861 is_deeply( Koha::CirculationRules->get_lostreturn_policy( $params ),
862 { lostreturn => 'restore', processingreturn => 'restore' },'return_branch undefined, fallback to ItemHomeBranch rule (restore)');
864 $schema->storage->txn_rollback;
868 my ( $rule, $expected, $message ) = @_;
870 ok( $rule, $message ) ?
871 cmp_methods( $rule, [ %$expected ], $message ) :
875 sub _prepare_tests_for_rule_scope_combinations {
876 my ( $scope, $rule_name ) = @_;
878 # Here we create a combinations of 1s and 0s the following way
889 # (the number of columns equals to the amount of rule scopes)
890 # The ... symbolizes possible future scopes.
892 # - 0 equals to circulation rule scope with any value (aka. *)
893 # - 1 equals to circulation rule scope exact value, e.g.
894 # "CPL" (for branchcode).
896 # The order is the same as the weight of scopes when sorting circulation
897 # rules. So the first column of numbers is the scope with most weight.
898 # This is defined by C<$order> which will be assigned next.
900 # We must maintain the order in order to keep the test valid. This should be
901 # equal to Koha/CirculationRules.pm "order_by" of C<get_effective_rule> sub.
902 # Let's explicitly define the order and fail test if we are missing a scope:
903 my $order = [ 'branchcode', 'categorycode', 'itemtype' ];
904 is( join(", ", sort keys %$scope),
905 join(", ", sort @$order), 'Missing a scope!' ) if keys %$scope ne scalar @$order;
908 foreach my $value ( glob( "{0,1}" x keys %$scope || 1 ) ) {
909 my $test = { %$scope };
910 for ( my $i=0; $i < keys %$scope; $i++ ) {
911 $test->{$order->[$i]} = '*' unless substr( $value, $i, 1 );
916 return \@tests, $order;