3 # Copyright 2019 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>.
22 use Test::More tests => 16;
26 use Koha::CirculationRules;
28 use Koha::DateUtils qw(dt_from_string);
29 use Koha::ArticleRequests;
31 use Koha::Patron::Relationships;
33 use t::lib::TestBuilder;
36 my $schema = Koha::Database->new->schema;
37 my $builder = t::lib::TestBuilder->new;
39 subtest 'add_guarantor() tests' => sub {
43 $schema->storage->txn_begin;
45 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'father1|father2' );
47 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
48 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
51 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber }); }
52 'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
53 'Exception is thrown as no relationship passed';
55 is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
58 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father' }); }
59 'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
60 'Exception is thrown as a wrong relationship was passed';
62 is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
64 $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father1' });
66 my $guarantors = $patron_1->guarantor_relationships;
68 is( $guarantors->count, 1, 'No guarantors added' );
72 open STDERR, '>', '/dev/null';
74 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father2' }); }
75 'Koha::Exceptions::Patron::Relationship::DuplicateRelationship',
76 'Exception is thrown for duplicated relationship';
80 $schema->storage->txn_rollback;
83 subtest 'relationships_debt() tests' => sub {
87 $schema->storage->txn_begin;
89 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
91 my $parent_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 1" } });
92 my $parent_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 2" } });
93 my $child_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 1" } });
94 my $child_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 2" } });
96 $child_1->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
97 $child_1->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
98 $child_2->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
99 $child_2->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
101 is( $child_1->guarantor_relationships->guarantors->count, 2, 'Child 1 has correct number of guarantors' );
102 is( $child_2->guarantor_relationships->guarantors->count, 2, 'Child 2 has correct number of guarantors' );
103 is( $parent_1->guarantee_relationships->guarantees->count, 2, 'Parent 1 has correct number of guarantees' );
104 is( $parent_2->guarantee_relationships->guarantees->count, 2, 'Parent 2 has correct number of guarantees' );
106 my $patrons = [ $parent_1, $parent_2, $child_1, $child_2 ];
108 # First test: No debt
109 my ($parent1_debt, $parent2_debt, $child1_debt, $child2_debt) = (0,0,0,0);
110 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
112 # Add debt to child_2
114 $child_2->account->add_debit({ type => 'ACCOUNT', amount => $child2_debt, interface => 'commandline' });
115 is( $child_2->account->non_issues_charges, $child2_debt, 'Debt added to Child 2' );
116 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
119 $parent_1->account->add_debit({ type => 'ACCOUNT', amount => $parent1_debt, interface => 'commandline' });
120 is( $parent_1->account->non_issues_charges, $parent1_debt, 'Debt added to Parent 1' );
121 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
124 $parent_2->account->add_debit({ type => 'ACCOUNT', amount => $parent2_debt, interface => 'commandline' });
125 is( $parent_2->account->non_issues_charges, $parent2_debt, 'Parent 2 owes correct amount' );
126 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
129 $child_1->account->add_debit({ type => 'ACCOUNT', amount => $child1_debt, interface => 'commandline' });
130 is( $child_1->account->non_issues_charges, $child1_debt, 'Child 1 owes correct amount' );
131 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
133 $schema->storage->txn_rollback;
136 sub _test_combinations {
137 my ( $patrons, $parent1_debt, $parent2_debt, $child1_debt, $child2_debt ) = @_;
138 note("Testing with parent 1 debt $parent1_debt | Parent 2 debt $parent2_debt | Child 1 debt $child1_debt | Child 2 debt $child2_debt");
140 # P1 => P1 + C1 + C2 ( - P1 ) ( + P2 )
141 # P2 => P2 + C1 + C2 ( - P2 ) ( + P1 )
142 # C1 => P1 + P2 + C1 + C2 ( - C1 )
143 # C2 => P1 + P2 + C1 + C2 ( - C2 )
145 # 3 params, count from 0 to 7 in binary ( 3 places ) to get the set of switches, then do that 4 times, one for each parent and child
146 for my $i ( 0 .. 7 ) {
147 my ( $only_this_guarantor, $include_guarantors, $include_this_patron )
148 = split '', sprintf( "%03b", $i );
149 note("---------------------");
150 for my $patron ( @$patrons ) {
151 if ( $only_this_guarantor
152 && !$patron->guarantee_relationships->count )
155 $patron->relationships_debt(
157 only_this_guarantor => $only_this_guarantor,
158 include_guarantors => $include_guarantors,
159 include_this_patron => $include_this_patron
163 'Koha::Exceptions::BadParameter',
164 'Exception is thrown as patron is not a guarantor';
170 if ( $patron->firstname eq 'Parent 1' ) {
171 $debt += $parent1_debt if ($include_this_patron && $include_guarantors);
172 $debt += $child1_debt + $child2_debt;
173 $debt += $parent2_debt unless ($only_this_guarantor || !$include_guarantors);
175 elsif ( $patron->firstname eq 'Parent 2' ) {
176 $debt += $parent2_debt if ($include_this_patron & $include_guarantors);
177 $debt += $child1_debt + $child2_debt;
178 $debt += $parent1_debt unless ($only_this_guarantor || !$include_guarantors);
180 elsif ( $patron->firstname eq ' Child 1' ) {
181 $debt += $child1_debt if ($include_this_patron);
182 $debt += $child2_debt;
183 $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
186 $debt += $child2_debt if ($include_this_patron);
187 $debt += $child1_debt;
188 $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
192 $patron->relationships_debt(
194 only_this_guarantor => $only_this_guarantor,
195 include_guarantors => $include_guarantors,
196 include_this_patron => $include_this_patron
201 . " debt of " . sprintf('%02d',$debt) . " calculated correctly for ( only_this_guarantor: $only_this_guarantor, include_guarantors: $include_guarantors, include_this_patron: $include_this_patron)"
208 subtest 'add_enrolment_fee_if_needed() tests' => sub {
212 subtest 'category has enrolment fee' => sub {
215 $schema->storage->txn_begin;
217 my $category = $builder->build_object(
219 class => 'Koha::Patron::Categories',
226 my $patron = $builder->build_object(
228 class => 'Koha::Patrons',
230 categorycode => $category->categorycode
235 my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
236 is( $enrollment_fee * 1, 20, 'Enrolment fee amount is correct' );
237 my $account = $patron->account;
238 is( $patron->account->balance * 1, 20, 'Patron charged the enrolment fee' );
239 # second enrolment fee, new
240 $enrollment_fee = $patron->add_enrolment_fee_if_needed(0);
241 # third enrolment fee, renewal
242 $enrollment_fee = $patron->add_enrolment_fee_if_needed(1);
243 is( $patron->account->balance * 1, 60, 'Patron charged the enrolment fees' );
245 my @debits = $account->outstanding_debits->as_list;
246 is( scalar @debits, 3, '3 enrolment fees' );
247 is( $debits[0]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
248 is( $debits[1]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
249 is( $debits[2]->debit_type_code, 'ACCOUNT_RENEW', 'Account type set correctly' );
251 $schema->storage->txn_rollback;
254 subtest 'no enrolment fee' => sub {
258 $schema->storage->txn_begin;
260 my $category = $builder->build_object(
262 class => 'Koha::Patron::Categories',
269 my $patron = $builder->build_object(
271 class => 'Koha::Patrons',
273 categorycode => $category->categorycode
278 my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
279 is( $enrollment_fee * 1, 0, 'No enrolment fee' );
280 my $account = $patron->account;
281 is( $patron->account->balance, 0, 'Patron not charged anything' );
283 my @debits = $account->outstanding_debits->as_list;
284 is( scalar @debits, 0, 'no debits' );
286 $schema->storage->txn_rollback;
290 subtest 'to_api() tests' => sub {
294 $schema->storage->txn_begin;
296 my $patron_class = Test::MockModule->new('Koha::Patron');
299 sub { return 'algo' }
302 my $patron = $builder->build_object(
304 class => 'Koha::Patrons',
311 my $restricted = $patron->to_api->{restricted};
312 ok( defined $restricted, 'restricted is defined' );
313 ok( !$restricted, 'debarred is undef, restricted evaluates to false' );
315 $patron->debarred( dt_from_string->add( days => 1 ) )->store->discard_changes;
316 $restricted = $patron->to_api->{restricted};
317 ok( defined $restricted, 'restricted is defined' );
318 ok( $restricted, 'debarred is defined, restricted evaluates to true' );
320 my $patron_json = $patron->to_api({ embed => { algo => {} } });
321 ok( exists $patron_json->{algo} );
322 is( $patron_json->{algo}, 'algo' );
324 $schema->storage->txn_rollback;
327 subtest 'login_attempts tests' => sub {
330 $schema->storage->txn_begin;
332 my $patron = $builder->build_object(
334 class => 'Koha::Patrons',
337 my $patron_info = $patron->unblessed;
339 delete $patron_info->{login_attempts};
340 my $new_patron = Koha::Patron->new($patron_info)->store;
341 is( $new_patron->discard_changes->login_attempts, 0, "login_attempts defaults to 0 as expected");
343 $schema->storage->txn_rollback;
346 subtest 'is_superlibrarian() tests' => sub {
350 $schema->storage->txn_begin;
352 my $patron = $builder->build_object(
354 class => 'Koha::Patrons',
362 is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
364 $patron->flags(1)->store->discard_changes;
365 is( $patron->is_superlibrarian, 1, 'Patron is a superlibrarian and the method returns the correct value' );
367 $patron->flags(0)->store->discard_changes;
368 is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
370 $schema->storage->txn_rollback;
373 subtest 'extended_attributes' => sub {
377 my $schema = Koha::Database->new->schema;
378 $schema->storage->txn_begin;
380 my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
381 my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
383 t::lib::Mocks::mock_userenv({ patron => $patron_1 });
385 my $attribute_type1 = Koha::Patron::Attribute::Type->new(
388 description => 'my description1',
392 my $attribute_type2 = Koha::Patron::Attribute::Type->new(
395 description => 'my description2',
397 staff_searchable => 1
401 my $new_library = $builder->build( { source => 'Branch' } );
402 my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
403 { code => 'my code3', description => 'my description3' } )->store;
404 $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
406 my $attributes_for_1 = [
408 attribute => 'my attribute1',
409 code => $attribute_type1->code(),
412 attribute => 'my attribute2',
413 code => $attribute_type2->code(),
416 attribute => 'my attribute limited',
417 code => $attribute_type_limited->code(),
421 my $attributes_for_2 = [
423 attribute => 'my attribute12',
424 code => $attribute_type1->code(),
427 attribute => 'my attribute limited 2',
428 code => $attribute_type_limited->code(),
432 my $extended_attributes = $patron_1->extended_attributes;
433 is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
434 is( $extended_attributes->count, 0, 'There should not be attribute yet');
436 $patron_1->extended_attributes->filter_by_branch_limitations->delete;
437 $patron_2->extended_attributes->filter_by_branch_limitations->delete;
438 $patron_1->extended_attributes($attributes_for_1);
439 $patron_2->extended_attributes($attributes_for_2);
441 my $extended_attributes_for_1 = $patron_1->extended_attributes;
442 is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
444 my $extended_attributes_for_2 = $patron_2->extended_attributes;
445 is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
447 my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
448 is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
450 $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
451 is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
453 my $expected_attributes_for_2 = [
455 code => $attribute_type1->code(),
456 attribute => 'my attribute12',
459 code => $attribute_type_limited->code(),
460 attribute => 'my attribute limited 2',
463 # Sorting them by code
464 $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
465 my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
470 code => $extended_attributes_for_2[0]->code,
471 attribute => $extended_attributes_for_2[0]->attribute
474 code => $extended_attributes_for_2[1]->code,
475 attribute => $extended_attributes_for_2[1]->attribute
478 $expected_attributes_for_2
481 # TODO - What about multiple? POD explains the problem
482 my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
483 is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
485 # Test branch limitations
486 t::lib::Mocks::mock_userenv({ patron => $patron_2 });
488 $extended_attributes_for_1 = $patron_1->extended_attributes;
489 is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
492 $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
493 is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
496 my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
497 is( $limited_value->attribute, 'my attribute limited', );
499 ## Do we need a filtered?
500 #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
501 #is( $limited_value, undef, );
503 $schema->storage->txn_rollback;
505 subtest 'non-repeatable attributes tests' => sub {
509 $schema->storage->txn_begin;
511 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
512 my $attribute_type = $builder->build_object(
514 class => 'Koha::Patron::Attribute::Types',
515 value => { repeatable => 0 }
519 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
523 $patron->extended_attributes(
525 { code => $attribute_type->code, attribute => 'a' },
526 { code => $attribute_type->code, attribute => 'b' }
530 'Koha::Exceptions::Patron::Attribute::NonRepeatable',
531 'Exception thrown on non-repeatable attribute';
533 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
535 $schema->storage->txn_rollback;
539 subtest 'unique attributes tests' => sub {
543 $schema->storage->txn_begin;
545 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
546 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
548 my $attribute_type_1 = $builder->build_object(
550 class => 'Koha::Patron::Attribute::Types',
551 value => { unique => 1 }
555 my $attribute_type_2 = $builder->build_object(
557 class => 'Koha::Patron::Attribute::Types',
558 value => { unique => 0 }
562 is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
563 is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
565 $patron_1->extended_attributes(
567 { code => $attribute_type_1->code, attribute => 'a' },
568 { code => $attribute_type_2->code, attribute => 'a' }
574 $patron_2->extended_attributes(
576 { code => $attribute_type_1->code, attribute => 'a' },
577 { code => $attribute_type_2->code, attribute => 'a' }
581 'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
582 'Exception thrown on unique attribute';
584 is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
585 is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
587 $schema->storage->txn_rollback;
591 subtest 'invalid type attributes tests' => sub {
595 $schema->storage->txn_begin;
597 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
599 my $attribute_type_1 = $builder->build_object(
601 class => 'Koha::Patron::Attribute::Types',
602 value => { repeatable => 0 }
606 my $attribute_type_2 = $builder->build_object(
608 class => 'Koha::Patron::Attribute::Types'
612 my $type_2 = $attribute_type_2->code;
613 $attribute_type_2->delete;
615 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
619 $patron->extended_attributes(
621 { code => $attribute_type_1->code, attribute => 'a' },
622 { code => $attribute_type_2->code, attribute => 'b' }
626 'Koha::Exceptions::Patron::Attribute::InvalidType',
627 'Exception thrown on invalid attribute type';
629 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
631 $schema->storage->txn_rollback;
635 subtest 'globally mandatory attributes tests' => sub {
639 $schema->storage->txn_begin;
641 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
643 my $attribute_type_1 = $builder->build_object(
645 class => 'Koha::Patron::Attribute::Types',
646 value => { mandatory => 1, class => 'a' }
650 my $attribute_type_2 = $builder->build_object(
652 class => 'Koha::Patron::Attribute::Types',
653 value => { mandatory => 0, class => 'a' }
657 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
661 $patron->extended_attributes(
663 { code => $attribute_type_2->code, attribute => 'b' }
667 'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
668 'Exception thrown on missing mandatory attribute type';
670 is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
672 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
674 $patron->extended_attributes(
676 { code => $attribute_type_1->code, attribute => 'b' }
680 is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
682 $schema->storage->txn_rollback;
688 subtest 'can_log_into() tests' => sub {
692 $schema->storage->txn_begin;
694 my $patron = $builder->build_object(
696 class => 'Koha::Patrons',
702 my $library = $builder->build_object({ class => 'Koha::Libraries' });
704 t::lib::Mocks::mock_preference('IndependentBranches', 1);
706 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
707 ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
709 # make it a superlibrarian
710 $patron->set({ flags => 1 })->store->discard_changes;
711 ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
713 t::lib::Mocks::mock_preference('IndependentBranches', 0);
715 # No special permissions
716 $patron->set({ flags => undef })->store->discard_changes;
717 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
718 ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
720 $schema->storage->txn_rollback;
723 subtest 'can_request_article() tests' => sub {
727 $schema->storage->txn_begin;
729 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
731 my $item = $builder->build_sample_item;
733 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
734 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
735 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
737 t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
739 Koha::CirculationRules->set_rule(
741 categorycode => undef,
742 branchcode => $library_1->id,
743 rule_name => 'open_article_requests_limit',
748 $builder->build_object(
750 class => 'Koha::ArticleRequests',
751 value => { status => 'REQUESTED', borrowernumber => $patron->id }
754 $builder->build_object(
756 class => 'Koha::ArticleRequests',
757 value => { status => 'PENDING', borrowernumber => $patron->id }
760 $builder->build_object(
762 class => 'Koha::ArticleRequests',
763 value => { status => 'PROCESSING', borrowernumber => $patron->id }
766 $builder->build_object(
768 class => 'Koha::ArticleRequests',
769 value => { status => 'CANCELED', borrowernumber => $patron->id }
774 $patron->can_request_article( $library_1->id ),
775 '3 current requests, 4 is the limit: allowed'
778 # Completed request, same day
779 my $completed = $builder->build_object(
781 class => 'Koha::ArticleRequests',
783 status => 'COMPLETED',
784 borrowernumber => $patron->id
789 ok( !$patron->can_request_article( $library_1->id ),
790 '3 current requests and a completed one the same day: denied' );
792 $completed->updated_on(
793 dt_from_string->add( days => -1 )->set(
800 ok( $patron->can_request_article( $library_1->id ),
801 '3 current requests and a completed one the day before: allowed' );
803 Koha::CirculationRules->set_rule(
805 categorycode => undef,
806 branchcode => $library_2->id,
807 rule_name => 'open_article_requests_limit',
812 ok( !$patron->can_request_article,
813 'Not passing the library_id param makes it fallback to userenv: denied'
816 $schema->storage->txn_rollback;
819 subtest 'article_requests() tests' => sub {
823 $schema->storage->txn_begin;
825 my $library = $builder->build_object({ class => 'Koha::Libraries' });
826 t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
828 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
830 my $article_requests = $patron->article_requests;
831 is( ref($article_requests), 'Koha::ArticleRequests',
832 'In scalar context, type is correct' );
833 is( $article_requests->count, 0, 'No article requests' );
835 foreach my $i ( 0 .. 3 ) {
837 my $item = $builder->build_sample_item;
839 Koha::ArticleRequest->new(
841 borrowernumber => $patron->id,
842 biblionumber => $item->biblionumber,
843 itemnumber => $item->id,
849 $article_requests = $patron->article_requests;
850 is( $article_requests->count, 4, '4 article requests' );
852 $schema->storage->txn_rollback;
856 subtest 'can_patron_change_staff_only_lists() tests' => sub {
860 $schema->storage->txn_begin;
862 # make a user with no special permissions
863 my $patron = $builder->build_object(
865 class => 'Koha::Patrons',
871 is( $patron->can_patron_change_staff_only_lists(), 0, 'Patron without permissions cannot change staff only lists');
873 # make it a 'Catalogue' permission
874 $patron->set({ flags => 4 })->store->discard_changes;
875 is( $patron->can_patron_change_staff_only_lists(), 1, 'Catalogue patron can change staff only lists');
878 # make it a superlibrarian
879 $patron->set({ flags => 1 })->store->discard_changes;
880 is( $patron->can_patron_change_staff_only_lists(), 1, 'Superlibrarian patron can change staff only lists');
882 $schema->storage->txn_rollback;
885 subtest 'safe_to_delete() tests' => sub {
889 $schema->storage->txn_begin;
891 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
893 ## Make it the anonymous
894 t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
896 ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
897 my $message = $patron->safe_to_delete->messages->[0];
898 is( $message->type, 'error', 'Type is error' );
899 is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
901 t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
903 ## Make it have a checkout
904 my $checkout = $builder->build_object(
906 class => 'Koha::Checkouts',
907 value => { borrowernumber => $patron->id }
911 ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
912 $message = $patron->safe_to_delete->messages->[0];
913 is( $message->type, 'error', 'Type is error' );
914 is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
918 ## Make it have a guarantee
919 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
920 $builder->build_object({ class => 'Koha::Patrons' })
921 ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
923 ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
924 $message = $patron->safe_to_delete->messages->[0];
925 is( $message->type, 'error', 'Type is error' );
926 is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
929 $patron->guarantee_relationships->delete;
932 my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
934 ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
935 $message = $patron->safe_to_delete->messages->[0];
936 is( $message->type, 'error', 'Type is error' );
937 is( $message->message, 'has_debt', 'Cannot delete, has debt' );
939 $patron->account->pay({ amount => 10, debits => [ $debit ] });
942 ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
943 my $messages = $patron->safe_to_delete->messages;
944 is_deeply( $messages, [], 'Patron can be deleted, no messages' );
947 subtest 'article_request_fee() tests' => sub {
951 $schema->storage->txn_begin;
953 # Cleanup, to avoid interference
954 Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
956 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
958 my $item = $builder->build_sample_item;
960 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
961 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
962 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
964 # Rule that should never be picked, because the patron's category is always picked
965 Koha::CirculationRules->set_rule(
966 { categorycode => undef,
968 rule_name => 'article_request_fee',
973 is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
975 Koha::CirculationRules->set_rule(
976 { categorycode => $patron->categorycode,
978 rule_name => 'article_request_fee',
983 Koha::CirculationRules->set_rule(
984 { categorycode => $patron->categorycode,
985 branchcode => $library_1->id,
986 rule_name => 'article_request_fee',
991 is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
993 t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
995 is( $patron->article_request_fee(), 3, 'env used correctly' );
997 $schema->storage->txn_rollback;
1000 subtest 'add_article_request_fee_if_needed() tests' => sub {
1004 $schema->storage->txn_begin;
1008 my $patron_mock = Test::MockModule->new('Koha::Patron');
1009 $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
1011 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1013 is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
1015 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1016 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1017 my $staff = $builder->build_object( { class => 'Koha::Patrons' } );
1018 my $item = $builder->build_sample_item;
1020 t::lib::Mocks::mock_userenv(
1021 { branchcode => $library_1->id, patron => $staff } );
1023 my $debit = $patron->add_article_request_fee_if_needed();
1024 is( $debit, undef, 'No fee, no debit line' );
1029 $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
1030 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1031 is( $debit->amount, $amount,
1032 'amount set to $patron->article_request_fee value' );
1033 is( $debit->manager_id, $staff->id,
1034 'manager_id set to userenv session user' );
1035 is( $debit->branchcode, $library_1->id,
1036 'branchcode set to userenv session library' );
1037 is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1038 'debit_type_code set correctly' );
1039 is( $debit->itemnumber, $item->id,
1040 'itemnumber set correctly' );
1044 $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1045 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1046 is( $debit->amount, $amount,
1047 'amount set to $patron->article_request_fee value' );
1048 is( $debit->branchcode, $library_2->id,
1049 'branchcode set to userenv session library' );
1050 is( $debit->itemnumber, undef,
1051 'itemnumber set correctly to undef' );
1053 $schema->storage->txn_rollback;
1056 subtest 'messages' => sub {
1059 $schema->storage->txn_begin;
1061 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1062 my $messages = $patron->messages;
1063 is( $messages->count, 0, "No message yet" );
1064 my $message_1 = $builder->build_object(
1066 class => 'Koha::Patron::Messages',
1067 value => { borrowernumber => $patron->borrowernumber }
1070 my $message_2 = $builder->build_object(
1072 class => 'Koha::Patron::Messages',
1073 value => { borrowernumber => $patron->borrowernumber }
1077 $messages = $patron->messages;
1078 is( $messages->count, 2, "There are two messages for this patron" );
1079 is( $messages->next->message, $message_1->message );
1080 is( $messages->next->message, $message_2->message );
1081 $schema->storage->txn_rollback;
1084 subtest 'recalls() tests' => sub {
1088 $schema->storage->txn_begin;
1090 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1091 my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
1092 my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
1093 my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
1094 my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
1097 { biblionumber => $biblio1->biblionumber,
1098 borrowernumber => $patron->borrowernumber,
1099 itemnumber => $item1->itemnumber,
1100 branchcode => $patron->branchcode,
1101 recalldate => \'NOW()',
1102 item_level_recall => 1,
1106 { biblionumber => $biblio2->biblionumber,
1107 borrowernumber => $patron->borrowernumber,
1108 itemnumber => $item2->itemnumber,
1109 branchcode => $patron->branchcode,
1110 recalldate => \'NOW()',
1111 item_level_recall => 1,
1115 { biblionumber => $biblio1->biblionumber,
1116 borrowernumber => $patron->borrowernumber,
1117 itemnumber => undef,
1118 branchcode => $patron->branchcode,
1119 recalldate => \'NOW()',
1120 item_level_recall => 0,
1123 my $recall = Koha::Recall->new(
1124 { biblionumber => $biblio1->biblionumber,
1125 borrowernumber => $patron->borrowernumber,
1126 itemnumber => undef,
1127 branchcode => $patron->branchcode,
1128 recalldate => \'NOW()',
1129 item_level_recall => 0,
1132 $recall->set_cancelled;
1134 is( $patron->recalls->count, 4, "Correctly gets this patron's recalls" );
1135 is( $patron->recalls->filter_by_current->count, 3, "Correctly gets this patron's active recalls" );
1136 is( $patron->recalls->filter_by_current->search( { biblionumber => $biblio1->biblionumber } )->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
1138 $schema->storage->txn_rollback;