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 => 21;
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 'password expiration tests' => sub {
931 $schema->storage->txn_begin;
932 my $date = dt_from_string();
933 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
934 password_expiry_days => 10,
935 require_strong_password => 0,
938 my $patron = $builder->build_object({ class=> 'Koha::Patrons', value => {
939 categorycode => $category->categorycode,
944 $patron->delete()->store()->discard_changes(); # Make sure we are storing a 'new' patron
946 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd() , "Password expiration date set correctly on patron creation");
948 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
949 categorycode => $category->categorycode,
953 $patron->delete()->store()->discard_changes();
955 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if patron does not have a password");
957 $category->password_expiry_days(undef)->store();
958 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
959 categorycode => $category->categorycode
962 $patron->delete()->store()->discard_changes();
963 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if category does not have expiry days set");
965 $schema->storage->txn_rollback;
967 subtest 'password_expired' => sub {
971 $schema->storage->txn_begin;
972 my $date = dt_from_string();
973 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
974 password_expiration_date => undef
977 is( $patron->password_expired, 0, "Patron with no password expiration date, password not expired");
978 $patron->password_expiration_date( $date )->store;
979 $patron->discard_changes();
980 is( $patron->password_expired, 1, "Patron with password expiration date of today, password expired");
981 $date->subtract( days => 1 );
982 $patron->password_expiration_date( $date )->store;
983 $patron->discard_changes();
984 is( $patron->password_expired, 1, "Patron with password expiration date in past, password expired");
986 $schema->storage->txn_rollback;
989 subtest 'set_password' => sub {
993 $schema->storage->txn_begin;
995 my $date = dt_from_string();
996 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
997 password_expiry_days => 10
1000 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1001 categorycode => $category->categorycode,
1002 password_expiration_date => $date->subtract( days => 1 )
1005 is( $patron->password_expired, 1, "Patron password is expired");
1007 $date = dt_from_string();
1008 $patron->set_password({ password => "kitten", skip_validation => 1 })->discard_changes();
1009 is( $patron->password_expired, 0, "Patron password no longer expired when new password set");
1010 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd(), "Password expiration date set correctly on patron creation");
1013 $category->password_expiry_days( undef )->store();
1014 $patron->set_password({ password => "puppies", skip_validation => 1 })->discard_changes();
1015 is( $patron->password_expiration_date(), undef, "Password expiration date is unset if category does not have expiry days");
1017 $schema->storage->txn_rollback;
1022 subtest 'safe_to_delete() tests' => sub {
1026 $schema->storage->txn_begin;
1028 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1030 ## Make it the anonymous
1031 t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
1033 ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
1034 my $message = $patron->safe_to_delete->messages->[0];
1035 is( $message->type, 'error', 'Type is error' );
1036 is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
1038 t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
1040 ## Make it have a checkout
1041 my $checkout = $builder->build_object(
1043 class => 'Koha::Checkouts',
1044 value => { borrowernumber => $patron->id }
1048 ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
1049 $message = $patron->safe_to_delete->messages->[0];
1050 is( $message->type, 'error', 'Type is error' );
1051 is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
1055 ## Make it have a guarantee
1056 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
1057 $builder->build_object({ class => 'Koha::Patrons' })
1058 ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
1060 ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
1061 $message = $patron->safe_to_delete->messages->[0];
1062 is( $message->type, 'error', 'Type is error' );
1063 is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
1066 $patron->guarantee_relationships->delete;
1068 ## Make it have debt
1069 my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
1071 ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
1072 $message = $patron->safe_to_delete->messages->[0];
1073 is( $message->type, 'error', 'Type is error' );
1074 is( $message->message, 'has_debt', 'Cannot delete, has debt' );
1076 $patron->account->pay({ amount => 10, debits => [ $debit ] });
1079 ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
1080 my $messages = $patron->safe_to_delete->messages;
1081 is_deeply( $messages, [], 'Patron can be deleted, no messages' );
1084 subtest 'article_request_fee() tests' => sub {
1088 $schema->storage->txn_begin;
1090 # Cleanup, to avoid interference
1091 Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
1093 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1095 my $item = $builder->build_sample_item;
1097 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1098 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1099 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1101 # Rule that should never be picked, because the patron's category is always picked
1102 Koha::CirculationRules->set_rule(
1103 { categorycode => undef,
1104 branchcode => undef,
1105 rule_name => 'article_request_fee',
1110 is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
1112 Koha::CirculationRules->set_rule(
1113 { categorycode => $patron->categorycode,
1114 branchcode => undef,
1115 rule_name => 'article_request_fee',
1120 Koha::CirculationRules->set_rule(
1121 { categorycode => $patron->categorycode,
1122 branchcode => $library_1->id,
1123 rule_name => 'article_request_fee',
1128 is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
1130 t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
1132 is( $patron->article_request_fee(), 3, 'env used correctly' );
1134 $schema->storage->txn_rollback;
1137 subtest 'add_article_request_fee_if_needed() tests' => sub {
1141 $schema->storage->txn_begin;
1145 my $patron_mock = Test::MockModule->new('Koha::Patron');
1146 $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
1148 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1150 is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
1152 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1153 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1154 my $staff = $builder->build_object( { class => 'Koha::Patrons' } );
1155 my $item = $builder->build_sample_item;
1157 t::lib::Mocks::mock_userenv(
1158 { branchcode => $library_1->id, patron => $staff } );
1160 my $debit = $patron->add_article_request_fee_if_needed();
1161 is( $debit, undef, 'No fee, no debit line' );
1166 $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
1167 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1168 is( $debit->amount, $amount,
1169 'amount set to $patron->article_request_fee value' );
1170 is( $debit->manager_id, $staff->id,
1171 'manager_id set to userenv session user' );
1172 is( $debit->branchcode, $library_1->id,
1173 'branchcode set to userenv session library' );
1174 is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1175 'debit_type_code set correctly' );
1176 is( $debit->itemnumber, $item->id,
1177 'itemnumber set correctly' );
1181 $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1182 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1183 is( $debit->amount, $amount,
1184 'amount set to $patron->article_request_fee value' );
1185 is( $debit->branchcode, $library_2->id,
1186 'branchcode set to userenv session library' );
1187 is( $debit->itemnumber, undef,
1188 'itemnumber set correctly to undef' );
1190 $schema->storage->txn_rollback;
1193 subtest 'messages' => sub {
1196 $schema->storage->txn_begin;
1198 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1199 my $messages = $patron->messages;
1200 is( $messages->count, 0, "No message yet" );
1201 my $message_1 = $builder->build_object(
1203 class => 'Koha::Patron::Messages',
1204 value => { borrowernumber => $patron->borrowernumber }
1207 my $message_2 = $builder->build_object(
1209 class => 'Koha::Patron::Messages',
1210 value => { borrowernumber => $patron->borrowernumber }
1214 $messages = $patron->messages;
1215 is( $messages->count, 2, "There are two messages for this patron" );
1216 is( $messages->next->message, $message_1->message );
1217 is( $messages->next->message, $message_2->message );
1218 $schema->storage->txn_rollback;
1221 subtest 'recalls() tests' => sub {
1225 $schema->storage->txn_begin;
1227 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1228 my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
1229 my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
1230 my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
1231 my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
1234 { biblio_id => $biblio1->biblionumber,
1235 patron_id => $patron->borrowernumber,
1236 item_id => $item1->itemnumber,
1237 pickup_library_id => $patron->branchcode,
1238 created_date => \'NOW()',
1243 { biblio_id => $biblio2->biblionumber,
1244 patron_id => $patron->borrowernumber,
1245 item_id => $item2->itemnumber,
1246 pickup_library_id => $patron->branchcode,
1247 created_date => \'NOW()',
1252 { biblio_id => $biblio1->biblionumber,
1253 patron_id => $patron->borrowernumber,
1255 pickup_library_id => $patron->branchcode,
1256 created_date => \'NOW()',
1260 my $recall = Koha::Recall->new(
1261 { biblio_id => $biblio1->biblionumber,
1262 patron_id => $patron->borrowernumber,
1264 pickup_library_id => $patron->branchcode,
1265 created_date => \'NOW()',
1269 $recall->set_cancelled;
1271 is( $patron->recalls->count, 4, "Correctly gets this patron's recalls" );
1272 is( $patron->recalls->filter_by_current->count, 3, "Correctly gets this patron's active recalls" );
1273 is( $patron->recalls->filter_by_current->search( { biblio_id => $biblio1->biblionumber } )->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
1275 $schema->storage->txn_rollback;
1278 subtest 'encode_secret and decoded_secret' => sub {
1280 $schema->storage->txn_begin;
1282 t::lib::Mocks::mock_config('encryption_key', 't0P_secret');
1284 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1285 is( $patron->decoded_secret, undef, 'TestBuilder does not initialize it' );
1286 $patron->secret(q{});
1287 is( $patron->decoded_secret, q{}, 'Empty string case' );
1289 $patron->encode_secret('encrypt_me'); # Note: lazy testing; should be base32 string normally.
1290 is( length($patron->secret) > 0, 1, 'Secret length' );
1291 isnt( $patron->secret, 'encrypt_me', 'Encrypted column' );
1292 is( $patron->decoded_secret, 'encrypt_me', 'Decrypted column' );
1294 $schema->storage->txn_rollback;
1297 subtest 'notify_library_of_registration()' => sub {
1301 $schema->storage->txn_begin;
1302 my $dbh = C4::Context->dbh;
1304 my $library = $builder->build_object(
1306 class => 'Koha::Libraries',
1308 branchemail => 'from@mybranch.com',
1309 branchreplyto => 'to@mybranch.com'
1313 my $patron = $builder->build_object(
1315 class => 'Koha::Patrons',
1317 branchcode => $library->branchcode
1322 t::lib::Mocks::mock_preference( 'KohaAdminEmailAddress', 'root@localhost' );
1323 t::lib::Mocks::mock_preference( 'EmailAddressForPatronRegistrations', 'library@localhost' );
1325 # Test when EmailPatronRegistrations equals BranchEmailAddress
1326 t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'BranchEmailAddress' );
1327 is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals BranchEmailAddress');
1328 my $sth = $dbh->prepare("SELECT to_address FROM message_queue where borrowernumber = ?");
1329 $sth->execute( $patron->borrowernumber );
1330 my $to_address = $sth->fetchrow_array;
1331 is( $to_address, 'to@mybranch.com', 'OPAC_REG email queued to go to branchreplyto address when EmailPatronRegistration equals BranchEmailAddress' );
1332 $dbh->do(q|DELETE FROM message_queue|);
1334 # Test when EmailPatronRegistrations equals EmailAddressForPatronRegistrations
1335 t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'EmailAddressForPatronRegistrations' );
1336 is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals EmailAddressForPatronRegistrations');
1337 $sth->execute( $patron->borrowernumber );
1338 $to_address = $sth->fetchrow_array;
1339 is( $to_address, 'library@localhost', 'OPAC_REG email queued to go to EmailAddressForPatronRegistrations syspref when EmailPatronRegistration equals EmailAddressForPatronRegistrations' );
1340 $dbh->do(q|DELETE FROM message_queue|);
1342 # Test when EmailPatronRegistrations equals KohaAdminEmailAddress
1343 t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'KohaAdminEmailAddress' );
1344 t::lib::Mocks::mock_preference( 'ReplyToDefault', 'root@localhost' ); # FIXME Remove localhost
1345 is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals KohaAdminEmailAddress');
1346 $sth->execute( $patron->borrowernumber );
1347 $to_address = $sth->fetchrow_array;
1348 is( $to_address, 'root@localhost', 'OPAC_REG email queued to go to KohaAdminEmailAddress syspref when EmailPatronRegistration equals KohaAdminEmailAddress' );
1349 $dbh->do(q|DELETE FROM message_queue|);
1351 $schema->storage->txn_rollback;
1354 subtest 'get_savings tests' => sub {
1358 $schema->storage->txn_begin;
1360 my $library = $builder->build_object({ class => 'Koha::Libraries' });
1361 my $patron = $builder->build_object({ class => 'Koha::Patrons' }, { value => { branchcode => $library->branchcode } });
1363 t::lib::Mocks::mock_userenv({ patron => $patron, branchcode => $library->branchcode });
1365 my $biblio = $builder->build_sample_biblio;
1366 my $item1 = $builder->build_sample_item(
1368 biblionumber => $biblio->biblionumber,
1369 library => $library->branchcode,
1370 replacementprice => rand(20),
1373 my $item2 = $builder->build_sample_item(
1375 biblionumber => $biblio->biblionumber,
1376 library => $library->branchcode,
1377 replacementprice => rand(20),
1381 is( $patron->get_savings, 0, 'No checkouts, no savings' );
1383 # Add an old checkout with deleted itemnumber
1384 $builder->build_object({ class => 'Koha::Old::Checkouts', value => { itemnumber => undef, borrowernumber => $patron->id } });
1386 is( $patron->get_savings, 0, 'No checkouts with itemnumber, no savings' );
1388 AddIssue( $patron->unblessed, $item1->barcode );
1389 AddIssue( $patron->unblessed, $item2->barcode );
1391 my $savings = $patron->get_savings;
1392 is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current issues" );
1394 AddReturn( $item2->barcode, $item2->homebranch );
1396 $savings = $patron->get_savings;
1397 is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current and old issues" );
1399 $schema->storage->txn_rollback;
1402 subtest 'update privacy tests' => sub {
1406 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy => 1 } });
1408 my $old_checkout = $builder->build_object({ class => 'Koha::Old::Checkouts', value => { borrowernumber => $patron->id } });
1410 t::lib::Mocks::mock_preference( 'AnonymousPatron', '0' );
1412 $patron->privacy(2); #set to never
1414 throws_ok{ $patron->store } 'Koha::Exceptions::Patron::FailedAnonymizing', 'We throw an exception when anonymizing fails';
1416 $old_checkout->discard_changes; #refresh from db
1417 $patron->discard_changes;
1419 is( $old_checkout->borrowernumber, $patron->id, "When anonymizing fails, we don't clear the checkouts");
1420 is( $patron->privacy(), 1, "When anonymizing fails, we don't chaneg the privacy");
1422 my $anon_patron = $builder->build_object({ class => 'Koha::Patrons'});
1423 t::lib::Mocks::mock_preference( 'AnonymousPatron', $anon_patron->id );
1425 $patron->privacy(2)->store(); #set to never
1427 $old_checkout->discard_changes; #refresh from db
1428 $patron->discard_changes;
1430 is( $old_checkout->borrowernumber, $anon_patron->id, "Checkout is successfully anonymized");
1431 is( $patron->privacy(), 2, "Patron privacy is successfully updated");