Bug 34932: Patron.t - Pass borrowernumber of manager to userenv
[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 => 25;
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 use C4::Circulation qw( AddIssue AddReturn );
33
34 use t::lib::TestBuilder;
35 use t::lib::Mocks;
36
37 my $schema  = Koha::Database->new->schema;
38 my $builder = t::lib::TestBuilder->new;
39
40 subtest 'add_guarantor() tests' => sub {
41
42     plan tests => 6;
43
44     $schema->storage->txn_begin;
45
46     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'father1|father2' );
47
48     my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
49     my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
50
51     throws_ok
52         { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber }); }
53         'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
54         'Exception is thrown as no relationship passed';
55
56     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
57
58     throws_ok
59         { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father' }); }
60         'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
61         'Exception is thrown as a wrong relationship was passed';
62
63     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
64
65     $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father1' });
66
67     my $guarantors = $patron_1->guarantor_relationships;
68
69     is( $guarantors->count, 1, 'No guarantors added' );
70
71     {
72         local *STDERR;
73         open STDERR, '>', '/dev/null';
74         throws_ok
75             { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father2' }); }
76             'Koha::Exceptions::Patron::Relationship::DuplicateRelationship',
77             'Exception is thrown for duplicated relationship';
78         close STDERR;
79     }
80
81     $schema->storage->txn_rollback;
82 };
83
84 subtest 'relationships_debt() tests' => sub {
85
86     plan tests => 168;
87
88     $schema->storage->txn_begin;
89
90     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
91
92     my $parent_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 1" } });
93     my $parent_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 2" } });
94     my $child_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 1" } });
95     my $child_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 2" } });
96
97     $child_1->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
98     $child_1->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
99     $child_2->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
100     $child_2->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
101
102     is( $child_1->guarantor_relationships->guarantors->count, 2, 'Child 1 has correct number of guarantors' );
103     is( $child_2->guarantor_relationships->guarantors->count, 2, 'Child 2 has correct number of guarantors' );
104     is( $parent_1->guarantee_relationships->guarantees->count, 2, 'Parent 1 has correct number of guarantees' );
105     is( $parent_2->guarantee_relationships->guarantees->count, 2, 'Parent 2 has correct number of guarantees' );
106
107     my $patrons = [ $parent_1, $parent_2, $child_1, $child_2 ];
108
109     # First test: No debt
110     my ($parent1_debt, $parent2_debt, $child1_debt, $child2_debt) = (0,0,0,0);
111     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
112
113     # Add debt to child_2
114     $child2_debt = 2;
115     $child_2->account->add_debit({ type => 'ACCOUNT', amount => $child2_debt, interface => 'commandline' });
116     is( $child_2->account->non_issues_charges, $child2_debt, 'Debt added to Child 2' );
117     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
118
119     $parent1_debt = 3;
120     $parent_1->account->add_debit({ type => 'ACCOUNT', amount => $parent1_debt, interface => 'commandline' });
121     is( $parent_1->account->non_issues_charges, $parent1_debt, 'Debt added to Parent 1' );
122     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
123
124     $parent2_debt = 5;
125     $parent_2->account->add_debit({ type => 'ACCOUNT', amount => $parent2_debt, interface => 'commandline' });
126     is( $parent_2->account->non_issues_charges, $parent2_debt, 'Parent 2 owes correct amount' );
127     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
128
129     $child1_debt = 7;
130     $child_1->account->add_debit({ type => 'ACCOUNT', amount => $child1_debt, interface => 'commandline' });
131     is( $child_1->account->non_issues_charges, $child1_debt, 'Child 1 owes correct amount' );
132     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
133
134     $schema->storage->txn_rollback;
135 };
136
137 sub _test_combinations {
138     my ( $patrons, $parent1_debt, $parent2_debt, $child1_debt, $child2_debt ) = @_;
139     note("Testing with parent 1 debt $parent1_debt | Parent 2 debt $parent2_debt | Child 1 debt $child1_debt | Child 2 debt $child2_debt");
140     # Options
141     # P1 => P1 + C1 + C2 ( - P1 ) ( + P2 )
142     # P2 => P2 + C1 + C2 ( - P2 ) ( + P1 )
143     # C1 => P1 + P2 + C1 + C2 ( - C1 )
144     # C2 => P1 + P2 + C1 + C2 ( - C2 )
145
146 # 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
147     for my $i ( 0 .. 7 ) {
148         my ( $only_this_guarantor, $include_guarantors, $include_this_patron )
149           = split '', sprintf( "%03b", $i );
150         note("---------------------");
151         for my $patron ( @$patrons ) {
152             if ( $only_this_guarantor
153                 && !$patron->guarantee_relationships->count )
154             {
155                 throws_ok {
156                     $patron->relationships_debt(
157                         {
158                             only_this_guarantor => $only_this_guarantor,
159                             include_guarantors  => $include_guarantors,
160                             include_this_patron => $include_this_patron
161                         }
162                     );
163                 }
164                 'Koha::Exceptions::BadParameter',
165                   'Exception is thrown as patron is not a guarantor';
166
167             }
168             else {
169
170                 my $debt = 0;
171                 if ( $patron->firstname eq 'Parent 1' ) {
172                     $debt += $parent1_debt if ($include_this_patron && $include_guarantors);
173                     $debt += $child1_debt + $child2_debt;
174                     $debt += $parent2_debt unless ($only_this_guarantor || !$include_guarantors);
175                 }
176                 elsif ( $patron->firstname eq 'Parent 2' ) {
177                     $debt += $parent2_debt if ($include_this_patron & $include_guarantors);
178                     $debt += $child1_debt + $child2_debt;
179                     $debt += $parent1_debt unless ($only_this_guarantor || !$include_guarantors);
180                 }
181                 elsif ( $patron->firstname eq ' Child 1' ) {
182                     $debt += $child1_debt if ($include_this_patron);
183                     $debt += $child2_debt;
184                     $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
185                 }
186                 else {
187                     $debt += $child2_debt if ($include_this_patron);
188                     $debt += $child1_debt;
189                     $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
190                 }
191
192                 is(
193                     $patron->relationships_debt(
194                         {
195                             only_this_guarantor => $only_this_guarantor,
196                             include_guarantors  => $include_guarantors,
197                             include_this_patron => $include_this_patron
198                         }
199                     ),
200                     $debt,
201                     $patron->firstname
202                       . " debt of " . sprintf('%02d',$debt) . " calculated correctly for ( only_this_guarantor: $only_this_guarantor, include_guarantors: $include_guarantors, include_this_patron: $include_this_patron)"
203                 );
204             }
205         }
206     }
207 }
208
209 subtest 'add_enrolment_fee_if_needed() tests' => sub {
210
211     plan tests => 2;
212
213     subtest 'category has enrolment fee' => sub {
214         plan tests => 7;
215
216         $schema->storage->txn_begin;
217
218         my $category = $builder->build_object(
219             {
220                 class => 'Koha::Patron::Categories',
221                 value => {
222                     enrolmentfee => 20
223                 }
224             }
225         );
226
227         my $patron = $builder->build_object(
228             {
229                 class => 'Koha::Patrons',
230                 value => {
231                     categorycode => $category->categorycode
232                 }
233             }
234         );
235
236         my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
237         is( $enrollment_fee * 1, 20, 'Enrolment fee amount is correct' );
238         my $account = $patron->account;
239         is( $patron->account->balance * 1, 20, 'Patron charged the enrolment fee' );
240         # second enrolment fee, new
241         $enrollment_fee = $patron->add_enrolment_fee_if_needed(0);
242         # third enrolment fee, renewal
243         $enrollment_fee = $patron->add_enrolment_fee_if_needed(1);
244         is( $patron->account->balance * 1, 60, 'Patron charged the enrolment fees' );
245
246         my @debits = $account->outstanding_debits->as_list;
247         is( scalar @debits, 3, '3 enrolment fees' );
248         is( $debits[0]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
249         is( $debits[1]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
250         is( $debits[2]->debit_type_code, 'ACCOUNT_RENEW', 'Account type set correctly' );
251
252         $schema->storage->txn_rollback;
253     };
254
255     subtest 'no enrolment fee' => sub {
256
257         plan tests => 3;
258
259         $schema->storage->txn_begin;
260
261         my $category = $builder->build_object(
262             {
263                 class => 'Koha::Patron::Categories',
264                 value => {
265                     enrolmentfee => 0
266                 }
267             }
268         );
269
270         my $patron = $builder->build_object(
271             {
272                 class => 'Koha::Patrons',
273                 value => {
274                     categorycode => $category->categorycode
275                 }
276             }
277         );
278
279         my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
280         is( $enrollment_fee * 1, 0, 'No enrolment fee' );
281         my $account = $patron->account;
282         is( $patron->account->balance, 0, 'Patron not charged anything' );
283
284         my @debits = $account->outstanding_debits->as_list;
285         is( scalar @debits, 0, 'no debits' );
286
287         $schema->storage->txn_rollback;
288     };
289 };
290
291 subtest 'messaging_preferences() tests' => sub {
292     plan tests => 5;
293
294     $schema->storage->txn_begin;
295
296     my $mtt = $builder->build_object({
297         class => 'Koha::Patron::MessagePreference::Transport::Types'
298     });
299     my $attribute = $builder->build_object({
300         class => 'Koha::Patron::MessagePreference::Attributes'
301     });
302     my $branchcode     = $builder->build({
303         source => 'Branch' })->{branchcode};
304     my $letter = $builder->build_object({
305         class => 'Koha::Notice::Templates',
306         value => {
307             branchcode => '',
308             is_html => 0,
309             message_transport_type => $mtt->message_transport_type
310         }
311     });
312
313     Koha::Patron::MessagePreference::Transport->new({
314         message_attribute_id   => $attribute->message_attribute_id,
315         message_transport_type => $mtt->message_transport_type,
316         is_digest              => 0,
317         letter_module          => $letter->module,
318         letter_code            => $letter->code,
319     })->store;
320
321     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
322
323     my $preference = Koha::Patron::MessagePreference->new({
324         borrowernumber => $patron->borrowernumber,
325         message_attribute_id => $attribute->message_attribute_id,
326         wants_digest => 0,
327         days_in_advance => undef,
328     })->store;
329
330     my $messaging_preferences = $patron->messaging_preferences();
331     is($messaging_preferences->count, 1, 'Found one preference');
332
333     my $messaging_preference = $messaging_preferences->next;
334     is($messaging_preference->borrowernumber, $patron->borrowernumber);
335     is($messaging_preference->message_attribute_id, $attribute->message_attribute_id);
336     is($messaging_preference->wants_digest, 0);
337     is($messaging_preference->days_in_advance, undef);
338
339     $schema->storage->txn_rollback;
340 };
341
342 subtest 'to_api() tests' => sub {
343
344     plan tests => 6;
345
346     $schema->storage->txn_begin;
347
348     my $patron_class = Test::MockModule->new('Koha::Patron');
349     $patron_class->mock(
350         'algo',
351         sub { return 'algo' }
352     );
353
354     my $patron = $builder->build_object(
355         {
356             class => 'Koha::Patrons',
357             value => {
358                 debarred => undef
359             }
360         }
361     );
362
363     my $restricted = $patron->to_api->{restricted};
364     ok( defined $restricted, 'restricted is defined' );
365     ok( !$restricted, 'debarred is undef, restricted evaluates to false' );
366
367     $patron->debarred( dt_from_string->add( days => 1 ) )->store->discard_changes;
368     $restricted = $patron->to_api->{restricted};
369     ok( defined $restricted, 'restricted is defined' );
370     ok( $restricted, 'debarred is defined, restricted evaluates to true' );
371
372     my $patron_json = $patron->to_api({ embed => { algo => {} } });
373     ok( exists $patron_json->{algo} );
374     is( $patron_json->{algo}, 'algo' );
375
376     $schema->storage->txn_rollback;
377 };
378
379 subtest 'login_attempts tests' => sub {
380     plan tests => 1;
381
382     $schema->storage->txn_begin;
383
384     my $patron = $builder->build_object(
385         {
386             class => 'Koha::Patrons',
387         }
388     );
389     my $patron_info = $patron->unblessed;
390     $patron->delete;
391     delete $patron_info->{login_attempts};
392     my $new_patron = Koha::Patron->new($patron_info)->store;
393     is( $new_patron->discard_changes->login_attempts, 0, "login_attempts defaults to 0 as expected");
394
395     $schema->storage->txn_rollback;
396 };
397
398 subtest 'is_superlibrarian() tests' => sub {
399
400     plan tests => 3;
401
402     $schema->storage->txn_begin;
403
404     my $patron = $builder->build_object(
405         {
406             class => 'Koha::Patrons',
407
408             value => {
409                 flags => 16
410             }
411         }
412     );
413
414     is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
415
416     $patron->flags(1)->store->discard_changes;
417     is( $patron->is_superlibrarian, 1, 'Patron is a superlibrarian and the method returns the correct value' );
418
419     $patron->flags(0)->store->discard_changes;
420     is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
421
422     $schema->storage->txn_rollback;
423 };
424
425 subtest 'extended_attributes' => sub {
426
427     plan tests => 16;
428
429     my $schema = Koha::Database->new->schema;
430     $schema->storage->txn_begin;
431
432     Koha::Patron::Attribute::Types->search->delete;
433
434     my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
435     my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
436
437     t::lib::Mocks::mock_userenv({ patron => $patron_1 });
438
439     my $attribute_type1 = Koha::Patron::Attribute::Type->new(
440         {
441             code        => 'my code1',
442             description => 'my description1',
443             unique_id   => 1
444         }
445     )->store;
446     my $attribute_type2 = Koha::Patron::Attribute::Type->new(
447         {
448             code             => 'my code2',
449             description      => 'my description2',
450             opac_display     => 1,
451             staff_searchable => 1
452         }
453     )->store;
454
455     my $new_library = $builder->build( { source => 'Branch' } );
456     my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
457         { code => 'my code3', description => 'my description3' } )->store;
458     $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
459
460     my $attributes_for_1 = [
461         {
462             attribute => 'my attribute1',
463             code => $attribute_type1->code(),
464         },
465         {
466             attribute => 'my attribute2',
467             code => $attribute_type2->code(),
468         },
469         {
470             attribute => 'my attribute limited',
471             code => $attribute_type_limited->code(),
472         }
473     ];
474
475     my $attributes_for_2 = [
476         {
477             attribute => 'my attribute12',
478             code => $attribute_type1->code(),
479         },
480         {
481             attribute => 'my attribute limited 2',
482             code => $attribute_type_limited->code(),
483         }
484     ];
485
486     my $extended_attributes = $patron_1->extended_attributes;
487     is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
488     is( $extended_attributes->count, 0, 'There should not be attribute yet');
489
490     $patron_1->extended_attributes->filter_by_branch_limitations->delete;
491     $patron_2->extended_attributes->filter_by_branch_limitations->delete;
492     $patron_1->extended_attributes($attributes_for_1);
493     $patron_2->extended_attributes($attributes_for_2);
494
495     my $extended_attributes_for_1 = $patron_1->extended_attributes;
496     is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
497
498     my $extended_attributes_for_2 = $patron_2->extended_attributes;
499     is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
500
501     my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
502     is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
503
504     $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
505     is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
506
507     my $expected_attributes_for_2 = [
508         {
509             code      => $attribute_type1->code(),
510             attribute => 'my attribute12',
511         },
512         {
513             code      => $attribute_type_limited->code(),
514             attribute => 'my attribute limited 2',
515         }
516     ];
517     # Sorting them by code
518     $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
519     my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
520
521     is_deeply(
522         [
523             {
524                 code      => $extended_attributes_for_2[0]->code,
525                 attribute => $extended_attributes_for_2[0]->attribute
526             },
527             {
528                 code      => $extended_attributes_for_2[1]->code,
529                 attribute => $extended_attributes_for_2[1]->attribute
530             }
531         ],
532         $expected_attributes_for_2
533     );
534
535     # TODO - What about multiple? POD explains the problem
536     my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
537     is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
538
539     # Test branch limitations
540     t::lib::Mocks::mock_userenv({ patron => $patron_2 });
541     # Return all
542     $extended_attributes_for_1 = $patron_1->extended_attributes;
543     is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
544
545     # Return filtered
546     $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
547     is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
548
549     # Not filtered
550     my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
551     is( $limited_value->attribute, 'my attribute limited', );
552
553     ## Do we need a filtered?
554     #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
555     #is( $limited_value, undef, );
556
557     $schema->storage->txn_rollback;
558
559     subtest 'non-repeatable attributes tests' => sub {
560
561         plan tests => 3;
562
563         $schema->storage->txn_begin;
564         Koha::Patron::Attribute::Types->search->delete;
565
566         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
567         my $attribute_type = $builder->build_object(
568             {
569                 class => 'Koha::Patron::Attribute::Types',
570                 value => { repeatable => 0 }
571             }
572         );
573
574         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
575
576         throws_ok
577             {
578                 $patron->extended_attributes(
579                     [
580                         { code => $attribute_type->code, attribute => 'a' },
581                         { code => $attribute_type->code, attribute => 'b' }
582                     ]
583                 );
584             }
585             'Koha::Exceptions::Patron::Attribute::NonRepeatable',
586             'Exception thrown on non-repeatable attribute';
587
588         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
589
590         $schema->storage->txn_rollback;
591
592     };
593
594     subtest 'unique attributes tests' => sub {
595
596         plan tests => 5;
597
598         $schema->storage->txn_begin;
599         Koha::Patron::Attribute::Types->search->delete;
600
601         my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
602         my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
603
604         my $attribute_type_1 = $builder->build_object(
605             {
606                 class => 'Koha::Patron::Attribute::Types',
607                 value => { unique_id => 1 }
608             }
609         );
610
611         my $attribute_type_2 = $builder->build_object(
612             {
613                 class => 'Koha::Patron::Attribute::Types',
614                 value => { unique_id => 0 }
615             }
616         );
617
618         is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
619         is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
620
621         $patron_1->extended_attributes(
622             [
623                 { code => $attribute_type_1->code, attribute => 'a' },
624                 { code => $attribute_type_2->code, attribute => 'a' }
625             ]
626         );
627
628         throws_ok
629             {
630                 $patron_2->extended_attributes(
631                     [
632                         { code => $attribute_type_1->code, attribute => 'a' },
633                         { code => $attribute_type_2->code, attribute => 'a' }
634                     ]
635                 );
636             }
637             'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
638             'Exception thrown on unique attribute';
639
640         is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
641         is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
642
643         $schema->storage->txn_rollback;
644
645     };
646
647     subtest 'invalid type attributes tests' => sub {
648
649         plan tests => 3;
650
651         $schema->storage->txn_begin;
652         Koha::Patron::Attribute::Types->search->delete;
653
654         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
655
656         my $attribute_type_1 = $builder->build_object(
657             {
658                 class => 'Koha::Patron::Attribute::Types',
659                 value => { repeatable => 0 }
660             }
661         );
662
663         my $attribute_type_2 = $builder->build_object(
664             {
665                 class => 'Koha::Patron::Attribute::Types'
666             }
667         );
668
669         my $type_2 = $attribute_type_2->code;
670         $attribute_type_2->delete;
671
672         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
673
674         throws_ok
675             {
676                 $patron->extended_attributes(
677                     [
678                         { code => $attribute_type_1->code, attribute => 'a' },
679                         { code => $attribute_type_2->code, attribute => 'b' }
680                     ]
681                 );
682             }
683             'Koha::Exceptions::Patron::Attribute::InvalidType',
684             'Exception thrown on invalid attribute type';
685
686         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
687
688         $schema->storage->txn_rollback;
689
690     };
691
692     subtest 'globally mandatory attributes tests' => sub {
693
694         plan tests => 5;
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 => undef }
705             }
706         );
707
708         my $attribute_type_2 = $builder->build_object(
709             {
710                 class => 'Koha::Patron::Attribute::Types',
711                 value => { mandatory => 0, class => 'a', category_code => undef }
712             }
713         );
714
715         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
716
717         throws_ok
718             {
719                 $patron->extended_attributes(
720                     [
721                         { code => $attribute_type_2->code, attribute => 'b' }
722                     ]
723                 );
724             }
725             'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
726             'Exception thrown on missing mandatory attribute type';
727
728         is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
729
730         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
731
732         $patron->extended_attributes(
733             [
734                 { code => $attribute_type_1->code, attribute => 'b' }
735             ]
736         );
737
738         is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
739
740         $schema->storage->txn_rollback;
741
742     };
743
744     subtest 'limited category mandatory attributes tests' => sub {
745
746         plan tests => 2;
747
748         $schema->storage->txn_begin;
749         Koha::Patron::Attribute::Types->search->delete;
750
751         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
752
753         my $attribute_type_1 = $builder->build_object(
754             {
755                 class => 'Koha::Patron::Attribute::Types',
756                 value => { mandatory => 1, class => 'a', category_code => $patron->categorycode }
757             }
758         );
759
760         $patron->extended_attributes(
761             [
762                 { code => $attribute_type_1->code, attribute => 'a' }
763             ]
764         );
765
766         is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
767
768         $patron = $builder->build_object({ class => 'Koha::Patrons' });
769         # new patron, new category - they shouldn't be required to have any attributes
770
771
772         ok( $patron->extended_attributes([]), "We can set no attributes, mandatory attribute for other category not required");
773
774
775     };
776
777
778
779 };
780
781 subtest 'can_log_into() tests' => sub {
782
783     plan tests => 5;
784
785     $schema->storage->txn_begin;
786
787     my $patron = $builder->build_object(
788         {
789             class => 'Koha::Patrons',
790             value => {
791                 flags => undef
792             }
793         }
794     );
795     my $library = $builder->build_object({ class => 'Koha::Libraries' });
796
797     t::lib::Mocks::mock_preference('IndependentBranches', 1);
798
799     ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
800     ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
801
802     # make it a superlibrarian
803     $patron->set({ flags => 1 })->store->discard_changes;
804     ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
805
806     t::lib::Mocks::mock_preference('IndependentBranches', 0);
807
808     # No special permissions
809     $patron->set({ flags => undef })->store->discard_changes;
810     ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
811     ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
812
813     $schema->storage->txn_rollback;
814 };
815
816 subtest 'can_request_article() tests' => sub {
817
818     plan tests => 4;
819
820     $schema->storage->txn_begin;
821
822     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
823
824     my $item = $builder->build_sample_item;
825
826     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
827     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
828     my $patron    = $builder->build_object( { class => 'Koha::Patrons' } );
829
830     t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
831
832     Koha::CirculationRules->set_rule(
833         {
834             categorycode => undef,
835             branchcode   => $library_1->id,
836             rule_name    => 'open_article_requests_limit',
837             rule_value   => 4,
838         }
839     );
840
841     $builder->build_object(
842         {
843             class => 'Koha::ArticleRequests',
844             value => { status => 'REQUESTED', borrowernumber => $patron->id }
845         }
846     );
847     $builder->build_object(
848         {
849             class => 'Koha::ArticleRequests',
850             value => { status => 'PENDING', borrowernumber => $patron->id }
851         }
852     );
853     $builder->build_object(
854         {
855             class => 'Koha::ArticleRequests',
856             value => { status => 'PROCESSING', borrowernumber => $patron->id }
857         }
858     );
859     $builder->build_object(
860         {
861             class => 'Koha::ArticleRequests',
862             value => { status => 'CANCELED', borrowernumber => $patron->id }
863         }
864     );
865
866     ok(
867         $patron->can_request_article( $library_1->id ),
868         '3 current requests, 4 is the limit: allowed'
869     );
870
871     # Completed request, same day
872     my $completed = $builder->build_object(
873         {
874             class => 'Koha::ArticleRequests',
875             value => {
876                 status         => 'COMPLETED',
877                 borrowernumber => $patron->id
878             }
879         }
880     );
881
882     ok( !$patron->can_request_article( $library_1->id ),
883         '3 current requests and a completed one the same day: denied' );
884
885     $completed->updated_on(
886         dt_from_string->add( days => -1 )->set(
887             hour   => 23,
888             minute => 59,
889             second => 59,
890         )
891     )->store;
892
893     ok( $patron->can_request_article( $library_1->id ),
894         '3 current requests and a completed one the day before: allowed' );
895
896     Koha::CirculationRules->set_rule(
897         {
898             categorycode => undef,
899             branchcode   => $library_2->id,
900             rule_name    => 'open_article_requests_limit',
901             rule_value   => 3,
902         }
903     );
904
905     ok( !$patron->can_request_article,
906         'Not passing the library_id param makes it fallback to userenv: denied'
907     );
908
909     $schema->storage->txn_rollback;
910 };
911
912 subtest 'article_requests() tests' => sub {
913
914     plan tests => 3;
915
916     $schema->storage->txn_begin;
917
918     my $library = $builder->build_object({ class => 'Koha::Libraries' });
919     t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
920
921     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
922
923     my $article_requests = $patron->article_requests;
924     is( ref($article_requests), 'Koha::ArticleRequests',
925         'In scalar context, type is correct' );
926     is( $article_requests->count, 0, 'No article requests' );
927
928     foreach my $i ( 0 .. 3 ) {
929
930         my $item = $builder->build_sample_item;
931
932         Koha::ArticleRequest->new(
933             {
934                 borrowernumber => $patron->id,
935                 biblionumber   => $item->biblionumber,
936                 itemnumber     => $item->id,
937                 title          => "Title",
938             }
939         )->request;
940     }
941
942     $article_requests = $patron->article_requests;
943     is( $article_requests->count, 4, '4 article requests' );
944
945     $schema->storage->txn_rollback;
946
947 };
948
949 subtest 'can_patron_change_staff_only_lists() tests' => sub {
950
951     plan tests => 3;
952
953     $schema->storage->txn_begin;
954
955     # make a user with no special permissions
956     my $patron = $builder->build_object(
957         {
958             class => 'Koha::Patrons',
959             value => {
960                 flags => undef
961             }
962         }
963     );
964     is( $patron->can_patron_change_staff_only_lists(), 0, 'Patron without permissions cannot change staff only lists');
965
966     # make it a 'Catalogue' permission
967     $patron->set({ flags => 4 })->store->discard_changes;
968     is( $patron->can_patron_change_staff_only_lists(), 1, 'Catalogue patron can change staff only lists');
969
970
971     # make it a superlibrarian
972     $patron->set({ flags => 1 })->store->discard_changes;
973     is( $patron->can_patron_change_staff_only_lists(), 1, 'Superlibrarian patron can change staff only lists');
974
975     $schema->storage->txn_rollback;
976 };
977
978 subtest 'can_patron_change_permitted_staff_lists() tests' => sub {
979
980     plan tests => 4;
981
982     $schema->storage->txn_begin;
983
984     # make a user with no special permissions
985     my $patron = $builder->build_object(
986         {
987             class => 'Koha::Patrons',
988             value => {
989                 flags => undef
990             }
991         }
992     );
993     is( $patron->can_patron_change_permitted_staff_lists(), 0, 'Patron without permissions cannot change permitted staff lists');
994
995     # make it a 'Catalogue' permission
996     $patron->set({ flags => 4 })->store->discard_changes;
997     is( $patron->can_patron_change_permitted_staff_lists(), 0, 'Catalogue patron cannot change permitted staff lists');
998
999     # make it a 'Catalogue' permission and 'edit_public_list_contents' sub-permission
1000     $patron->set({ flags => 4 })->store->discard_changes;
1001     $builder->build(
1002         {
1003             source => 'UserPermission',
1004             value  => {
1005                 borrowernumber => $patron->borrowernumber,
1006                 module_bit     => 20,                            # lists
1007                 code           => 'edit_public_list_contents',
1008             },
1009         }
1010     );
1011     is( $patron->can_patron_change_permitted_staff_lists(), 1, 'Catalogue and "edit_public_list_contents" patron can change permitted staff lists');
1012
1013     # make it a superlibrarian
1014     $patron->set({ flags => 1 })->store->discard_changes;
1015     is( $patron->can_patron_change_permitted_staff_lists(), 1, 'Superlibrarian patron can change permitted staff lists');
1016
1017     $schema->storage->txn_rollback;
1018 };
1019
1020 subtest 'password expiration tests' => sub {
1021
1022     plan tests => 5;
1023
1024     $schema->storage->txn_begin;
1025     my $date = dt_from_string();
1026     my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
1027             password_expiry_days => 10,
1028             require_strong_password => 0,
1029         }
1030     });
1031     my $patron = $builder->build_object({ class=> 'Koha::Patrons', value => {
1032             categorycode => $category->categorycode,
1033             password => 'hats'
1034         }
1035     });
1036
1037     $patron->delete()->store()->discard_changes(); # Make sure we are storing a 'new' patron
1038
1039     is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd() , "Password expiration date set correctly on patron creation");
1040
1041     $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1042             categorycode => $category->categorycode,
1043             password => undef
1044         }
1045     });
1046     $patron->delete()->store()->discard_changes();
1047
1048     is( $patron->password_expiration_date(), undef, "Password expiration date is not set if patron does not have a password");
1049
1050     $category->password_expiry_days(undef)->store();
1051     $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1052             categorycode => $category->categorycode
1053         }
1054     });
1055     $patron->delete()->store()->discard_changes();
1056     is( $patron->password_expiration_date(), undef, "Password expiration date is not set if category does not have expiry days set");
1057
1058     $schema->storage->txn_rollback;
1059
1060     subtest 'password_expired' => sub {
1061
1062         plan tests => 3;
1063
1064         $schema->storage->txn_begin;
1065         my $date = dt_from_string();
1066         $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1067                 password_expiration_date => undef
1068             }
1069         });
1070         is( $patron->password_expired, 0, "Patron with no password expiration date, password not expired");
1071         $patron->password_expiration_date( $date )->store;
1072         $patron->discard_changes();
1073         is( $patron->password_expired, 1, "Patron with password expiration date of today, password expired");
1074         $date->subtract( days => 1 );
1075         $patron->password_expiration_date( $date )->store;
1076         $patron->discard_changes();
1077         is( $patron->password_expired, 1, "Patron with password expiration date in past, password expired");
1078
1079         $schema->storage->txn_rollback;
1080     };
1081
1082     subtest 'set_password' => sub {
1083
1084         plan tests => 4;
1085
1086         $schema->storage->txn_begin;
1087
1088         my $date = dt_from_string();
1089         my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
1090                 password_expiry_days => 10
1091             }
1092         });
1093         my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1094                 categorycode => $category->categorycode,
1095                 password_expiration_date =>  $date->subtract( days => 1 )
1096             }
1097         });
1098         is( $patron->password_expired, 1, "Patron password is expired");
1099
1100         $date = dt_from_string();
1101         $patron->set_password({ password => "kitten", skip_validation => 1 })->discard_changes();
1102         is( $patron->password_expired, 0, "Patron password no longer expired when new password set");
1103         is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd(), "Password expiration date set correctly on patron creation");
1104
1105
1106         $category->password_expiry_days( undef )->store();
1107         $patron->set_password({ password => "puppies", skip_validation => 1 })->discard_changes();
1108         is( $patron->password_expiration_date(), undef, "Password expiration date is unset if category does not have expiry days");
1109
1110         $schema->storage->txn_rollback;
1111     };
1112
1113 };
1114
1115 subtest 'safe_to_delete() tests' => sub {
1116
1117     plan tests => 14;
1118
1119     $schema->storage->txn_begin;
1120
1121     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1122
1123     ## Make it the anonymous
1124     t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
1125
1126     ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
1127     my $message = $patron->safe_to_delete->messages->[0];
1128     is( $message->type, 'error', 'Type is error' );
1129     is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
1130     # cleanup
1131     t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
1132
1133     ## Make it have a checkout
1134     my $checkout = $builder->build_object(
1135         {
1136             class => 'Koha::Checkouts',
1137             value => { borrowernumber => $patron->id }
1138         }
1139     );
1140
1141     ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
1142     $message = $patron->safe_to_delete->messages->[0];
1143     is( $message->type, 'error', 'Type is error' );
1144     is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
1145     # cleanup
1146     $checkout->delete;
1147
1148     ## Make it have a guarantee
1149     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
1150     $builder->build_object({ class => 'Koha::Patrons' })
1151             ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
1152
1153     ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
1154     $message = $patron->safe_to_delete->messages->[0];
1155     is( $message->type, 'error', 'Type is error' );
1156     is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
1157
1158     # cleanup
1159     $patron->guarantee_relationships->delete;
1160
1161     ## Make it have debt
1162     my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
1163
1164     ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
1165     $message = $patron->safe_to_delete->messages->[0];
1166     is( $message->type, 'error', 'Type is error' );
1167     is( $message->message, 'has_debt', 'Cannot delete, has debt' );
1168     # cleanup
1169     my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
1170     t::lib::Mocks::mock_userenv( { borrowernumber => $manager->id } );
1171     $patron->account->pay({ amount => 10, debits => [ $debit ] });
1172
1173     ## Happy case :-D
1174     ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
1175     my $messages = $patron->safe_to_delete->messages;
1176     is_deeply( $messages, [], 'Patron can be deleted, no messages' );
1177 };
1178
1179 subtest 'article_request_fee() tests' => sub {
1180
1181     plan tests => 3;
1182
1183     $schema->storage->txn_begin;
1184
1185     # Cleanup, to avoid interference
1186     Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
1187
1188     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1189
1190     my $item = $builder->build_sample_item;
1191
1192     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1193     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1194     my $patron    = $builder->build_object( { class => 'Koha::Patrons' } );
1195
1196     # Rule that should never be picked, because the patron's category is always picked
1197     Koha::CirculationRules->set_rule(
1198         {   categorycode => undef,
1199             branchcode   => undef,
1200             rule_name    => 'article_request_fee',
1201             rule_value   => 1,
1202         }
1203     );
1204
1205     is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
1206
1207     Koha::CirculationRules->set_rule(
1208         {   categorycode => $patron->categorycode,
1209             branchcode   => undef,
1210             rule_name    => 'article_request_fee',
1211             rule_value   => 2,
1212         }
1213     );
1214
1215     Koha::CirculationRules->set_rule(
1216         {   categorycode => $patron->categorycode,
1217             branchcode   => $library_1->id,
1218             rule_name    => 'article_request_fee',
1219             rule_value   => 3,
1220         }
1221     );
1222
1223     is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
1224
1225     t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
1226
1227     is( $patron->article_request_fee(), 3, 'env used correctly' );
1228
1229     $schema->storage->txn_rollback;
1230 };
1231
1232 subtest 'add_article_request_fee_if_needed() tests' => sub {
1233
1234     plan tests => 12;
1235
1236     $schema->storage->txn_begin;
1237
1238     my $amount = 0;
1239
1240     my $patron_mock = Test::MockModule->new('Koha::Patron');
1241     $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
1242
1243     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1244
1245     is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
1246
1247     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1248     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1249     my $staff     = $builder->build_object( { class => 'Koha::Patrons' } );
1250     my $item      = $builder->build_sample_item;
1251
1252     t::lib::Mocks::mock_userenv(
1253         { branchcode => $library_1->id, patron => $staff } );
1254
1255     my $debit = $patron->add_article_request_fee_if_needed();
1256     is( $debit, undef, 'No fee, no debit line' );
1257
1258     # positive value
1259     $amount = 1;
1260
1261     $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
1262     is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1263     is( $debit->amount, $amount,
1264         'amount set to $patron->article_request_fee value' );
1265     is( $debit->manager_id, $staff->id,
1266         'manager_id set to userenv session user' );
1267     is( $debit->branchcode, $library_1->id,
1268         'branchcode set to userenv session library' );
1269     is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1270         'debit_type_code set correctly' );
1271     is( $debit->itemnumber, $item->id,
1272         'itemnumber set correctly' );
1273
1274     $amount = 100;
1275
1276     $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1277     is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1278     is( $debit->amount, $amount,
1279         'amount set to $patron->article_request_fee value' );
1280     is( $debit->branchcode, $library_2->id,
1281         'branchcode set to userenv session library' );
1282     is( $debit->itemnumber, undef,
1283         'itemnumber set correctly to undef' );
1284
1285     $schema->storage->txn_rollback;
1286 };
1287
1288 subtest 'messages' => sub {
1289     plan tests => 4;
1290
1291     $schema->storage->txn_begin;
1292
1293     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1294     my $messages = $patron->messages;
1295     is( $messages->count, 0, "No message yet" );
1296     my $message_1 = $builder->build_object(
1297         {
1298             class => 'Koha::Patron::Messages',
1299             value => { borrowernumber => $patron->borrowernumber }
1300         }
1301     );
1302     my $message_2 = $builder->build_object(
1303         {
1304             class => 'Koha::Patron::Messages',
1305             value => { borrowernumber => $patron->borrowernumber }
1306         }
1307     );
1308
1309     $messages = $patron->messages;
1310     is( $messages->count, 2, "There are two messages for this patron" );
1311     is( $messages->next->message, $message_1->message );
1312     is( $messages->next->message, $message_2->message );
1313     $schema->storage->txn_rollback;
1314 };
1315
1316 subtest 'recalls() tests' => sub {
1317
1318     plan tests => 3;
1319
1320     $schema->storage->txn_begin;
1321
1322     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1323     my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
1324     my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
1325     my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
1326     my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
1327
1328     Koha::Recall->new(
1329         {   biblio_id         => $biblio1->biblionumber,
1330             patron_id         => $patron->borrowernumber,
1331             item_id           => $item1->itemnumber,
1332             pickup_library_id => $patron->branchcode,
1333             created_date      => \'NOW()',
1334             item_level        => 1,
1335         }
1336     )->store;
1337     Koha::Recall->new(
1338         {   biblio_id         => $biblio2->biblionumber,
1339             patron_id         => $patron->borrowernumber,
1340             item_id           => $item2->itemnumber,
1341             pickup_library_id => $patron->branchcode,
1342             created_date      => \'NOW()',
1343             item_level        => 1,
1344         }
1345     )->store;
1346     Koha::Recall->new(
1347         {   biblio_id         => $biblio1->biblionumber,
1348             patron_id         => $patron->borrowernumber,
1349             item_id           => undef,
1350             pickup_library_id => $patron->branchcode,
1351             created_date      => \'NOW()',
1352             item_level        => 0,
1353         }
1354     )->store;
1355     my $recall = Koha::Recall->new(
1356         {   biblio_id         => $biblio1->biblionumber,
1357             patron_id         => $patron->borrowernumber,
1358             item_id           => undef,
1359             pickup_library_id => $patron->branchcode,
1360             created_date      => \'NOW()',
1361             item_level        => 0,
1362         }
1363     )->store;
1364     $recall->set_cancelled;
1365
1366     is( $patron->recalls->count,                                                                       4, "Correctly gets this patron's recalls" );
1367     is( $patron->recalls->filter_by_current->count,                                                    3, "Correctly gets this patron's active recalls" );
1368     is( $patron->recalls->filter_by_current->search( { biblio_id => $biblio1->biblionumber } )->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
1369
1370     $schema->storage->txn_rollback;
1371 };
1372
1373 subtest 'encode_secret and decoded_secret' => sub {
1374     plan tests => 5;
1375     $schema->storage->txn_begin;
1376
1377     t::lib::Mocks::mock_config('encryption_key', 't0P_secret');
1378
1379     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1380     is( $patron->decoded_secret, undef, 'TestBuilder does not initialize it' );
1381     $patron->secret(q{});
1382     is( $patron->decoded_secret, q{}, 'Empty string case' );
1383
1384     $patron->encode_secret('encrypt_me'); # Note: lazy testing; should be base32 string normally.
1385     is( length($patron->secret) > 0, 1, 'Secret length' );
1386     isnt( $patron->secret, 'encrypt_me', 'Encrypted column' );
1387     is( $patron->decoded_secret, 'encrypt_me', 'Decrypted column' );
1388
1389     $schema->storage->txn_rollback;
1390 };
1391
1392 subtest 'notify_library_of_registration()' => sub {
1393
1394     plan tests => 6;
1395
1396     $schema->storage->txn_begin;
1397     my $dbh = C4::Context->dbh;
1398
1399     my $library = $builder->build_object(
1400         {
1401             class => 'Koha::Libraries',
1402             value => {
1403                 branchemail   => 'from@mybranch.com',
1404                 branchreplyto => 'to@mybranch.com'
1405             }
1406         }
1407     );
1408     my $patron = $builder->build_object(
1409         {
1410             class => 'Koha::Patrons',
1411             value => {
1412                 branchcode => $library->branchcode
1413             }
1414         }
1415     );
1416
1417     t::lib::Mocks::mock_preference( 'KohaAdminEmailAddress', 'root@localhost' );
1418     t::lib::Mocks::mock_preference( 'EmailAddressForPatronRegistrations', 'library@localhost' );
1419
1420     # Test when EmailPatronRegistrations equals BranchEmailAddress
1421     t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'BranchEmailAddress' );
1422     is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals BranchEmailAddress');
1423     my $sth = $dbh->prepare("SELECT to_address FROM message_queue where borrowernumber = ?");
1424     $sth->execute( $patron->borrowernumber );
1425     my $to_address = $sth->fetchrow_array;
1426     is( $to_address, 'to@mybranch.com', 'OPAC_REG email queued to go to branchreplyto address when EmailPatronRegistration equals BranchEmailAddress' );
1427     $dbh->do(q|DELETE FROM message_queue|);
1428
1429     # Test when EmailPatronRegistrations equals EmailAddressForPatronRegistrations
1430     t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'EmailAddressForPatronRegistrations' );
1431     is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals EmailAddressForPatronRegistrations');
1432     $sth->execute( $patron->borrowernumber );
1433     $to_address = $sth->fetchrow_array;
1434     is( $to_address, 'library@localhost', 'OPAC_REG email queued to go to EmailAddressForPatronRegistrations syspref when EmailPatronRegistration equals EmailAddressForPatronRegistrations' );
1435     $dbh->do(q|DELETE FROM message_queue|);
1436
1437     # Test when EmailPatronRegistrations equals KohaAdminEmailAddress
1438     t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'KohaAdminEmailAddress' );
1439     t::lib::Mocks::mock_preference( 'ReplyToDefault', 'root@localhost' ); # FIXME Remove localhost
1440     is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals KohaAdminEmailAddress');
1441     $sth->execute( $patron->borrowernumber );
1442     $to_address = $sth->fetchrow_array;
1443     is( $to_address, 'root@localhost', 'OPAC_REG email queued to go to KohaAdminEmailAddress syspref when EmailPatronRegistration equals KohaAdminEmailAddress' );
1444     $dbh->do(q|DELETE FROM message_queue|);
1445
1446     $schema->storage->txn_rollback;
1447 };
1448
1449 subtest 'notice_email_address' => sub {
1450     plan tests => 2;
1451
1452     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1453
1454     t::lib::Mocks::mock_preference( 'EmailFieldPrecedence', 'email|emailpro' );
1455     t::lib::Mocks::mock_preference( 'EmailFieldPrimary', 'OFF' );
1456     is ($patron->notice_email_address, $patron->email, "Koha::Patron->notice_email_address returns correct value when EmailFieldPrimary is off");
1457
1458     t::lib::Mocks::mock_preference( 'EmailFieldPrimary', 'emailpro' );
1459     is ($patron->notice_email_address, $patron->emailpro, "Koha::Patron->notice_email_address returns correct value when EmailFieldPrimary is emailpro");
1460
1461     $patron->delete;
1462 };
1463
1464 subtest 'first_valid_email_address' => sub {
1465     plan tests => 1;
1466
1467     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { emailpro => ''}});
1468
1469     t::lib::Mocks::mock_preference( 'EmailFieldPrecedence', 'emailpro|email' );
1470     is ($patron->first_valid_email_address, $patron->email, "Koha::Patron->first_valid_email_address returns correct value when EmailFieldPrecedence is 'emailpro|email' and emailpro is empty");
1471
1472     $patron->delete;
1473 };
1474
1475 subtest 'get_savings tests' => sub {
1476
1477     plan tests => 4;
1478
1479     $schema->storage->txn_begin;
1480
1481     my $library = $builder->build_object({ class => 'Koha::Libraries' });
1482     my $patron = $builder->build_object({ class => 'Koha::Patrons' }, { value => { branchcode => $library->branchcode } });
1483
1484     t::lib::Mocks::mock_userenv({ patron => $patron, branchcode => $library->branchcode });
1485
1486     my $biblio = $builder->build_sample_biblio;
1487     my $item1 = $builder->build_sample_item(
1488         {
1489             biblionumber     => $biblio->biblionumber,
1490             library          => $library->branchcode,
1491             replacementprice => rand(20),
1492         }
1493     );
1494     my $item2 = $builder->build_sample_item(
1495         {
1496             biblionumber     => $biblio->biblionumber,
1497             library          => $library->branchcode,
1498             replacementprice => rand(20),
1499         }
1500     );
1501
1502     is( $patron->get_savings, 0, 'No checkouts, no savings' );
1503
1504     # Add an old checkout with deleted itemnumber
1505     $builder->build_object({ class => 'Koha::Old::Checkouts', value => { itemnumber => undef, borrowernumber => $patron->id } });
1506
1507     is( $patron->get_savings, 0, 'No checkouts with itemnumber, no savings' );
1508
1509     AddIssue( $patron, $item1->barcode );
1510     AddIssue( $patron, $item2->barcode );
1511
1512     my $savings = $patron->get_savings;
1513     is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current issues" );
1514
1515     AddReturn( $item2->barcode, $item2->homebranch );
1516
1517     $savings = $patron->get_savings;
1518     is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current and old issues" );
1519
1520     $schema->storage->txn_rollback;
1521 };
1522
1523 subtest 'update privacy tests' => sub {
1524
1525     plan tests => 5;
1526
1527     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy => 1 } });
1528
1529     my $old_checkout = $builder->build_object({ class => 'Koha::Old::Checkouts', value => { borrowernumber => $patron->id } });
1530
1531     t::lib::Mocks::mock_preference( 'AnonymousPatron', '0' );
1532
1533     $patron->privacy(2); #set to never
1534
1535     throws_ok{ $patron->store } 'Koha::Exceptions::Patron::FailedAnonymizing', 'We throw an exception when anonymizing fails';
1536
1537     $old_checkout->discard_changes; #refresh from db
1538     $patron->discard_changes;
1539
1540     is( $old_checkout->borrowernumber, $patron->id, "When anonymizing fails, we don't clear the checkouts");
1541     is( $patron->privacy(), 1, "When anonymizing fails, we don't chaneg the privacy");
1542
1543     my $anon_patron = $builder->build_object({ class => 'Koha::Patrons'});
1544     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anon_patron->id );
1545
1546     $patron->privacy(2)->store(); #set to never
1547
1548     $old_checkout->discard_changes; #refresh from db
1549     $patron->discard_changes;
1550
1551     is( $old_checkout->borrowernumber, $anon_patron->id, "Checkout is successfully anonymized");
1552     is( $patron->privacy(), 2, "Patron privacy is successfully updated");
1553 };