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 => 18;
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 Koha::Patron::Attribute::Types->search->delete;
382 my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
383 my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
385 t::lib::Mocks::mock_userenv({ patron => $patron_1 });
387 my $attribute_type1 = Koha::Patron::Attribute::Type->new(
390 description => 'my description1',
394 my $attribute_type2 = Koha::Patron::Attribute::Type->new(
397 description => 'my description2',
399 staff_searchable => 1
403 my $new_library = $builder->build( { source => 'Branch' } );
404 my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
405 { code => 'my code3', description => 'my description3' } )->store;
406 $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
408 my $attributes_for_1 = [
410 attribute => 'my attribute1',
411 code => $attribute_type1->code(),
414 attribute => 'my attribute2',
415 code => $attribute_type2->code(),
418 attribute => 'my attribute limited',
419 code => $attribute_type_limited->code(),
423 my $attributes_for_2 = [
425 attribute => 'my attribute12',
426 code => $attribute_type1->code(),
429 attribute => 'my attribute limited 2',
430 code => $attribute_type_limited->code(),
434 my $extended_attributes = $patron_1->extended_attributes;
435 is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
436 is( $extended_attributes->count, 0, 'There should not be attribute yet');
438 $patron_1->extended_attributes->filter_by_branch_limitations->delete;
439 $patron_2->extended_attributes->filter_by_branch_limitations->delete;
440 $patron_1->extended_attributes($attributes_for_1);
441 $patron_2->extended_attributes($attributes_for_2);
443 my $extended_attributes_for_1 = $patron_1->extended_attributes;
444 is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
446 my $extended_attributes_for_2 = $patron_2->extended_attributes;
447 is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
449 my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
450 is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
452 $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
453 is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
455 my $expected_attributes_for_2 = [
457 code => $attribute_type1->code(),
458 attribute => 'my attribute12',
461 code => $attribute_type_limited->code(),
462 attribute => 'my attribute limited 2',
465 # Sorting them by code
466 $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
467 my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
472 code => $extended_attributes_for_2[0]->code,
473 attribute => $extended_attributes_for_2[0]->attribute
476 code => $extended_attributes_for_2[1]->code,
477 attribute => $extended_attributes_for_2[1]->attribute
480 $expected_attributes_for_2
483 # TODO - What about multiple? POD explains the problem
484 my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
485 is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
487 # Test branch limitations
488 t::lib::Mocks::mock_userenv({ patron => $patron_2 });
490 $extended_attributes_for_1 = $patron_1->extended_attributes;
491 is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
494 $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
495 is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
498 my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
499 is( $limited_value->attribute, 'my attribute limited', );
501 ## Do we need a filtered?
502 #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
503 #is( $limited_value, undef, );
505 $schema->storage->txn_rollback;
507 subtest 'non-repeatable attributes tests' => sub {
511 $schema->storage->txn_begin;
512 Koha::Patron::Attribute::Types->search->delete;
514 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
515 my $attribute_type = $builder->build_object(
517 class => 'Koha::Patron::Attribute::Types',
518 value => { repeatable => 0 }
522 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
526 $patron->extended_attributes(
528 { code => $attribute_type->code, attribute => 'a' },
529 { code => $attribute_type->code, attribute => 'b' }
533 'Koha::Exceptions::Patron::Attribute::NonRepeatable',
534 'Exception thrown on non-repeatable attribute';
536 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
538 $schema->storage->txn_rollback;
542 subtest 'unique attributes tests' => sub {
546 $schema->storage->txn_begin;
547 Koha::Patron::Attribute::Types->search->delete;
549 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
550 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
552 my $attribute_type_1 = $builder->build_object(
554 class => 'Koha::Patron::Attribute::Types',
555 value => { unique => 1 }
559 my $attribute_type_2 = $builder->build_object(
561 class => 'Koha::Patron::Attribute::Types',
562 value => { unique => 0 }
566 is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
567 is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
569 $patron_1->extended_attributes(
571 { code => $attribute_type_1->code, attribute => 'a' },
572 { code => $attribute_type_2->code, attribute => 'a' }
578 $patron_2->extended_attributes(
580 { code => $attribute_type_1->code, attribute => 'a' },
581 { code => $attribute_type_2->code, attribute => 'a' }
585 'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
586 'Exception thrown on unique attribute';
588 is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
589 is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
591 $schema->storage->txn_rollback;
595 subtest 'invalid type attributes tests' => sub {
599 $schema->storage->txn_begin;
600 Koha::Patron::Attribute::Types->search->delete;
602 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
604 my $attribute_type_1 = $builder->build_object(
606 class => 'Koha::Patron::Attribute::Types',
607 value => { repeatable => 0 }
611 my $attribute_type_2 = $builder->build_object(
613 class => 'Koha::Patron::Attribute::Types'
617 my $type_2 = $attribute_type_2->code;
618 $attribute_type_2->delete;
620 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
624 $patron->extended_attributes(
626 { code => $attribute_type_1->code, attribute => 'a' },
627 { code => $attribute_type_2->code, attribute => 'b' }
631 'Koha::Exceptions::Patron::Attribute::InvalidType',
632 'Exception thrown on invalid attribute type';
634 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
636 $schema->storage->txn_rollback;
640 subtest 'globally mandatory attributes tests' => sub {
644 $schema->storage->txn_begin;
645 Koha::Patron::Attribute::Types->search->delete;
647 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
649 my $attribute_type_1 = $builder->build_object(
651 class => 'Koha::Patron::Attribute::Types',
652 value => { mandatory => 1, class => 'a' }
656 my $attribute_type_2 = $builder->build_object(
658 class => 'Koha::Patron::Attribute::Types',
659 value => { mandatory => 0, class => 'a' }
663 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
667 $patron->extended_attributes(
669 { code => $attribute_type_2->code, attribute => 'b' }
673 'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
674 'Exception thrown on missing mandatory attribute type';
676 is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
678 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
680 $patron->extended_attributes(
682 { code => $attribute_type_1->code, attribute => 'b' }
686 is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
688 $schema->storage->txn_rollback;
694 subtest 'can_log_into() tests' => sub {
698 $schema->storage->txn_begin;
700 my $patron = $builder->build_object(
702 class => 'Koha::Patrons',
708 my $library = $builder->build_object({ class => 'Koha::Libraries' });
710 t::lib::Mocks::mock_preference('IndependentBranches', 1);
712 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
713 ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
715 # make it a superlibrarian
716 $patron->set({ flags => 1 })->store->discard_changes;
717 ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
719 t::lib::Mocks::mock_preference('IndependentBranches', 0);
721 # No special permissions
722 $patron->set({ flags => undef })->store->discard_changes;
723 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
724 ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
726 $schema->storage->txn_rollback;
729 subtest 'can_request_article() tests' => sub {
733 $schema->storage->txn_begin;
735 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
737 my $item = $builder->build_sample_item;
739 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
740 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
741 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
743 t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
745 Koha::CirculationRules->set_rule(
747 categorycode => undef,
748 branchcode => $library_1->id,
749 rule_name => 'open_article_requests_limit',
754 $builder->build_object(
756 class => 'Koha::ArticleRequests',
757 value => { status => 'REQUESTED', borrowernumber => $patron->id }
760 $builder->build_object(
762 class => 'Koha::ArticleRequests',
763 value => { status => 'PENDING', borrowernumber => $patron->id }
766 $builder->build_object(
768 class => 'Koha::ArticleRequests',
769 value => { status => 'PROCESSING', borrowernumber => $patron->id }
772 $builder->build_object(
774 class => 'Koha::ArticleRequests',
775 value => { status => 'CANCELED', borrowernumber => $patron->id }
780 $patron->can_request_article( $library_1->id ),
781 '3 current requests, 4 is the limit: allowed'
784 # Completed request, same day
785 my $completed = $builder->build_object(
787 class => 'Koha::ArticleRequests',
789 status => 'COMPLETED',
790 borrowernumber => $patron->id
795 ok( !$patron->can_request_article( $library_1->id ),
796 '3 current requests and a completed one the same day: denied' );
798 $completed->updated_on(
799 dt_from_string->add( days => -1 )->set(
806 ok( $patron->can_request_article( $library_1->id ),
807 '3 current requests and a completed one the day before: allowed' );
809 Koha::CirculationRules->set_rule(
811 categorycode => undef,
812 branchcode => $library_2->id,
813 rule_name => 'open_article_requests_limit',
818 ok( !$patron->can_request_article,
819 'Not passing the library_id param makes it fallback to userenv: denied'
822 $schema->storage->txn_rollback;
825 subtest 'article_requests() tests' => sub {
829 $schema->storage->txn_begin;
831 my $library = $builder->build_object({ class => 'Koha::Libraries' });
832 t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
834 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
836 my $article_requests = $patron->article_requests;
837 is( ref($article_requests), 'Koha::ArticleRequests',
838 'In scalar context, type is correct' );
839 is( $article_requests->count, 0, 'No article requests' );
841 foreach my $i ( 0 .. 3 ) {
843 my $item = $builder->build_sample_item;
845 Koha::ArticleRequest->new(
847 borrowernumber => $patron->id,
848 biblionumber => $item->biblionumber,
849 itemnumber => $item->id,
855 $article_requests = $patron->article_requests;
856 is( $article_requests->count, 4, '4 article requests' );
858 $schema->storage->txn_rollback;
862 subtest 'can_patron_change_staff_only_lists() tests' => sub {
866 $schema->storage->txn_begin;
868 # make a user with no special permissions
869 my $patron = $builder->build_object(
871 class => 'Koha::Patrons',
877 is( $patron->can_patron_change_staff_only_lists(), 0, 'Patron without permissions cannot change staff only lists');
879 # make it a 'Catalogue' permission
880 $patron->set({ flags => 4 })->store->discard_changes;
881 is( $patron->can_patron_change_staff_only_lists(), 1, 'Catalogue patron can change staff only lists');
884 # make it a superlibrarian
885 $patron->set({ flags => 1 })->store->discard_changes;
886 is( $patron->can_patron_change_staff_only_lists(), 1, 'Superlibrarian patron can change staff only lists');
888 $schema->storage->txn_rollback;
891 subtest 'password expiration tests' => sub {
895 $schema->storage->txn_begin;
896 my $date = dt_from_string();
897 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
898 password_expiry_days => 10,
899 require_strong_password => 0,
902 my $patron = $builder->build_object({ class=> 'Koha::Patrons', value => {
903 categorycode => $category->categorycode,
908 $patron->delete()->store()->discard_changes(); # Make sure we are storing a 'new' patron
910 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd() , "Password expiration date set correctly on patron creation");
912 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
913 categorycode => $category->categorycode,
917 $patron->delete()->store()->discard_changes();
919 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if patron does not have a password");
921 $category->password_expiry_days(undef)->store();
922 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
923 categorycode => $category->categorycode
926 $patron->delete()->store()->discard_changes();
927 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if category does not have expiry days set");
929 $schema->storage->txn_rollback;
931 subtest 'password_expired' => sub {
935 $schema->storage->txn_begin;
936 my $date = dt_from_string();
937 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
938 password_expiration_date => undef
941 is( $patron->password_expired, 0, "Patron with no password expiration date, password not expired");
942 $patron->password_expiration_date( $date )->store;
943 $patron->discard_changes();
944 is( $patron->password_expired, 1, "Patron with password expiration date of today, password expired");
945 $date->subtract( days => 1 );
946 $patron->password_expiration_date( $date )->store;
947 $patron->discard_changes();
948 is( $patron->password_expired, 1, "Patron with password expiration date in past, password expired");
950 $schema->storage->txn_rollback;
953 subtest 'set_password' => sub {
957 $schema->storage->txn_begin;
959 my $date = dt_from_string();
960 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
961 password_expiry_days => 10
964 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
965 categorycode => $category->categorycode,
966 password_expiration_date => $date->subtract( days => 1 )
969 is( $patron->password_expired, 1, "Patron password is expired");
971 $date = dt_from_string();
972 $patron->set_password({ password => "kitten", skip_validation => 1 })->discard_changes();
973 is( $patron->password_expired, 0, "Patron password no longer expired when new password set");
974 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd(), "Password expiration date set correctly on patron creation");
977 $category->password_expiry_days( undef )->store();
978 $patron->set_password({ password => "puppies", skip_validation => 1 })->discard_changes();
979 is( $patron->password_expiration_date(), undef, "Password expiration date is unset if category does not have expiry days");
981 $schema->storage->txn_rollback;
986 subtest 'safe_to_delete() tests' => sub {
990 $schema->storage->txn_begin;
992 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
994 ## Make it the anonymous
995 t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
997 ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
998 my $message = $patron->safe_to_delete->messages->[0];
999 is( $message->type, 'error', 'Type is error' );
1000 is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
1002 t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
1004 ## Make it have a checkout
1005 my $checkout = $builder->build_object(
1007 class => 'Koha::Checkouts',
1008 value => { borrowernumber => $patron->id }
1012 ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
1013 $message = $patron->safe_to_delete->messages->[0];
1014 is( $message->type, 'error', 'Type is error' );
1015 is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
1019 ## Make it have a guarantee
1020 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
1021 $builder->build_object({ class => 'Koha::Patrons' })
1022 ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
1024 ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
1025 $message = $patron->safe_to_delete->messages->[0];
1026 is( $message->type, 'error', 'Type is error' );
1027 is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
1030 $patron->guarantee_relationships->delete;
1032 ## Make it have debt
1033 my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
1035 ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
1036 $message = $patron->safe_to_delete->messages->[0];
1037 is( $message->type, 'error', 'Type is error' );
1038 is( $message->message, 'has_debt', 'Cannot delete, has debt' );
1040 $patron->account->pay({ amount => 10, debits => [ $debit ] });
1043 ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
1044 my $messages = $patron->safe_to_delete->messages;
1045 is_deeply( $messages, [], 'Patron can be deleted, no messages' );
1048 subtest 'article_request_fee() tests' => sub {
1052 $schema->storage->txn_begin;
1054 # Cleanup, to avoid interference
1055 Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
1057 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1059 my $item = $builder->build_sample_item;
1061 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1062 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1063 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1065 # Rule that should never be picked, because the patron's category is always picked
1066 Koha::CirculationRules->set_rule(
1067 { categorycode => undef,
1068 branchcode => undef,
1069 rule_name => 'article_request_fee',
1074 is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
1076 Koha::CirculationRules->set_rule(
1077 { categorycode => $patron->categorycode,
1078 branchcode => undef,
1079 rule_name => 'article_request_fee',
1084 Koha::CirculationRules->set_rule(
1085 { categorycode => $patron->categorycode,
1086 branchcode => $library_1->id,
1087 rule_name => 'article_request_fee',
1092 is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
1094 t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
1096 is( $patron->article_request_fee(), 3, 'env used correctly' );
1098 $schema->storage->txn_rollback;
1101 subtest 'add_article_request_fee_if_needed() tests' => sub {
1105 $schema->storage->txn_begin;
1109 my $patron_mock = Test::MockModule->new('Koha::Patron');
1110 $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
1112 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1114 is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
1116 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1117 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1118 my $staff = $builder->build_object( { class => 'Koha::Patrons' } );
1119 my $item = $builder->build_sample_item;
1121 t::lib::Mocks::mock_userenv(
1122 { branchcode => $library_1->id, patron => $staff } );
1124 my $debit = $patron->add_article_request_fee_if_needed();
1125 is( $debit, undef, 'No fee, no debit line' );
1130 $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
1131 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1132 is( $debit->amount, $amount,
1133 'amount set to $patron->article_request_fee value' );
1134 is( $debit->manager_id, $staff->id,
1135 'manager_id set to userenv session user' );
1136 is( $debit->branchcode, $library_1->id,
1137 'branchcode set to userenv session library' );
1138 is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1139 'debit_type_code set correctly' );
1140 is( $debit->itemnumber, $item->id,
1141 'itemnumber set correctly' );
1145 $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1146 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1147 is( $debit->amount, $amount,
1148 'amount set to $patron->article_request_fee value' );
1149 is( $debit->branchcode, $library_2->id,
1150 'branchcode set to userenv session library' );
1151 is( $debit->itemnumber, undef,
1152 'itemnumber set correctly to undef' );
1154 $schema->storage->txn_rollback;
1157 subtest 'messages' => sub {
1160 $schema->storage->txn_begin;
1162 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1163 my $messages = $patron->messages;
1164 is( $messages->count, 0, "No message yet" );
1165 my $message_1 = $builder->build_object(
1167 class => 'Koha::Patron::Messages',
1168 value => { borrowernumber => $patron->borrowernumber }
1171 my $message_2 = $builder->build_object(
1173 class => 'Koha::Patron::Messages',
1174 value => { borrowernumber => $patron->borrowernumber }
1178 $messages = $patron->messages;
1179 is( $messages->count, 2, "There are two messages for this patron" );
1180 is( $messages->next->message, $message_1->message );
1181 is( $messages->next->message, $message_2->message );
1182 $schema->storage->txn_rollback;
1185 subtest 'recalls() tests' => sub {
1189 $schema->storage->txn_begin;
1191 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1192 my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
1193 my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
1194 my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
1195 my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
1198 { biblio_id => $biblio1->biblionumber,
1199 patron_id => $patron->borrowernumber,
1200 item_id => $item1->itemnumber,
1201 pickup_library_id => $patron->branchcode,
1202 created_date => \'NOW()',
1207 { biblio_id => $biblio2->biblionumber,
1208 patron_id => $patron->borrowernumber,
1209 item_id => $item2->itemnumber,
1210 pickup_library_id => $patron->branchcode,
1211 created_date => \'NOW()',
1216 { biblio_id => $biblio1->biblionumber,
1217 patron_id => $patron->borrowernumber,
1219 pickup_library_id => $patron->branchcode,
1220 created_date => \'NOW()',
1224 my $recall = Koha::Recall->new(
1225 { biblio_id => $biblio1->biblionumber,
1226 patron_id => $patron->borrowernumber,
1228 pickup_library_id => $patron->branchcode,
1229 created_date => \'NOW()',
1233 $recall->set_cancelled;
1235 is( $patron->recalls->count, 4, "Correctly gets this patron's recalls" );
1236 is( $patron->recalls->filter_by_current->count, 3, "Correctly gets this patron's active recalls" );
1237 is( $patron->recalls->filter_by_current->search( { biblio_id => $biblio1->biblionumber } )->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
1239 $schema->storage->txn_rollback;
1242 subtest 'encode_secret and decoded_secret' => sub {
1244 $schema->storage->txn_begin;
1246 t::lib::Mocks::mock_config('encryption_key', 't0P_secret');
1248 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1249 is( $patron->decoded_secret, undef, 'TestBuilder does not initialize it' );
1250 $patron->secret(q{});
1251 is( $patron->decoded_secret, q{}, 'Empty string case' );
1253 $patron->encode_secret('encrypt_me'); # Note: lazy testing; should be base32 string normally.
1254 is( length($patron->secret) > 0, 1, 'Secret length' );
1255 isnt( $patron->secret, 'encrypt_me', 'Encrypted column' );
1256 is( $patron->decoded_secret, 'encrypt_me', 'Decrypted column' );
1258 $schema->storage->txn_rollback;