3 # Copyright 2018 Koha Development team
5 # This file is part of Koha
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.
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.
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>
22 use Test
::More tests
=> 9;
25 use C4
::Circulation qw
/AddIssue AddReturn/;
27 use Koha
::Account
::Lines
;
28 use Koha
::Account
::Offsets
;
32 use t
::lib
::TestBuilder
;
34 my $schema = Koha
::Database
->new->schema;
35 my $builder = t
::lib
::TestBuilder
->new;
37 subtest
'patron() tests' => sub {
41 $schema->storage->txn_begin;
43 my $library = $builder->build( { source
=> 'Branch' } );
44 my $patron = $builder->build( { source
=> 'Borrower' } );
46 my $line = Koha
::Account
::Line
->new(
48 borrowernumber
=> $patron->{borrowernumber
},
49 accounttype
=> "OVERDUE",
52 interface
=> 'commandline',
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' );
59 $line->borrowernumber(undef)->store;
60 is
( $line->patron, undef, 'Koha::Account::Line->patron should return undef if no patron linked' );
62 $schema->storage->txn_rollback;
66 subtest
'item() tests' => sub {
70 $schema->storage->txn_begin;
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(
77 biblionumber
=> $biblioitem->{biblionumber
},
78 biblioitemnumber
=> $biblioitem->{biblioitemnumber
},
79 homebranch
=> $library->{branchcode
},
80 holdingbranch
=> $library->{branchcode
},
81 barcode
=> 'some_barcode_12',
85 my $line = Koha
::Account
::Line
->new(
87 borrowernumber
=> $patron->{borrowernumber
},
88 itemnumber
=> $item->itemnumber,
89 accounttype
=> "OVERDUE",
92 interface
=> 'commandline',
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' );
99 $line->itemnumber(undef)->store;
100 is
( $line->item, undef, 'Koha::Account::Line->item should return undef if no item linked' );
102 $schema->storage->txn_rollback;
105 subtest
'total_outstanding() tests' => sub {
109 $schema->storage->txn_begin;
111 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
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)' );
116 my $debit_1 = Koha
::Account
::Line
->new(
117 { borrowernumber
=> $patron->id,
118 accounttype
=> "OVERDUE",
119 status
=> "RETURNED",
121 amountoutstanding
=> 10,
122 interface
=> 'commandline',
126 my $debit_2 = Koha
::Account
::Line
->new(
127 { borrowernumber
=> $patron->id,
128 accounttype
=> "OVERDUE",
129 status
=> "RETURNED",
131 amountoutstanding
=> 10,
132 interface
=> 'commandline',
136 $lines = Koha
::Account
::Lines
->search({ borrowernumber
=> $patron->id });
137 is
( $lines->total_outstanding, 20, 'total_outstanding sums correctly' );
139 my $credit_1 = Koha
::Account
::Line
->new(
140 { borrowernumber
=> $patron->id,
141 accounttype
=> "OVERDUE",
142 status
=> "RETURNED",
144 amountoutstanding
=> -10,
145 interface
=> 'commandline',
149 $lines = Koha
::Account
::Lines
->search({ borrowernumber
=> $patron->id });
150 is
( $lines->total_outstanding, 10, 'total_outstanding sums correctly' );
152 my $credit_2 = Koha
::Account
::Line
->new(
153 { borrowernumber
=> $patron->id,
154 accounttype
=> "OVERDUE",
155 status
=> "RETURNED",
157 amountoutstanding
=> -10,
158 interface
=> 'commandline',
162 $lines = Koha
::Account
::Lines
->search({ borrowernumber
=> $patron->id });
163 is
( $lines->total_outstanding, 0, 'total_outstanding sums correctly' );
165 my $credit_3 = Koha
::Account
::Line
->new(
166 { borrowernumber
=> $patron->id,
167 accounttype
=> "OVERDUE",
168 status
=> "RETURNED",
170 amountoutstanding
=> -100,
171 interface
=> 'commandline',
175 $lines = Koha
::Account
::Lines
->search({ borrowernumber
=> $patron->id });
176 is
( $lines->total_outstanding, -100, 'total_outstanding sums correctly' );
178 $schema->storage->txn_rollback;
181 subtest
'is_credit() and is_debit() tests' => sub {
185 $schema->storage->txn_begin;
187 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
188 my $account = $patron->account;
190 my $credit = $account->add_credit({ amount
=> 100, user_id
=> $patron->id, interface
=> 'commandline' });
192 ok
( $credit->is_credit, 'is_credit detects credits' );
193 ok
( !$credit->is_debit, 'is_debit detects credits' );
195 my $debit = Koha
::Account
::Line
->new(
197 borrowernumber
=> $patron->id,
198 accounttype
=> "OVERDUE",
199 status
=> "RETURNED",
201 interface
=> 'commandline',
204 ok
( !$debit->is_credit, 'is_credit detects debits' );
205 ok
( $debit->is_debit, 'is_debit detects debits');
207 $schema->storage->txn_rollback;
210 subtest
'apply() tests' => sub {
214 $schema->storage->txn_begin;
216 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
217 my $account = $patron->account;
219 my $credit = $account->add_credit( { amount
=> 100, user_id
=> $patron->id, interface
=> 'commandline' } );
221 my $debit_1 = Koha
::Account
::Line
->new(
222 { borrowernumber
=> $patron->id,
223 accounttype
=> "OVERDUE",
224 status
=> "RETURNED",
226 amountoutstanding
=> 10,
227 interface
=> 'commandline',
231 my $debit_2 = Koha
::Account
::Line
->new(
232 { borrowernumber
=> $patron->id,
233 accounttype
=> "OVERDUE",
234 status
=> "RETURNED",
236 amountoutstanding
=> 100,
237 interface
=> 'commandline',
241 $credit->discard_changes;
242 $debit_1->discard_changes;
244 my $debits = Koha
::Account
::Lines
->search({ accountlines_id
=> $debit_1->id });
245 my $remaining_credit = $credit->apply( { debits
=> $debits, 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' );
251 $debit_1->discard_changes;
252 is
( $debit_1->amountoutstanding * 1, 0, 'Debit has been cancelled' );
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' );
260 $debits = Koha
::Account
::Lines
->search({ accountlines_id
=> $debit_2->id });
261 $remaining_credit = $credit->apply( { debits
=> $debits } );
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' );
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' );
274 $debits = Koha
::Account
::Lines
->search({ accountlines_id
=> $debit_1->id });
276 { $credit->apply({ debits
=> $debits }); }
277 'Koha::Exceptions::Account::NoAvailableCredit',
278 '->apply() can only be used with outstanding credits';
280 $debits = Koha
::Account
::Lines
->search({ accountlines_id
=> $credit->id });
282 { $debit_1->apply({ debits
=> $debits }); }
283 'Koha::Exceptions::Account::IsNotCredit',
284 '->apply() can only be used with credits';
286 $debits = Koha
::Account
::Lines
->search({ accountlines_id
=> $credit->id });
287 my $credit_3 = $account->add_credit({ amount
=> 1, interface
=> 'commandline' });
289 { $credit_3->apply({ debits
=> $debits }); }
290 'Koha::Exceptions::Account::IsNotDebit',
291 '->apply() can only be applied to credits';
293 my $credit_2 = $account->add_credit({ amount
=> 20, interface
=> 'commandline' });
294 my $debit_3 = Koha
::Account
::Line
->new(
295 { borrowernumber
=> $patron->id,
296 accounttype
=> "OVERDUE",
297 status
=> "RETURNED",
299 amountoutstanding
=> 100,
300 interface
=> 'commandline',
304 $debits = Koha
::Account
::Lines
->search({ accountlines_id
=> { -in => [ $debit_1->id, $debit_2->id, $debit_3->id, $credit->id ] } });
306 $credit_2->apply( { debits
=> $debits, offset_type
=> 'Manual Credit' } ); }
307 'Koha::Exceptions::Account::IsNotDebit',
308 '->apply() rolls back if any of the passed lines is not a debit';
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' );
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, offset_type
=> 'Manual Credit' } );
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' );
323 $schema->storage->txn_rollback;
326 subtest
'Keep account info when related patron, staff or item is deleted' => sub {
330 $schema->storage->txn_begin;
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(
337 class => 'Koha::Checkouts',
338 value
=> { itemnumber
=> $item->itemnumber }
341 my $line = Koha
::Account
::Line
->new(
343 borrowernumber
=> $patron->borrowernumber,
344 manager_id
=> $staff->borrowernumber,
345 itemnumber
=> $item->itemnumber,
346 accounttype
=> "OVERDUE",
347 status
=> "RETURNED",
349 interface
=> 'commandline',
354 $line = $line->get_from_storage;
355 is
( $line->itemnumber, undef, "The account line should not be deleted when the related item is 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");
362 $line = $line->get_from_storage;
363 is
( $line->borrowernumber, undef, "The account line should not be deleted when the related patron is delete");
365 $schema->storage->txn_rollback;
368 subtest
'adjust() tests' => sub {
372 $schema->storage->txn_begin;
374 # count logs before any actions
375 my $action_logs = $schema->resultset('ActionLog')->search()->count;
378 t
::lib
::Mocks
::mock_preference
( 'FinesLog', 0 );
380 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
381 my $account = $patron->account;
383 my $debit_1 = Koha
::Account
::Line
->new(
384 { borrowernumber
=> $patron->id,
385 accounttype
=> "OVERDUE",
386 status
=> "RETURNED",
388 amountoutstanding
=> 10,
389 interface
=> 'commandline',
393 my $debit_2 = Koha
::Account
::Line
->new(
394 { borrowernumber
=> $patron->id,
395 accounttype
=> "OVERDUE",
396 status
=> "UNRETURNED",
398 amountoutstanding
=> 100,
399 interface
=> 'commandline'
403 my $credit = $account->add_credit( { amount
=> 40, user_id
=> $patron->id, interface
=> 'commandline' } );
405 throws_ok
{ $debit_1->adjust( { amount
=> 50, type
=> 'bad', interface
=> 'commandline' } ) }
406 qr/Update type not recognised/, 'Exception thrown for unrecognised type';
408 throws_ok
{ $debit_1->adjust( { amount
=> 50, type
=> 'overdue_update', interface
=> 'commandline' } ) }
409 qr/Update type not allowed on this accounttype/,
410 'Exception thrown for type conflict';
412 # Increment an unpaid fine
413 $debit_2->adjust( { amount
=> 150, type
=> 'overdue_update', interface
=> 'commandline' } )->discard_changes;
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' );
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' );
425 is
( $schema->resultset('ActionLog')->count(), $action_logs + 0, 'No log was added' );
427 # Update fine to partially paid
428 my $debits = Koha
::Account
::Lines
->search({ accountlines_id
=> $debit_2->id });
429 $credit->apply( { debits
=> $debits, offset_type
=> 'Manual Credit' } );
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' );
436 t
::lib
::Mocks
::mock_preference
( 'FinesLog', 1 );
438 # Increment the partially paid fine
439 $debit_2->adjust( { amount
=> 160, type
=> 'overdue_update', interface
=> 'commandline' } )->discard_changes;
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' );
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' );
450 is
( $schema->resultset('ActionLog')->count(), $action_logs + 1, 'Log was added' );
452 # Decrement the partially paid fine, less than what was paid
453 $debit_2->adjust( { amount
=> 50, type
=> 'overdue_update', interface
=> 'commandline' } )->discard_changes;
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' );
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' );
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)' );
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' );
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' );
479 $schema->storage->txn_rollback;
482 subtest
'checkout() tests' => sub {
485 $schema->storage->txn_begin;
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;
492 t
::lib
::Mocks
::mock_userenv
({ branchcode
=> $library->branchcode });
493 my $checkout = AddIssue
( $patron->unblessed, $item->barcode );
495 my $line = $account->add_debit({
497 interface
=> 'commandline',
498 item_id
=> $item->itemnumber,
499 issue_id
=> $checkout->issue_id,
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');
507 my ( $returned, undef, $old_checkout) = C4
::Circulation
::AddReturn
( $item->barcode, $library->branchcode );
508 is
( $returned, 1, 'The item should have been returned' );
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' );
515 $line->issue_id(undef)->store;
516 is
( $line->checkout, undef, 'Koha::Account::Line->checkout should return undef if no checkout linked' );
518 $schema->storage->txn_rollback;
521 subtest
"void() tests" => sub {
525 $schema->storage->txn_begin;
528 my $categorycode = $builder->build({ source
=> 'Category' })->{ categorycode
};
529 my $branchcode = $builder->build({ source
=> 'Branch' })->{ branchcode
};
531 my $borrower = Koha
::Patron
->new( {
532 cardnumber
=> 'dariahall',
534 firstname
=> 'Daria',
536 $borrower->categorycode( $categorycode );
537 $borrower->branchcode( $branchcode );
540 my $account = Koha
::Account
->new({ patron_id
=> $borrower->id });
542 my $line1 = Koha
::Account
::Line
->new({ borrowernumber
=> $borrower->borrowernumber, amount
=> 10, amountoutstanding
=> 10, interface
=> 'commandline' })->store();
543 my $line2 = Koha
::Account
::Line
->new({ borrowernumber
=> $borrower->borrowernumber, amount
=> 20, amountoutstanding
=> 20, interface
=> 'commandline' })->store();
545 is
( $account->balance(), 30, "Account balance is 30" );
546 is
( $line1->amountoutstanding, 10, 'First fee has amount outstanding of 10' );
547 is
( $line2->amountoutstanding, 20, 'Second fee has amount outstanding of 20' );
549 my $id = $account->pay(
551 lines
=> [$line1, $line2],
556 my $account_payment = Koha
::Account
::Lines
->find( $id );
558 is
( $account->balance(), 0, "Account balance is 0" );
560 $line1->_result->discard_changes();
561 $line2->_result->discard_changes();
562 is
( $line1->amountoutstanding+0, 0, 'First fee has amount outstanding of 0' );
563 is
( $line2->amountoutstanding+0, 0, 'Second fee has amount outstanding of 0' );
565 my $ret = $account_payment->void();
567 is
( ref($ret), 'Koha::Account::Line', 'Void returns the account line' );
568 is
( $account->balance(), 30, "Account balance is again 30" );
570 $account_payment->_result->discard_changes();
571 $line1->_result->discard_changes();
572 $line2->_result->discard_changes();
574 is
( $account_payment->accounttype, 'Pay', 'Voided payment accounttype is still Pay' );
575 is
( $account_payment->status, 'VOID', 'Voided payment status is VOID' );
576 is
( $account_payment->amount+0, 0, 'Voided payment amount is 0' );
577 is
( $account_payment->amountoutstanding+0, 0, 'Voided payment amount outstanding is 0' );
579 is
( $line1->amountoutstanding+0, 10, 'First fee again has amount outstanding of 10' );
580 is
( $line2->amountoutstanding+0, 20, 'Second fee again has amount outstanding of 20' );
582 # Accountlines that are not credits should be un-voidable
583 my $line1_pre = $line1->unblessed();
584 $ret = $line1->void();
585 $line1->_result->discard_changes();
586 my $line1_post = $line1->unblessed();
587 is
( $ret, undef, 'Attempted void on non-credit returns undef' );
588 is_deeply
( $line1_pre, $line1_post, 'Non-credit account line cannot be voided' );
590 $schema->storage->txn_rollback;