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 => 24;
26 use Koha::CirculationRules;
28 use Koha::DateUtils qw(dt_from_string);
29 use Koha::ArticleRequests;
31 use Koha::Patron::Relationships;
32 use C4::Circulation qw( AddIssue AddReturn );
34 use t::lib::TestBuilder;
37 my $schema = Koha::Database->new->schema;
38 my $builder = t::lib::TestBuilder->new;
40 subtest 'add_guarantor() tests' => sub {
44 $schema->storage->txn_begin;
46 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'father1|father2' );
48 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
49 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
52 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber }); }
53 'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
54 'Exception is thrown as no relationship passed';
56 is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
59 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father' }); }
60 'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
61 'Exception is thrown as a wrong relationship was passed';
63 is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
65 $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father1' });
67 my $guarantors = $patron_1->guarantor_relationships;
69 is( $guarantors->count, 1, 'No guarantors added' );
73 open STDERR, '>', '/dev/null';
75 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father2' }); }
76 'Koha::Exceptions::Patron::Relationship::DuplicateRelationship',
77 'Exception is thrown for duplicated relationship';
81 $schema->storage->txn_rollback;
84 subtest 'relationships_debt() tests' => sub {
88 $schema->storage->txn_begin;
90 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
92 my $parent_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 1" } });
93 my $parent_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 2" } });
94 my $child_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 1" } });
95 my $child_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 2" } });
97 $child_1->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
98 $child_1->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
99 $child_2->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
100 $child_2->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
102 is( $child_1->guarantor_relationships->guarantors->count, 2, 'Child 1 has correct number of guarantors' );
103 is( $child_2->guarantor_relationships->guarantors->count, 2, 'Child 2 has correct number of guarantors' );
104 is( $parent_1->guarantee_relationships->guarantees->count, 2, 'Parent 1 has correct number of guarantees' );
105 is( $parent_2->guarantee_relationships->guarantees->count, 2, 'Parent 2 has correct number of guarantees' );
107 my $patrons = [ $parent_1, $parent_2, $child_1, $child_2 ];
109 # First test: No debt
110 my ($parent1_debt, $parent2_debt, $child1_debt, $child2_debt) = (0,0,0,0);
111 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
113 # Add debt to child_2
115 $child_2->account->add_debit({ type => 'ACCOUNT', amount => $child2_debt, interface => 'commandline' });
116 is( $child_2->account->non_issues_charges, $child2_debt, 'Debt added to Child 2' );
117 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
120 $parent_1->account->add_debit({ type => 'ACCOUNT', amount => $parent1_debt, interface => 'commandline' });
121 is( $parent_1->account->non_issues_charges, $parent1_debt, 'Debt added to Parent 1' );
122 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
125 $parent_2->account->add_debit({ type => 'ACCOUNT', amount => $parent2_debt, interface => 'commandline' });
126 is( $parent_2->account->non_issues_charges, $parent2_debt, 'Parent 2 owes correct amount' );
127 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
130 $child_1->account->add_debit({ type => 'ACCOUNT', amount => $child1_debt, interface => 'commandline' });
131 is( $child_1->account->non_issues_charges, $child1_debt, 'Child 1 owes correct amount' );
132 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
134 $schema->storage->txn_rollback;
137 sub _test_combinations {
138 my ( $patrons, $parent1_debt, $parent2_debt, $child1_debt, $child2_debt ) = @_;
139 note("Testing with parent 1 debt $parent1_debt | Parent 2 debt $parent2_debt | Child 1 debt $child1_debt | Child 2 debt $child2_debt");
141 # P1 => P1 + C1 + C2 ( - P1 ) ( + P2 )
142 # P2 => P2 + C1 + C2 ( - P2 ) ( + P1 )
143 # C1 => P1 + P2 + C1 + C2 ( - C1 )
144 # C2 => P1 + P2 + C1 + C2 ( - C2 )
146 # 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
147 for my $i ( 0 .. 7 ) {
148 my ( $only_this_guarantor, $include_guarantors, $include_this_patron )
149 = split '', sprintf( "%03b", $i );
150 note("---------------------");
151 for my $patron ( @$patrons ) {
152 if ( $only_this_guarantor
153 && !$patron->guarantee_relationships->count )
156 $patron->relationships_debt(
158 only_this_guarantor => $only_this_guarantor,
159 include_guarantors => $include_guarantors,
160 include_this_patron => $include_this_patron
164 'Koha::Exceptions::BadParameter',
165 'Exception is thrown as patron is not a guarantor';
171 if ( $patron->firstname eq 'Parent 1' ) {
172 $debt += $parent1_debt if ($include_this_patron && $include_guarantors);
173 $debt += $child1_debt + $child2_debt;
174 $debt += $parent2_debt unless ($only_this_guarantor || !$include_guarantors);
176 elsif ( $patron->firstname eq 'Parent 2' ) {
177 $debt += $parent2_debt if ($include_this_patron & $include_guarantors);
178 $debt += $child1_debt + $child2_debt;
179 $debt += $parent1_debt unless ($only_this_guarantor || !$include_guarantors);
181 elsif ( $patron->firstname eq ' Child 1' ) {
182 $debt += $child1_debt if ($include_this_patron);
183 $debt += $child2_debt;
184 $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
187 $debt += $child2_debt if ($include_this_patron);
188 $debt += $child1_debt;
189 $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
193 $patron->relationships_debt(
195 only_this_guarantor => $only_this_guarantor,
196 include_guarantors => $include_guarantors,
197 include_this_patron => $include_this_patron
202 . " debt of " . sprintf('%02d',$debt) . " calculated correctly for ( only_this_guarantor: $only_this_guarantor, include_guarantors: $include_guarantors, include_this_patron: $include_this_patron)"
209 subtest 'add_enrolment_fee_if_needed() tests' => sub {
213 subtest 'category has enrolment fee' => sub {
216 $schema->storage->txn_begin;
218 my $category = $builder->build_object(
220 class => 'Koha::Patron::Categories',
227 my $patron = $builder->build_object(
229 class => 'Koha::Patrons',
231 categorycode => $category->categorycode
236 my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
237 is( $enrollment_fee * 1, 20, 'Enrolment fee amount is correct' );
238 my $account = $patron->account;
239 is( $patron->account->balance * 1, 20, 'Patron charged the enrolment fee' );
240 # second enrolment fee, new
241 $enrollment_fee = $patron->add_enrolment_fee_if_needed(0);
242 # third enrolment fee, renewal
243 $enrollment_fee = $patron->add_enrolment_fee_if_needed(1);
244 is( $patron->account->balance * 1, 60, 'Patron charged the enrolment fees' );
246 my @debits = $account->outstanding_debits->as_list;
247 is( scalar @debits, 3, '3 enrolment fees' );
248 is( $debits[0]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
249 is( $debits[1]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
250 is( $debits[2]->debit_type_code, 'ACCOUNT_RENEW', 'Account type set correctly' );
252 $schema->storage->txn_rollback;
255 subtest 'no enrolment fee' => sub {
259 $schema->storage->txn_begin;
261 my $category = $builder->build_object(
263 class => 'Koha::Patron::Categories',
270 my $patron = $builder->build_object(
272 class => 'Koha::Patrons',
274 categorycode => $category->categorycode
279 my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
280 is( $enrollment_fee * 1, 0, 'No enrolment fee' );
281 my $account = $patron->account;
282 is( $patron->account->balance, 0, 'Patron not charged anything' );
284 my @debits = $account->outstanding_debits->as_list;
285 is( scalar @debits, 0, 'no debits' );
287 $schema->storage->txn_rollback;
291 subtest 'to_api() tests' => sub {
295 $schema->storage->txn_begin;
297 my $patron_class = Test::MockModule->new('Koha::Patron');
300 sub { return 'algo' }
303 my $patron = $builder->build_object(
305 class => 'Koha::Patrons',
312 my $restricted = $patron->to_api->{restricted};
313 ok( defined $restricted, 'restricted is defined' );
314 ok( !$restricted, 'debarred is undef, restricted evaluates to false' );
316 $patron->debarred( dt_from_string->add( days => 1 ) )->store->discard_changes;
317 $restricted = $patron->to_api->{restricted};
318 ok( defined $restricted, 'restricted is defined' );
319 ok( $restricted, 'debarred is defined, restricted evaluates to true' );
321 my $patron_json = $patron->to_api({ embed => { algo => {} } });
322 ok( exists $patron_json->{algo} );
323 is( $patron_json->{algo}, 'algo' );
325 $schema->storage->txn_rollback;
328 subtest 'login_attempts tests' => sub {
331 $schema->storage->txn_begin;
333 my $patron = $builder->build_object(
335 class => 'Koha::Patrons',
338 my $patron_info = $patron->unblessed;
340 delete $patron_info->{login_attempts};
341 my $new_patron = Koha::Patron->new($patron_info)->store;
342 is( $new_patron->discard_changes->login_attempts, 0, "login_attempts defaults to 0 as expected");
344 $schema->storage->txn_rollback;
347 subtest 'is_superlibrarian() tests' => sub {
351 $schema->storage->txn_begin;
353 my $patron = $builder->build_object(
355 class => 'Koha::Patrons',
363 is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
365 $patron->flags(1)->store->discard_changes;
366 is( $patron->is_superlibrarian, 1, 'Patron is a superlibrarian and the method returns the correct value' );
368 $patron->flags(0)->store->discard_changes;
369 is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
371 $schema->storage->txn_rollback;
374 subtest 'extended_attributes' => sub {
378 my $schema = Koha::Database->new->schema;
379 $schema->storage->txn_begin;
381 Koha::Patron::Attribute::Types->search->delete;
383 my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
384 my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
386 t::lib::Mocks::mock_userenv({ patron => $patron_1 });
388 my $attribute_type1 = Koha::Patron::Attribute::Type->new(
391 description => 'my description1',
395 my $attribute_type2 = Koha::Patron::Attribute::Type->new(
398 description => 'my description2',
400 staff_searchable => 1
404 my $new_library = $builder->build( { source => 'Branch' } );
405 my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
406 { code => 'my code3', description => 'my description3' } )->store;
407 $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
409 my $attributes_for_1 = [
411 attribute => 'my attribute1',
412 code => $attribute_type1->code(),
415 attribute => 'my attribute2',
416 code => $attribute_type2->code(),
419 attribute => 'my attribute limited',
420 code => $attribute_type_limited->code(),
424 my $attributes_for_2 = [
426 attribute => 'my attribute12',
427 code => $attribute_type1->code(),
430 attribute => 'my attribute limited 2',
431 code => $attribute_type_limited->code(),
435 my $extended_attributes = $patron_1->extended_attributes;
436 is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
437 is( $extended_attributes->count, 0, 'There should not be attribute yet');
439 $patron_1->extended_attributes->filter_by_branch_limitations->delete;
440 $patron_2->extended_attributes->filter_by_branch_limitations->delete;
441 $patron_1->extended_attributes($attributes_for_1);
442 $patron_2->extended_attributes($attributes_for_2);
444 my $extended_attributes_for_1 = $patron_1->extended_attributes;
445 is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
447 my $extended_attributes_for_2 = $patron_2->extended_attributes;
448 is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
450 my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
451 is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
453 $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
454 is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
456 my $expected_attributes_for_2 = [
458 code => $attribute_type1->code(),
459 attribute => 'my attribute12',
462 code => $attribute_type_limited->code(),
463 attribute => 'my attribute limited 2',
466 # Sorting them by code
467 $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
468 my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
473 code => $extended_attributes_for_2[0]->code,
474 attribute => $extended_attributes_for_2[0]->attribute
477 code => $extended_attributes_for_2[1]->code,
478 attribute => $extended_attributes_for_2[1]->attribute
481 $expected_attributes_for_2
484 # TODO - What about multiple? POD explains the problem
485 my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
486 is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
488 # Test branch limitations
489 t::lib::Mocks::mock_userenv({ patron => $patron_2 });
491 $extended_attributes_for_1 = $patron_1->extended_attributes;
492 is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
495 $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
496 is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
499 my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
500 is( $limited_value->attribute, 'my attribute limited', );
502 ## Do we need a filtered?
503 #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
504 #is( $limited_value, undef, );
506 $schema->storage->txn_rollback;
508 subtest 'non-repeatable attributes tests' => sub {
512 $schema->storage->txn_begin;
513 Koha::Patron::Attribute::Types->search->delete;
515 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
516 my $attribute_type = $builder->build_object(
518 class => 'Koha::Patron::Attribute::Types',
519 value => { repeatable => 0 }
523 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
527 $patron->extended_attributes(
529 { code => $attribute_type->code, attribute => 'a' },
530 { code => $attribute_type->code, attribute => 'b' }
534 'Koha::Exceptions::Patron::Attribute::NonRepeatable',
535 'Exception thrown on non-repeatable attribute';
537 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
539 $schema->storage->txn_rollback;
543 subtest 'unique attributes tests' => sub {
547 $schema->storage->txn_begin;
548 Koha::Patron::Attribute::Types->search->delete;
550 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
551 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
553 my $attribute_type_1 = $builder->build_object(
555 class => 'Koha::Patron::Attribute::Types',
556 value => { unique_id => 1 }
560 my $attribute_type_2 = $builder->build_object(
562 class => 'Koha::Patron::Attribute::Types',
563 value => { unique_id => 0 }
567 is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
568 is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
570 $patron_1->extended_attributes(
572 { code => $attribute_type_1->code, attribute => 'a' },
573 { code => $attribute_type_2->code, attribute => 'a' }
579 $patron_2->extended_attributes(
581 { code => $attribute_type_1->code, attribute => 'a' },
582 { code => $attribute_type_2->code, attribute => 'a' }
586 'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
587 'Exception thrown on unique attribute';
589 is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
590 is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
592 $schema->storage->txn_rollback;
596 subtest 'invalid type attributes tests' => sub {
600 $schema->storage->txn_begin;
601 Koha::Patron::Attribute::Types->search->delete;
603 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
605 my $attribute_type_1 = $builder->build_object(
607 class => 'Koha::Patron::Attribute::Types',
608 value => { repeatable => 0 }
612 my $attribute_type_2 = $builder->build_object(
614 class => 'Koha::Patron::Attribute::Types'
618 my $type_2 = $attribute_type_2->code;
619 $attribute_type_2->delete;
621 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
625 $patron->extended_attributes(
627 { code => $attribute_type_1->code, attribute => 'a' },
628 { code => $attribute_type_2->code, attribute => 'b' }
632 'Koha::Exceptions::Patron::Attribute::InvalidType',
633 'Exception thrown on invalid attribute type';
635 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
637 $schema->storage->txn_rollback;
641 subtest 'globally mandatory attributes tests' => sub {
645 $schema->storage->txn_begin;
646 Koha::Patron::Attribute::Types->search->delete;
648 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
650 my $attribute_type_1 = $builder->build_object(
652 class => 'Koha::Patron::Attribute::Types',
653 value => { mandatory => 1, class => 'a', category_code => undef }
657 my $attribute_type_2 = $builder->build_object(
659 class => 'Koha::Patron::Attribute::Types',
660 value => { mandatory => 0, class => 'a', category_code => undef }
664 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
668 $patron->extended_attributes(
670 { code => $attribute_type_2->code, attribute => 'b' }
674 'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
675 'Exception thrown on missing mandatory attribute type';
677 is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
679 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
681 $patron->extended_attributes(
683 { code => $attribute_type_1->code, attribute => 'b' }
687 is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
689 $schema->storage->txn_rollback;
693 subtest 'limited category mandatory attributes tests' => sub {
697 $schema->storage->txn_begin;
698 Koha::Patron::Attribute::Types->search->delete;
700 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
702 my $attribute_type_1 = $builder->build_object(
704 class => 'Koha::Patron::Attribute::Types',
705 value => { mandatory => 1, class => 'a', category_code => $patron->categorycode }
709 $patron->extended_attributes(
711 { code => $attribute_type_1->code, attribute => 'a' }
715 is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
717 $patron = $builder->build_object({ class => 'Koha::Patrons' });
718 # new patron, new category - they shouldn't be required to have any attributes
721 ok( $patron->extended_attributes([]), "We can set no attributes, mandatory attribute for other category not required");
730 subtest 'can_log_into() tests' => sub {
734 $schema->storage->txn_begin;
736 my $patron = $builder->build_object(
738 class => 'Koha::Patrons',
744 my $library = $builder->build_object({ class => 'Koha::Libraries' });
746 t::lib::Mocks::mock_preference('IndependentBranches', 1);
748 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
749 ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
751 # make it a superlibrarian
752 $patron->set({ flags => 1 })->store->discard_changes;
753 ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
755 t::lib::Mocks::mock_preference('IndependentBranches', 0);
757 # No special permissions
758 $patron->set({ flags => undef })->store->discard_changes;
759 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
760 ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
762 $schema->storage->txn_rollback;
765 subtest 'can_request_article() tests' => sub {
769 $schema->storage->txn_begin;
771 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
773 my $item = $builder->build_sample_item;
775 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
776 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
777 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
779 t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
781 Koha::CirculationRules->set_rule(
783 categorycode => undef,
784 branchcode => $library_1->id,
785 rule_name => 'open_article_requests_limit',
790 $builder->build_object(
792 class => 'Koha::ArticleRequests',
793 value => { status => 'REQUESTED', borrowernumber => $patron->id }
796 $builder->build_object(
798 class => 'Koha::ArticleRequests',
799 value => { status => 'PENDING', borrowernumber => $patron->id }
802 $builder->build_object(
804 class => 'Koha::ArticleRequests',
805 value => { status => 'PROCESSING', borrowernumber => $patron->id }
808 $builder->build_object(
810 class => 'Koha::ArticleRequests',
811 value => { status => 'CANCELED', borrowernumber => $patron->id }
816 $patron->can_request_article( $library_1->id ),
817 '3 current requests, 4 is the limit: allowed'
820 # Completed request, same day
821 my $completed = $builder->build_object(
823 class => 'Koha::ArticleRequests',
825 status => 'COMPLETED',
826 borrowernumber => $patron->id
831 ok( !$patron->can_request_article( $library_1->id ),
832 '3 current requests and a completed one the same day: denied' );
834 $completed->updated_on(
835 dt_from_string->add( days => -1 )->set(
842 ok( $patron->can_request_article( $library_1->id ),
843 '3 current requests and a completed one the day before: allowed' );
845 Koha::CirculationRules->set_rule(
847 categorycode => undef,
848 branchcode => $library_2->id,
849 rule_name => 'open_article_requests_limit',
854 ok( !$patron->can_request_article,
855 'Not passing the library_id param makes it fallback to userenv: denied'
858 $schema->storage->txn_rollback;
861 subtest 'article_requests() tests' => sub {
865 $schema->storage->txn_begin;
867 my $library = $builder->build_object({ class => 'Koha::Libraries' });
868 t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
870 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
872 my $article_requests = $patron->article_requests;
873 is( ref($article_requests), 'Koha::ArticleRequests',
874 'In scalar context, type is correct' );
875 is( $article_requests->count, 0, 'No article requests' );
877 foreach my $i ( 0 .. 3 ) {
879 my $item = $builder->build_sample_item;
881 Koha::ArticleRequest->new(
883 borrowernumber => $patron->id,
884 biblionumber => $item->biblionumber,
885 itemnumber => $item->id,
891 $article_requests = $patron->article_requests;
892 is( $article_requests->count, 4, '4 article requests' );
894 $schema->storage->txn_rollback;
898 subtest 'can_patron_change_staff_only_lists() tests' => sub {
902 $schema->storage->txn_begin;
904 # make a user with no special permissions
905 my $patron = $builder->build_object(
907 class => 'Koha::Patrons',
913 is( $patron->can_patron_change_staff_only_lists(), 0, 'Patron without permissions cannot change staff only lists');
915 # make it a 'Catalogue' permission
916 $patron->set({ flags => 4 })->store->discard_changes;
917 is( $patron->can_patron_change_staff_only_lists(), 1, 'Catalogue patron can change staff only lists');
920 # make it a superlibrarian
921 $patron->set({ flags => 1 })->store->discard_changes;
922 is( $patron->can_patron_change_staff_only_lists(), 1, 'Superlibrarian patron can change staff only lists');
924 $schema->storage->txn_rollback;
927 subtest 'can_patron_change_permitted_staff_lists() tests' => sub {
931 $schema->storage->txn_begin;
933 # make a user with no special permissions
934 my $patron = $builder->build_object(
936 class => 'Koha::Patrons',
942 is( $patron->can_patron_change_permitted_staff_lists(), 0, 'Patron without permissions cannot change permitted staff lists');
944 # make it a 'Catalogue' permission
945 $patron->set({ flags => 4 })->store->discard_changes;
946 is( $patron->can_patron_change_permitted_staff_lists(), 0, 'Catalogue patron cannot change permitted staff lists');
948 # make it a 'Catalogue' permission and 'edit_public_list_contents' sub-permission
949 $patron->set({ flags => 4 })->store->discard_changes;
952 source => 'UserPermission',
954 borrowernumber => $patron->borrowernumber,
955 module_bit => 20, # lists
956 code => 'edit_public_list_contents',
960 is( $patron->can_patron_change_permitted_staff_lists(), 1, 'Catalogue and "edit_public_list_contents" patron can change permitted staff lists');
962 # make it a superlibrarian
963 $patron->set({ flags => 1 })->store->discard_changes;
964 is( $patron->can_patron_change_permitted_staff_lists(), 1, 'Superlibrarian patron can change permitted staff lists');
966 $schema->storage->txn_rollback;
969 subtest 'password expiration tests' => sub {
973 $schema->storage->txn_begin;
974 my $date = dt_from_string();
975 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
976 password_expiry_days => 10,
977 require_strong_password => 0,
980 my $patron = $builder->build_object({ class=> 'Koha::Patrons', value => {
981 categorycode => $category->categorycode,
986 $patron->delete()->store()->discard_changes(); # Make sure we are storing a 'new' patron
988 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd() , "Password expiration date set correctly on patron creation");
990 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
991 categorycode => $category->categorycode,
995 $patron->delete()->store()->discard_changes();
997 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if patron does not have a password");
999 $category->password_expiry_days(undef)->store();
1000 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1001 categorycode => $category->categorycode
1004 $patron->delete()->store()->discard_changes();
1005 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if category does not have expiry days set");
1007 $schema->storage->txn_rollback;
1009 subtest 'password_expired' => sub {
1013 $schema->storage->txn_begin;
1014 my $date = dt_from_string();
1015 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1016 password_expiration_date => undef
1019 is( $patron->password_expired, 0, "Patron with no password expiration date, password not expired");
1020 $patron->password_expiration_date( $date )->store;
1021 $patron->discard_changes();
1022 is( $patron->password_expired, 1, "Patron with password expiration date of today, password expired");
1023 $date->subtract( days => 1 );
1024 $patron->password_expiration_date( $date )->store;
1025 $patron->discard_changes();
1026 is( $patron->password_expired, 1, "Patron with password expiration date in past, password expired");
1028 $schema->storage->txn_rollback;
1031 subtest 'set_password' => sub {
1035 $schema->storage->txn_begin;
1037 my $date = dt_from_string();
1038 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
1039 password_expiry_days => 10
1042 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1043 categorycode => $category->categorycode,
1044 password_expiration_date => $date->subtract( days => 1 )
1047 is( $patron->password_expired, 1, "Patron password is expired");
1049 $date = dt_from_string();
1050 $patron->set_password({ password => "kitten", skip_validation => 1 })->discard_changes();
1051 is( $patron->password_expired, 0, "Patron password no longer expired when new password set");
1052 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd(), "Password expiration date set correctly on patron creation");
1055 $category->password_expiry_days( undef )->store();
1056 $patron->set_password({ password => "puppies", skip_validation => 1 })->discard_changes();
1057 is( $patron->password_expiration_date(), undef, "Password expiration date is unset if category does not have expiry days");
1059 $schema->storage->txn_rollback;
1064 subtest 'safe_to_delete() tests' => sub {
1068 $schema->storage->txn_begin;
1070 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1072 ## Make it the anonymous
1073 t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
1075 ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
1076 my $message = $patron->safe_to_delete->messages->[0];
1077 is( $message->type, 'error', 'Type is error' );
1078 is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
1080 t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
1082 ## Make it have a checkout
1083 my $checkout = $builder->build_object(
1085 class => 'Koha::Checkouts',
1086 value => { borrowernumber => $patron->id }
1090 ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
1091 $message = $patron->safe_to_delete->messages->[0];
1092 is( $message->type, 'error', 'Type is error' );
1093 is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
1097 ## Make it have a guarantee
1098 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
1099 $builder->build_object({ class => 'Koha::Patrons' })
1100 ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
1102 ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
1103 $message = $patron->safe_to_delete->messages->[0];
1104 is( $message->type, 'error', 'Type is error' );
1105 is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
1108 $patron->guarantee_relationships->delete;
1110 ## Make it have debt
1111 my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
1113 ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
1114 $message = $patron->safe_to_delete->messages->[0];
1115 is( $message->type, 'error', 'Type is error' );
1116 is( $message->message, 'has_debt', 'Cannot delete, has debt' );
1118 my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
1119 t::lib::Mocks::mock_userenv( { borrowernumber => $manager->id } );
1120 $patron->account->pay({ amount => 10, debits => [ $debit ] });
1123 ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
1124 my $messages = $patron->safe_to_delete->messages;
1125 is_deeply( $messages, [], 'Patron can be deleted, no messages' );
1128 subtest 'article_request_fee() tests' => sub {
1132 $schema->storage->txn_begin;
1134 # Cleanup, to avoid interference
1135 Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
1137 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1139 my $item = $builder->build_sample_item;
1141 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1142 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1143 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1145 # Rule that should never be picked, because the patron's category is always picked
1146 Koha::CirculationRules->set_rule(
1147 { categorycode => undef,
1148 branchcode => undef,
1149 rule_name => 'article_request_fee',
1154 is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
1156 Koha::CirculationRules->set_rule(
1157 { categorycode => $patron->categorycode,
1158 branchcode => undef,
1159 rule_name => 'article_request_fee',
1164 Koha::CirculationRules->set_rule(
1165 { categorycode => $patron->categorycode,
1166 branchcode => $library_1->id,
1167 rule_name => 'article_request_fee',
1172 is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
1174 t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
1176 is( $patron->article_request_fee(), 3, 'env used correctly' );
1178 $schema->storage->txn_rollback;
1181 subtest 'add_article_request_fee_if_needed() tests' => sub {
1185 $schema->storage->txn_begin;
1189 my $patron_mock = Test::MockModule->new('Koha::Patron');
1190 $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
1192 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1194 is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
1196 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1197 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1198 my $staff = $builder->build_object( { class => 'Koha::Patrons' } );
1199 my $item = $builder->build_sample_item;
1201 t::lib::Mocks::mock_userenv(
1202 { branchcode => $library_1->id, patron => $staff } );
1204 my $debit = $patron->add_article_request_fee_if_needed();
1205 is( $debit, undef, 'No fee, no debit line' );
1210 $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
1211 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1212 is( $debit->amount, $amount,
1213 'amount set to $patron->article_request_fee value' );
1214 is( $debit->manager_id, $staff->id,
1215 'manager_id set to userenv session user' );
1216 is( $debit->branchcode, $library_1->id,
1217 'branchcode set to userenv session library' );
1218 is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1219 'debit_type_code set correctly' );
1220 is( $debit->itemnumber, $item->id,
1221 'itemnumber set correctly' );
1225 $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1226 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1227 is( $debit->amount, $amount,
1228 'amount set to $patron->article_request_fee value' );
1229 is( $debit->branchcode, $library_2->id,
1230 'branchcode set to userenv session library' );
1231 is( $debit->itemnumber, undef,
1232 'itemnumber set correctly to undef' );
1234 $schema->storage->txn_rollback;
1237 subtest 'messages' => sub {
1240 $schema->storage->txn_begin;
1242 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1243 my $messages = $patron->messages;
1244 is( $messages->count, 0, "No message yet" );
1245 my $message_1 = $builder->build_object(
1247 class => 'Koha::Patron::Messages',
1248 value => { borrowernumber => $patron->borrowernumber }
1251 my $message_2 = $builder->build_object(
1253 class => 'Koha::Patron::Messages',
1254 value => { borrowernumber => $patron->borrowernumber }
1258 $messages = $patron->messages;
1259 is( $messages->count, 2, "There are two messages for this patron" );
1260 is( $messages->next->message, $message_1->message );
1261 is( $messages->next->message, $message_2->message );
1262 $schema->storage->txn_rollback;
1265 subtest 'recalls() tests' => sub {
1269 $schema->storage->txn_begin;
1271 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1272 my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
1273 my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
1274 my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
1275 my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
1278 { biblio_id => $biblio1->biblionumber,
1279 patron_id => $patron->borrowernumber,
1280 item_id => $item1->itemnumber,
1281 pickup_library_id => $patron->branchcode,
1282 created_date => \'NOW()',
1287 { biblio_id => $biblio2->biblionumber,
1288 patron_id => $patron->borrowernumber,
1289 item_id => $item2->itemnumber,
1290 pickup_library_id => $patron->branchcode,
1291 created_date => \'NOW()',
1296 { biblio_id => $biblio1->biblionumber,
1297 patron_id => $patron->borrowernumber,
1299 pickup_library_id => $patron->branchcode,
1300 created_date => \'NOW()',
1304 my $recall = Koha::Recall->new(
1305 { biblio_id => $biblio1->biblionumber,
1306 patron_id => $patron->borrowernumber,
1308 pickup_library_id => $patron->branchcode,
1309 created_date => \'NOW()',
1313 $recall->set_cancelled;
1315 is( $patron->recalls->count, 4, "Correctly gets this patron's recalls" );
1316 is( $patron->recalls->filter_by_current->count, 3, "Correctly gets this patron's active recalls" );
1317 is( $patron->recalls->filter_by_current->search( { biblio_id => $biblio1->biblionumber } )->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
1319 $schema->storage->txn_rollback;
1322 subtest 'encode_secret and decoded_secret' => sub {
1324 $schema->storage->txn_begin;
1326 t::lib::Mocks::mock_config('encryption_key', 't0P_secret');
1328 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1329 is( $patron->decoded_secret, undef, 'TestBuilder does not initialize it' );
1330 $patron->secret(q{});
1331 is( $patron->decoded_secret, q{}, 'Empty string case' );
1333 $patron->encode_secret('encrypt_me'); # Note: lazy testing; should be base32 string normally.
1334 is( length($patron->secret) > 0, 1, 'Secret length' );
1335 isnt( $patron->secret, 'encrypt_me', 'Encrypted column' );
1336 is( $patron->decoded_secret, 'encrypt_me', 'Decrypted column' );
1338 $schema->storage->txn_rollback;
1341 subtest 'notify_library_of_registration()' => sub {
1345 $schema->storage->txn_begin;
1346 my $dbh = C4::Context->dbh;
1348 my $library = $builder->build_object(
1350 class => 'Koha::Libraries',
1352 branchemail => 'from@mybranch.com',
1353 branchreplyto => 'to@mybranch.com'
1357 my $patron = $builder->build_object(
1359 class => 'Koha::Patrons',
1361 branchcode => $library->branchcode
1366 t::lib::Mocks::mock_preference( 'KohaAdminEmailAddress', 'root@localhost' );
1367 t::lib::Mocks::mock_preference( 'EmailAddressForPatronRegistrations', 'library@localhost' );
1369 # Test when EmailPatronRegistrations equals BranchEmailAddress
1370 t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'BranchEmailAddress' );
1371 is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals BranchEmailAddress');
1372 my $sth = $dbh->prepare("SELECT to_address FROM message_queue where borrowernumber = ?");
1373 $sth->execute( $patron->borrowernumber );
1374 my $to_address = $sth->fetchrow_array;
1375 is( $to_address, 'to@mybranch.com', 'OPAC_REG email queued to go to branchreplyto address when EmailPatronRegistration equals BranchEmailAddress' );
1376 $dbh->do(q|DELETE FROM message_queue|);
1378 # Test when EmailPatronRegistrations equals EmailAddressForPatronRegistrations
1379 t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'EmailAddressForPatronRegistrations' );
1380 is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals EmailAddressForPatronRegistrations');
1381 $sth->execute( $patron->borrowernumber );
1382 $to_address = $sth->fetchrow_array;
1383 is( $to_address, 'library@localhost', 'OPAC_REG email queued to go to EmailAddressForPatronRegistrations syspref when EmailPatronRegistration equals EmailAddressForPatronRegistrations' );
1384 $dbh->do(q|DELETE FROM message_queue|);
1386 # Test when EmailPatronRegistrations equals KohaAdminEmailAddress
1387 t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'KohaAdminEmailAddress' );
1388 t::lib::Mocks::mock_preference( 'ReplyToDefault', 'root@localhost' ); # FIXME Remove localhost
1389 is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals KohaAdminEmailAddress');
1390 $sth->execute( $patron->borrowernumber );
1391 $to_address = $sth->fetchrow_array;
1392 is( $to_address, 'root@localhost', 'OPAC_REG email queued to go to KohaAdminEmailAddress syspref when EmailPatronRegistration equals KohaAdminEmailAddress' );
1393 $dbh->do(q|DELETE FROM message_queue|);
1395 $schema->storage->txn_rollback;
1398 subtest 'notice_email_address' => sub {
1401 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1403 t::lib::Mocks::mock_preference( 'EmailFieldPrecedence', 'email|emailpro' );
1404 t::lib::Mocks::mock_preference( 'EmailFieldPrimary', 'OFF' );
1405 is ($patron->notice_email_address, $patron->email, "Koha::Patron->notice_email_address returns correct value when EmailFieldPrimary is off");
1407 t::lib::Mocks::mock_preference( 'EmailFieldPrimary', 'emailpro' );
1408 is ($patron->notice_email_address, $patron->emailpro, "Koha::Patron->notice_email_address returns correct value when EmailFieldPrimary is emailpro");
1413 subtest 'first_valid_email_address' => sub {
1416 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { emailpro => ''}});
1418 t::lib::Mocks::mock_preference( 'EmailFieldPrecedence', 'emailpro|email' );
1419 is ($patron->first_valid_email_address, $patron->email, "Koha::Patron->first_valid_email_address returns correct value when EmailFieldPrecedence is 'emailpro|email' and emailpro is empty");
1424 subtest 'get_savings tests' => sub {
1428 $schema->storage->txn_begin;
1430 my $library = $builder->build_object({ class => 'Koha::Libraries' });
1431 my $patron = $builder->build_object({ class => 'Koha::Patrons' }, { value => { branchcode => $library->branchcode } });
1433 t::lib::Mocks::mock_userenv({ patron => $patron, branchcode => $library->branchcode });
1435 my $biblio = $builder->build_sample_biblio;
1436 my $item1 = $builder->build_sample_item(
1438 biblionumber => $biblio->biblionumber,
1439 library => $library->branchcode,
1440 replacementprice => rand(20),
1443 my $item2 = $builder->build_sample_item(
1445 biblionumber => $biblio->biblionumber,
1446 library => $library->branchcode,
1447 replacementprice => rand(20),
1451 is( $patron->get_savings, 0, 'No checkouts, no savings' );
1453 # Add an old checkout with deleted itemnumber
1454 $builder->build_object({ class => 'Koha::Old::Checkouts', value => { itemnumber => undef, borrowernumber => $patron->id } });
1456 is( $patron->get_savings, 0, 'No checkouts with itemnumber, no savings' );
1458 AddIssue( $patron->unblessed, $item1->barcode );
1459 AddIssue( $patron->unblessed, $item2->barcode );
1461 my $savings = $patron->get_savings;
1462 is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current issues" );
1464 AddReturn( $item2->barcode, $item2->homebranch );
1466 $savings = $patron->get_savings;
1467 is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current and old issues" );
1469 $schema->storage->txn_rollback;
1472 subtest 'update privacy tests' => sub {
1476 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy => 1 } });
1478 my $old_checkout = $builder->build_object({ class => 'Koha::Old::Checkouts', value => { borrowernumber => $patron->id } });
1480 t::lib::Mocks::mock_preference( 'AnonymousPatron', '0' );
1482 $patron->privacy(2); #set to never
1484 throws_ok{ $patron->store } 'Koha::Exceptions::Patron::FailedAnonymizing', 'We throw an exception when anonymizing fails';
1486 $old_checkout->discard_changes; #refresh from db
1487 $patron->discard_changes;
1489 is( $old_checkout->borrowernumber, $patron->id, "When anonymizing fails, we don't clear the checkouts");
1490 is( $patron->privacy(), 1, "When anonymizing fails, we don't chaneg the privacy");
1492 my $anon_patron = $builder->build_object({ class => 'Koha::Patrons'});
1493 t::lib::Mocks::mock_preference( 'AnonymousPatron', $anon_patron->id );
1495 $patron->privacy(2)->store(); #set to never
1497 $old_checkout->discard_changes; #refresh from db
1498 $patron->discard_changes;
1500 is( $old_checkout->borrowernumber, $anon_patron->id, "Checkout is successfully anonymized");
1501 is( $patron->privacy(), 2, "Patron privacy is successfully updated");