baeb0052c947a27c5d8857f9168f4277789f80cd
[koha.git] / t / db_dependent / Koha / Account.t
1 #!/usr/bin/perl
2
3 # Copyright 2018 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 => 11;
23 use Test::MockModule;
24 use Test::Exception;
25
26 use DateTime;
27
28 use Koha::Account;
29 use Koha::Account::Lines;
30 use Koha::Account::Offsets;
31 use Koha::DateUtils qw( dt_from_string );
32
33 use t::lib::Mocks;
34 use t::lib::TestBuilder;
35
36 my $schema  = Koha::Database->new->schema;
37 $schema->storage->dbh->{PrintError} = 0;
38 my $builder = t::lib::TestBuilder->new;
39 C4::Context->interface('commandline');
40
41 subtest 'new' => sub {
42
43     plan tests => 2;
44
45     $schema->storage->txn_begin;
46
47     throws_ok { Koha::Account->new(); } qr/No patron id passed in!/, 'Croaked on bad call to new';
48
49     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
50     my $account = Koha::Account->new( { patron_id => $patron->borrowernumber } );
51     is( defined $account, 1, "Account is defined" );
52
53     $schema->storage->txn_rollback;
54 };
55
56 subtest 'outstanding_debits() tests' => sub {
57
58     plan tests => 22;
59
60     $schema->storage->txn_begin;
61
62     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
63     my $account = $patron->account;
64
65     my @generated_lines;
66     push @generated_lines, $account->add_debit({ amount => 1, interface => 'commandline', type => 'OVERDUE' });
67     push @generated_lines, $account->add_debit({ amount => 2, interface => 'commandline', type => 'OVERDUE' });
68     push @generated_lines, $account->add_debit({ amount => 3, interface => 'commandline', type => 'OVERDUE' });
69     push @generated_lines, $account->add_debit({ amount => 4, interface => 'commandline', type => 'OVERDUE' });
70
71     my $lines     = $account->outstanding_debits();
72     my @lines_arr = $account->outstanding_debits();
73
74     is( ref($lines), 'Koha::Account::Lines', 'Called in scalar context, outstanding_debits returns a Koha::Account::Lines object' );
75     is( $lines->total_outstanding, 10, 'Outstandig debits total is correctly calculated' );
76
77     my $i = 0;
78     foreach my $line ( @{ $lines->as_list } ) {
79         my $fetched_line = Koha::Account::Lines->find( $generated_lines[$i]->id );
80         is_deeply( $line->unblessed, $fetched_line->unblessed, "Fetched line matches the generated one ($i)" );
81         is_deeply( $lines_arr[$i]->unblessed, $fetched_line->unblessed, "Fetched line matches the generated one ($i)" );
82         is( ref($lines_arr[$i]), 'Koha::Account::Line', 'outstanding_debits returns a list of Koha::Account::Line objects in list context' );
83         $i++;
84     }
85     my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
86     Koha::Account::Line->new(
87         {
88             borrowernumber    => $patron_2->id,
89             amountoutstanding => -2,
90             interface         => 'commandline',
91             credit_type_code  => 'PAYMENT'
92         }
93     )->store;
94     my $just_one = Koha::Account::Line->new(
95         {
96             borrowernumber    => $patron_2->id,
97             amount            => 3,
98             amountoutstanding => 3,
99             interface         => 'commandline',
100             debit_type_code   => 'OVERDUE'
101         }
102     )->store;
103     Koha::Account::Line->new(
104         {
105             borrowernumber    => $patron_2->id,
106             amount            => -6,
107             amountoutstanding => -6,
108             interface         => 'commandline',
109             credit_type_code  => 'PAYMENT'
110         }
111     )->store;
112     $lines = $patron_2->account->outstanding_debits();
113     is( $lines->total_outstanding, 3, "Total if some outstanding debits and some credits is only debits" );
114     is( $lines->count, 1, "With 1 outstanding debits, we get back a Lines object with 1 lines" );
115     my $the_line = Koha::Account::Lines->find( $just_one->id );
116     is_deeply( $the_line->unblessed, $lines->next->unblessed, "We get back the one correct line");
117
118     my $patron_3  = $builder->build_object({ class => 'Koha::Patrons' });
119     my $account_3 = $patron_3->account;
120     $account_3->add_credit( { amount => 2,   interface => 'commandline' } );
121     $account_3->add_credit( { amount => 20,  interface => 'commandline' } );
122     $account_3->add_credit( { amount => 200, interface => 'commandline' } );
123     $lines = $account_3->outstanding_debits();
124     is( $lines->total_outstanding, 0, "Total if no outstanding debits total is 0" );
125     is( $lines->count, 0, "With 0 outstanding debits, we get back a Lines object with 0 lines" );
126
127     my $patron_4  = $builder->build_object({ class => 'Koha::Patrons' });
128     my $account_4 = $patron_4->account;
129     $lines = $account_4->outstanding_debits();
130     is( $lines->total_outstanding, 0, "Total if no outstanding debits is 0" );
131     is( $lines->count, 0, "With no outstanding debits, we get back a Lines object with 0 lines" );
132
133     # create a pathological credit with amountoutstanding > 0 (BZ 14591)
134     Koha::Account::Line->new(
135         {
136             borrowernumber    => $patron_4->id,
137             amount            => -3,
138             amountoutstanding => 3,
139             interface         => 'commandline',
140             credit_type_code  => 'PAYMENT'
141         }
142     )->store();
143     $lines = $account_4->outstanding_debits();
144     is( $lines->count, 0, 'No credits are confused with debits because of the amountoutstanding value' );
145
146     $schema->storage->txn_rollback;
147 };
148
149 subtest 'outstanding_credits() tests' => sub {
150
151     plan tests => 17;
152
153     $schema->storage->txn_begin;
154
155     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
156     my $account = $patron->account;
157
158     my @generated_lines;
159     push @generated_lines, $account->add_credit({ amount => 1, interface => 'commandline' });
160     push @generated_lines, $account->add_credit({ amount => 2, interface => 'commandline' });
161     push @generated_lines, $account->add_credit({ amount => 3, interface => 'commandline' });
162     push @generated_lines, $account->add_credit({ amount => 4, interface => 'commandline' });
163
164     my $lines     = $account->outstanding_credits();
165     my @lines_arr = $account->outstanding_credits();
166
167     is( ref($lines), 'Koha::Account::Lines', 'Called in scalar context, outstanding_credits returns a Koha::Account::Lines object' );
168     is( $lines->total_outstanding, -10, 'Outstandig credits total is correctly calculated' );
169
170     my $i = 0;
171     foreach my $line ( @{ $lines->as_list } ) {
172         my $fetched_line = Koha::Account::Lines->find( $generated_lines[$i]->id );
173         is_deeply( $line->unblessed, $fetched_line->unblessed, "Fetched line matches the generated one ($i)" );
174         is_deeply( $lines_arr[$i]->unblessed, $fetched_line->unblessed, "Fetched line matches the generated one ($i)" );
175         is( ref($lines_arr[$i]), 'Koha::Account::Line', 'outstanding_debits returns a list of Koha::Account::Line objects in list context' );
176         $i++;
177     }
178
179     my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
180     $account  = $patron_2->account;
181     $lines       = $account->outstanding_credits();
182     is( $lines->total_outstanding, 0, "Total if no outstanding credits is 0" );
183     is( $lines->count, 0, "With no outstanding credits, we get back a Lines object with 0 lines" );
184
185     # create a pathological debit with amountoutstanding < 0 (BZ 14591)
186     Koha::Account::Line->new(
187         {
188             borrowernumber    => $patron_2->id,
189             amount            => 2,
190             amountoutstanding => -3,
191             interface         => 'commandline',
192             debit_type_code   => 'OVERDUE'
193         }
194     )->store();
195     $lines = $account->outstanding_credits();
196     is( $lines->count, 0, 'No debits are confused with credits because of the amountoutstanding value' );
197
198     $schema->storage->txn_rollback;
199 };
200
201 subtest 'add_credit() tests' => sub {
202
203     plan tests => 17;
204
205     $schema->storage->txn_begin;
206
207     # delete logs and statistics
208     my $action_logs = $schema->resultset('ActionLog')->search()->count;
209     my $statistics = $schema->resultset('Statistic')->search()->count;
210
211     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
212     my $account = Koha::Account->new( { patron_id => $patron->borrowernumber } );
213
214     is( $account->balance, 0, 'Patron has no balance' );
215
216     # Disable logs
217     t::lib::Mocks::mock_preference( 'FinesLog', 0 );
218
219     throws_ok {
220         $account->add_credit(
221             {   amount      => 25,
222                 description => 'Payment of 25',
223                 library_id  => $patron->branchcode,
224                 note        => 'not really important',
225                 type        => 'PAYMENT',
226                 user_id     => $patron->id
227             }
228         );
229     }
230     'Koha::Exceptions::MissingParameter', 'Exception thrown if interface parameter missing';
231
232     my $line_1 = $account->add_credit(
233         {   amount      => 25,
234             description => 'Payment of 25',
235             library_id  => $patron->branchcode,
236             note        => 'not really important',
237             type        => 'PAYMENT',
238             user_id     => $patron->id,
239             interface   => 'commandline'
240         }
241     );
242
243     is( $account->balance, -25, 'Patron has a balance of -25' );
244     is( $schema->resultset('ActionLog')->count(), $action_logs + 0, 'No log was added' );
245     is( $schema->resultset('Statistic')->count(), $statistics + 1, 'Action added to statistics' );
246     is( $line_1->credit_type_code, 'PAYMENT', 'Account type is correctly set' );
247
248     # Enable logs
249     t::lib::Mocks::mock_preference( 'FinesLog', 1 );
250
251     my $line_2 = $account->add_credit(
252         {   amount      => 37,
253             description => 'Payment of 37',
254             library_id  => $patron->branchcode,
255             note        => 'not really important',
256             user_id     => $patron->id,
257             interface   => 'commandline'
258         }
259     );
260
261     is( $account->balance, -62, 'Patron has a balance of -25' );
262     is( $schema->resultset('ActionLog')->count(), $action_logs + 1, 'Log was added' );
263     is( $schema->resultset('Statistic')->count(), $statistics + 2, 'Action added to statistics' );
264     is( $line_2->credit_type_code, 'PAYMENT', 'Account type is correctly set' );
265
266     # offsets have the credit_id set to accountlines_id, and debit_id is undef
267     my $offset_1 = Koha::Account::Offsets->search({ credit_id => $line_1->id })->next;
268     my $offset_2 = Koha::Account::Offsets->search({ credit_id => $line_2->id })->next;
269
270     is( $offset_1->credit_id, $line_1->id, 'No debit_id is set for credits' );
271     is( $offset_1->debit_id, undef, 'No debit_id is set for credits' );
272     is( $offset_2->credit_id, $line_2->id, 'No debit_id is set for credits' );
273     is( $offset_2->debit_id, undef, 'No debit_id is set for credits' );
274
275     my $line_3 = $account->add_credit(
276         {
277             amount      => 20,
278             description => 'Manual credit applied',
279             library_id  => $patron->branchcode,
280             user_id     => $patron->id,
281             type        => 'FORGIVEN',
282             interface   => 'commandline'
283         }
284     );
285
286     is( $schema->resultset('ActionLog')->count(), $action_logs + 2, 'Log was added' );
287     is( $schema->resultset('Statistic')->count(), $statistics + 2, 'No action added to statistics, because of credit type' );
288
289     # Enable cash registers
290     t::lib::Mocks::mock_preference( 'UseCashRegisters', 1 );
291     throws_ok {
292         $account->add_credit(
293             {
294                 amount       => 20,
295                 description  => 'Cash payment without cash register',
296                 library_id   => $patron->branchcode,
297                 user_id      => $patron->id,
298                 payment_type => 'CASH',
299                 interface    => 'intranet'
300             }
301         );
302     }
303     'Koha::Exceptions::Account::RegisterRequired',
304       'Exception thrown for UseCashRegisters:1 + payment_type:CASH + cash_register:undef';
305
306     # Disable cash registers
307     t::lib::Mocks::mock_preference( 'UseCashRegisters', 1 );
308
309     $schema->storage->txn_rollback;
310 };
311
312 subtest 'add_debit() tests' => sub {
313
314     plan tests => 14;
315
316     $schema->storage->txn_begin;
317
318     # delete logs and statistics
319     my $action_logs = $schema->resultset('ActionLog')->search()->count;
320     my $statistics  = $schema->resultset('Statistic')->search()->count;
321
322     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
323     my $account =
324       Koha::Account->new( { patron_id => $patron->borrowernumber } );
325
326     is( $account->balance, 0, 'Patron has no balance' );
327
328     throws_ok {
329     $account->add_debit(
330         {
331             amount      => -5,
332             description => 'amount validation failure',
333             library_id  => $patron->branchcode,
334             note        => 'this should fail anyway',
335             type        => 'RENT',
336             user_id     => $patron->id,
337             interface   => 'commandline'
338         }
339     ); } 'Koha::Exceptions::Account::AmountNotPositive', 'Expected validation exception thrown (amount)';
340
341     throws_ok {
342     $account->add_debit(
343         {
344             amount      => 5,
345             description => 'type validation failure',
346             library_id  => $patron->branchcode,
347             note        => 'this should fail anyway',
348             type        => 'failure',
349             user_id     => $patron->id,
350             interface   => 'commandline'
351         }
352     ); } 'Koha::Exceptions::Account::UnrecognisedType', 'Expected validation exception thrown (type)';
353
354     throws_ok {
355     $account->add_debit(
356         {
357             amount      => 25,
358             description => 'Rental charge of 25',
359             library_id  => $patron->branchcode,
360             note        => 'not really important',
361             type        => 'RENT',
362             user_id     => $patron->id
363         }
364     ); } 'Koha::Exceptions::MissingParameter', 'Exception thrown if interface parameter missing';
365
366     # Disable logs
367     t::lib::Mocks::mock_preference( 'FinesLog', 0 );
368
369     my $line_1 = $account->add_debit(
370         {
371             amount      => 25,
372             description => 'Rental charge of 25',
373             library_id  => $patron->branchcode,
374             note        => 'not really important',
375             type        => 'RENT',
376             user_id     => $patron->id,
377             interface   => 'commandline'
378         }
379     );
380
381     is( $account->balance, 25, 'Patron has a balance of 25' );
382     is(
383         $schema->resultset('ActionLog')->count(),
384         $action_logs + 0,
385         'No log was added'
386     );
387     is(
388         $line_1->debit_type_code,
389         'RENT',
390         'Account type is correctly set'
391     );
392
393     # Enable logs
394     t::lib::Mocks::mock_preference( 'FinesLog', 1 );
395
396     my $line_2   = $account->add_debit(
397         {
398             amount      => 37,
399             description => 'Rental charge of 37',
400             library_id  => $patron->branchcode,
401             note        => 'not really important',
402             type        => 'RENT',
403             user_id     => $patron->id,
404             interface   => 'commandline'
405         }
406     );
407
408     is( $account->balance, 62, 'Patron has a balance of 62' );
409     is(
410         $schema->resultset('ActionLog')->count(),
411         $action_logs + 1,
412         'Log was added'
413     );
414     is(
415         $line_2->debit_type_code,
416         'RENT',
417         'Account type is correctly set'
418     );
419
420     # offsets have the debit_id set to accountlines_id, and credit_id is undef
421     my $offset_1 =
422       Koha::Account::Offsets->search( { debit_id => $line_1->id } )->next;
423     my $offset_2 =
424       Koha::Account::Offsets->search( { debit_id => $line_2->id } )->next;
425
426     is( $offset_1->debit_id,  $line_1->id, 'debit_id is set for debit 1' );
427     is( $offset_1->credit_id, undef,       'credit_id is not set for debit 1' );
428     is( $offset_2->debit_id,  $line_2->id, 'debit_id is set for debit 2' );
429     is( $offset_2->credit_id, undef,       'credit_id is not set for debit 2' );
430
431     $schema->storage->txn_rollback;
432 };
433
434 subtest 'lines() tests' => sub {
435
436     plan tests => 1;
437
438     $schema->storage->txn_begin;
439
440     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
441     my $account = $patron->account;
442
443     # Add Credits
444     $account->add_credit({ amount => 1, interface => 'commandline' });
445     $account->add_credit({ amount => 2, interface => 'commandline' });
446     $account->add_credit({ amount => 3, interface => 'commandline' });
447     $account->add_credit({ amount => 4, interface => 'commandline' });
448
449     # Add Debits
450     $account->add_debit({ amount => 1, interface => 'commandline', type => 'OVERDUE' });
451     $account->add_debit({ amount => 2, interface => 'commandline', type => 'OVERDUE' });
452     $account->add_debit({ amount => 3, interface => 'commandline', type => 'OVERDUE' });
453     $account->add_debit({ amount => 4, interface => 'commandline', type => 'OVERDUE' });
454
455     # Paid Off
456     $account->add_credit( { amount => 1, interface => 'commandline' } )
457         ->apply( { debits => [ $account->outstanding_debits->as_list ] } );
458
459     my $lines = $account->lines;
460     is( $lines->_resultset->count, 9, "All accountlines (debits, credits and paid off) were fetched");
461
462     $schema->storage->txn_rollback;
463 };
464
465 subtest 'reconcile_balance' => sub {
466
467     plan tests => 4;
468
469     subtest 'more credit than debit' => sub {
470
471         plan tests => 6;
472
473         $schema->storage->txn_begin;
474
475         my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
476         my $account = $patron->account;
477
478         # Add Credits
479         $account->add_credit({ amount => 1, interface => 'commandline' });
480         $account->add_credit({ amount => 2, interface => 'commandline' });
481         $account->add_credit({ amount => 3, interface => 'commandline' });
482         $account->add_credit({ amount => 4, interface => 'commandline' });
483         $account->add_credit({ amount => 5, interface => 'commandline' });
484
485         # Add Debits
486         $account->add_debit({ amount => 1, interface => 'commandline', type => 'OVERDUE' });
487         $account->add_debit({ amount => 2, interface => 'commandline', type => 'OVERDUE' });
488         $account->add_debit({ amount => 3, interface => 'commandline', type => 'OVERDUE' });
489         $account->add_debit({ amount => 4, interface => 'commandline', type => 'OVERDUE' });
490
491         # Paid Off
492         Koha::Account::Line->new(
493             {
494                 borrowernumber    => $patron->id,
495                 amount            => 1,
496                 amountoutstanding => 0,
497                 interface         => 'commandline',
498                 debit_type_code   => 'OVERDUE'
499             }
500         )->store;
501         Koha::Account::Line->new(
502             {
503                 borrowernumber    => $patron->id,
504                 amount            => 1,
505                 amountoutstanding => 0,
506                 interface         => 'commandline',
507                 debit_type_code   => 'OVERDUE'
508             }
509         )->store;
510
511         is( $account->balance(), -5, "Account balance is -5" );
512         is( $account->outstanding_debits->total_outstanding, 10, 'Outstanding debits sum 10' );
513         is( $account->outstanding_credits->total_outstanding, -15, 'Outstanding credits sum -15' );
514
515         $account->reconcile_balance();
516
517         is( $account->balance(), -5, "Account balance is -5" );
518         is( $account->outstanding_debits->total_outstanding, 0, 'No outstanding debits' );
519         is( $account->outstanding_credits->total_outstanding, -5, 'Outstanding credits sum -5' );
520
521         $schema->storage->txn_rollback;
522     };
523
524     subtest 'same debit as credit' => sub {
525
526         plan tests => 6;
527
528         $schema->storage->txn_begin;
529
530         my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
531         my $account = $patron->account;
532
533         # Add Credits
534         $account->add_credit({ amount => 1, interface => 'commandline' });
535         $account->add_credit({ amount => 2, interface => 'commandline' });
536         $account->add_credit({ amount => 3, interface => 'commandline' });
537         $account->add_credit({ amount => 4, interface => 'commandline' });
538
539         # Add Debits
540         $account->add_debit({ amount => 1, interface => 'commandline', type => 'OVERDUE' });
541         $account->add_debit({ amount => 2, interface => 'commandline', type => 'OVERDUE' });
542         $account->add_debit({ amount => 3, interface => 'commandline', type => 'OVERDUE' });
543         $account->add_debit({ amount => 4, interface => 'commandline', type => 'OVERDUE' });
544
545         # Paid Off
546         Koha::Account::Line->new(
547             {
548                 borrowernumber    => $patron->id,
549                 amount            => 1,
550                 amountoutstanding => 0,
551                 interface         => 'commandline',
552                 debit_type_code   => 'OVERDUE'
553             }
554         )->store;
555         Koha::Account::Line->new(
556             {
557                 borrowernumber    => $patron->id,
558                 amount            => 1,
559                 amountoutstanding => 0,
560                 interface         => 'commandline',
561                 debit_type_code   => 'OVERDUE'
562             }
563         )->store;
564
565         is( $account->balance(), 0, "Account balance is 0" );
566         is( $account->outstanding_debits->total_outstanding, 10, 'Outstanding debits sum 10' );
567         is( $account->outstanding_credits->total_outstanding, -10, 'Outstanding credits sum -10' );
568
569         $account->reconcile_balance();
570
571         is( $account->balance(), 0, "Account balance is 0" );
572         is( $account->outstanding_debits->total_outstanding, 0, 'No outstanding debits' );
573         is( $account->outstanding_credits->total_outstanding, 0, 'Outstanding credits sum 0' );
574
575         $schema->storage->txn_rollback;
576     };
577
578     subtest 'more debit than credit' => sub {
579
580         plan tests => 6;
581
582         $schema->storage->txn_begin;
583
584         my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
585         my $account = $patron->account;
586
587         # Add Credits
588         $account->add_credit({ amount => 1, interface => 'commandline' });
589         $account->add_credit({ amount => 2, interface => 'commandline' });
590         $account->add_credit({ amount => 3, interface => 'commandline' });
591         $account->add_credit({ amount => 4, interface => 'commandline' });
592
593         # Add Debits
594         $account->add_debit({ amount => 1, interface => 'commandline', type => 'OVERDUE' });
595         $account->add_debit({ amount => 2, interface => 'commandline', type => 'OVERDUE' });
596         $account->add_debit({ amount => 3, interface => 'commandline', type => 'OVERDUE' });
597         $account->add_debit({ amount => 4, interface => 'commandline', type => 'OVERDUE' });
598         $account->add_debit({ amount => 5, interface => 'commandline', type => 'OVERDUE' });
599
600         # Paid Off
601         Koha::Account::Line->new(
602             {
603                 borrowernumber    => $patron->id,
604                 amount            => 1,
605                 amountoutstanding => 0,
606                 interface         => 'commandline',
607                 debit_type_code   => 'OVERDUE'
608             }
609         )->store;
610         Koha::Account::Line->new(
611             {
612                 borrowernumber    => $patron->id,
613                 amount            => 1,
614                 amountoutstanding => 0,
615                 interface         => 'commandline',
616                 debit_type_code   => 'OVERDUE'
617             }
618         )->store;
619
620         is( $account->balance(), 5, "Account balance is 5" );
621         is( $account->outstanding_debits->total_outstanding, 15, 'Outstanding debits sum 15' );
622         is( $account->outstanding_credits->total_outstanding, -10, 'Outstanding credits sum -10' );
623
624         $account->reconcile_balance();
625
626         is( $account->balance(), 5, "Account balance is 5" );
627         is( $account->outstanding_debits->total_outstanding, 5, 'Outstanding debits sum 5' );
628         is( $account->outstanding_credits->total_outstanding, 0, 'Outstanding credits sum 0' );
629
630         $schema->storage->txn_rollback;
631     };
632
633     subtest 'credits are applied to older debits first' => sub {
634
635         plan tests => 9;
636
637         $schema->storage->txn_begin;
638
639         my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
640         my $account = $patron->account;
641
642         # Add Credits
643         $account->add_credit({ amount => 1, interface => 'commandline' });
644         $account->add_credit({ amount => 3, interface => 'commandline' });
645
646         # Add Debits
647         my $debit_1 = $account->add_debit({ amount => 1, interface => 'commandline', type => 'OVERDUE' });
648         my $debit_2 = $account->add_debit({ amount => 2, interface => 'commandline', type => 'OVERDUE' });
649         my $debit_3 = $account->add_debit({ amount => 3, interface => 'commandline', type => 'OVERDUE' });
650
651         is( $account->balance(), 2, "Account balance is 2" );
652         is( $account->outstanding_debits->total_outstanding, 6, 'Outstanding debits sum 6' );
653         is( $account->outstanding_credits->total_outstanding, -4, 'Outstanding credits sum -4' );
654
655         $account->reconcile_balance();
656
657         is( $account->balance(), 2, "Account balance is 2" );
658         is( $account->outstanding_debits->total_outstanding, 2, 'Outstanding debits sum 2' );
659         is( $account->outstanding_credits->total_outstanding, 0, 'Outstanding credits sum 0' );
660
661         $debit_1->discard_changes;
662         is( $debit_1->amountoutstanding + 0, 0, 'Old debit payed' );
663         $debit_2->discard_changes;
664         is( $debit_2->amountoutstanding + 0, 0, 'Old debit payed' );
665         $debit_3->discard_changes;
666         is( $debit_3->amountoutstanding + 0, 2, 'Newest debit only partially payed' );
667
668         $schema->storage->txn_rollback;
669     };
670 };
671
672 subtest 'pay() tests' => sub {
673
674     plan tests => 3;
675
676     $schema->storage->txn_begin;
677
678     # Disable renewing upon fine payment
679     t::lib::Mocks::mock_preference( 'RenewAccruingItemWhenPaid', 0 );
680
681     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
682     my $library = $builder->build_object({ class => 'Koha::Libraries' });
683     my $account = $patron->account;
684
685     my $context = Test::MockModule->new('C4::Context');
686     $context->mock( 'userenv', { branch => $library->id } );
687
688     my $credit_1_id = $account->pay({ amount => 200 });
689     my $credit_1    = Koha::Account::Lines->find( $credit_1_id );
690
691     is( $credit_1->branchcode, undef, 'No branchcode is set if library_id was not passed' );
692
693     my $credit_2_id = $account->pay({ amount => 150, library_id => $library->id });
694     my $credit_2    = Koha::Account::Lines->find( $credit_2_id );
695
696     is( $credit_2->branchcode, $library->id, 'branchcode set because library_id was passed' );
697
698     # Enable cash registers
699     t::lib::Mocks::mock_preference( 'UseCashRegisters', 1 );
700     throws_ok {
701         $account->pay(
702             {
703                 amount       => 20,
704                 payment_type => 'CASH',
705                 interface    => 'intranet'
706             }
707         );
708     }
709     'Koha::Exceptions::Account::RegisterRequired',
710       'Exception thrown for UseCashRegisters:1 + payment_type:CASH + cash_register:undef';
711
712     # Disable cash registers
713     t::lib::Mocks::mock_preference( 'UseCashRegisters', 1 );
714
715     $schema->storage->txn_rollback;
716 };
717
718 subtest 'pay() handles lost items when paying a specific lost fee' => sub {
719
720     plan tests => 5;
721
722     $schema->storage->txn_begin;
723
724     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
725     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
726     my $account = $patron->account;
727
728     my $context = Test::MockModule->new('C4::Context');
729     $context->mock( 'userenv', { branch => $library->id } );
730
731     my $biblio = $builder->build_sample_biblio();
732     my $item =
733       $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
734
735     my $checkout = Koha::Checkout->new(
736         {
737             borrowernumber => $patron->id,
738             itemnumber     => $item->id,
739             date_due       => \'NOW()',
740             branchcode     => $patron->branchcode,
741             issuedate      => \'NOW()',
742         }
743     )->store();
744
745     $item->itemlost('1')->store();
746
747     my $accountline = Koha::Account::Line->new(
748         {
749             issue_id       => $checkout->id,
750             borrowernumber => $patron->id,
751             itemnumber     => $item->id,
752             date           => \'NOW()',
753             debit_type_code    => 'LOST',
754             interface      => 'cli',
755             amount => '1',
756             amountoutstanding => '1',
757         }
758     )->store();
759
760     $account->pay(
761         {
762             amount     => .5,
763             library_id => $library->id,
764             lines      => [$accountline],
765         }
766     );
767
768     $accountline = Koha::Account::Lines->find( $accountline->id );
769     is( $accountline->amountoutstanding+0, .5, 'Account line was paid down by half' );
770
771     $checkout = Koha::Checkouts->find( $checkout->id );
772     ok( $checkout, 'Item still checked out to patron' );
773
774     $account->pay(
775         {
776             amount     => 0.5,
777             library_id => $library->id,
778             lines      => [$accountline],
779         }
780     );
781
782     $accountline = Koha::Account::Lines->find( $accountline->id );
783     is( $accountline->amountoutstanding+0, 0, 'Account line was paid down by half' );
784
785     $checkout = Koha::Checkouts->find( $checkout->id );
786     ok( !$checkout, 'Item was removed from patron account' );
787
788     subtest 'item was not checked out to the same patron' => sub {
789         plan tests => 1;
790
791         my $patron_2 = $builder->build_object(
792             {
793                 class => 'Koha::Patrons',
794                 value => { branchcode => $library->branchcode }
795             }
796         );
797         $item->itemlost('1')->store();
798         C4::Accounts::chargelostitem( $patron->borrowernumber, $item->itemnumber, 5, "lost" );
799         my $accountline = Koha::Account::Lines->search(
800             {
801                 borrowernumber  => $patron->borrowernumber,
802                 itemnumber      => $item->itemnumber,
803                 debit_type_code => 'LOST'
804             }
805         )->next;
806         my $checkout = Koha::Checkout->new(
807             {
808                 borrowernumber => $patron_2->borrowernumber,
809                 itemnumber     => $item->itemnumber,
810                 date_due       => \'NOW()',
811                 branchcode     => $patron_2->branchcode,
812                 issuedate      => \'NOW()',
813             }
814         )->store();
815
816         $patron->account->pay(
817             {
818                 amount     => 5,
819                 library_id => $library->branchcode,
820                 lines      => [$accountline],
821             }
822         );
823
824         ok(
825             Koha::Checkouts->find( $checkout->issue_id ),
826             'If the item is checked out to another patron, a lost item should not be returned if lost fee is paid'
827         );
828
829     };
830
831     $schema->storage->txn_rollback;
832 };
833
834 subtest 'pay() handles lost items when paying by amount ( not specifying the lost fee )' => sub {
835
836     plan tests => 4;
837
838     $schema->storage->txn_begin;
839
840     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
841     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
842     my $account = $patron->account;
843
844     my $context = Test::MockModule->new('C4::Context');
845     $context->mock( 'userenv', { branch => $library->id } );
846
847     my $biblio = $builder->build_sample_biblio();
848     my $item =
849       $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
850
851     my $checkout = Koha::Checkout->new(
852         {
853             borrowernumber => $patron->id,
854             itemnumber     => $item->id,
855             date_due       => \'NOW()',
856             branchcode     => $patron->branchcode,
857             issuedate      => \'NOW()',
858         }
859     )->store();
860
861     $item->itemlost('1')->store();
862
863     my $accountline = Koha::Account::Line->new(
864         {
865             issue_id       => $checkout->id,
866             borrowernumber => $patron->id,
867             itemnumber     => $item->id,
868             date           => \'NOW()',
869             debit_type_code    => 'LOST',
870             interface      => 'cli',
871             amount => '1',
872             amountoutstanding => '1',
873         }
874     )->store();
875
876     $account->pay(
877         {
878             amount     => .5,
879             library_id => $library->id,
880         }
881     );
882
883     $accountline = Koha::Account::Lines->find( $accountline->id );
884     is( $accountline->amountoutstanding+0, .5, 'Account line was paid down by half' );
885
886     $checkout = Koha::Checkouts->find( $checkout->id );
887     ok( $checkout, 'Item still checked out to patron' );
888
889     $account->pay(
890         {
891             amount     => .5,,
892             library_id => $library->id,
893         }
894     );
895
896     $accountline = Koha::Account::Lines->find( $accountline->id );
897     is( $accountline->amountoutstanding+0, 0, 'Account line was paid down by half' );
898
899     $checkout = Koha::Checkouts->find( $checkout->id );
900     ok( !$checkout, 'Item was removed from patron account' );
901
902     $schema->storage->txn_rollback;
903 };
904
905 subtest 'pay() renews items when appropriate' => sub {
906
907     plan tests => 1;
908
909     $schema->storage->txn_begin;
910
911     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
912     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
913     my $account = $patron->account;
914
915     my $context = Test::MockModule->new('C4::Context');
916     $context->mock( 'userenv', { branch => $library->id } );
917
918     my $biblio = $builder->build_sample_biblio();
919     my $item =
920       $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
921
922     my $now = dt_from_string();
923     my $seven_weeks = DateTime::Duration->new(weeks => 7);
924     my $five_weeks = DateTime::Duration->new(weeks => 5);
925     my $seven_weeks_ago = $now - $seven_weeks;
926     my $five_weeks_ago = $now - $five_weeks;
927
928     my $checkout = Koha::Checkout->new(
929         {
930             borrowernumber => $patron->id,
931             itemnumber     => $item->id,
932             date_due       => $five_weeks_ago,
933             branchcode     => $patron->branchcode,
934             issuedate      => $seven_weeks_ago
935         }
936     )->store();
937
938     my $accountline = Koha::Account::Line->new(
939         {
940             issue_id       => $checkout->id,
941             borrowernumber => $patron->id,
942             itemnumber     => $item->id,
943             date           => \'NOW()',
944             accounttype    => 'OVERDUE',
945             status         => 'UNRETURNED',
946             interface      => 'cli',
947             amount => '1',
948             amountoutstanding => '1',
949         }
950     )->store();
951
952     # Enable renewing upon fine payment
953     t::lib::Mocks::mock_preference( 'RenewAccruingItemWhenPaid', 1 );
954     my $called = 0;
955     my $module = new Test::MockModule('C4::Circulation');
956     $module->mock('AddRenewal', sub { $called = 1; });
957     $account->pay(
958         {
959             amount     => '1',
960             library_id => $library->id,
961         }
962     );
963
964     is( $called, 1, 'RenewAccruingItemWhenPaid causes C4::Circulation::AddRenew to be called when appropriate' );
965
966     $schema->storage->txn_rollback;
967 };
968
969 subtest 'Koha::Account::Line::apply() handles lost items' => sub {
970
971     plan tests => 4;
972
973     $schema->storage->txn_begin;
974
975     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
976     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
977     my $account = $patron->account;
978
979     my $context = Test::MockModule->new('C4::Context');
980     $context->mock( 'userenv', { branch => $library->id } );
981
982     my $biblio = $builder->build_sample_biblio();
983     my $item =
984       $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
985
986     my $checkout = Koha::Checkout->new(
987         {
988             borrowernumber => $patron->id,
989             itemnumber     => $item->id,
990             date_due       => \'NOW()',
991             branchcode     => $patron->branchcode,
992             issuedate      => \'NOW()',
993         }
994     )->store();
995
996     $item->itemlost('1')->store();
997
998     my $debit = Koha::Account::Line->new(
999         {
1000             issue_id          => $checkout->id,
1001             borrowernumber    => $patron->id,
1002             itemnumber        => $item->id,
1003             date              => \'NOW()',
1004             debit_type_code       => 'LOST',
1005             interface         => 'cli',
1006             amount            => '1',
1007             amountoutstanding => '1',
1008         }
1009     )->store();
1010
1011     my $credit = Koha::Account::Line->new(
1012         {
1013             borrowernumber    => $patron->id,
1014             date              => '1900-01-01',
1015             amount            => -.5,
1016             amountoutstanding => -.5,
1017             interface         => 'commandline',
1018             credit_type_code  => 'PAYMENT'
1019         }
1020     )->store();
1021     my $debits = $account->outstanding_debits;
1022     $credit->apply({ debits => [ $debits->as_list ] });
1023
1024     $debit = Koha::Account::Lines->find( $debit->id );
1025     is( $debit->amountoutstanding+0, .5, 'Account line was paid down by half' );
1026
1027     $checkout = Koha::Checkouts->find( $checkout->id );
1028     ok( $checkout, 'Item still checked out to patron' );
1029
1030     $credit = Koha::Account::Line->new(
1031         {
1032             borrowernumber    => $patron->id,
1033             date              => '1900-01-01',
1034             amount            => -.5,
1035             amountoutstanding => -.5,
1036             interface         => 'commandline',
1037             credit_type_code  => 'PAYMENT'
1038         }
1039     )->store();
1040     $debits = $account->outstanding_debits;
1041     $credit->apply({ debits => [ $debits->as_list ] });
1042
1043     $debit = Koha::Account::Lines->find( $debit->id );
1044     is( $debit->amountoutstanding+0, 0, 'Account line was paid down by half' );
1045
1046     $checkout = Koha::Checkouts->find( $checkout->id );
1047     ok( !$checkout, 'Item was removed from patron account' );
1048
1049     $schema->storage->txn_rollback;
1050 };