Bug 19613: Scrub borrowers fields: borrowernotes opacnote
[koha.git] / t / db_dependent / Koha / Patron.t
1 #!/usr/bin/perl
2
3 # Copyright 2019 Koha Development team
4 #
5 # This file is part of Koha
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21
22 use Test::More tests => 19;
23 use Test::Exception;
24 use Test::Warn;
25
26 use Koha::CirculationRules;
27 use Koha::Database;
28 use Koha::DateUtils qw(dt_from_string);
29 use Koha::ArticleRequests;
30 use Koha::Patrons;
31 use Koha::Patron::Relationships;
32
33 use t::lib::TestBuilder;
34 use t::lib::Mocks;
35
36 my $schema  = Koha::Database->new->schema;
37 my $builder = t::lib::TestBuilder->new;
38
39 subtest 'add_guarantor() tests' => sub {
40
41     plan tests => 6;
42
43     $schema->storage->txn_begin;
44
45     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'father1|father2' );
46
47     my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
48     my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
49
50     throws_ok
51         { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber }); }
52         'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
53         'Exception is thrown as no relationship passed';
54
55     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
56
57     throws_ok
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';
61
62     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
63
64     $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father1' });
65
66     my $guarantors = $patron_1->guarantor_relationships;
67
68     is( $guarantors->count, 1, 'No guarantors added' );
69
70     {
71         local *STDERR;
72         open STDERR, '>', '/dev/null';
73         throws_ok
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';
77         close STDERR;
78     }
79
80     $schema->storage->txn_rollback;
81 };
82
83 subtest 'relationships_debt() tests' => sub {
84
85     plan tests => 168;
86
87     $schema->storage->txn_begin;
88
89     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
90
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" } });
95
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' });
100
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' );
105
106     my $patrons = [ $parent_1, $parent_2, $child_1, $child_2 ];
107
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);
111
112     # Add debt to child_2
113     $child2_debt = 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);
117
118     $parent1_debt = 3;
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);
122
123     $parent2_debt = 5;
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);
127
128     $child1_debt = 7;
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);
132
133     $schema->storage->txn_rollback;
134 };
135
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");
139     # Options
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 )
144
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 )
153             {
154                 throws_ok {
155                     $patron->relationships_debt(
156                         {
157                             only_this_guarantor => $only_this_guarantor,
158                             include_guarantors  => $include_guarantors,
159                             include_this_patron => $include_this_patron
160                         }
161                     );
162                 }
163                 'Koha::Exceptions::BadParameter',
164                   'Exception is thrown as patron is not a guarantor';
165
166             }
167             else {
168
169                 my $debt = 0;
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);
174                 }
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);
179                 }
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);
184                 }
185                 else {
186                     $debt += $child2_debt if ($include_this_patron);
187                     $debt += $child1_debt;
188                     $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
189                 }
190
191                 is(
192                     $patron->relationships_debt(
193                         {
194                             only_this_guarantor => $only_this_guarantor,
195                             include_guarantors  => $include_guarantors,
196                             include_this_patron => $include_this_patron
197                         }
198                     ),
199                     $debt,
200                     $patron->firstname
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)"
202                 );
203             }
204         }
205     }
206 }
207
208 subtest 'add_enrolment_fee_if_needed() tests' => sub {
209
210     plan tests => 2;
211
212     subtest 'category has enrolment fee' => sub {
213         plan tests => 7;
214
215         $schema->storage->txn_begin;
216
217         my $category = $builder->build_object(
218             {
219                 class => 'Koha::Patron::Categories',
220                 value => {
221                     enrolmentfee => 20
222                 }
223             }
224         );
225
226         my $patron = $builder->build_object(
227             {
228                 class => 'Koha::Patrons',
229                 value => {
230                     categorycode => $category->categorycode
231                 }
232             }
233         );
234
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' );
244
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' );
250
251         $schema->storage->txn_rollback;
252     };
253
254     subtest 'no enrolment fee' => sub {
255
256         plan tests => 3;
257
258         $schema->storage->txn_begin;
259
260         my $category = $builder->build_object(
261             {
262                 class => 'Koha::Patron::Categories',
263                 value => {
264                     enrolmentfee => 0
265                 }
266             }
267         );
268
269         my $patron = $builder->build_object(
270             {
271                 class => 'Koha::Patrons',
272                 value => {
273                     categorycode => $category->categorycode
274                 }
275             }
276         );
277
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' );
282
283         my @debits = $account->outstanding_debits->as_list;
284         is( scalar @debits, 0, 'no debits' );
285
286         $schema->storage->txn_rollback;
287     };
288 };
289
290 subtest 'to_api() tests' => sub {
291
292     plan tests => 6;
293
294     $schema->storage->txn_begin;
295
296     my $patron_class = Test::MockModule->new('Koha::Patron');
297     $patron_class->mock(
298         'algo',
299         sub { return 'algo' }
300     );
301
302     my $patron = $builder->build_object(
303         {
304             class => 'Koha::Patrons',
305             value => {
306                 debarred => undef
307             }
308         }
309     );
310
311     my $restricted = $patron->to_api->{restricted};
312     ok( defined $restricted, 'restricted is defined' );
313     ok( !$restricted, 'debarred is undef, restricted evaluates to false' );
314
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' );
319
320     my $patron_json = $patron->to_api({ embed => { algo => {} } });
321     ok( exists $patron_json->{algo} );
322     is( $patron_json->{algo}, 'algo' );
323
324     $schema->storage->txn_rollback;
325 };
326
327 subtest 'login_attempts tests' => sub {
328     plan tests => 1;
329
330     $schema->storage->txn_begin;
331
332     my $patron = $builder->build_object(
333         {
334             class => 'Koha::Patrons',
335         }
336     );
337     my $patron_info = $patron->unblessed;
338     $patron->delete;
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");
342
343     $schema->storage->txn_rollback;
344 };
345
346 subtest 'is_superlibrarian() tests' => sub {
347
348     plan tests => 3;
349
350     $schema->storage->txn_begin;
351
352     my $patron = $builder->build_object(
353         {
354             class => 'Koha::Patrons',
355
356             value => {
357                 flags => 16
358             }
359         }
360     );
361
362     is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
363
364     $patron->flags(1)->store->discard_changes;
365     is( $patron->is_superlibrarian, 1, 'Patron is a superlibrarian and the method returns the correct value' );
366
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' );
369
370     $schema->storage->txn_rollback;
371 };
372
373 subtest 'extended_attributes' => sub {
374
375     plan tests => 16;
376
377     my $schema = Koha::Database->new->schema;
378     $schema->storage->txn_begin;
379
380     Koha::Patron::Attribute::Types->search->delete;
381
382     my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
383     my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
384
385     t::lib::Mocks::mock_userenv({ patron => $patron_1 });
386
387     my $attribute_type1 = Koha::Patron::Attribute::Type->new(
388         {
389             code        => 'my code1',
390             description => 'my description1',
391             unique_id   => 1
392         }
393     )->store;
394     my $attribute_type2 = Koha::Patron::Attribute::Type->new(
395         {
396             code             => 'my code2',
397             description      => 'my description2',
398             opac_display     => 1,
399             staff_searchable => 1
400         }
401     )->store;
402
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} ] );
407
408     my $attributes_for_1 = [
409         {
410             attribute => 'my attribute1',
411             code => $attribute_type1->code(),
412         },
413         {
414             attribute => 'my attribute2',
415             code => $attribute_type2->code(),
416         },
417         {
418             attribute => 'my attribute limited',
419             code => $attribute_type_limited->code(),
420         }
421     ];
422
423     my $attributes_for_2 = [
424         {
425             attribute => 'my attribute12',
426             code => $attribute_type1->code(),
427         },
428         {
429             attribute => 'my attribute limited 2',
430             code => $attribute_type_limited->code(),
431         }
432     ];
433
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');
437
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);
442
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');
445
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');
448
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' );
451
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' );
454
455     my $expected_attributes_for_2 = [
456         {
457             code      => $attribute_type1->code(),
458             attribute => 'my attribute12',
459         },
460         {
461             code      => $attribute_type_limited->code(),
462             attribute => 'my attribute limited 2',
463         }
464     ];
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;
468
469     is_deeply(
470         [
471             {
472                 code      => $extended_attributes_for_2[0]->code,
473                 attribute => $extended_attributes_for_2[0]->attribute
474             },
475             {
476                 code      => $extended_attributes_for_2[1]->code,
477                 attribute => $extended_attributes_for_2[1]->attribute
478             }
479         ],
480         $expected_attributes_for_2
481     );
482
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' );
486
487     # Test branch limitations
488     t::lib::Mocks::mock_userenv({ patron => $patron_2 });
489     # Return all
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');
492
493     # Return filtered
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');
496
497     # Not filtered
498     my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
499     is( $limited_value->attribute, 'my attribute limited', );
500
501     ## Do we need a filtered?
502     #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
503     #is( $limited_value, undef, );
504
505     $schema->storage->txn_rollback;
506
507     subtest 'non-repeatable attributes tests' => sub {
508
509         plan tests => 3;
510
511         $schema->storage->txn_begin;
512         Koha::Patron::Attribute::Types->search->delete;
513
514         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
515         my $attribute_type = $builder->build_object(
516             {
517                 class => 'Koha::Patron::Attribute::Types',
518                 value => { repeatable => 0 }
519             }
520         );
521
522         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
523
524         throws_ok
525             {
526                 $patron->extended_attributes(
527                     [
528                         { code => $attribute_type->code, attribute => 'a' },
529                         { code => $attribute_type->code, attribute => 'b' }
530                     ]
531                 );
532             }
533             'Koha::Exceptions::Patron::Attribute::NonRepeatable',
534             'Exception thrown on non-repeatable attribute';
535
536         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
537
538         $schema->storage->txn_rollback;
539
540     };
541
542     subtest 'unique attributes tests' => sub {
543
544         plan tests => 5;
545
546         $schema->storage->txn_begin;
547         Koha::Patron::Attribute::Types->search->delete;
548
549         my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
550         my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
551
552         my $attribute_type_1 = $builder->build_object(
553             {
554                 class => 'Koha::Patron::Attribute::Types',
555                 value => { unique => 1 }
556             }
557         );
558
559         my $attribute_type_2 = $builder->build_object(
560             {
561                 class => 'Koha::Patron::Attribute::Types',
562                 value => { unique => 0 }
563             }
564         );
565
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' );
568
569         $patron_1->extended_attributes(
570             [
571                 { code => $attribute_type_1->code, attribute => 'a' },
572                 { code => $attribute_type_2->code, attribute => 'a' }
573             ]
574         );
575
576         throws_ok
577             {
578                 $patron_2->extended_attributes(
579                     [
580                         { code => $attribute_type_1->code, attribute => 'a' },
581                         { code => $attribute_type_2->code, attribute => 'a' }
582                     ]
583                 );
584             }
585             'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
586             'Exception thrown on unique attribute';
587
588         is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
589         is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
590
591         $schema->storage->txn_rollback;
592
593     };
594
595     subtest 'invalid type attributes tests' => sub {
596
597         plan tests => 3;
598
599         $schema->storage->txn_begin;
600         Koha::Patron::Attribute::Types->search->delete;
601
602         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
603
604         my $attribute_type_1 = $builder->build_object(
605             {
606                 class => 'Koha::Patron::Attribute::Types',
607                 value => { repeatable => 0 }
608             }
609         );
610
611         my $attribute_type_2 = $builder->build_object(
612             {
613                 class => 'Koha::Patron::Attribute::Types'
614             }
615         );
616
617         my $type_2 = $attribute_type_2->code;
618         $attribute_type_2->delete;
619
620         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
621
622         throws_ok
623             {
624                 $patron->extended_attributes(
625                     [
626                         { code => $attribute_type_1->code, attribute => 'a' },
627                         { code => $attribute_type_2->code, attribute => 'b' }
628                     ]
629                 );
630             }
631             'Koha::Exceptions::Patron::Attribute::InvalidType',
632             'Exception thrown on invalid attribute type';
633
634         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
635
636         $schema->storage->txn_rollback;
637
638     };
639
640     subtest 'globally mandatory attributes tests' => sub {
641
642         plan tests => 5;
643
644         $schema->storage->txn_begin;
645         Koha::Patron::Attribute::Types->search->delete;
646
647         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
648
649         my $attribute_type_1 = $builder->build_object(
650             {
651                 class => 'Koha::Patron::Attribute::Types',
652                 value => { mandatory => 1, class => 'a', category_code => undef }
653             }
654         );
655
656         my $attribute_type_2 = $builder->build_object(
657             {
658                 class => 'Koha::Patron::Attribute::Types',
659                 value => { mandatory => 0, class => 'a', category_code => undef }
660             }
661         );
662
663         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
664
665         throws_ok
666             {
667                 $patron->extended_attributes(
668                     [
669                         { code => $attribute_type_2->code, attribute => 'b' }
670                     ]
671                 );
672             }
673             'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
674             'Exception thrown on missing mandatory attribute type';
675
676         is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
677
678         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
679
680         $patron->extended_attributes(
681             [
682                 { code => $attribute_type_1->code, attribute => 'b' }
683             ]
684         );
685
686         is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
687
688         $schema->storage->txn_rollback;
689
690     };
691
692     subtest 'limited category mandatory attributes tests' => sub {
693
694         plan tests => 2;
695
696         $schema->storage->txn_begin;
697         Koha::Patron::Attribute::Types->search->delete;
698
699         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
700
701         my $attribute_type_1 = $builder->build_object(
702             {
703                 class => 'Koha::Patron::Attribute::Types',
704                 value => { mandatory => 1, class => 'a', category_code => $patron->categorycode }
705             }
706         );
707
708         $patron->extended_attributes(
709             [
710                 { code => $attribute_type_1->code, attribute => 'a' }
711             ]
712         );
713
714         is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
715
716         $patron = $builder->build_object({ class => 'Koha::Patrons' });
717         # new patron, new category - they shouldn't be required to have any attributes
718
719
720         ok( $patron->extended_attributes([]), "We can set no attributes, mandatory attribute for other category not required");
721
722
723     };
724
725
726
727 };
728
729 subtest 'can_log_into() tests' => sub {
730
731     plan tests => 5;
732
733     $schema->storage->txn_begin;
734
735     my $patron = $builder->build_object(
736         {
737             class => 'Koha::Patrons',
738             value => {
739                 flags => undef
740             }
741         }
742     );
743     my $library = $builder->build_object({ class => 'Koha::Libraries' });
744
745     t::lib::Mocks::mock_preference('IndependentBranches', 1);
746
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' );
749
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' );
753
754     t::lib::Mocks::mock_preference('IndependentBranches', 0);
755
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' );
760
761     $schema->storage->txn_rollback;
762 };
763
764 subtest 'can_request_article() tests' => sub {
765
766     plan tests => 4;
767
768     $schema->storage->txn_begin;
769
770     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
771
772     my $item = $builder->build_sample_item;
773
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' } );
777
778     t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
779
780     Koha::CirculationRules->set_rule(
781         {
782             categorycode => undef,
783             branchcode   => $library_1->id,
784             rule_name    => 'open_article_requests_limit',
785             rule_value   => 4,
786         }
787     );
788
789     $builder->build_object(
790         {
791             class => 'Koha::ArticleRequests',
792             value => { status => 'REQUESTED', borrowernumber => $patron->id }
793         }
794     );
795     $builder->build_object(
796         {
797             class => 'Koha::ArticleRequests',
798             value => { status => 'PENDING', borrowernumber => $patron->id }
799         }
800     );
801     $builder->build_object(
802         {
803             class => 'Koha::ArticleRequests',
804             value => { status => 'PROCESSING', borrowernumber => $patron->id }
805         }
806     );
807     $builder->build_object(
808         {
809             class => 'Koha::ArticleRequests',
810             value => { status => 'CANCELED', borrowernumber => $patron->id }
811         }
812     );
813
814     ok(
815         $patron->can_request_article( $library_1->id ),
816         '3 current requests, 4 is the limit: allowed'
817     );
818
819     # Completed request, same day
820     my $completed = $builder->build_object(
821         {
822             class => 'Koha::ArticleRequests',
823             value => {
824                 status         => 'COMPLETED',
825                 borrowernumber => $patron->id
826             }
827         }
828     );
829
830     ok( !$patron->can_request_article( $library_1->id ),
831         '3 current requests and a completed one the same day: denied' );
832
833     $completed->updated_on(
834         dt_from_string->add( days => -1 )->set(
835             hour   => 23,
836             minute => 59,
837             second => 59,
838         )
839     )->store;
840
841     ok( $patron->can_request_article( $library_1->id ),
842         '3 current requests and a completed one the day before: allowed' );
843
844     Koha::CirculationRules->set_rule(
845         {
846             categorycode => undef,
847             branchcode   => $library_2->id,
848             rule_name    => 'open_article_requests_limit',
849             rule_value   => 3,
850         }
851     );
852
853     ok( !$patron->can_request_article,
854         'Not passing the library_id param makes it fallback to userenv: denied'
855     );
856
857     $schema->storage->txn_rollback;
858 };
859
860 subtest 'article_requests() tests' => sub {
861
862     plan tests => 3;
863
864     $schema->storage->txn_begin;
865
866     my $library = $builder->build_object({ class => 'Koha::Libraries' });
867     t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
868
869     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
870
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' );
875
876     foreach my $i ( 0 .. 3 ) {
877
878         my $item = $builder->build_sample_item;
879
880         Koha::ArticleRequest->new(
881             {
882                 borrowernumber => $patron->id,
883                 biblionumber   => $item->biblionumber,
884                 itemnumber     => $item->id,
885                 title          => "Title",
886             }
887         )->request;
888     }
889
890     $article_requests = $patron->article_requests;
891     is( $article_requests->count, 4, '4 article requests' );
892
893     $schema->storage->txn_rollback;
894
895 };
896
897 subtest 'can_patron_change_staff_only_lists() tests' => sub {
898
899     plan tests => 3;
900
901     $schema->storage->txn_begin;
902
903     # make a user with no special permissions
904     my $patron = $builder->build_object(
905         {
906             class => 'Koha::Patrons',
907             value => {
908                 flags => undef
909             }
910         }
911     );
912     is( $patron->can_patron_change_staff_only_lists(), 0, 'Patron without permissions cannot change staff only lists');
913
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');
917
918
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');
922
923     $schema->storage->txn_rollback;
924 };
925
926 subtest 'password expiration tests' => sub {
927
928     plan tests => 5;
929
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,
935         }
936     });
937     my $patron = $builder->build_object({ class=> 'Koha::Patrons', value => {
938             categorycode => $category->categorycode,
939             password => 'hats'
940         }
941     });
942
943     $patron->delete()->store()->discard_changes(); # Make sure we are storing a 'new' patron
944
945     is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd() , "Password expiration date set correctly on patron creation");
946
947     $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
948             categorycode => $category->categorycode,
949             password => undef
950         }
951     });
952     $patron->delete()->store()->discard_changes();
953
954     is( $patron->password_expiration_date(), undef, "Password expiration date is not set if patron does not have a password");
955
956     $category->password_expiry_days(undef)->store();
957     $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
958             categorycode => $category->categorycode
959         }
960     });
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");
963
964     $schema->storage->txn_rollback;
965
966     subtest 'password_expired' => sub {
967
968         plan tests => 3;
969
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
974             }
975         });
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");
984
985         $schema->storage->txn_rollback;
986     };
987
988     subtest 'set_password' => sub {
989
990         plan tests => 4;
991
992         $schema->storage->txn_begin;
993
994         my $date = dt_from_string();
995         my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
996                 password_expiry_days => 10
997             }
998         });
999         my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1000                 categorycode => $category->categorycode,
1001                 password_expiration_date =>  $date->subtract( days => 1 )
1002             }
1003         });
1004         is( $patron->password_expired, 1, "Patron password is expired");
1005
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");
1010
1011
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");
1015
1016         $schema->storage->txn_rollback;
1017     };
1018
1019 };
1020
1021 subtest 'safe_to_delete() tests' => sub {
1022
1023     plan tests => 14;
1024
1025     $schema->storage->txn_begin;
1026
1027     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1028
1029     ## Make it the anonymous
1030     t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
1031
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' );
1036     # cleanup
1037     t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
1038
1039     ## Make it have a checkout
1040     my $checkout = $builder->build_object(
1041         {
1042             class => 'Koha::Checkouts',
1043             value => { borrowernumber => $patron->id }
1044         }
1045     );
1046
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' );
1051     # cleanup
1052     $checkout->delete;
1053
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' });
1058
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' );
1063
1064     # cleanup
1065     $patron->guarantee_relationships->delete;
1066
1067     ## Make it have debt
1068     my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
1069
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' );
1074     # cleanup
1075     $patron->account->pay({ amount => 10, debits => [ $debit ] });
1076
1077     ## Happy case :-D
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' );
1081 };
1082
1083 subtest 'article_request_fee() tests' => sub {
1084
1085     plan tests => 3;
1086
1087     $schema->storage->txn_begin;
1088
1089     # Cleanup, to avoid interference
1090     Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
1091
1092     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1093
1094     my $item = $builder->build_sample_item;
1095
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' } );
1099
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',
1105             rule_value   => 1,
1106         }
1107     );
1108
1109     is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
1110
1111     Koha::CirculationRules->set_rule(
1112         {   categorycode => $patron->categorycode,
1113             branchcode   => undef,
1114             rule_name    => 'article_request_fee',
1115             rule_value   => 2,
1116         }
1117     );
1118
1119     Koha::CirculationRules->set_rule(
1120         {   categorycode => $patron->categorycode,
1121             branchcode   => $library_1->id,
1122             rule_name    => 'article_request_fee',
1123             rule_value   => 3,
1124         }
1125     );
1126
1127     is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
1128
1129     t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
1130
1131     is( $patron->article_request_fee(), 3, 'env used correctly' );
1132
1133     $schema->storage->txn_rollback;
1134 };
1135
1136 subtest 'add_article_request_fee_if_needed() tests' => sub {
1137
1138     plan tests => 12;
1139
1140     $schema->storage->txn_begin;
1141
1142     my $amount = 0;
1143
1144     my $patron_mock = Test::MockModule->new('Koha::Patron');
1145     $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
1146
1147     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1148
1149     is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
1150
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;
1155
1156     t::lib::Mocks::mock_userenv(
1157         { branchcode => $library_1->id, patron => $staff } );
1158
1159     my $debit = $patron->add_article_request_fee_if_needed();
1160     is( $debit, undef, 'No fee, no debit line' );
1161
1162     # positive value
1163     $amount = 1;
1164
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' );
1177
1178     $amount = 100;
1179
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' );
1188
1189     $schema->storage->txn_rollback;
1190 };
1191
1192 subtest 'messages' => sub {
1193     plan tests => 4;
1194
1195     $schema->storage->txn_begin;
1196
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(
1201         {
1202             class => 'Koha::Patron::Messages',
1203             value => { borrowernumber => $patron->borrowernumber }
1204         }
1205     );
1206     my $message_2 = $builder->build_object(
1207         {
1208             class => 'Koha::Patron::Messages',
1209             value => { borrowernumber => $patron->borrowernumber }
1210         }
1211     );
1212
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;
1218 };
1219
1220 subtest 'recalls() tests' => sub {
1221
1222     plan tests => 3;
1223
1224     $schema->storage->txn_begin;
1225
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 } });
1231
1232     Koha::Recall->new(
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()',
1238             item_level        => 1,
1239         }
1240     )->store;
1241     Koha::Recall->new(
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()',
1247             item_level        => 1,
1248         }
1249     )->store;
1250     Koha::Recall->new(
1251         {   biblio_id         => $biblio1->biblionumber,
1252             patron_id         => $patron->borrowernumber,
1253             item_id           => undef,
1254             pickup_library_id => $patron->branchcode,
1255             created_date      => \'NOW()',
1256             item_level        => 0,
1257         }
1258     )->store;
1259     my $recall = Koha::Recall->new(
1260         {   biblio_id         => $biblio1->biblionumber,
1261             patron_id         => $patron->borrowernumber,
1262             item_id           => undef,
1263             pickup_library_id => $patron->branchcode,
1264             created_date      => \'NOW()',
1265             item_level        => 0,
1266         }
1267     )->store;
1268     $recall->set_cancelled;
1269
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" );
1273
1274     $schema->storage->txn_rollback;
1275 };
1276
1277 subtest 'encode_secret and decoded_secret' => sub {
1278     plan tests => 5;
1279     $schema->storage->txn_begin;
1280
1281     t::lib::Mocks::mock_config('encryption_key', 't0P_secret');
1282
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' );
1287
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' );
1292
1293     $schema->storage->txn_rollback;
1294 };
1295 subtest 'Scrub the note fields' => sub {
1296     plan tests => 4;
1297
1298     $schema->storage->txn_begin;
1299
1300     my $bad_message     = '<script>alert("booh!")</script><span>all</span><b>good</b>now';
1301     my $cleaned_message = '<span>all</span><b>good</b>now';
1302     my $tmp_patron      = $builder->build_object( { class => 'Koha::Patrons' } );
1303     my $patron_data     = $tmp_patron->unblessed;
1304     $tmp_patron->delete;
1305     delete $tmp_patron->{borrowernumber};
1306
1307     my $patron = Koha::Patron->new(
1308
1309         {
1310             %$patron_data,
1311             borrowernotes => $bad_message, opacnote => $bad_message,
1312         }
1313     )->store;
1314
1315     is( $patron->get_from_storage->borrowernotes, $cleaned_message );
1316     is( $patron->get_from_storage->opacnote,      $cleaned_message );
1317
1318     $patron->borrowernotes($bad_message)->store;
1319     $patron->opacnote($bad_message)->store;
1320
1321     is( $patron->get_from_storage->borrowernotes, $cleaned_message );
1322     is( $patron->get_from_storage->opacnote,      $cleaned_message );
1323
1324     $schema->storage->txn_rollback;
1325 };
1326