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