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