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 => 7;
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 $attribute_type3 = $builder->build_object({ class => 'Koha::Patron::Attribute::Types' });
400 my $new_library = $builder->build( { source => 'Branch' } );
401 my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
402 { code => 'my code3', description => 'my description3' } )->store;
403 $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
405 my $attributes_for_1 = [
407 attribute => 'my attribute1',
408 code => $attribute_type1->code(),
411 attribute => 'my attribute2',
412 code => $attribute_type2->code(),
415 attribute => 'my attribute limited',
416 code => $attribute_type_limited->code(),
420 my $attributes_for_2 = [
422 attribute => 'my attribute12',
423 code => $attribute_type1->code(),
426 attribute => 'my attribute limited 2',
427 code => $attribute_type_limited->code(),
431 my $extended_attributes = $patron_1->extended_attributes;
432 is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
433 is( $extended_attributes->count, 0, 'There should not be attribute yet');
435 $patron_1->extended_attributes->filter_by_branch_limitations->delete;
436 $patron_2->extended_attributes->filter_by_branch_limitations->delete;
437 $patron_1->extended_attributes($attributes_for_1);
438 $patron_2->extended_attributes($attributes_for_2);
440 my $extended_attributes_for_1 = $patron_1->extended_attributes;
441 is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
443 my $extended_attributes_for_2 = $patron_2->extended_attributes;
444 is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
446 my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
447 is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
449 $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
450 is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
452 my $expected_attributes_for_2 = [
454 code => $attribute_type1->code(),
455 attribute => 'my attribute12',
458 code => $attribute_type_limited->code(),
459 attribute => 'my attribute limited 2',
462 # Sorting them by code
463 $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
464 my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
469 code => $extended_attributes_for_2[0]->code,
470 attribute => $extended_attributes_for_2[0]->attribute
473 code => $extended_attributes_for_2[1]->code,
474 attribute => $extended_attributes_for_2[1]->attribute
477 $expected_attributes_for_2
480 # TODO - What about multiple? POD explains the problem
481 my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
482 is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
484 # Test branch limitations
485 t::lib::Mocks::mock_userenv({ patron => $patron_2 });
487 $extended_attributes_for_1 = $patron_1->extended_attributes;
488 is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
491 $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
492 is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
495 my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
496 is( $limited_value->attribute, 'my attribute limited', );
498 ## Do we need a filtered?
499 #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
500 #is( $limited_value, undef, );
502 subtest 'non-repeatable attributes tests' => sub {
506 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
507 my $attribute_type = $builder->build_object(
509 class => 'Koha::Patron::Attribute::Types',
510 value => { repeatable => 0 }
514 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
518 $patron->extended_attributes(
520 { code => $attribute_type->code, attribute => 'a' },
521 { code => $attribute_type->code, attribute => 'b' }
525 'Koha::Exceptions::Patron::Attribute::NonRepeatable',
526 'Exception thrown on non-repeatable attribute';
528 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
531 subtest 'unique attributes tests' => sub {
535 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
536 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
538 my $attribute_type_1 = $builder->build_object(
540 class => 'Koha::Patron::Attribute::Types',
541 value => { unique => 1 }
545 my $attribute_type_2 = $builder->build_object(
547 class => 'Koha::Patron::Attribute::Types',
548 value => { unique => 0 }
552 is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
553 is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
555 $patron_1->extended_attributes(
557 { code => $attribute_type_1->code, attribute => 'a' },
558 { code => $attribute_type_2->code, attribute => 'a' }
564 $patron_2->extended_attributes(
566 { code => $attribute_type_1->code, attribute => 'a' },
567 { code => $attribute_type_2->code, attribute => 'a' }
571 'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
572 'Exception thrown on unique attribute';
574 is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
575 is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
578 subtest 'invalid type attributes tests' => sub {
582 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
584 my $attribute_type_1 = $builder->build_object(
586 class => 'Koha::Patron::Attribute::Types',
587 value => { repeatable => 0 }
591 my $attribute_type_2 = $builder->build_object(
593 class => 'Koha::Patron::Attribute::Types'
597 my $type_2 = $attribute_type_2->code;
598 $attribute_type_2->delete;
600 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
604 $patron->extended_attributes(
606 { code => $attribute_type_1->code, attribute => 'a' },
607 { code => $attribute_type_2->code, attribute => 'b' }
611 'Koha::Exceptions::Patron::Attribute::InvalidType',
612 'Exception thrown on invalid attribute type';
614 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
617 $schema->storage->txn_rollback;