Bug 27946: Article request fee methods in Koha::Patron
[koha-ffzg.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 => 13;
23 use Test::Exception;
24 use Test::Warn;
25
26 use Koha::CirculationRules;
27 use Koha::Database;
28 use Koha::DateUtils qw(dt_from_string);
29 use Koha::ArticleRequests;
30 use Koha::Patrons;
31 use Koha::Patron::Relationships;
32
33 use t::lib::TestBuilder;
34 use t::lib::Mocks;
35
36 my $schema  = Koha::Database->new->schema;
37 my $builder = t::lib::TestBuilder->new;
38
39 subtest 'add_guarantor() tests' => sub {
40
41     plan tests => 6;
42
43     $schema->storage->txn_begin;
44
45     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'father1|father2' );
46
47     my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
48     my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
49
50     throws_ok
51         { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber }); }
52         'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
53         'Exception is thrown as no relationship passed';
54
55     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
56
57     throws_ok
58         { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father' }); }
59         'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
60         'Exception is thrown as a wrong relationship was passed';
61
62     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
63
64     $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father1' });
65
66     my $guarantors = $patron_1->guarantor_relationships;
67
68     is( $guarantors->count, 1, 'No guarantors added' );
69
70     {
71         local *STDERR;
72         open STDERR, '>', '/dev/null';
73         throws_ok
74             { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father2' }); }
75             'Koha::Exceptions::Patron::Relationship::DuplicateRelationship',
76             'Exception is thrown for duplicated relationship';
77         close STDERR;
78     }
79
80     $schema->storage->txn_rollback;
81 };
82
83 subtest 'relationships_debt() tests' => sub {
84
85     plan tests => 168;
86
87     $schema->storage->txn_begin;
88
89     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
90
91     my $parent_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 1" } });
92     my $parent_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 2" } });
93     my $child_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Child 1" } });
94     my $child_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Child 2" } });
95
96     $child_1->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
97     $child_1->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
98     $child_2->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
99     $child_2->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
100
101     is( $child_1->guarantor_relationships->guarantors->count, 2, 'Child 1 has correct number of guarantors' );
102     is( $child_2->guarantor_relationships->guarantors->count, 2, 'Child 2 has correct number of guarantors' );
103     is( $parent_1->guarantee_relationships->guarantees->count, 2, 'Parent 1 has correct number of guarantees' );
104     is( $parent_2->guarantee_relationships->guarantees->count, 2, 'Parent 2 has correct number of guarantees' );
105
106     my $patrons = [ $parent_1, $parent_2, $child_1, $child_2 ];
107
108     # First test: No debt
109     my ($parent1_debt, $parent2_debt, $child1_debt, $child2_debt) = (0,0,0,0);
110     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
111
112     # Add debt to child_2
113     $child2_debt = 2;
114     $child_2->account->add_debit({ type => 'ACCOUNT', amount => $child2_debt, interface => 'commandline' });
115     is( $child_2->account->non_issues_charges, $child2_debt, 'Debt added to Child 2' );
116     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
117
118     $parent1_debt = 3;
119     $parent_1->account->add_debit({ type => 'ACCOUNT', amount => $parent1_debt, interface => 'commandline' });
120     is( $parent_1->account->non_issues_charges, $parent1_debt, 'Debt added to Parent 1' );
121     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
122
123     $parent2_debt = 5;
124     $parent_2->account->add_debit({ type => 'ACCOUNT', amount => $parent2_debt, interface => 'commandline' });
125     is( $parent_2->account->non_issues_charges, $parent2_debt, 'Parent 2 owes correct amount' );
126     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
127
128     $child1_debt = 7;
129     $child_1->account->add_debit({ type => 'ACCOUNT', amount => $child1_debt, interface => 'commandline' });
130     is( $child_1->account->non_issues_charges, $child1_debt, 'Child 1 owes correct amount' );
131     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
132
133     $schema->storage->txn_rollback;
134 };
135
136 sub _test_combinations {
137     my ( $patrons, $parent1_debt, $parent2_debt, $child1_debt, $child2_debt ) = @_;
138
139     # Options
140     # P1 => P1 + C1 + C2 ( - P1 ) ( + P2 )
141     # P2 => P2 + C1 + C2 ( - P2 ) ( + P1 )
142     # C1 => P1 + P2 + C1 + C2 ( - C1 )
143     # C2 => P1 + P2 + C1 + C2 ( - C2 )
144
145 # 3 params, count from 0 to 7 in binary ( 3 places ) to get the set of switches, then do that 4 times, one for each parent and child
146     for my $i ( 0 .. 7 ) {
147         my ( $only_this_guarantor, $include_guarantors, $include_this_patron )
148           = split '', sprintf( "%03b", $i );
149         for my $patron ( @$patrons ) {
150             if ( $only_this_guarantor
151                 && !$patron->guarantee_relationships->count )
152             {
153                 throws_ok {
154                     $patron->relationships_debt(
155                         {
156                             only_this_guarantor => $only_this_guarantor,
157                             include_guarantors  => $include_guarantors,
158                             include_this_patron => $include_this_patron
159                         }
160                     );
161                 }
162                 'Koha::Exceptions::BadParameter',
163                   'Exception is thrown as patron is not a guarantor';
164
165             }
166             else {
167
168                 my $debt = 0;
169                 if ( $patron->firstname eq 'Parent 1' ) {
170                     $debt += $parent1_debt if ($include_this_patron && $include_guarantors);
171                     $debt += $child1_debt + $child2_debt;
172                     $debt += $parent2_debt unless ($only_this_guarantor || !$include_guarantors);
173                 }
174                 elsif ( $patron->firstname eq 'Parent 2' ) {
175                     $debt += $parent2_debt if ($include_this_patron & $include_guarantors);
176                     $debt += $child1_debt + $child2_debt;
177                     $debt += $parent1_debt unless ($only_this_guarantor || !$include_guarantors);
178                 }
179                 elsif ( $patron->firstname eq 'Child 1' ) {
180                     $debt += $child1_debt if ($include_this_patron);
181                     $debt += $child2_debt;
182                     $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
183                 }
184                 else {
185                     $debt += $child2_debt if ($include_this_patron);
186                     $debt += $child1_debt;
187                     $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
188                 }
189
190                 is(
191                     $patron->relationships_debt(
192                         {
193                             only_this_guarantor => $only_this_guarantor,
194                             include_guarantors  => $include_guarantors,
195                             include_this_patron => $include_this_patron
196                         }
197                     ),
198                     $debt,
199                     $patron->firstname
200                       . " debt of $debt calculated correctly for ( only_this_guarantor: $only_this_guarantor, include_guarantors: $include_guarantors, include_this_patron: $include_this_patron)"
201                 );
202             }
203         }
204     }
205 }
206
207 subtest 'add_enrolment_fee_if_needed() tests' => sub {
208
209     plan tests => 2;
210
211     subtest 'category has enrolment fee' => sub {
212         plan tests => 7;
213
214         $schema->storage->txn_begin;
215
216         my $category = $builder->build_object(
217             {
218                 class => 'Koha::Patron::Categories',
219                 value => {
220                     enrolmentfee => 20
221                 }
222             }
223         );
224
225         my $patron = $builder->build_object(
226             {
227                 class => 'Koha::Patrons',
228                 value => {
229                     categorycode => $category->categorycode
230                 }
231             }
232         );
233
234         my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
235         is( $enrollment_fee * 1, 20, 'Enrolment fee amount is correct' );
236         my $account = $patron->account;
237         is( $patron->account->balance * 1, 20, 'Patron charged the enrolment fee' );
238         # second enrolment fee, new
239         $enrollment_fee = $patron->add_enrolment_fee_if_needed(0);
240         # third enrolment fee, renewal
241         $enrollment_fee = $patron->add_enrolment_fee_if_needed(1);
242         is( $patron->account->balance * 1, 60, 'Patron charged the enrolment fees' );
243
244         my @debits = $account->outstanding_debits;
245         is( scalar @debits, 3, '3 enrolment fees' );
246         is( $debits[0]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
247         is( $debits[1]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
248         is( $debits[2]->debit_type_code, 'ACCOUNT_RENEW', 'Account type set correctly' );
249
250         $schema->storage->txn_rollback;
251     };
252
253     subtest 'no enrolment fee' => sub {
254
255         plan tests => 3;
256
257         $schema->storage->txn_begin;
258
259         my $category = $builder->build_object(
260             {
261                 class => 'Koha::Patron::Categories',
262                 value => {
263                     enrolmentfee => 0
264                 }
265             }
266         );
267
268         my $patron = $builder->build_object(
269             {
270                 class => 'Koha::Patrons',
271                 value => {
272                     categorycode => $category->categorycode
273                 }
274             }
275         );
276
277         my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
278         is( $enrollment_fee * 1, 0, 'No enrolment fee' );
279         my $account = $patron->account;
280         is( $patron->account->balance, 0, 'Patron not charged anything' );
281
282         my @debits = $account->outstanding_debits;
283         is( scalar @debits, 0, 'no debits' );
284
285         $schema->storage->txn_rollback;
286     };
287 };
288
289 subtest 'to_api() tests' => sub {
290
291     plan tests => 6;
292
293     $schema->storage->txn_begin;
294
295     my $patron_class = Test::MockModule->new('Koha::Patron');
296     $patron_class->mock(
297         'algo',
298         sub { return 'algo' }
299     );
300
301     my $patron = $builder->build_object(
302         {
303             class => 'Koha::Patrons',
304             value => {
305                 debarred => undef
306             }
307         }
308     );
309
310     my $restricted = $patron->to_api->{restricted};
311     ok( defined $restricted, 'restricted is defined' );
312     ok( !$restricted, 'debarred is undef, restricted evaluates to false' );
313
314     $patron->debarred( dt_from_string->add( days => 1 ) )->store->discard_changes;
315     $restricted = $patron->to_api->{restricted};
316     ok( defined $restricted, 'restricted is defined' );
317     ok( $restricted, 'debarred is defined, restricted evaluates to true' );
318
319     my $patron_json = $patron->to_api({ embed => { algo => {} } });
320     ok( exists $patron_json->{algo} );
321     is( $patron_json->{algo}, 'algo' );
322
323     $schema->storage->txn_rollback;
324 };
325
326 subtest 'login_attempts tests' => sub {
327     plan tests => 1;
328
329     $schema->storage->txn_begin;
330
331     my $patron = $builder->build_object(
332         {
333             class => 'Koha::Patrons',
334         }
335     );
336     my $patron_info = $patron->unblessed;
337     $patron->delete;
338     delete $patron_info->{login_attempts};
339     my $new_patron = Koha::Patron->new($patron_info)->store;
340     is( $new_patron->discard_changes->login_attempts, 0, "login_attempts defaults to 0 as expected");
341
342     $schema->storage->txn_rollback;
343 };
344
345 subtest 'is_superlibrarian() tests' => sub {
346
347     plan tests => 3;
348
349     $schema->storage->txn_begin;
350
351     my $patron = $builder->build_object(
352         {
353             class => 'Koha::Patrons',
354
355             value => {
356                 flags => 16
357             }
358         }
359     );
360
361     is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
362
363     $patron->flags(1)->store->discard_changes;
364     is( $patron->is_superlibrarian, 1, 'Patron is a superlibrarian and the method returns the correct value' );
365
366     $patron->flags(0)->store->discard_changes;
367     is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
368
369     $schema->storage->txn_rollback;
370 };
371
372 subtest 'extended_attributes' => sub {
373
374     plan tests => 15;
375
376     my $schema = Koha::Database->new->schema;
377     $schema->storage->txn_begin;
378
379     my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
380     my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
381
382     t::lib::Mocks::mock_userenv({ patron => $patron_1 });
383
384     my $attribute_type1 = Koha::Patron::Attribute::Type->new(
385         {
386             code        => 'my code1',
387             description => 'my description1',
388             unique_id   => 1
389         }
390     )->store;
391     my $attribute_type2 = Koha::Patron::Attribute::Type->new(
392         {
393             code             => 'my code2',
394             description      => 'my description2',
395             opac_display     => 1,
396             staff_searchable => 1
397         }
398     )->store;
399
400     my $new_library = $builder->build( { source => 'Branch' } );
401     my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
402         { code => 'my code3', description => 'my description3' } )->store;
403     $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
404
405     my $attributes_for_1 = [
406         {
407             attribute => 'my attribute1',
408             code => $attribute_type1->code(),
409         },
410         {
411             attribute => 'my attribute2',
412             code => $attribute_type2->code(),
413         },
414         {
415             attribute => 'my attribute limited',
416             code => $attribute_type_limited->code(),
417         }
418     ];
419
420     my $attributes_for_2 = [
421         {
422             attribute => 'my attribute12',
423             code => $attribute_type1->code(),
424         },
425         {
426             attribute => 'my attribute limited 2',
427             code => $attribute_type_limited->code(),
428         }
429     ];
430
431     my $extended_attributes = $patron_1->extended_attributes;
432     is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
433     is( $extended_attributes->count, 0, 'There should not be attribute yet');
434
435     $patron_1->extended_attributes->filter_by_branch_limitations->delete;
436     $patron_2->extended_attributes->filter_by_branch_limitations->delete;
437     $patron_1->extended_attributes($attributes_for_1);
438     $patron_2->extended_attributes($attributes_for_2);
439
440     my $extended_attributes_for_1 = $patron_1->extended_attributes;
441     is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
442
443     my $extended_attributes_for_2 = $patron_2->extended_attributes;
444     is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
445
446     my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
447     is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
448
449     $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
450     is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
451
452     my $expected_attributes_for_2 = [
453         {
454             code      => $attribute_type1->code(),
455             attribute => 'my attribute12',
456         },
457         {
458             code      => $attribute_type_limited->code(),
459             attribute => 'my attribute limited 2',
460         }
461     ];
462     # Sorting them by code
463     $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
464     my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
465
466     is_deeply(
467         [
468             {
469                 code      => $extended_attributes_for_2[0]->code,
470                 attribute => $extended_attributes_for_2[0]->attribute
471             },
472             {
473                 code      => $extended_attributes_for_2[1]->code,
474                 attribute => $extended_attributes_for_2[1]->attribute
475             }
476         ],
477         $expected_attributes_for_2
478     );
479
480     # TODO - What about multiple? POD explains the problem
481     my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
482     is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
483
484     # Test branch limitations
485     t::lib::Mocks::mock_userenv({ patron => $patron_2 });
486     # Return all
487     $extended_attributes_for_1 = $patron_1->extended_attributes;
488     is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
489
490     # Return filtered
491     $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
492     is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
493
494     # Not filtered
495     my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
496     is( $limited_value->attribute, 'my attribute limited', );
497
498     ## Do we need a filtered?
499     #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
500     #is( $limited_value, undef, );
501
502     $schema->storage->txn_rollback;
503
504     subtest 'non-repeatable attributes tests' => sub {
505
506         plan tests => 3;
507
508         $schema->storage->txn_begin;
509
510         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
511         my $attribute_type = $builder->build_object(
512             {
513                 class => 'Koha::Patron::Attribute::Types',
514                 value => { repeatable => 0 }
515             }
516         );
517
518         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
519
520         throws_ok
521             {
522                 $patron->extended_attributes(
523                     [
524                         { code => $attribute_type->code, attribute => 'a' },
525                         { code => $attribute_type->code, attribute => 'b' }
526                     ]
527                 );
528             }
529             'Koha::Exceptions::Patron::Attribute::NonRepeatable',
530             'Exception thrown on non-repeatable attribute';
531
532         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
533
534         $schema->storage->txn_rollback;
535
536     };
537
538     subtest 'unique attributes tests' => sub {
539
540         plan tests => 5;
541
542         $schema->storage->txn_begin;
543
544         my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
545         my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
546
547         my $attribute_type_1 = $builder->build_object(
548             {
549                 class => 'Koha::Patron::Attribute::Types',
550                 value => { unique => 1 }
551             }
552         );
553
554         my $attribute_type_2 = $builder->build_object(
555             {
556                 class => 'Koha::Patron::Attribute::Types',
557                 value => { unique => 0 }
558             }
559         );
560
561         is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
562         is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
563
564         $patron_1->extended_attributes(
565             [
566                 { code => $attribute_type_1->code, attribute => 'a' },
567                 { code => $attribute_type_2->code, attribute => 'a' }
568             ]
569         );
570
571         throws_ok
572             {
573                 $patron_2->extended_attributes(
574                     [
575                         { code => $attribute_type_1->code, attribute => 'a' },
576                         { code => $attribute_type_2->code, attribute => 'a' }
577                     ]
578                 );
579             }
580             'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
581             'Exception thrown on unique attribute';
582
583         is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
584         is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
585
586         $schema->storage->txn_rollback;
587
588     };
589
590     subtest 'invalid type attributes tests' => sub {
591
592         plan tests => 3;
593
594         $schema->storage->txn_begin;
595
596         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
597
598         my $attribute_type_1 = $builder->build_object(
599             {
600                 class => 'Koha::Patron::Attribute::Types',
601                 value => { repeatable => 0 }
602             }
603         );
604
605         my $attribute_type_2 = $builder->build_object(
606             {
607                 class => 'Koha::Patron::Attribute::Types'
608             }
609         );
610
611         my $type_2 = $attribute_type_2->code;
612         $attribute_type_2->delete;
613
614         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
615
616         throws_ok
617             {
618                 $patron->extended_attributes(
619                     [
620                         { code => $attribute_type_1->code, attribute => 'a' },
621                         { code => $attribute_type_2->code, attribute => 'b' }
622                     ]
623                 );
624             }
625             'Koha::Exceptions::Patron::Attribute::InvalidType',
626             'Exception thrown on invalid attribute type';
627
628         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
629
630         $schema->storage->txn_rollback;
631
632     };
633
634     subtest 'globally mandatory attributes tests' => sub {
635
636         plan tests => 5;
637
638         $schema->storage->txn_begin;
639
640         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
641
642         my $attribute_type_1 = $builder->build_object(
643             {
644                 class => 'Koha::Patron::Attribute::Types',
645                 value => { mandatory => 1, class => 'a' }
646             }
647         );
648
649         my $attribute_type_2 = $builder->build_object(
650             {
651                 class => 'Koha::Patron::Attribute::Types',
652                 value => { mandatory => 0, class => 'a' }
653             }
654         );
655
656         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
657
658         throws_ok
659             {
660                 $patron->extended_attributes(
661                     [
662                         { code => $attribute_type_2->code, attribute => 'b' }
663                     ]
664                 );
665             }
666             'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
667             'Exception thrown on missing mandatory attribute type';
668
669         is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
670
671         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
672
673         $patron->extended_attributes(
674             [
675                 { code => $attribute_type_1->code, attribute => 'b' }
676             ]
677         );
678
679         is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
680
681         $schema->storage->txn_rollback;
682
683     };
684
685 };
686
687 subtest 'can_log_into() tests' => sub {
688
689     plan tests => 5;
690
691     $schema->storage->txn_begin;
692
693     my $patron = $builder->build_object(
694         {
695             class => 'Koha::Patrons',
696             value => {
697                 flags => undef
698             }
699         }
700     );
701     my $library = $builder->build_object({ class => 'Koha::Libraries' });
702
703     t::lib::Mocks::mock_preference('IndependentBranches', 1);
704
705     ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
706     ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
707
708     # make it a superlibrarian
709     $patron->set({ flags => 1 })->store->discard_changes;
710     ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
711
712     t::lib::Mocks::mock_preference('IndependentBranches', 0);
713
714     # No special permissions
715     $patron->set({ flags => undef })->store->discard_changes;
716     ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
717     ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
718
719     $schema->storage->txn_rollback;
720 };
721
722 subtest 'can_request_article() tests' => sub {
723
724     plan tests => 4;
725
726     $schema->storage->txn_begin;
727
728     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
729
730     my $item = $builder->build_sample_item;
731
732     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
733     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
734     my $patron    = $builder->build_object( { class => 'Koha::Patrons' } );
735
736     t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
737
738     Koha::CirculationRules->set_rule(
739         {
740             categorycode => undef,
741             branchcode   => $library_1->id,
742             rule_name    => 'open_article_requests_limit',
743             rule_value   => 4,
744         }
745     );
746
747     $builder->build_object(
748         {
749             class => 'Koha::ArticleRequests',
750             value => { status => 'REQUESTED', borrowernumber => $patron->id }
751         }
752     );
753     $builder->build_object(
754         {
755             class => 'Koha::ArticleRequests',
756             value => { status => 'PENDING', borrowernumber => $patron->id }
757         }
758     );
759     $builder->build_object(
760         {
761             class => 'Koha::ArticleRequests',
762             value => { status => 'PROCESSING', borrowernumber => $patron->id }
763         }
764     );
765     $builder->build_object(
766         {
767             class => 'Koha::ArticleRequests',
768             value => { status => 'CANCELED', borrowernumber => $patron->id }
769         }
770     );
771
772     ok(
773         $patron->can_request_article( $library_1->id ),
774         '3 current requests, 4 is the limit: allowed'
775     );
776
777     # Completed request, same day
778     my $completed = $builder->build_object(
779         {
780             class => 'Koha::ArticleRequests',
781             value => {
782                 status         => 'COMPLETED',
783                 borrowernumber => $patron->id
784             }
785         }
786     );
787
788     ok( !$patron->can_request_article( $library_1->id ),
789         '3 current requests and a completed one the same day: denied' );
790
791     $completed->updated_on(
792         dt_from_string->add( days => -1 )->set(
793             hour   => 23,
794             minute => 59,
795             second => 59,
796         )
797     )->store;
798
799     ok( $patron->can_request_article( $library_1->id ),
800         '3 current requests and a completed one the day before: allowed' );
801
802     Koha::CirculationRules->set_rule(
803         {
804             categorycode => undef,
805             branchcode   => $library_2->id,
806             rule_name    => 'open_article_requests_limit',
807             rule_value   => 3,
808         }
809     );
810
811     ok( !$patron->can_request_article,
812         'Not passing the library_id param makes it fallback to userenv: denied'
813     );
814
815     $schema->storage->txn_rollback;
816 };
817
818 subtest 'article_requests() tests' => sub {
819
820     plan tests => 3;
821
822     $schema->storage->txn_begin;
823
824     my $library = $builder->build_object({ class => 'Koha::Libraries' });
825     t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
826
827     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
828
829     my $article_requests = $patron->article_requests;
830     is( ref($article_requests), 'Koha::ArticleRequests',
831         'In scalar context, type is correct' );
832     is( $article_requests->count, 0, 'No article requests' );
833
834     foreach my $i ( 0 .. 3 ) {
835
836         my $item = $builder->build_sample_item;
837
838         Koha::ArticleRequest->new(
839             {
840                 borrowernumber => $patron->id,
841                 biblionumber   => $item->biblionumber,
842                 itemnumber     => $item->id,
843                 title          => "Title",
844             }
845         )->request;
846     }
847
848     $article_requests = $patron->article_requests;
849     is( $article_requests->count, 4, '4 article requests' );
850
851     $schema->storage->txn_rollback;
852 };
853
854 subtest 'safe_to_delete() tests' => sub {
855
856     plan tests => 14;
857
858     $schema->storage->txn_begin;
859
860     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
861
862     ## Make it the anonymous
863     t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
864
865     ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
866     my $message = $patron->safe_to_delete->messages->[0];
867     is( $message->type, 'error', 'Type is error' );
868     is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
869     # cleanup
870     t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
871
872     ## Make it have a checkout
873     my $checkout = $builder->build_object(
874         {
875             class => 'Koha::Checkouts',
876             value => { borrowernumber => $patron->id }
877         }
878     );
879
880     ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
881     $message = $patron->safe_to_delete->messages->[0];
882     is( $message->type, 'error', 'Type is error' );
883     is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
884     # cleanup
885     $checkout->delete;
886
887     ## Make it have a guarantee
888     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
889     $builder->build_object({ class => 'Koha::Patrons' })
890             ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
891
892     ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
893     $message = $patron->safe_to_delete->messages->[0];
894     is( $message->type, 'error', 'Type is error' );
895     is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
896
897     # cleanup
898     $patron->guarantee_relationships->delete;
899
900     ## Make it have debt
901     my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
902
903     ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
904     $message = $patron->safe_to_delete->messages->[0];
905     is( $message->type, 'error', 'Type is error' );
906     is( $message->message, 'has_debt', 'Cannot delete, has debt' );
907     # cleanup
908     $patron->account->pay({ amount => 10, debits => [ $debit ] });
909
910     ## Happy case :-D
911     ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
912     my $messages = $patron->safe_to_delete->messages;
913     is_deeply( $messages, [], 'Patron can be deleted, no messages' );
914 };
915
916 subtest 'article_request_fee() tests' => sub {
917
918     plan tests => 3;
919
920     $schema->storage->txn_begin;
921
922     # Cleanup, to avoid interference
923     Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
924
925     t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
926
927     my $item = $builder->build_sample_item;
928
929     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
930     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
931     my $patron    = $builder->build_object( { class => 'Koha::Patrons' } );
932
933     # Rule that should never be picked, because the patron's category is always picked
934     Koha::CirculationRules->set_rule(
935         {   categorycode => undef,
936             branchcode   => undef,
937             rule_name    => 'article_request_fee',
938             rule_value   => 1,
939         }
940     );
941
942     is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
943
944     Koha::CirculationRules->set_rule(
945         {   categorycode => $patron->categorycode,
946             branchcode   => undef,
947             rule_name    => 'article_request_fee',
948             rule_value   => 2,
949         }
950     );
951
952     Koha::CirculationRules->set_rule(
953         {   categorycode => $patron->categorycode,
954             branchcode   => $library_1->id,
955             rule_name    => 'article_request_fee',
956             rule_value   => 3,
957         }
958     );
959
960     is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
961
962     t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
963
964     is( $patron->article_request_fee(), 3, 'env used correctly' );
965
966     $schema->storage->txn_rollback;
967 };
968
969 subtest 'add_article_request_fee_if_needed() tests' => sub {
970
971     plan tests => 12;
972
973     $schema->storage->txn_begin;
974
975     my $amount = 0;
976
977     my $patron_mock = Test::MockModule->new('Koha::Patron');
978     $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
979
980     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
981
982     is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
983
984     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
985     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
986     my $staff     = $builder->build_object( { class => 'Koha::Patrons' } );
987     my $item      = $builder->build_sample_item;
988
989     t::lib::Mocks::mock_userenv(
990         { branchcode => $library_1->id, patron => $staff } );
991
992     my $debit = $patron->add_article_request_fee_if_needed();
993     is( $debit, undef, 'No fee, no debit line' );
994
995     # positive value
996     $amount = 1;
997
998     $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
999     is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1000     is( $debit->amount, $amount,
1001         'amount set to $patron->article_request_fee value' );
1002     is( $debit->manager_id, $staff->id,
1003         'manager_id set to userenv session user' );
1004     is( $debit->branchcode, $library_1->id,
1005         'branchcode set to userenv session library' );
1006     is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1007         'debit_type_code set correctly' );
1008     is( $debit->itemnumber, $item->id,
1009         'itemnumber set correctly' );
1010
1011     $amount = 100;
1012
1013     $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1014     is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1015     is( $debit->amount, $amount,
1016         'amount set to $patron->article_request_fee value' );
1017     is( $debit->branchcode, $library_2->id,
1018         'branchcode set to userenv session library' );
1019     is( $debit->itemnumber, undef,
1020         'itemnumber set correctly to undef' );
1021
1022     $schema->storage->txn_rollback;
1023 };