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 => 8;
27 use Koha::DateUtils qw(dt_from_string);
29 use Koha::Patron::Relationships;
31 use t::lib::TestBuilder;
34 my $schema = Koha::Database->new->schema;
35 my $builder = t::lib::TestBuilder->new;
37 subtest 'add_guarantor() tests' => sub {
41 $schema->storage->txn_begin;
43 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'father1|father2' );
45 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
46 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
49 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber }); }
50 'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
51 'Exception is thrown as no relationship passed';
53 is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
56 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father' }); }
57 'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
58 'Exception is thrown as a wrong relationship was passed';
60 is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
62 $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father1' });
64 my $guarantors = $patron_1->guarantor_relationships;
66 is( $guarantors->count, 1, 'No guarantors added' );
70 open STDERR, '>', '/dev/null';
72 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father2' }); }
73 'Koha::Exceptions::Patron::Relationship::DuplicateRelationship',
74 'Exception is thrown for duplicated relationship';
78 $schema->storage->txn_rollback;
81 subtest 'relationships_debt() tests' => sub {
85 $schema->storage->txn_begin;
87 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
89 my $parent_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 1" } });
90 my $parent_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 2" } });
91 my $child_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Child 1" } });
92 my $child_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Child 2" } });
94 $child_1->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
95 $child_1->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
96 $child_2->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
97 $child_2->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
99 is( $child_1->guarantor_relationships->guarantors->count, 2, 'Child 1 has correct number of guarantors' );
100 is( $child_2->guarantor_relationships->guarantors->count, 2, 'Child 2 has correct number of guarantors' );
101 is( $parent_1->guarantee_relationships->guarantees->count, 2, 'Parent 1 has correct number of guarantees' );
102 is( $parent_2->guarantee_relationships->guarantees->count, 2, 'Parent 2 has correct number of guarantees' );
104 my $patrons = [ $parent_1, $parent_2, $child_1, $child_2 ];
106 # First test: No debt
107 my ($parent1_debt, $parent2_debt, $child1_debt, $child2_debt) = (0,0,0,0);
108 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
110 # Add debt to child_2
112 $child_2->account->add_debit({ type => 'ACCOUNT', amount => $child2_debt, interface => 'commandline' });
113 is( $child_2->account->non_issues_charges, $child2_debt, 'Debt added to Child 2' );
114 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
117 $parent_1->account->add_debit({ type => 'ACCOUNT', amount => $parent1_debt, interface => 'commandline' });
118 is( $parent_1->account->non_issues_charges, $parent1_debt, 'Debt added to Parent 1' );
119 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
122 $parent_2->account->add_debit({ type => 'ACCOUNT', amount => $parent2_debt, interface => 'commandline' });
123 is( $parent_2->account->non_issues_charges, $parent2_debt, 'Parent 2 owes correct amount' );
124 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
127 $child_1->account->add_debit({ type => 'ACCOUNT', amount => $child1_debt, interface => 'commandline' });
128 is( $child_1->account->non_issues_charges, $child1_debt, 'Child 1 owes correct amount' );
129 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
131 $schema->storage->txn_rollback;
134 sub _test_combinations {
135 my ( $patrons, $parent1_debt, $parent2_debt, $child1_debt, $child2_debt ) = @_;
138 # P1 => P1 + C1 + C2 ( - P1 ) ( + P2 )
139 # P2 => P2 + C1 + C2 ( - P2 ) ( + P1 )
140 # C1 => P1 + P2 + C1 + C2 ( - C1 )
141 # C2 => P1 + P2 + C1 + C2 ( - C2 )
143 # 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
144 for my $i ( 0 .. 7 ) {
145 my ( $only_this_guarantor, $include_guarantors, $include_this_patron )
146 = split '', sprintf( "%03b", $i );
147 for my $patron ( @$patrons ) {
148 if ( $only_this_guarantor
149 && !$patron->guarantee_relationships->count )
152 $patron->relationships_debt(
154 only_this_guarantor => $only_this_guarantor,
155 include_guarantors => $include_guarantors,
156 include_this_patron => $include_this_patron
160 'Koha::Exceptions::BadParameter',
161 'Exception is thrown as patron is not a guarantor';
167 if ( $patron->firstname eq 'Parent 1' ) {
168 $debt += $parent1_debt if ($include_this_patron && $include_guarantors);
169 $debt += $child1_debt + $child2_debt;
170 $debt += $parent2_debt unless ($only_this_guarantor || !$include_guarantors);
172 elsif ( $patron->firstname eq 'Parent 2' ) {
173 $debt += $parent2_debt if ($include_this_patron & $include_guarantors);
174 $debt += $child1_debt + $child2_debt;
175 $debt += $parent1_debt unless ($only_this_guarantor || !$include_guarantors);
177 elsif ( $patron->firstname eq 'Child 1' ) {
178 $debt += $child1_debt if ($include_this_patron);
179 $debt += $child2_debt;
180 $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
183 $debt += $child2_debt if ($include_this_patron);
184 $debt += $child1_debt;
185 $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
189 $patron->relationships_debt(
191 only_this_guarantor => $only_this_guarantor,
192 include_guarantors => $include_guarantors,
193 include_this_patron => $include_this_patron
198 . " debt of $debt calculated correctly for ( only_this_guarantor: $only_this_guarantor, include_guarantors: $include_guarantors, include_this_patron: $include_this_patron)"
205 subtest 'add_enrolment_fee_if_needed() tests' => sub {
209 subtest 'category has enrolment fee' => sub {
212 $schema->storage->txn_begin;
214 my $category = $builder->build_object(
216 class => 'Koha::Patron::Categories',
223 my $patron = $builder->build_object(
225 class => 'Koha::Patrons',
227 categorycode => $category->categorycode
232 my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
233 is( $enrollment_fee * 1, 20, 'Enrolment fee amount is correct' );
234 my $account = $patron->account;
235 is( $patron->account->balance * 1, 20, 'Patron charged the enrolment fee' );
236 # second enrolment fee, new
237 $enrollment_fee = $patron->add_enrolment_fee_if_needed(0);
238 # third enrolment fee, renewal
239 $enrollment_fee = $patron->add_enrolment_fee_if_needed(1);
240 is( $patron->account->balance * 1, 60, 'Patron charged the enrolment fees' );
242 my @debits = $account->outstanding_debits;
243 is( scalar @debits, 3, '3 enrolment fees' );
244 is( $debits[0]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
245 is( $debits[1]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
246 is( $debits[2]->debit_type_code, 'ACCOUNT_RENEW', 'Account type set correctly' );
248 $schema->storage->txn_rollback;
251 subtest 'no enrolment fee' => sub {
255 $schema->storage->txn_begin;
257 my $category = $builder->build_object(
259 class => 'Koha::Patron::Categories',
266 my $patron = $builder->build_object(
268 class => 'Koha::Patrons',
270 categorycode => $category->categorycode
275 my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
276 is( $enrollment_fee * 1, 0, 'No enrolment fee' );
277 my $account = $patron->account;
278 is( $patron->account->balance, 0, 'Patron not charged anything' );
280 my @debits = $account->outstanding_debits;
281 is( scalar @debits, 0, 'no debits' );
283 $schema->storage->txn_rollback;
287 subtest 'to_api() tests' => sub {
291 $schema->storage->txn_begin;
293 my $patron_class = Test::MockModule->new('Koha::Patron');
296 sub { return 'algo' }
299 my $patron = $builder->build_object(
301 class => 'Koha::Patrons',
308 my $restricted = $patron->to_api->{restricted};
309 ok( defined $restricted, 'restricted is defined' );
310 ok( !$restricted, 'debarred is undef, restricted evaluates to false' );
312 $patron->debarred( dt_from_string->add( days => 1 ) )->store->discard_changes;
313 $restricted = $patron->to_api->{restricted};
314 ok( defined $restricted, 'restricted is defined' );
315 ok( $restricted, 'debarred is defined, restricted evaluates to true' );
317 my $patron_json = $patron->to_api({ embed => { algo => {} } });
318 ok( exists $patron_json->{algo} );
319 is( $patron_json->{algo}, 'algo' );
321 $schema->storage->txn_rollback;
324 subtest 'login_attempts tests' => sub {
327 $schema->storage->txn_begin;
329 my $patron = $builder->build_object(
331 class => 'Koha::Patrons',
334 my $patron_info = $patron->unblessed;
336 delete $patron_info->{login_attempts};
337 my $new_patron = Koha::Patron->new($patron_info)->store;
338 is( $new_patron->discard_changes->login_attempts, 0, "login_attempts defaults to 0 as expected");
340 $schema->storage->txn_rollback;
343 subtest 'is_superlibrarian() tests' => sub {
347 $schema->storage->txn_begin;
349 my $patron = $builder->build_object(
351 class => 'Koha::Patrons',
359 is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
361 $patron->flags(1)->store->discard_changes;
362 is( $patron->is_superlibrarian, 1, 'Patron is a superlibrarian and the method returns the correct value' );
364 $patron->flags(0)->store->discard_changes;
365 is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
367 $schema->storage->txn_rollback;
370 subtest 'extended_attributes' => sub {
374 my $schema = Koha::Database->new->schema;
375 $schema->storage->txn_begin;
377 my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
378 my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
380 t::lib::Mocks::mock_userenv({ patron => $patron_1 });
382 my $attribute_type1 = Koha::Patron::Attribute::Type->new(
385 description => 'my description1',
389 my $attribute_type2 = Koha::Patron::Attribute::Type->new(
392 description => 'my description2',
394 staff_searchable => 1
398 my $new_library = $builder->build( { source => 'Branch' } );
399 my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
400 { code => 'my code3', description => 'my description3' } )->store;
401 $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
403 my $attributes_for_1 = [
405 attribute => 'my attribute1',
406 code => $attribute_type1->code(),
409 attribute => 'my attribute2',
410 code => $attribute_type2->code(),
413 attribute => 'my attribute limited',
414 code => $attribute_type_limited->code(),
418 my $attributes_for_2 = [
420 attribute => 'my attribute12',
421 code => $attribute_type1->code(),
424 attribute => 'my attribute limited 2',
425 code => $attribute_type_limited->code(),
429 my $extended_attributes = $patron_1->extended_attributes;
430 is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
431 is( $extended_attributes->count, 0, 'There should not be attribute yet');
433 $patron_1->extended_attributes->filter_by_branch_limitations->delete;
434 $patron_2->extended_attributes->filter_by_branch_limitations->delete;
435 $patron_1->extended_attributes($attributes_for_1);
436 $patron_2->extended_attributes($attributes_for_2);
438 my $extended_attributes_for_1 = $patron_1->extended_attributes;
439 is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
441 my $extended_attributes_for_2 = $patron_2->extended_attributes;
442 is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
444 my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
445 is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
447 $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
448 is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
450 my $expected_attributes_for_2 = [
452 code => $attribute_type1->code(),
453 attribute => 'my attribute12',
456 code => $attribute_type_limited->code(),
457 attribute => 'my attribute limited 2',
460 # Sorting them by code
461 $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
462 my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
467 code => $extended_attributes_for_2[0]->code,
468 attribute => $extended_attributes_for_2[0]->attribute
471 code => $extended_attributes_for_2[1]->code,
472 attribute => $extended_attributes_for_2[1]->attribute
475 $expected_attributes_for_2
478 # TODO - What about multiple? POD explains the problem
479 my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
480 is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
482 # Test branch limitations
483 t::lib::Mocks::mock_userenv({ patron => $patron_2 });
485 $extended_attributes_for_1 = $patron_1->extended_attributes;
486 is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
489 $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
490 is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
493 my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
494 is( $limited_value->attribute, 'my attribute limited', );
496 ## Do we need a filtered?
497 #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
498 #is( $limited_value, undef, );
500 $schema->storage->txn_rollback;
502 subtest 'non-repeatable attributes tests' => sub {
506 $schema->storage->txn_begin;
508 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
509 my $attribute_type = $builder->build_object(
511 class => 'Koha::Patron::Attribute::Types',
512 value => { repeatable => 0 }
516 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
520 $patron->extended_attributes(
522 { code => $attribute_type->code, attribute => 'a' },
523 { code => $attribute_type->code, attribute => 'b' }
527 'Koha::Exceptions::Patron::Attribute::NonRepeatable',
528 'Exception thrown on non-repeatable attribute';
530 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
532 $schema->storage->txn_rollback;
536 subtest 'unique attributes tests' => sub {
540 $schema->storage->txn_begin;
542 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
543 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
545 my $attribute_type_1 = $builder->build_object(
547 class => 'Koha::Patron::Attribute::Types',
548 value => { unique => 1 }
552 my $attribute_type_2 = $builder->build_object(
554 class => 'Koha::Patron::Attribute::Types',
555 value => { unique => 0 }
559 is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
560 is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
562 $patron_1->extended_attributes(
564 { code => $attribute_type_1->code, attribute => 'a' },
565 { code => $attribute_type_2->code, attribute => 'a' }
571 $patron_2->extended_attributes(
573 { code => $attribute_type_1->code, attribute => 'a' },
574 { code => $attribute_type_2->code, attribute => 'a' }
578 'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
579 'Exception thrown on unique attribute';
581 is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
582 is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
584 $schema->storage->txn_rollback;
588 subtest 'invalid type attributes tests' => sub {
592 $schema->storage->txn_begin;
594 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
596 my $attribute_type_1 = $builder->build_object(
598 class => 'Koha::Patron::Attribute::Types',
599 value => { repeatable => 0 }
603 my $attribute_type_2 = $builder->build_object(
605 class => 'Koha::Patron::Attribute::Types'
609 my $type_2 = $attribute_type_2->code;
610 $attribute_type_2->delete;
612 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
616 $patron->extended_attributes(
618 { code => $attribute_type_1->code, attribute => 'a' },
619 { code => $attribute_type_2->code, attribute => 'b' }
623 'Koha::Exceptions::Patron::Attribute::InvalidType',
624 'Exception thrown on invalid attribute type';
626 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
628 $schema->storage->txn_rollback;
632 subtest 'globally mandatory attributes tests' => sub {
636 $schema->storage->txn_begin;
638 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
640 my $attribute_type_1 = $builder->build_object(
642 class => 'Koha::Patron::Attribute::Types',
643 value => { mandatory => 1, class => 'a' }
647 my $attribute_type_2 = $builder->build_object(
649 class => 'Koha::Patron::Attribute::Types',
650 value => { mandatory => 0, class => 'a' }
654 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
658 $patron->extended_attributes(
660 { code => $attribute_type_2->code, attribute => 'b' }
664 'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
665 'Exception thrown on missing mandatory attribute type';
667 is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
669 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
671 $patron->extended_attributes(
673 { code => $attribute_type_1->code, attribute => 'b' }
677 is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
679 $schema->storage->txn_rollback;
685 subtest 'can_log_into() tests' => sub {
689 $schema->storage->txn_begin;
691 my $patron = $builder->build_object(
693 class => 'Koha::Patrons',
699 my $library = $builder->build_object({ class => 'Koha::Libraries' });
701 t::lib::Mocks::mock_preference('IndependentBranches', 1);
703 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
704 ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
706 # make it a superlibrarian
707 $patron->set({ flags => 1 })->store->discard_changes;
708 ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
710 t::lib::Mocks::mock_preference('IndependentBranches', 0);
712 # No special permissions
713 $patron->set({ flags => undef })->store->discard_changes;
714 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
715 ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
717 $schema->storage->txn_rollback;