Bug 19532: (follow-up) aria-hidden attr on OPAC, and more
[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 => 15;
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 => 15;
376
377     my $schema = Koha::Database->new->schema;
378     $schema->storage->txn_begin;
379
380     my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
381     my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
382
383     t::lib::Mocks::mock_userenv({ patron => $patron_1 });
384
385     my $attribute_type1 = Koha::Patron::Attribute::Type->new(
386         {
387             code        => 'my code1',
388             description => 'my description1',
389             unique_id   => 1
390         }
391     )->store;
392     my $attribute_type2 = Koha::Patron::Attribute::Type->new(
393         {
394             code             => 'my code2',
395             description      => 'my description2',
396             opac_display     => 1,
397             staff_searchable => 1
398         }
399     )->store;
400
401     my $new_library = $builder->build( { source => 'Branch' } );
402     my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
403         { code => 'my code3', description => 'my description3' } )->store;
404     $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
405
406     my $attributes_for_1 = [
407         {
408             attribute => 'my attribute1',
409             code => $attribute_type1->code(),
410         },
411         {
412             attribute => 'my attribute2',
413             code => $attribute_type2->code(),
414         },
415         {
416             attribute => 'my attribute limited',
417             code => $attribute_type_limited->code(),
418         }
419     ];
420
421     my $attributes_for_2 = [
422         {
423             attribute => 'my attribute12',
424             code => $attribute_type1->code(),
425         },
426         {
427             attribute => 'my attribute limited 2',
428             code => $attribute_type_limited->code(),
429         }
430     ];
431
432     my $extended_attributes = $patron_1->extended_attributes;
433     is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
434     is( $extended_attributes->count, 0, 'There should not be attribute yet');
435
436     $patron_1->extended_attributes->filter_by_branch_limitations->delete;
437     $patron_2->extended_attributes->filter_by_branch_limitations->delete;
438     $patron_1->extended_attributes($attributes_for_1);
439     $patron_2->extended_attributes($attributes_for_2);
440
441     my $extended_attributes_for_1 = $patron_1->extended_attributes;
442     is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
443
444     my $extended_attributes_for_2 = $patron_2->extended_attributes;
445     is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
446
447     my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
448     is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
449
450     $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
451     is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
452
453     my $expected_attributes_for_2 = [
454         {
455             code      => $attribute_type1->code(),
456             attribute => 'my attribute12',
457         },
458         {
459             code      => $attribute_type_limited->code(),
460             attribute => 'my attribute limited 2',
461         }
462     ];
463     # Sorting them by code
464     $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
465     my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
466
467     is_deeply(
468         [
469             {
470                 code      => $extended_attributes_for_2[0]->code,
471                 attribute => $extended_attributes_for_2[0]->attribute
472             },
473             {
474                 code      => $extended_attributes_for_2[1]->code,
475                 attribute => $extended_attributes_for_2[1]->attribute
476             }
477         ],
478         $expected_attributes_for_2
479     );
480
481     # TODO - What about multiple? POD explains the problem
482     my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
483     is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
484
485     # Test branch limitations
486     t::lib::Mocks::mock_userenv({ patron => $patron_2 });
487     # Return all
488     $extended_attributes_for_1 = $patron_1->extended_attributes;
489     is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
490
491     # Return filtered
492     $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
493     is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
494
495     # Not filtered
496     my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
497     is( $limited_value->attribute, 'my attribute limited', );
498
499     ## Do we need a filtered?
500     #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
501     #is( $limited_value, undef, );
502
503     $schema->storage->txn_rollback;
504
505     subtest 'non-repeatable attributes tests' => sub {
506
507         plan tests => 3;
508
509         $schema->storage->txn_begin;
510
511         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
512         my $attribute_type = $builder->build_object(
513             {
514                 class => 'Koha::Patron::Attribute::Types',
515                 value => { repeatable => 0 }
516             }
517         );
518
519         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
520
521         throws_ok
522             {
523                 $patron->extended_attributes(
524                     [
525                         { code => $attribute_type->code, attribute => 'a' },
526                         { code => $attribute_type->code, attribute => 'b' }
527                     ]
528                 );
529             }
530             'Koha::Exceptions::Patron::Attribute::NonRepeatable',
531             'Exception thrown on non-repeatable attribute';
532
533         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
534
535         $schema->storage->txn_rollback;
536
537     };
538
539     subtest 'unique attributes tests' => sub {
540
541         plan tests => 5;
542
543         $schema->storage->txn_begin;
544
545         my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
546         my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
547
548         my $attribute_type_1 = $builder->build_object(
549             {
550                 class => 'Koha::Patron::Attribute::Types',
551                 value => { unique => 1 }
552             }
553         );
554
555         my $attribute_type_2 = $builder->build_object(
556             {
557                 class => 'Koha::Patron::Attribute::Types',
558                 value => { unique => 0 }
559             }
560         );
561
562         is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
563         is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
564
565         $patron_1->extended_attributes(
566             [
567                 { code => $attribute_type_1->code, attribute => 'a' },
568                 { code => $attribute_type_2->code, attribute => 'a' }
569             ]
570         );
571
572         throws_ok
573             {
574                 $patron_2->extended_attributes(
575                     [
576                         { code => $attribute_type_1->code, attribute => 'a' },
577                         { code => $attribute_type_2->code, attribute => 'a' }
578                     ]
579                 );
580             }
581             'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
582             'Exception thrown on unique attribute';
583
584         is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
585         is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
586
587         $schema->storage->txn_rollback;
588
589     };
590
591     subtest 'invalid type attributes tests' => sub {
592
593         plan tests => 3;
594
595         $schema->storage->txn_begin;
596
597         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
598
599         my $attribute_type_1 = $builder->build_object(
600             {
601                 class => 'Koha::Patron::Attribute::Types',
602                 value => { repeatable => 0 }
603             }
604         );
605
606         my $attribute_type_2 = $builder->build_object(
607             {
608                 class => 'Koha::Patron::Attribute::Types'
609             }
610         );
611
612         my $type_2 = $attribute_type_2->code;
613         $attribute_type_2->delete;
614
615         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
616
617         throws_ok
618             {
619                 $patron->extended_attributes(
620                     [
621                         { code => $attribute_type_1->code, attribute => 'a' },
622                         { code => $attribute_type_2->code, attribute => 'b' }
623                     ]
624                 );
625             }
626             'Koha::Exceptions::Patron::Attribute::InvalidType',
627             'Exception thrown on invalid attribute type';
628
629         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
630
631         $schema->storage->txn_rollback;
632
633     };
634
635     subtest 'globally mandatory attributes tests' => sub {
636
637         plan tests => 5;
638
639         $schema->storage->txn_begin;
640
641         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
642
643         my $attribute_type_1 = $builder->build_object(
644             {
645                 class => 'Koha::Patron::Attribute::Types',
646                 value => { mandatory => 1, class => 'a' }
647             }
648         );
649
650         my $attribute_type_2 = $builder->build_object(
651             {
652                 class => 'Koha::Patron::Attribute::Types',
653                 value => { mandatory => 0, class => 'a' }
654             }
655         );
656
657         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
658
659         throws_ok
660             {
661                 $patron->extended_attributes(
662                     [
663                         { code => $attribute_type_2->code, attribute => 'b' }
664                     ]
665                 );
666             }
667             'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
668             'Exception thrown on missing mandatory attribute type';
669
670         is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
671
672         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
673
674         $patron->extended_attributes(
675             [
676                 { code => $attribute_type_1->code, attribute => 'b' }
677             ]
678         );
679
680         is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
681
682         $schema->storage->txn_rollback;
683
684     };
685
686 };
687
688 subtest 'can_log_into() tests' => sub {
689
690     plan tests => 5;
691
692     $schema->storage->txn_begin;
693
694     my $patron = $builder->build_object(
695         {
696             class => 'Koha::Patrons',
697             value => {
698                 flags => undef
699             }
700         }
701     );
702     my $library = $builder->build_object({ class => 'Koha::Libraries' });
703
704     t::lib::Mocks::mock_preference('IndependentBranches', 1);
705
706     ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
707     ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
708
709     # make it a superlibrarian
710     $patron->set({ flags => 1 })->store->discard_changes;
711     ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
712
713     t::lib::Mocks::mock_preference('IndependentBranches', 0);
714
715     # No special permissions
716     $patron->set({ flags => undef })->store->discard_changes;
717     ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
718     ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
719
720     $schema->storage->txn_rollback;
721 };
722
723 subtest 'can_request_article() tests' => sub {
724
725     plan tests => 4;
726
727     $schema->storage->txn_begin;
728
729     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
730
731     my $item = $builder->build_sample_item;
732
733     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
734     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
735     my $patron    = $builder->build_object( { class => 'Koha::Patrons' } );
736
737     t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
738
739     Koha::CirculationRules->set_rule(
740         {
741             categorycode => undef,
742             branchcode   => $library_1->id,
743             rule_name    => 'open_article_requests_limit',
744             rule_value   => 4,
745         }
746     );
747
748     $builder->build_object(
749         {
750             class => 'Koha::ArticleRequests',
751             value => { status => 'REQUESTED', borrowernumber => $patron->id }
752         }
753     );
754     $builder->build_object(
755         {
756             class => 'Koha::ArticleRequests',
757             value => { status => 'PENDING', borrowernumber => $patron->id }
758         }
759     );
760     $builder->build_object(
761         {
762             class => 'Koha::ArticleRequests',
763             value => { status => 'PROCESSING', borrowernumber => $patron->id }
764         }
765     );
766     $builder->build_object(
767         {
768             class => 'Koha::ArticleRequests',
769             value => { status => 'CANCELED', borrowernumber => $patron->id }
770         }
771     );
772
773     ok(
774         $patron->can_request_article( $library_1->id ),
775         '3 current requests, 4 is the limit: allowed'
776     );
777
778     # Completed request, same day
779     my $completed = $builder->build_object(
780         {
781             class => 'Koha::ArticleRequests',
782             value => {
783                 status         => 'COMPLETED',
784                 borrowernumber => $patron->id
785             }
786         }
787     );
788
789     ok( !$patron->can_request_article( $library_1->id ),
790         '3 current requests and a completed one the same day: denied' );
791
792     $completed->updated_on(
793         dt_from_string->add( days => -1 )->set(
794             hour   => 23,
795             minute => 59,
796             second => 59,
797         )
798     )->store;
799
800     ok( $patron->can_request_article( $library_1->id ),
801         '3 current requests and a completed one the day before: allowed' );
802
803     Koha::CirculationRules->set_rule(
804         {
805             categorycode => undef,
806             branchcode   => $library_2->id,
807             rule_name    => 'open_article_requests_limit',
808             rule_value   => 3,
809         }
810     );
811
812     ok( !$patron->can_request_article,
813         'Not passing the library_id param makes it fallback to userenv: denied'
814     );
815
816     $schema->storage->txn_rollback;
817 };
818
819 subtest 'article_requests() tests' => sub {
820
821     plan tests => 3;
822
823     $schema->storage->txn_begin;
824
825     my $library = $builder->build_object({ class => 'Koha::Libraries' });
826     t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
827
828     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
829
830     my $article_requests = $patron->article_requests;
831     is( ref($article_requests), 'Koha::ArticleRequests',
832         'In scalar context, type is correct' );
833     is( $article_requests->count, 0, 'No article requests' );
834
835     foreach my $i ( 0 .. 3 ) {
836
837         my $item = $builder->build_sample_item;
838
839         Koha::ArticleRequest->new(
840             {
841                 borrowernumber => $patron->id,
842                 biblionumber   => $item->biblionumber,
843                 itemnumber     => $item->id,
844                 title          => "Title",
845             }
846         )->request;
847     }
848
849     $article_requests = $patron->article_requests;
850     is( $article_requests->count, 4, '4 article requests' );
851
852     $schema->storage->txn_rollback;
853 };
854
855 subtest 'safe_to_delete() tests' => sub {
856
857     plan tests => 14;
858
859     $schema->storage->txn_begin;
860
861     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
862
863     ## Make it the anonymous
864     t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
865
866     ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
867     my $message = $patron->safe_to_delete->messages->[0];
868     is( $message->type, 'error', 'Type is error' );
869     is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
870     # cleanup
871     t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
872
873     ## Make it have a checkout
874     my $checkout = $builder->build_object(
875         {
876             class => 'Koha::Checkouts',
877             value => { borrowernumber => $patron->id }
878         }
879     );
880
881     ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
882     $message = $patron->safe_to_delete->messages->[0];
883     is( $message->type, 'error', 'Type is error' );
884     is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
885     # cleanup
886     $checkout->delete;
887
888     ## Make it have a guarantee
889     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
890     $builder->build_object({ class => 'Koha::Patrons' })
891             ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
892
893     ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
894     $message = $patron->safe_to_delete->messages->[0];
895     is( $message->type, 'error', 'Type is error' );
896     is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
897
898     # cleanup
899     $patron->guarantee_relationships->delete;
900
901     ## Make it have debt
902     my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
903
904     ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
905     $message = $patron->safe_to_delete->messages->[0];
906     is( $message->type, 'error', 'Type is error' );
907     is( $message->message, 'has_debt', 'Cannot delete, has debt' );
908     # cleanup
909     $patron->account->pay({ amount => 10, debits => [ $debit ] });
910
911     ## Happy case :-D
912     ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
913     my $messages = $patron->safe_to_delete->messages;
914     is_deeply( $messages, [], 'Patron can be deleted, no messages' );
915 };
916
917 subtest 'article_request_fee() tests' => sub {
918
919     plan tests => 3;
920
921     $schema->storage->txn_begin;
922
923     # Cleanup, to avoid interference
924     Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
925
926     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
927
928     my $item = $builder->build_sample_item;
929
930     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
931     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
932     my $patron    = $builder->build_object( { class => 'Koha::Patrons' } );
933
934     # Rule that should never be picked, because the patron's category is always picked
935     Koha::CirculationRules->set_rule(
936         {   categorycode => undef,
937             branchcode   => undef,
938             rule_name    => 'article_request_fee',
939             rule_value   => 1,
940         }
941     );
942
943     is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
944
945     Koha::CirculationRules->set_rule(
946         {   categorycode => $patron->categorycode,
947             branchcode   => undef,
948             rule_name    => 'article_request_fee',
949             rule_value   => 2,
950         }
951     );
952
953     Koha::CirculationRules->set_rule(
954         {   categorycode => $patron->categorycode,
955             branchcode   => $library_1->id,
956             rule_name    => 'article_request_fee',
957             rule_value   => 3,
958         }
959     );
960
961     is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
962
963     t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
964
965     is( $patron->article_request_fee(), 3, 'env used correctly' );
966
967     $schema->storage->txn_rollback;
968 };
969
970 subtest 'add_article_request_fee_if_needed() tests' => sub {
971
972     plan tests => 12;
973
974     $schema->storage->txn_begin;
975
976     my $amount = 0;
977
978     my $patron_mock = Test::MockModule->new('Koha::Patron');
979     $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
980
981     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
982
983     is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
984
985     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
986     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
987     my $staff     = $builder->build_object( { class => 'Koha::Patrons' } );
988     my $item      = $builder->build_sample_item;
989
990     t::lib::Mocks::mock_userenv(
991         { branchcode => $library_1->id, patron => $staff } );
992
993     my $debit = $patron->add_article_request_fee_if_needed();
994     is( $debit, undef, 'No fee, no debit line' );
995
996     # positive value
997     $amount = 1;
998
999     $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
1000     is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1001     is( $debit->amount, $amount,
1002         'amount set to $patron->article_request_fee value' );
1003     is( $debit->manager_id, $staff->id,
1004         'manager_id set to userenv session user' );
1005     is( $debit->branchcode, $library_1->id,
1006         'branchcode set to userenv session library' );
1007     is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1008         'debit_type_code set correctly' );
1009     is( $debit->itemnumber, $item->id,
1010         'itemnumber set correctly' );
1011
1012     $amount = 100;
1013
1014     $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1015     is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1016     is( $debit->amount, $amount,
1017         'amount set to $patron->article_request_fee value' );
1018     is( $debit->branchcode, $library_2->id,
1019         'branchcode set to userenv session library' );
1020     is( $debit->itemnumber, undef,
1021         'itemnumber set correctly to undef' );
1022
1023     $schema->storage->txn_rollback;
1024 };
1025
1026 subtest 'messages' => sub {
1027     plan tests => 4;
1028
1029     $schema->storage->txn_begin;
1030
1031     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1032     my $messages = $patron->messages;
1033     is( $messages->count, 0, "No message yet" );
1034     my $message_1 = $builder->build_object(
1035         {
1036             class => 'Koha::Patron::Messages',
1037             value => { borrowernumber => $patron->borrowernumber }
1038         }
1039     );
1040     my $message_2 = $builder->build_object(
1041         {
1042             class => 'Koha::Patron::Messages',
1043             value => { borrowernumber => $patron->borrowernumber }
1044         }
1045     );
1046
1047     $messages = $patron->messages;
1048     is( $messages->count, 2, "There are two messages for this patron" );
1049     is( $messages->next->message, $message_1->message );
1050     is( $messages->next->message, $message_2->message );
1051     $schema->storage->txn_rollback;
1052 };
1053
1054 subtest 'recalls() tests' => sub {
1055
1056     plan tests => 2;
1057     my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
1058     my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
1059     my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
1060     my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
1061
1062     Koha::Recall->new({
1063         biblionumber => $biblio1->biblionumber,
1064         borrowernumber => $patron->borrowernumber,
1065         itemnumber => $item1->itemnumber,
1066         branchcode => $patron->branchcode,
1067         recalldate => dt_from_string,
1068         status => 'R',
1069         item_level_recall => 1,
1070     })->store;
1071     Koha::Recall->new({
1072         biblionumber => $biblio2->biblionumber,
1073         borrowernumber => $patron->borrowernumber,
1074         itemnumber => $item2->itemnumber,
1075         branchcode => $patron->branchcode,
1076         recalldate => dt_from_string,
1077         status => 'R',
1078         item_level_recall => 1,
1079     })->store;
1080     Koha::Recall->new({
1081         biblionumber => $biblio1->biblionumber,
1082         borrowernumber => $patron->borrowernumber,
1083         itemnumber => undef,
1084         branchcode => $patron->branchcode,
1085         recalldate => dt_from_string,
1086         status => 'R',
1087         item_level_recall => 0,
1088     })->store;
1089     my $recall = Koha::Recall->new({
1090         biblionumber => $biblio1->biblionumber,
1091         borrowernumber => $patron->borrowernumber,
1092         itemnumber => undef,
1093         branchcode => $patron->branchcode,
1094         recalldate => dt_from_string,
1095         status => 'R',
1096         item_level_recall => 0,
1097     })->store;
1098     $recall->set_cancelled;
1099
1100     is( $patron->recalls->count, 3, "Correctly gets this patron's active recalls" );
1101     is( $patron->recalls({ biblionumber => $biblio1->biblionumber })->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
1102
1103     $schema->storage->txn_rollback;
1104 };