Bug 23355: Split tests for Line vs Lines
[koha.git] / t / db_dependent / Koha / Account / Line.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 => 8;
23 use Test::Exception;
24
25 use C4::Circulation qw/AddIssue AddReturn/;
26 use Koha::Account;
27 use Koha::Account::Lines;
28 use Koha::Account::Offsets;
29 use Koha::Items;
30
31 use t::lib::Mocks;
32 use t::lib::TestBuilder;
33
34 my $schema = Koha::Database->new->schema;
35 my $builder = t::lib::TestBuilder->new;
36
37 subtest 'patron() tests' => sub {
38
39     plan tests => 3;
40
41     $schema->storage->txn_begin;
42
43     my $library = $builder->build( { source => 'Branch' } );
44     my $patron = $builder->build( { source => 'Borrower' } );
45
46     my $line = Koha::Account::Line->new(
47     {
48         borrowernumber => $patron->{borrowernumber},
49         debit_type_code    => "OVERDUE",
50         status         => "RETURNED",
51         amount         => 10,
52         interface      => 'commandline',
53     })->store;
54
55     my $account_line_patron = $line->patron;
56     is( ref( $account_line_patron ), 'Koha::Patron', 'Koha::Account::Line->patron should return a Koha::Patron' );
57     is( $line->borrowernumber, $account_line_patron->borrowernumber, 'Koha::Account::Line->patron should return the correct borrower' );
58
59     $line->borrowernumber(undef)->store;
60     is( $line->patron, undef, 'Koha::Account::Line->patron should return undef if no patron linked' );
61
62     $schema->storage->txn_rollback;
63 };
64
65 subtest 'item() tests' => sub {
66
67     plan tests => 3;
68
69     $schema->storage->txn_begin;
70
71     my $library = $builder->build( { source => 'Branch' } );
72     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
73     my $patron = $builder->build( { source => 'Borrower' } );
74     my $item = Koha::Item->new(
75     {
76         biblionumber     => $biblioitem->{biblionumber},
77         biblioitemnumber => $biblioitem->{biblioitemnumber},
78         homebranch       => $library->{branchcode},
79         holdingbranch    => $library->{branchcode},
80         barcode          => 'some_barcode_12',
81         itype            => 'BK',
82     })->store;
83
84     my $line = Koha::Account::Line->new(
85     {
86         borrowernumber => $patron->{borrowernumber},
87         itemnumber     => $item->itemnumber,
88         debit_type_code    => "OVERDUE",
89         status         => "RETURNED",
90         amount         => 10,
91         interface      => 'commandline',
92     })->store;
93
94     my $account_line_item = $line->item;
95     is( ref( $account_line_item ), 'Koha::Item', 'Koha::Account::Line->item should return a Koha::Item' );
96     is( $line->itemnumber, $account_line_item->itemnumber, 'Koha::Account::Line->item should return the correct item' );
97
98     $line->itemnumber(undef)->store;
99     is( $line->item, undef, 'Koha::Account::Line->item should return undef if no item linked' );
100
101     $schema->storage->txn_rollback;
102 };
103
104 subtest 'is_credit() and is_debit() tests' => sub {
105
106     plan tests => 4;
107
108     $schema->storage->txn_begin;
109
110     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
111     my $account = $patron->account;
112
113     my $credit = $account->add_credit({ amount => 100, user_id => $patron->id, interface => 'commandline' });
114
115     ok( $credit->is_credit, 'is_credit detects credits' );
116     ok( !$credit->is_debit, 'is_debit detects credits' );
117
118     my $debit = Koha::Account::Line->new(
119     {
120         borrowernumber => $patron->id,
121         debit_type_code    => "OVERDUE",
122         status         => "RETURNED",
123         amount         => 10,
124         interface      => 'commandline',
125     })->store;
126
127     ok( !$debit->is_credit, 'is_credit detects debits' );
128     ok( $debit->is_debit, 'is_debit detects debits');
129
130     $schema->storage->txn_rollback;
131 };
132
133 subtest 'apply() tests' => sub {
134
135     plan tests => 24;
136
137     $schema->storage->txn_begin;
138
139     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
140     my $account = $patron->account;
141
142     my $credit = $account->add_credit( { amount => 100, user_id => $patron->id, interface => 'commandline' } );
143
144     my $debit_1 = Koha::Account::Line->new(
145         {   borrowernumber    => $patron->id,
146             debit_type_code       => "OVERDUE",
147             status            => "RETURNED",
148             amount            => 10,
149             amountoutstanding => 10,
150             interface         => 'commandline',
151         }
152     )->store;
153
154     my $debit_2 = Koha::Account::Line->new(
155         {   borrowernumber    => $patron->id,
156             debit_type_code       => "OVERDUE",
157             status            => "RETURNED",
158             amount            => 100,
159             amountoutstanding => 100,
160             interface         => 'commandline',
161         }
162     )->store;
163
164     $credit->discard_changes;
165     $debit_1->discard_changes;
166
167     my $debits = Koha::Account::Lines->search({ accountlines_id => $debit_1->id });
168     my $remaining_credit = $credit->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } );
169     is( $remaining_credit * 1, 90, 'Remaining credit is correctly calculated' );
170     $credit->discard_changes;
171     is( $credit->amountoutstanding * -1, $remaining_credit, 'Remaining credit correctly stored' );
172
173     # re-read debit info
174     $debit_1->discard_changes;
175     is( $debit_1->amountoutstanding * 1, 0, 'Debit has been cancelled' );
176
177     my $offsets = Koha::Account::Offsets->search( { credit_id => $credit->id, debit_id => $debit_1->id } );
178     is( $offsets->count, 1, 'Only one offset is generated' );
179     my $THE_offset = $offsets->next;
180     is( $THE_offset->amount * 1, -10, 'Amount was calculated correctly (less than the available credit)' );
181     is( $THE_offset->type, 'Manual Credit', 'Passed type stored correctly' );
182
183     $debits = Koha::Account::Lines->search({ accountlines_id => $debit_2->id });
184     $remaining_credit = $credit->apply( { debits => [ $debits->as_list ] } );
185     is( $remaining_credit, 0, 'No remaining credit left' );
186     $credit->discard_changes;
187     is( $credit->amountoutstanding * 1, 0, 'No outstanding credit' );
188     $debit_2->discard_changes;
189     is( $debit_2->amountoutstanding * 1, 10, 'Outstanding amount decremented correctly' );
190
191     $offsets = Koha::Account::Offsets->search( { credit_id => $credit->id, debit_id => $debit_2->id } );
192     is( $offsets->count, 1, 'Only one offset is generated' );
193     $THE_offset = $offsets->next;
194     is( $THE_offset->amount * 1, -90, 'Amount was calculated correctly (less than the available credit)' );
195     is( $THE_offset->type, 'Credit Applied', 'Defaults to \'Credit Applied\' offset type' );
196
197     $debits = Koha::Account::Lines->search({ accountlines_id => $debit_1->id });
198     throws_ok
199         { $credit->apply({ debits => [ $debits->as_list ] }); }
200         'Koha::Exceptions::Account::NoAvailableCredit',
201         '->apply() can only be used with outstanding credits';
202
203     $debits = Koha::Account::Lines->search({ accountlines_id => $credit->id });
204     throws_ok
205         { $debit_1->apply({ debits => [ $debits->as_list ] }); }
206         'Koha::Exceptions::Account::IsNotCredit',
207         '->apply() can only be used with credits';
208
209     $debits = Koha::Account::Lines->search({ accountlines_id => $credit->id });
210     my $credit_3 = $account->add_credit({ amount => 1, interface => 'commandline' });
211     throws_ok
212         { $credit_3->apply({ debits => [ $debits->as_list ] }); }
213         'Koha::Exceptions::Account::IsNotDebit',
214         '->apply() can only be applied to credits';
215
216     my $credit_2 = $account->add_credit({ amount => 20, interface => 'commandline' });
217     my $debit_3  = Koha::Account::Line->new(
218         {   borrowernumber    => $patron->id,
219             debit_type_code       => "OVERDUE",
220             status            => "RETURNED",
221             amount            => 100,
222             amountoutstanding => 100,
223             interface         => 'commandline',
224         }
225     )->store;
226
227     $debits = Koha::Account::Lines->search({ accountlines_id => { -in => [ $debit_1->id, $debit_2->id, $debit_3->id, $credit->id ] } });
228     throws_ok {
229         $credit_2->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } ); }
230         'Koha::Exceptions::Account::IsNotDebit',
231         '->apply() rolls back if any of the passed lines is not a debit';
232
233     is( $debit_1->discard_changes->amountoutstanding * 1,   0, 'No changes to already cancelled debit' );
234     is( $debit_2->discard_changes->amountoutstanding * 1,  10, 'Debit cancelled' );
235     is( $debit_3->discard_changes->amountoutstanding * 1, 100, 'Outstanding amount correctly calculated' );
236     is( $credit_2->discard_changes->amountoutstanding * -1, 20, 'No changes made' );
237
238     $debits = Koha::Account::Lines->search({ accountlines_id => { -in => [ $debit_1->id, $debit_2->id, $debit_3->id ] } });
239     $remaining_credit = $credit_2->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } );
240
241     is( $debit_1->discard_changes->amountoutstanding * 1,  0, 'No changes to already cancelled debit' );
242     is( $debit_2->discard_changes->amountoutstanding * 1,  0, 'Debit cancelled' );
243     is( $debit_3->discard_changes->amountoutstanding * 1, 90, 'Outstanding amount correctly calculated' );
244     is( $credit_2->discard_changes->amountoutstanding * 1, 0, 'No remaining credit' );
245
246     $schema->storage->txn_rollback;
247 };
248
249 subtest 'Keep account info when related patron, staff, item or cash_register is deleted' => sub {
250
251     plan tests => 4;
252
253     $schema->storage->txn_begin;
254
255     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
256     my $staff = $builder->build_object( { class => 'Koha::Patrons' } );
257     my $item = $builder->build_object({ class => 'Koha::Items' });
258     my $issue = $builder->build_object(
259         {
260             class => 'Koha::Checkouts',
261             value => { itemnumber => $item->itemnumber }
262         }
263     );
264     my $register = $builder->build_object({ class => 'Koha::Cash::Registers' });
265
266     my $line = Koha::Account::Line->new(
267     {
268         borrowernumber => $patron->borrowernumber,
269         manager_id     => $staff->borrowernumber,
270         itemnumber     => $item->itemnumber,
271         debit_type_code    => "OVERDUE",
272         status         => "RETURNED",
273         amount         => 10,
274         interface      => 'commandline',
275         register_id    => $register->id
276     })->store;
277
278     $issue->delete;
279     $item->delete;
280     $line = $line->get_from_storage;
281     is( $line->itemnumber, undef, "The account line should not be deleted when the related item is delete");
282
283     $staff->delete;
284     $line = $line->get_from_storage;
285     is( $line->manager_id, undef, "The account line should not be deleted when the related staff is delete");
286
287     $patron->delete;
288     $line = $line->get_from_storage;
289     is( $line->borrowernumber, undef, "The account line should not be deleted when the related patron is delete");
290
291     $register->delete;
292     $line = $line->get_from_storage;
293     is( $line->register_id, undef, "The account line should not be deleted when the related cash register is delete");
294
295     $schema->storage->txn_rollback;
296 };
297
298 subtest 'adjust() tests' => sub {
299
300     plan tests => 29;
301
302     $schema->storage->txn_begin;
303
304     # count logs before any actions
305     my $action_logs = $schema->resultset('ActionLog')->search()->count;
306
307     # Disable logs
308     t::lib::Mocks::mock_preference( 'FinesLog', 0 );
309
310     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
311     my $account = $patron->account;
312
313     my $debit_1 = Koha::Account::Line->new(
314         {   borrowernumber    => $patron->id,
315             debit_type_code       => "OVERDUE",
316             status            => "RETURNED",
317             amount            => 10,
318             amountoutstanding => 10,
319             interface         => 'commandline',
320         }
321     )->store;
322
323     my $debit_2 = Koha::Account::Line->new(
324         {   borrowernumber    => $patron->id,
325             debit_type_code       => "OVERDUE",
326             status            => "UNRETURNED",
327             amount            => 100,
328             amountoutstanding => 100,
329             interface         => 'commandline'
330         }
331     )->store;
332
333     my $credit = $account->add_credit( { amount => 40, user_id => $patron->id, interface => 'commandline' } );
334
335     throws_ok { $debit_1->adjust( { amount => 50, type => 'bad', interface => 'commandline' } ) }
336     qr/Update type not recognised/, 'Exception thrown for unrecognised type';
337
338     throws_ok { $debit_1->adjust( { amount => 50, type => 'overdue_update', interface => 'commandline' } ) }
339     qr/Update type not allowed on this debit_type/,
340       'Exception thrown for type conflict';
341
342     # Increment an unpaid fine
343     $debit_2->adjust( { amount => 150, type => 'overdue_update', interface => 'commandline' } )->discard_changes;
344
345     is( $debit_2->amount * 1, 150, 'Fine amount was updated in full' );
346     is( $debit_2->amountoutstanding * 1, 150, 'Fine amountoutstanding was update in full' );
347     isnt( $debit_2->date, undef, 'Date has been set' );
348
349     my $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } );
350     is( $offsets->count, 1, 'An offset is generated for the increment' );
351     my $THIS_offset = $offsets->next;
352     is( $THIS_offset->amount * 1, 50, 'Amount was calculated correctly (increment by 50)' );
353     is( $THIS_offset->type, 'OVERDUE_INCREASE', 'Adjust type stored correctly' );
354
355     is( $schema->resultset('ActionLog')->count(), $action_logs + 0, 'No log was added' );
356
357     # Update fine to partially paid
358     my $debits = Koha::Account::Lines->search({ accountlines_id => $debit_2->id });
359     $credit->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } );
360
361     $debit_2->discard_changes;
362     is( $debit_2->amount * 1, 150, 'Fine amount unaffected by partial payment' );
363     is( $debit_2->amountoutstanding * 1, 110, 'Fine amountoutstanding updated by partial payment' );
364
365     # Enable logs
366     t::lib::Mocks::mock_preference( 'FinesLog', 1 );
367
368     # Increment the partially paid fine
369     $debit_2->adjust( { amount => 160, type => 'overdue_update', interface => 'commandline' } )->discard_changes;
370
371     is( $debit_2->amount * 1, 160, 'Fine amount was updated in full' );
372     is( $debit_2->amountoutstanding * 1, 120, 'Fine amountoutstanding was updated by difference' );
373
374     $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } );
375     is( $offsets->count, 3, 'An offset is generated for the increment' );
376     $THIS_offset = $offsets->last;
377     is( $THIS_offset->amount * 1, 10, 'Amount was calculated correctly (increment by 10)' );
378     is( $THIS_offset->type, 'OVERDUE_INCREASE', 'Adjust type stored correctly' );
379
380     is( $schema->resultset('ActionLog')->count(), $action_logs + 1, 'Log was added' );
381
382     # Decrement the partially paid fine, less than what was paid
383     $debit_2->adjust( { amount => 50, type => 'overdue_update', interface => 'commandline' } )->discard_changes;
384
385     is( $debit_2->amount * 1, 50, 'Fine amount was updated in full' );
386     is( $debit_2->amountoutstanding * 1, 10, 'Fine amountoutstanding was updated by difference' );
387
388     $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } );
389     is( $offsets->count, 4, 'An offset is generated for the decrement' );
390     $THIS_offset = $offsets->last;
391     is( $THIS_offset->amount * 1, -110, 'Amount was calculated correctly (decrement by 110)' );
392     is( $THIS_offset->type, 'OVERDUE_DECREASE', 'Adjust type stored correctly' );
393
394     # Decrement the partially paid fine, more than what was paid
395     $debit_2->adjust( { amount => 30, type => 'overdue_update', interface => 'commandline' } )->discard_changes;
396     is( $debit_2->amount * 1, 30, 'Fine amount was updated in full' );
397     is( $debit_2->amountoutstanding * 1, 0, 'Fine amountoutstanding was zeroed (payment was 40)' );
398
399     $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } );
400     is( $offsets->count, 5, 'An offset is generated for the decrement' );
401     $THIS_offset = $offsets->last;
402     is( $THIS_offset->amount * 1, -20, 'Amount was calculated correctly (decrement by 20)' );
403     is( $THIS_offset->type, 'OVERDUE_DECREASE', 'Adjust type stored correctly' );
404
405     my $overpayment_refund = $account->lines->last;
406     is( $overpayment_refund->amount * 1, -10, 'A new credit has been added' );
407     is( $overpayment_refund->description, 'Overpayment refund', 'Credit generated with the expected description' );
408
409     $schema->storage->txn_rollback;
410 };
411
412 subtest 'checkout() tests' => sub {
413     plan tests => 6;
414
415     $schema->storage->txn_begin;
416
417     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
418     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
419     my $item = $builder->build_sample_item;
420     my $account = $patron->account;
421
422     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
423     my $checkout = AddIssue( $patron->unblessed, $item->barcode );
424
425     my $line = $account->add_debit({
426         amount    => 10,
427         interface => 'commandline',
428         item_id   => $item->itemnumber,
429         issue_id  => $checkout->issue_id,
430         type      => 'OVERDUE',
431     });
432
433     my $line_checkout = $line->checkout;
434     is( ref($line_checkout), 'Koha::Checkout', 'Result type is correct' );
435     is( $line_checkout->issue_id, $checkout->issue_id, 'Koha::Account::Line->checkout should return the correct checkout');
436
437     my ( $returned, undef, $old_checkout) = C4::Circulation::AddReturn( $item->barcode, $library->branchcode );
438     is( $returned, 1, 'The item should have been returned' );
439
440     $line = $line->get_from_storage;
441     my $old_line_checkout = $line->checkout;
442     is( ref($old_line_checkout), 'Koha::Old::Checkout', 'Result type is correct' );
443     is( $old_line_checkout->issue_id, $old_checkout->issue_id, 'Koha::Account::Line->checkout should return the correct old_checkout' );
444
445     $line->issue_id(undef)->store;
446     is( $line->checkout, undef, 'Koha::Account::Line->checkout should return undef if no checkout linked' );
447
448     $schema->storage->txn_rollback;
449 };
450
451 subtest 'credits() and debits() tests' => sub {
452     plan tests => 10;
453
454     $schema->storage->txn_begin;
455
456     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
457     my $account = $patron->account;
458
459     my $debit1 = $account->add_debit({
460         amount    => 8,
461         interface => 'commandline',
462         type      => 'ACCOUNT',
463     });
464     my $debit2 = $account->add_debit({
465         amount    => 12,
466         interface => 'commandline',
467         type      => 'ACCOUNT',
468     });
469     my $credit1 = $account->add_credit({
470         amount    => 5,
471         interface => 'commandline',
472         type      => 'CREDIT',
473     });
474     my $credit2 = $account->add_credit({
475         amount    => 10,
476         interface => 'commandline',
477         type      => 'CREDIT',
478     });
479
480     $credit1->apply({ debits => [ $debit1 ] });
481     $credit2->apply({ debits => [ $debit1, $debit2 ] });
482
483     my $credits = $debit1->credits;
484     is($credits->count, 2, '2 Credits applied to debit 1');
485     my $credit = $credits->next;
486     is($credit->amount + 0, -5, 'Correct first credit');
487     $credit = $credits->next;
488     is($credit->amount + 0, -10, 'Correct second credit');
489
490     $credits = $debit2->credits;
491     is($credits->count, 1, '1 Credits applied to debit 2');
492     $credit = $credits->next;
493     is($credit->amount + 0, -10, 'Correct first credit');
494
495     my $debits = $credit1->debits;
496     is($debits->count, 1, 'Credit 1 applied to 1 debit');
497     my $debit = $debits->next;
498     is($debit->amount + 0, 8, 'Correct first debit');
499
500     $debits = $credit2->debits;
501     is($debits->count, 2, 'Credit 2 applied to 2 debits');
502     $debit = $debits->next;
503     is($debit->amount + 0, 8, 'Correct first debit');
504     $debit = $debits->next;
505     is($debit->amount + 0, 12, 'Correct second debit');
506
507     $schema->storage->txn_rollback;
508 };
509
510 subtest "void() tests" => sub {
511
512     plan tests => 16;
513
514     $schema->storage->txn_begin;
515
516     # Create a borrower
517     my $categorycode = $builder->build({ source => 'Category' })->{ categorycode };
518     my $branchcode   = $builder->build({ source => 'Branch' })->{ branchcode };
519
520     my $borrower = Koha::Patron->new( {
521         cardnumber => 'dariahall',
522         surname => 'Hall',
523         firstname => 'Daria',
524     } );
525     $borrower->categorycode( $categorycode );
526     $borrower->branchcode( $branchcode );
527     $borrower->store;
528
529     my $account = Koha::Account->new({ patron_id => $borrower->id });
530
531     my $line1 = Koha::Account::Line->new(
532         {
533             borrowernumber    => $borrower->borrowernumber,
534             amount            => 10,
535             amountoutstanding => 10,
536             interface         => 'commandline',
537             debit_type_code   => 'OVERDUE'
538         }
539     )->store();
540     my $line2 = Koha::Account::Line->new(
541         {
542             borrowernumber    => $borrower->borrowernumber,
543             amount            => 20,
544             amountoutstanding => 20,
545             interface         => 'commandline',
546             debit_type_code   => 'OVERDUE'
547         }
548     )->store();
549
550     is( $account->balance(), 30, "Account balance is 30" );
551     is( $line1->amountoutstanding, 10, 'First fee has amount outstanding of 10' );
552     is( $line2->amountoutstanding, 20, 'Second fee has amount outstanding of 20' );
553
554     my $id = $account->pay(
555         {
556             lines  => [$line1, $line2],
557             amount => 30,
558         }
559     );
560
561     my $account_payment = Koha::Account::Lines->find( $id );
562
563     is( $account->balance(), 0, "Account balance is 0" );
564
565     $line1->_result->discard_changes();
566     $line2->_result->discard_changes();
567     is( $line1->amountoutstanding+0, 0, 'First fee has amount outstanding of 0' );
568     is( $line2->amountoutstanding+0, 0, 'Second fee has amount outstanding of 0' );
569
570     my $ret = $account_payment->void();
571
572     is( ref($ret), 'Koha::Account::Line', 'Void returns the account line' );
573     is( $account->balance(), 30, "Account balance is again 30" );
574
575     $account_payment->_result->discard_changes();
576     $line1->_result->discard_changes();
577     $line2->_result->discard_changes();
578
579     is( $account_payment->credit_type_code, 'PAYMENT', 'Voided payment credit_type_code is still PAYMENT' );
580     is( $account_payment->status, 'VOID', 'Voided payment status is VOID' );
581     is( $account_payment->amount+0, 0, 'Voided payment amount is 0' );
582     is( $account_payment->amountoutstanding+0, 0, 'Voided payment amount outstanding is 0' );
583
584     is( $line1->amountoutstanding+0, 10, 'First fee again has amount outstanding of 10' );
585     is( $line2->amountoutstanding+0, 20, 'Second fee again has amount outstanding of 20' );
586
587     # Accountlines that are not credits should be un-voidable
588     my $line1_pre = $line1->unblessed();
589     $ret = $line1->void();
590     $line1->_result->discard_changes();
591     my $line1_post = $line1->unblessed();
592     is( $ret, undef, 'Attempted void on non-credit returns undef' );
593     is_deeply( $line1_pre, $line1_post, 'Non-credit account line cannot be voided' );
594
595     $schema->storage->txn_rollback;
596 };
597
598 subtest "payout() tests" => sub {
599
600     plan tests => 18;
601
602     $schema->storage->txn_begin;
603
604     # Create a borrower
605     my $categorycode =
606       $builder->build( { source => 'Category' } )->{categorycode};
607     my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
608
609     my $borrower = Koha::Patron->new(
610         {
611             cardnumber => 'dariahall',
612             surname    => 'Hall',
613             firstname  => 'Daria',
614         }
615     );
616     $borrower->categorycode($categorycode);
617     $borrower->branchcode($branchcode);
618     $borrower->store;
619
620     my $staff = Koha::Patron->new(
621         {
622             cardnumber => 'bobby',
623             surname    => 'Bloggs',
624             firstname  => 'Bobby',
625         }
626     );
627     $staff->categorycode($categorycode);
628     $staff->branchcode($branchcode);
629     $staff->store;
630
631     my $account = Koha::Account->new( { patron_id => $borrower->id } );
632
633     my $debit1 = Koha::Account::Line->new(
634         {
635             borrowernumber    => $borrower->borrowernumber,
636             amount            => 10,
637             amountoutstanding => 10,
638             interface         => 'commandline',
639             debit_type_code   => 'OVERDUE'
640         }
641     )->store();
642     my $credit1 = Koha::Account::Line->new(
643         {
644             borrowernumber    => $borrower->borrowernumber,
645             amount            => -20,
646             amountoutstanding => -20,
647             interface         => 'commandline',
648             credit_type_code  => 'CREDIT'
649         }
650     )->store();
651
652     is( $account->balance(), -10, "Account balance is -10" );
653     is( $debit1->amountoutstanding + 0,
654         10, 'Overdue fee has an amount outstanding of 10' );
655     is( $credit1->amountoutstanding + 0,
656         -20, 'Credit has an amount outstanding of -20' );
657
658     my $pay_params = {
659         interface   => 'intranet',
660         staff_id    => $staff->borrowernumber,
661         branch      => $branchcode,
662         payout_type => 'CASH',
663         amount      => 20
664     };
665
666     throws_ok { $debit1->payout($pay_params); }
667     'Koha::Exceptions::Account::IsNotCredit',
668       '->payout() can only be used with credits';
669
670     my @required =
671       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
672     for my $required (@required) {
673         my $params = {%$pay_params};
674         delete( $params->{$required} );
675         throws_ok {
676             $credit1->payout($params);
677         }
678         'Koha::Exceptions::MissingParameter',
679           "->payout() requires the `$required` parameter is passed";
680     }
681
682     throws_ok {
683         $credit1->payout(
684             {
685                 interface   => 'intranet',
686                 staff_id    => $staff->borrowernumber,
687                 branch      => $branchcode,
688                 payout_type => 'CASH',
689                 amount      => 25
690             }
691         );
692     }
693     'Koha::Exceptions::ParameterTooHigh',
694       '->payout() cannot pay out more than the amountoutstanding';
695
696     t::lib::Mocks::mock_preference( 'UseCashRegisters', 1 );
697     throws_ok {
698         $credit1->payout(
699             {
700                 interface   => 'intranet',
701                 staff_id    => $staff->borrowernumber,
702                 branch      => $branchcode,
703                 payout_type => 'CASH',
704                 amount      => 10
705             }
706         );
707     }
708     'Koha::Exceptions::Account::RegisterRequired',
709       '->payout() requires a cash_register if payout_type is `CASH`';
710
711     t::lib::Mocks::mock_preference( 'UseCashRegisters', 0 );
712     my $payout = $credit1->payout(
713         {
714             interface   => 'intranet',
715             staff_id    => $staff->borrowernumber,
716             branch      => $branchcode,
717             payout_type => 'CASH',
718             amount      => 10
719         }
720     );
721
722     is( ref($payout), 'Koha::Account::Line',
723         '->payout() returns a Koha::Account::Line' );
724     is( $payout->amount() + 0,            10, "Payout amount is 10" );
725     is( $payout->amountoutstanding() + 0, 0,  "Payout amountoutstanding is 0" );
726     is( $account->balance() + 0,          0,  "Account balance is 0" );
727     is( $debit1->amountoutstanding + 0,
728         10, 'Overdue fee still has an amount outstanding of 10' );
729     is( $credit1->amountoutstanding + 0,
730         -10, 'Credit has an new amount outstanding of -10' );
731     is( $credit1->status(), 'PAID', "Credit has a new status of PAID" );
732
733     $schema->storage->txn_rollback;
734 };
735
736 subtest "reduce() tests" => sub {
737
738     plan tests => 27;
739
740     $schema->storage->txn_begin;
741
742     # Create a borrower
743     my $categorycode =
744       $builder->build( { source => 'Category' } )->{categorycode};
745     my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
746
747     my $borrower = Koha::Patron->new(
748         {
749             cardnumber => 'dariahall',
750             surname    => 'Hall',
751             firstname  => 'Daria',
752         }
753     );
754     $borrower->categorycode($categorycode);
755     $borrower->branchcode($branchcode);
756     $borrower->store;
757
758     my $staff = Koha::Patron->new(
759         {
760             cardnumber => 'bobby',
761             surname    => 'Bloggs',
762             firstname  => 'Bobby',
763         }
764     );
765     $staff->categorycode($categorycode);
766     $staff->branchcode($branchcode);
767     $staff->store;
768
769     my $account = Koha::Account->new( { patron_id => $borrower->id } );
770
771     my $debit1 = Koha::Account::Line->new(
772         {
773             borrowernumber    => $borrower->borrowernumber,
774             amount            => 20,
775             amountoutstanding => 20,
776             interface         => 'commandline',
777             debit_type_code   => 'LOST'
778         }
779     )->store();
780     my $credit1 = Koha::Account::Line->new(
781         {
782             borrowernumber    => $borrower->borrowernumber,
783             amount            => -20,
784             amountoutstanding => -20,
785             interface         => 'commandline',
786             credit_type_code  => 'CREDIT'
787         }
788     )->store();
789
790     is( $account->balance(), 0, "Account balance is 0" );
791     is( $debit1->amountoutstanding,
792         20, 'Overdue fee has an amount outstanding of 20' );
793     is( $credit1->amountoutstanding,
794         -20, 'Credit has an amount outstanding of -20' );
795
796     my $reduce_params = {
797         interface      => 'commandline',
798         reduction_type => 'REFUND',
799         amount         => 5,
800         staff_id       => $staff->borrowernumber,
801         branch         => $branchcode
802     };
803
804     throws_ok { $credit1->reduce($reduce_params); }
805     'Koha::Exceptions::Account::IsNotDebit',
806       '->reduce() can only be used with debits';
807
808     my @required = ( 'interface', 'reduction_type', 'amount' );
809     for my $required (@required) {
810         my $params = {%$reduce_params};
811         delete( $params->{$required} );
812         throws_ok {
813             $debit1->reduce($params);
814         }
815         'Koha::Exceptions::MissingParameter',
816           "->reduce() requires the `$required` parameter is passed";
817     }
818
819     $reduce_params->{interface} = 'intranet';
820     my @dependant_required = ( 'staff_id', 'branch' );
821     for my $d (@dependant_required) {
822         my $params = {%$reduce_params};
823         delete( $params->{$d} );
824         throws_ok {
825             $debit1->reduce($params);
826         }
827         'Koha::Exceptions::MissingParameter',
828 "->reduce() requires the `$d` parameter is passed when interface is intranet";
829     }
830
831     throws_ok {
832         $debit1->reduce(
833             {
834                 interface      => 'intranet',
835                 staff_id       => $staff->borrowernumber,
836                 branch         => $branchcode,
837                 reduction_type => 'REFUND',
838                 amount         => 25
839             }
840         );
841     }
842     'Koha::Exceptions::ParameterTooHigh',
843       '->reduce() cannot reduce more than original amount';
844
845     # Partial Reduction
846     # (Refund 5 on debt of 20)
847     my $reduction = $debit1->reduce($reduce_params);
848
849     is( ref($reduction), 'Koha::Account::Line',
850         '->reduce() returns a Koha::Account::Line' );
851     is( $reduction->amount() * 1, -5, "Reduce amount is -5" );
852     is( $reduction->amountoutstanding() * 1,
853         0, "Reduce amountoutstanding is 0" );
854     is( $debit1->amountoutstanding() * 1,
855         15, "Debit amountoutstanding reduced by 5 to 15" );
856     is( $account->balance() * 1, -5,        "Account balance is -5" );
857     is( $reduction->status(),    'APPLIED', "Reduction status is 'APPLIED'" );
858
859     my $offsets = Koha::Account::Offsets->search(
860         { credit_id => $reduction->id, debit_id => $debit1->id } );
861     is( $offsets->count, 1, 'Only one offset is generated' );
862     my $THE_offset = $offsets->next;
863     is( $THE_offset->amount * 1,
864         -5, 'Correct amount was applied against debit' );
865     is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" );
866
867     # Zero offset created when zero outstanding
868     # (Refund another 5 on paid debt of 20)
869     $credit1->apply( { debits => [$debit1] } );
870     is( $debit1->amountoutstanding + 0,
871         0, 'Debit1 amountoutstanding reduced to 0' );
872     $reduction = $debit1->reduce($reduce_params);
873     is( $reduction->amount() * 1, -5, "Reduce amount is -5" );
874     is( $reduction->amountoutstanding() * 1,
875         -5, "Reduce amountoutstanding is -5" );
876
877     $offsets = Koha::Account::Offsets->search(
878         { credit_id => $reduction->id, debit_id => $debit1->id } );
879     is( $offsets->count, 1, 'Only one new offset is generated' );
880     $THE_offset = $offsets->next;
881     is( $THE_offset->amount * 1,
882         0, 'Zero offset created for already paid off debit' );
883     is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" );
884
885     # Compound reduction should not allow more than original amount
886     # (Reduction of 5 + 5 + 20 > 20)
887     $reduce_params->{amount} = 20;
888     throws_ok {
889         $debit1->reduce($reduce_params);
890     }
891     'Koha::Exceptions::ParameterTooHigh',
892 '->reduce cannot reduce more than the original amount (combined reductions test)';
893
894     # Throw exception if attempting to reduce a payout
895     my $payout = $reduction->payout(
896         {
897             interface   => 'intranet',
898             staff_id    => $staff->borrowernumber,
899             branch      => $branchcode,
900             payout_type => 'CASH',
901             amount      => 5
902         }
903     );
904     throws_ok {
905         $payout->reduce($reduce_params);
906     }
907     'Koha::Exceptions::Account::IsNotDebit',
908       '->reduce() cannot be used on a payout debit';
909
910     $schema->storage->txn_rollback;
911 };
912
913 1;