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', category_code => undef }
656 my $attribute_type_2 = $builder->build_object(
658 class => 'Koha::Patron::Attribute::Types',
659 value => { mandatory => 0, class => 'a', category_code => undef }
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;
692 subtest 'limited category mandatory attributes tests' => sub {
696 $schema->storage->txn_begin;
697 Koha::Patron::Attribute::Types->search->delete;
699 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
701 my $attribute_type_1 = $builder->build_object(
703 class => 'Koha::Patron::Attribute::Types',
704 value => { mandatory => 1, class => 'a', category_code => $patron->categorycode }
708 $patron->extended_attributes(
710 { code => $attribute_type_1->code, attribute => 'a' }
714 is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
716 $patron = $builder->build_object({ class => 'Koha::Patrons' });
717 # new patron, new category - they shouldn't be required to have any attributes
720 ok( $patron->extended_attributes([]), "We can set no attributes, mandatory attribute for other category not required");
729 subtest 'can_log_into() tests' => sub {
733 $schema->storage->txn_begin;
735 my $patron = $builder->build_object(
737 class => 'Koha::Patrons',
743 my $library = $builder->build_object({ class => 'Koha::Libraries' });
745 t::lib::Mocks::mock_preference('IndependentBranches', 1);
747 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
748 ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
750 # make it a superlibrarian
751 $patron->set({ flags => 1 })->store->discard_changes;
752 ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
754 t::lib::Mocks::mock_preference('IndependentBranches', 0);
756 # No special permissions
757 $patron->set({ flags => undef })->store->discard_changes;
758 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
759 ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
761 $schema->storage->txn_rollback;
764 subtest 'can_request_article() tests' => sub {
768 $schema->storage->txn_begin;
770 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
772 my $item = $builder->build_sample_item;
774 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
775 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
776 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
778 t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
780 Koha::CirculationRules->set_rule(
782 categorycode => undef,
783 branchcode => $library_1->id,
784 rule_name => 'open_article_requests_limit',
789 $builder->build_object(
791 class => 'Koha::ArticleRequests',
792 value => { status => 'REQUESTED', borrowernumber => $patron->id }
795 $builder->build_object(
797 class => 'Koha::ArticleRequests',
798 value => { status => 'PENDING', borrowernumber => $patron->id }
801 $builder->build_object(
803 class => 'Koha::ArticleRequests',
804 value => { status => 'PROCESSING', borrowernumber => $patron->id }
807 $builder->build_object(
809 class => 'Koha::ArticleRequests',
810 value => { status => 'CANCELED', borrowernumber => $patron->id }
815 $patron->can_request_article( $library_1->id ),
816 '3 current requests, 4 is the limit: allowed'
819 # Completed request, same day
820 my $completed = $builder->build_object(
822 class => 'Koha::ArticleRequests',
824 status => 'COMPLETED',
825 borrowernumber => $patron->id
830 ok( !$patron->can_request_article( $library_1->id ),
831 '3 current requests and a completed one the same day: denied' );
833 $completed->updated_on(
834 dt_from_string->add( days => -1 )->set(
841 ok( $patron->can_request_article( $library_1->id ),
842 '3 current requests and a completed one the day before: allowed' );
844 Koha::CirculationRules->set_rule(
846 categorycode => undef,
847 branchcode => $library_2->id,
848 rule_name => 'open_article_requests_limit',
853 ok( !$patron->can_request_article,
854 'Not passing the library_id param makes it fallback to userenv: denied'
857 $schema->storage->txn_rollback;
860 subtest 'article_requests() tests' => sub {
864 $schema->storage->txn_begin;
866 my $library = $builder->build_object({ class => 'Koha::Libraries' });
867 t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
869 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
871 my $article_requests = $patron->article_requests;
872 is( ref($article_requests), 'Koha::ArticleRequests',
873 'In scalar context, type is correct' );
874 is( $article_requests->count, 0, 'No article requests' );
876 foreach my $i ( 0 .. 3 ) {
878 my $item = $builder->build_sample_item;
880 Koha::ArticleRequest->new(
882 borrowernumber => $patron->id,
883 biblionumber => $item->biblionumber,
884 itemnumber => $item->id,
890 $article_requests = $patron->article_requests;
891 is( $article_requests->count, 4, '4 article requests' );
893 $schema->storage->txn_rollback;
897 subtest 'can_patron_change_staff_only_lists() tests' => sub {
901 $schema->storage->txn_begin;
903 # make a user with no special permissions
904 my $patron = $builder->build_object(
906 class => 'Koha::Patrons',
912 is( $patron->can_patron_change_staff_only_lists(), 0, 'Patron without permissions cannot change staff only lists');
914 # make it a 'Catalogue' permission
915 $patron->set({ flags => 4 })->store->discard_changes;
916 is( $patron->can_patron_change_staff_only_lists(), 1, 'Catalogue patron can change staff only lists');
919 # make it a superlibrarian
920 $patron->set({ flags => 1 })->store->discard_changes;
921 is( $patron->can_patron_change_staff_only_lists(), 1, 'Superlibrarian patron can change staff only lists');
923 $schema->storage->txn_rollback;
926 subtest 'password expiration tests' => sub {
930 $schema->storage->txn_begin;
931 my $date = dt_from_string();
932 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
933 password_expiry_days => 10,
934 require_strong_password => 0,
937 my $patron = $builder->build_object({ class=> 'Koha::Patrons', value => {
938 categorycode => $category->categorycode,
943 $patron->delete()->store()->discard_changes(); # Make sure we are storing a 'new' patron
945 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd() , "Password expiration date set correctly on patron creation");
947 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
948 categorycode => $category->categorycode,
952 $patron->delete()->store()->discard_changes();
954 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if patron does not have a password");
956 $category->password_expiry_days(undef)->store();
957 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
958 categorycode => $category->categorycode
961 $patron->delete()->store()->discard_changes();
962 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if category does not have expiry days set");
964 $schema->storage->txn_rollback;
966 subtest 'password_expired' => sub {
970 $schema->storage->txn_begin;
971 my $date = dt_from_string();
972 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
973 password_expiration_date => undef
976 is( $patron->password_expired, 0, "Patron with no password expiration date, password not expired");
977 $patron->password_expiration_date( $date )->store;
978 $patron->discard_changes();
979 is( $patron->password_expired, 1, "Patron with password expiration date of today, password expired");
980 $date->subtract( days => 1 );
981 $patron->password_expiration_date( $date )->store;
982 $patron->discard_changes();
983 is( $patron->password_expired, 1, "Patron with password expiration date in past, password expired");
985 $schema->storage->txn_rollback;
988 subtest 'set_password' => sub {
992 $schema->storage->txn_begin;
994 my $date = dt_from_string();
995 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
996 password_expiry_days => 10
999 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1000 categorycode => $category->categorycode,
1001 password_expiration_date => $date->subtract( days => 1 )
1004 is( $patron->password_expired, 1, "Patron password is expired");
1006 $date = dt_from_string();
1007 $patron->set_password({ password => "kitten", skip_validation => 1 })->discard_changes();
1008 is( $patron->password_expired, 0, "Patron password no longer expired when new password set");
1009 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd(), "Password expiration date set correctly on patron creation");
1012 $category->password_expiry_days( undef )->store();
1013 $patron->set_password({ password => "puppies", skip_validation => 1 })->discard_changes();
1014 is( $patron->password_expiration_date(), undef, "Password expiration date is unset if category does not have expiry days");
1016 $schema->storage->txn_rollback;
1021 subtest 'safe_to_delete() tests' => sub {
1025 $schema->storage->txn_begin;
1027 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1029 ## Make it the anonymous
1030 t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
1032 ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
1033 my $message = $patron->safe_to_delete->messages->[0];
1034 is( $message->type, 'error', 'Type is error' );
1035 is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
1037 t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
1039 ## Make it have a checkout
1040 my $checkout = $builder->build_object(
1042 class => 'Koha::Checkouts',
1043 value => { borrowernumber => $patron->id }
1047 ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
1048 $message = $patron->safe_to_delete->messages->[0];
1049 is( $message->type, 'error', 'Type is error' );
1050 is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
1054 ## Make it have a guarantee
1055 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
1056 $builder->build_object({ class => 'Koha::Patrons' })
1057 ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
1059 ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
1060 $message = $patron->safe_to_delete->messages->[0];
1061 is( $message->type, 'error', 'Type is error' );
1062 is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
1065 $patron->guarantee_relationships->delete;
1067 ## Make it have debt
1068 my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
1070 ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
1071 $message = $patron->safe_to_delete->messages->[0];
1072 is( $message->type, 'error', 'Type is error' );
1073 is( $message->message, 'has_debt', 'Cannot delete, has debt' );
1075 $patron->account->pay({ amount => 10, debits => [ $debit ] });
1078 ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
1079 my $messages = $patron->safe_to_delete->messages;
1080 is_deeply( $messages, [], 'Patron can be deleted, no messages' );
1083 subtest 'article_request_fee() tests' => sub {
1087 $schema->storage->txn_begin;
1089 # Cleanup, to avoid interference
1090 Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
1092 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1094 my $item = $builder->build_sample_item;
1096 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1097 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1098 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1100 # Rule that should never be picked, because the patron's category is always picked
1101 Koha::CirculationRules->set_rule(
1102 { categorycode => undef,
1103 branchcode => undef,
1104 rule_name => 'article_request_fee',
1109 is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
1111 Koha::CirculationRules->set_rule(
1112 { categorycode => $patron->categorycode,
1113 branchcode => undef,
1114 rule_name => 'article_request_fee',
1119 Koha::CirculationRules->set_rule(
1120 { categorycode => $patron->categorycode,
1121 branchcode => $library_1->id,
1122 rule_name => 'article_request_fee',
1127 is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
1129 t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
1131 is( $patron->article_request_fee(), 3, 'env used correctly' );
1133 $schema->storage->txn_rollback;
1136 subtest 'add_article_request_fee_if_needed() tests' => sub {
1140 $schema->storage->txn_begin;
1144 my $patron_mock = Test::MockModule->new('Koha::Patron');
1145 $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
1147 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1149 is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
1151 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1152 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1153 my $staff = $builder->build_object( { class => 'Koha::Patrons' } );
1154 my $item = $builder->build_sample_item;
1156 t::lib::Mocks::mock_userenv(
1157 { branchcode => $library_1->id, patron => $staff } );
1159 my $debit = $patron->add_article_request_fee_if_needed();
1160 is( $debit, undef, 'No fee, no debit line' );
1165 $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
1166 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1167 is( $debit->amount, $amount,
1168 'amount set to $patron->article_request_fee value' );
1169 is( $debit->manager_id, $staff->id,
1170 'manager_id set to userenv session user' );
1171 is( $debit->branchcode, $library_1->id,
1172 'branchcode set to userenv session library' );
1173 is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1174 'debit_type_code set correctly' );
1175 is( $debit->itemnumber, $item->id,
1176 'itemnumber set correctly' );
1180 $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1181 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1182 is( $debit->amount, $amount,
1183 'amount set to $patron->article_request_fee value' );
1184 is( $debit->branchcode, $library_2->id,
1185 'branchcode set to userenv session library' );
1186 is( $debit->itemnumber, undef,
1187 'itemnumber set correctly to undef' );
1189 $schema->storage->txn_rollback;
1192 subtest 'messages' => sub {
1195 $schema->storage->txn_begin;
1197 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1198 my $messages = $patron->messages;
1199 is( $messages->count, 0, "No message yet" );
1200 my $message_1 = $builder->build_object(
1202 class => 'Koha::Patron::Messages',
1203 value => { borrowernumber => $patron->borrowernumber }
1206 my $message_2 = $builder->build_object(
1208 class => 'Koha::Patron::Messages',
1209 value => { borrowernumber => $patron->borrowernumber }
1213 $messages = $patron->messages;
1214 is( $messages->count, 2, "There are two messages for this patron" );
1215 is( $messages->next->message, $message_1->message );
1216 is( $messages->next->message, $message_2->message );
1217 $schema->storage->txn_rollback;
1220 subtest 'recalls() tests' => sub {
1224 $schema->storage->txn_begin;
1226 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1227 my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
1228 my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
1229 my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
1230 my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
1233 { biblio_id => $biblio1->biblionumber,
1234 patron_id => $patron->borrowernumber,
1235 item_id => $item1->itemnumber,
1236 pickup_library_id => $patron->branchcode,
1237 created_date => \'NOW()',
1242 { biblio_id => $biblio2->biblionumber,
1243 patron_id => $patron->borrowernumber,
1244 item_id => $item2->itemnumber,
1245 pickup_library_id => $patron->branchcode,
1246 created_date => \'NOW()',
1251 { biblio_id => $biblio1->biblionumber,
1252 patron_id => $patron->borrowernumber,
1254 pickup_library_id => $patron->branchcode,
1255 created_date => \'NOW()',
1259 my $recall = Koha::Recall->new(
1260 { biblio_id => $biblio1->biblionumber,
1261 patron_id => $patron->borrowernumber,
1263 pickup_library_id => $patron->branchcode,
1264 created_date => \'NOW()',
1268 $recall->set_cancelled;
1270 is( $patron->recalls->count, 4, "Correctly gets this patron's recalls" );
1271 is( $patron->recalls->filter_by_current->count, 3, "Correctly gets this patron's active recalls" );
1272 is( $patron->recalls->filter_by_current->search( { biblio_id => $biblio1->biblionumber } )->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
1274 $schema->storage->txn_rollback;
1277 subtest 'encode_secret and decoded_secret' => sub {
1279 $schema->storage->txn_begin;
1281 t::lib::Mocks::mock_config('encryption_key', 't0P_secret');
1283 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1284 is( $patron->decoded_secret, undef, 'TestBuilder does not initialize it' );
1285 $patron->secret(q{});
1286 is( $patron->decoded_secret, q{}, 'Empty string case' );
1288 $patron->encode_secret('encrypt_me'); # Note: lazy testing; should be base32 string normally.
1289 is( length($patron->secret) > 0, 1, 'Secret length' );
1290 isnt( $patron->secret, 'encrypt_me', 'Encrypted column' );
1291 is( $patron->decoded_secret, 'encrypt_me', 'Decrypted column' );
1293 $schema->storage->txn_rollback;