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 {
372 my $schema = Koha::Database->new->schema;
373 $schema->storage->txn_begin;
375 my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
376 my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
378 t::lib::Mocks::mock_userenv({ patron => $patron_1 });
380 my $attribute_type1 = Koha::Patron::Attribute::Type->new(
383 description => 'my description1',
387 my $attribute_type2 = Koha::Patron::Attribute::Type->new(
390 description => 'my description2',
392 staff_searchable => 1
396 my $attribute_type3 = $builder->build_object({ class => 'Koha::Patron::Attribute::Types' });
398 my $deleted_attribute_type = $builder->build_object({ class => 'Koha::Patron::Attribute::Types' });
399 my $deleted_attribute_type_code = $deleted_attribute_type->code;
400 $deleted_attribute_type->delete;
402 my $new_library = $builder->build( { source => 'Branch' } );
403 my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
404 { code => 'my code3', description => 'my description3' } )->store;
405 $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
407 my $attributes_for_1 = [
409 attribute => 'my attribute1',
410 code => $attribute_type1->code(),
413 attribute => 'my attribute2',
414 code => $attribute_type2->code(),
417 attribute => 'my attribute limited',
418 code => $attribute_type_limited->code(),
422 my $attributes_for_2 = [
424 attribute => 'my attribute12',
425 code => $attribute_type1->code(),
428 attribute => 'my attribute limited 2',
429 code => $attribute_type_limited->code(),
432 attribute => 'my nonexistent attribute 2',
433 code => $deleted_attribute_type_code,
437 my $extended_attributes = $patron_1->extended_attributes;
438 is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
439 is( $extended_attributes->count, 0, 'There should not be attribute yet');
441 $patron_1->extended_attributes->filter_by_branch_limitations->delete;
442 $patron_2->extended_attributes->filter_by_branch_limitations->delete;
443 $patron_1->extended_attributes($attributes_for_1);
446 $patron_2->extended_attributes($attributes_for_2);
447 } [ qr/a foreign key constraint fails/ ], 'nonexistent attribute should have not exploded but print a warning';
449 my $extended_attributes_for_1 = $patron_1->extended_attributes;
450 is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
452 my $extended_attributes_for_2 = $patron_2->extended_attributes;
453 is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
455 my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code });
456 is( $attribute_12->next->attribute, 'my attribute12', 'search by code should return the correct attribute' );
458 $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
459 is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
462 $extended_attributes_for_2 = $patron_2->extended_attributes->merge_with(
465 attribute => 'my attribute12 XXX',
466 code => $attribute_type1->code(),
469 attribute => 'my nonexistent attribute 2',
470 code => $deleted_attribute_type_code,
473 attribute => 'my attribute 3', # Adding a new attribute using merge_with
474 code => $attribute_type3->code,
479 "Cannot merge element: unrecognized code = '$deleted_attribute_type_code'",
480 "Trying to merge_with using a nonexistent attribute code should display a warning";
482 is( @$extended_attributes_for_2, 3, 'There should be 3 attributes now for patron 3');
483 my $expected_attributes_for_2 = [
485 code => $attribute_type1->code(),
486 attribute => 'my attribute12 XXX',
489 code => $attribute_type_limited->code(),
490 attribute => 'my attribute limited 2',
493 attribute => 'my attribute 3',
494 code => $attribute_type3->code,
497 # Sorting them by code
498 $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
503 code => $extended_attributes_for_2->[0]->{code},
504 attribute => $extended_attributes_for_2->[0]->{attribute}
507 code => $extended_attributes_for_2->[1]->{code},
508 attribute => $extended_attributes_for_2->[1]->{attribute}
511 code => $extended_attributes_for_2->[2]->{code},
512 attribute => $extended_attributes_for_2->[2]->{attribute}
515 $expected_attributes_for_2
518 # TODO - What about multiple? POD explains the problem
519 my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
520 is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
522 # Test branch limitations
523 t::lib::Mocks::mock_userenv({ patron => $patron_2 });
525 $extended_attributes_for_1 = $patron_1->extended_attributes;
526 is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
529 $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
530 is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
533 my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
534 is( $limited_value->attribute, 'my attribute limited', );
536 ## Do we need a filtered?
537 #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
538 #is( $limited_value, undef, );
540 $schema->storage->txn_rollback;