@@ -, +, @@ --- t/db_dependent/Koha/Account/Line.t | 913 ++++++++++++++++++++++++++++ t/db_dependent/Koha/Account/Lines.t | 891 +-------------------------- 2 files changed, 919 insertions(+), 885 deletions(-) create mode 100644 t/db_dependent/Koha/Account/Line.t --- a/t/db_dependent/Koha/Account/Line.t +++ a/t/db_dependent/Koha/Account/Line.t @@ -0,0 +1,913 @@ +#!/usr/bin/perl + +# Copyright 2018 Koha Development team +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see + +use Modern::Perl; + +use Test::More tests => 8; +use Test::Exception; + +use C4::Circulation qw/AddIssue AddReturn/; +use Koha::Account; +use Koha::Account::Lines; +use Koha::Account::Offsets; +use Koha::Items; + +use t::lib::Mocks; +use t::lib::TestBuilder; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +subtest 'patron() tests' => sub { + + plan tests => 3; + + $schema->storage->txn_begin; + + my $library = $builder->build( { source => 'Branch' } ); + my $patron = $builder->build( { source => 'Borrower' } ); + + my $line = Koha::Account::Line->new( + { + borrowernumber => $patron->{borrowernumber}, + debit_type_code => "OVERDUE", + status => "RETURNED", + amount => 10, + interface => 'commandline', + })->store; + + my $account_line_patron = $line->patron; + is( ref( $account_line_patron ), 'Koha::Patron', 'Koha::Account::Line->patron should return a Koha::Patron' ); + is( $line->borrowernumber, $account_line_patron->borrowernumber, 'Koha::Account::Line->patron should return the correct borrower' ); + + $line->borrowernumber(undef)->store; + is( $line->patron, undef, 'Koha::Account::Line->patron should return undef if no patron linked' ); + + $schema->storage->txn_rollback; +}; + +subtest 'item() tests' => sub { + + plan tests => 3; + + $schema->storage->txn_begin; + + my $library = $builder->build( { source => 'Branch' } ); + my $biblioitem = $builder->build( { source => 'Biblioitem' } ); + my $patron = $builder->build( { source => 'Borrower' } ); + my $item = Koha::Item->new( + { + biblionumber => $biblioitem->{biblionumber}, + biblioitemnumber => $biblioitem->{biblioitemnumber}, + homebranch => $library->{branchcode}, + holdingbranch => $library->{branchcode}, + barcode => 'some_barcode_12', + itype => 'BK', + })->store; + + my $line = Koha::Account::Line->new( + { + borrowernumber => $patron->{borrowernumber}, + itemnumber => $item->itemnumber, + debit_type_code => "OVERDUE", + status => "RETURNED", + amount => 10, + interface => 'commandline', + })->store; + + my $account_line_item = $line->item; + is( ref( $account_line_item ), 'Koha::Item', 'Koha::Account::Line->item should return a Koha::Item' ); + is( $line->itemnumber, $account_line_item->itemnumber, 'Koha::Account::Line->item should return the correct item' ); + + $line->itemnumber(undef)->store; + is( $line->item, undef, 'Koha::Account::Line->item should return undef if no item linked' ); + + $schema->storage->txn_rollback; +}; + +subtest 'is_credit() and is_debit() tests' => sub { + + plan tests => 4; + + $schema->storage->txn_begin; + + my $patron = $builder->build_object({ class => 'Koha::Patrons' }); + my $account = $patron->account; + + my $credit = $account->add_credit({ amount => 100, user_id => $patron->id, interface => 'commandline' }); + + ok( $credit->is_credit, 'is_credit detects credits' ); + ok( !$credit->is_debit, 'is_debit detects credits' ); + + my $debit = Koha::Account::Line->new( + { + borrowernumber => $patron->id, + debit_type_code => "OVERDUE", + status => "RETURNED", + amount => 10, + interface => 'commandline', + })->store; + + ok( !$debit->is_credit, 'is_credit detects debits' ); + ok( $debit->is_debit, 'is_debit detects debits'); + + $schema->storage->txn_rollback; +}; + +subtest 'apply() tests' => sub { + + plan tests => 24; + + $schema->storage->txn_begin; + + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $account = $patron->account; + + my $credit = $account->add_credit( { amount => 100, user_id => $patron->id, interface => 'commandline' } ); + + my $debit_1 = Koha::Account::Line->new( + { borrowernumber => $patron->id, + debit_type_code => "OVERDUE", + status => "RETURNED", + amount => 10, + amountoutstanding => 10, + interface => 'commandline', + } + )->store; + + my $debit_2 = Koha::Account::Line->new( + { borrowernumber => $patron->id, + debit_type_code => "OVERDUE", + status => "RETURNED", + amount => 100, + amountoutstanding => 100, + interface => 'commandline', + } + )->store; + + $credit->discard_changes; + $debit_1->discard_changes; + + my $debits = Koha::Account::Lines->search({ accountlines_id => $debit_1->id }); + my $remaining_credit = $credit->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } ); + is( $remaining_credit * 1, 90, 'Remaining credit is correctly calculated' ); + $credit->discard_changes; + is( $credit->amountoutstanding * -1, $remaining_credit, 'Remaining credit correctly stored' ); + + # re-read debit info + $debit_1->discard_changes; + is( $debit_1->amountoutstanding * 1, 0, 'Debit has been cancelled' ); + + my $offsets = Koha::Account::Offsets->search( { credit_id => $credit->id, debit_id => $debit_1->id } ); + is( $offsets->count, 1, 'Only one offset is generated' ); + my $THE_offset = $offsets->next; + is( $THE_offset->amount * 1, -10, 'Amount was calculated correctly (less than the available credit)' ); + is( $THE_offset->type, 'Manual Credit', 'Passed type stored correctly' ); + + $debits = Koha::Account::Lines->search({ accountlines_id => $debit_2->id }); + $remaining_credit = $credit->apply( { debits => [ $debits->as_list ] } ); + is( $remaining_credit, 0, 'No remaining credit left' ); + $credit->discard_changes; + is( $credit->amountoutstanding * 1, 0, 'No outstanding credit' ); + $debit_2->discard_changes; + is( $debit_2->amountoutstanding * 1, 10, 'Outstanding amount decremented correctly' ); + + $offsets = Koha::Account::Offsets->search( { credit_id => $credit->id, debit_id => $debit_2->id } ); + is( $offsets->count, 1, 'Only one offset is generated' ); + $THE_offset = $offsets->next; + is( $THE_offset->amount * 1, -90, 'Amount was calculated correctly (less than the available credit)' ); + is( $THE_offset->type, 'Credit Applied', 'Defaults to \'Credit Applied\' offset type' ); + + $debits = Koha::Account::Lines->search({ accountlines_id => $debit_1->id }); + throws_ok + { $credit->apply({ debits => [ $debits->as_list ] }); } + 'Koha::Exceptions::Account::NoAvailableCredit', + '->apply() can only be used with outstanding credits'; + + $debits = Koha::Account::Lines->search({ accountlines_id => $credit->id }); + throws_ok + { $debit_1->apply({ debits => [ $debits->as_list ] }); } + 'Koha::Exceptions::Account::IsNotCredit', + '->apply() can only be used with credits'; + + $debits = Koha::Account::Lines->search({ accountlines_id => $credit->id }); + my $credit_3 = $account->add_credit({ amount => 1, interface => 'commandline' }); + throws_ok + { $credit_3->apply({ debits => [ $debits->as_list ] }); } + 'Koha::Exceptions::Account::IsNotDebit', + '->apply() can only be applied to credits'; + + my $credit_2 = $account->add_credit({ amount => 20, interface => 'commandline' }); + my $debit_3 = Koha::Account::Line->new( + { borrowernumber => $patron->id, + debit_type_code => "OVERDUE", + status => "RETURNED", + amount => 100, + amountoutstanding => 100, + interface => 'commandline', + } + )->store; + + $debits = Koha::Account::Lines->search({ accountlines_id => { -in => [ $debit_1->id, $debit_2->id, $debit_3->id, $credit->id ] } }); + throws_ok { + $credit_2->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } ); } + 'Koha::Exceptions::Account::IsNotDebit', + '->apply() rolls back if any of the passed lines is not a debit'; + + is( $debit_1->discard_changes->amountoutstanding * 1, 0, 'No changes to already cancelled debit' ); + is( $debit_2->discard_changes->amountoutstanding * 1, 10, 'Debit cancelled' ); + is( $debit_3->discard_changes->amountoutstanding * 1, 100, 'Outstanding amount correctly calculated' ); + is( $credit_2->discard_changes->amountoutstanding * -1, 20, 'No changes made' ); + + $debits = Koha::Account::Lines->search({ accountlines_id => { -in => [ $debit_1->id, $debit_2->id, $debit_3->id ] } }); + $remaining_credit = $credit_2->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } ); + + is( $debit_1->discard_changes->amountoutstanding * 1, 0, 'No changes to already cancelled debit' ); + is( $debit_2->discard_changes->amountoutstanding * 1, 0, 'Debit cancelled' ); + is( $debit_3->discard_changes->amountoutstanding * 1, 90, 'Outstanding amount correctly calculated' ); + is( $credit_2->discard_changes->amountoutstanding * 1, 0, 'No remaining credit' ); + + $schema->storage->txn_rollback; +}; + +subtest 'Keep account info when related patron, staff, item or cash_register is deleted' => sub { + + plan tests => 4; + + $schema->storage->txn_begin; + + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $staff = $builder->build_object( { class => 'Koha::Patrons' } ); + my $item = $builder->build_object({ class => 'Koha::Items' }); + my $issue = $builder->build_object( + { + class => 'Koha::Checkouts', + value => { itemnumber => $item->itemnumber } + } + ); + my $register = $builder->build_object({ class => 'Koha::Cash::Registers' }); + + my $line = Koha::Account::Line->new( + { + borrowernumber => $patron->borrowernumber, + manager_id => $staff->borrowernumber, + itemnumber => $item->itemnumber, + debit_type_code => "OVERDUE", + status => "RETURNED", + amount => 10, + interface => 'commandline', + register_id => $register->id + })->store; + + $issue->delete; + $item->delete; + $line = $line->get_from_storage; + is( $line->itemnumber, undef, "The account line should not be deleted when the related item is delete"); + + $staff->delete; + $line = $line->get_from_storage; + is( $line->manager_id, undef, "The account line should not be deleted when the related staff is delete"); + + $patron->delete; + $line = $line->get_from_storage; + is( $line->borrowernumber, undef, "The account line should not be deleted when the related patron is delete"); + + $register->delete; + $line = $line->get_from_storage; + is( $line->register_id, undef, "The account line should not be deleted when the related cash register is delete"); + + $schema->storage->txn_rollback; +}; + +subtest 'adjust() tests' => sub { + + plan tests => 29; + + $schema->storage->txn_begin; + + # count logs before any actions + my $action_logs = $schema->resultset('ActionLog')->search()->count; + + # Disable logs + t::lib::Mocks::mock_preference( 'FinesLog', 0 ); + + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $account = $patron->account; + + my $debit_1 = Koha::Account::Line->new( + { borrowernumber => $patron->id, + debit_type_code => "OVERDUE", + status => "RETURNED", + amount => 10, + amountoutstanding => 10, + interface => 'commandline', + } + )->store; + + my $debit_2 = Koha::Account::Line->new( + { borrowernumber => $patron->id, + debit_type_code => "OVERDUE", + status => "UNRETURNED", + amount => 100, + amountoutstanding => 100, + interface => 'commandline' + } + )->store; + + my $credit = $account->add_credit( { amount => 40, user_id => $patron->id, interface => 'commandline' } ); + + throws_ok { $debit_1->adjust( { amount => 50, type => 'bad', interface => 'commandline' } ) } + qr/Update type not recognised/, 'Exception thrown for unrecognised type'; + + throws_ok { $debit_1->adjust( { amount => 50, type => 'overdue_update', interface => 'commandline' } ) } + qr/Update type not allowed on this debit_type/, + 'Exception thrown for type conflict'; + + # Increment an unpaid fine + $debit_2->adjust( { amount => 150, type => 'overdue_update', interface => 'commandline' } )->discard_changes; + + is( $debit_2->amount * 1, 150, 'Fine amount was updated in full' ); + is( $debit_2->amountoutstanding * 1, 150, 'Fine amountoutstanding was update in full' ); + isnt( $debit_2->date, undef, 'Date has been set' ); + + my $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } ); + is( $offsets->count, 1, 'An offset is generated for the increment' ); + my $THIS_offset = $offsets->next; + is( $THIS_offset->amount * 1, 50, 'Amount was calculated correctly (increment by 50)' ); + is( $THIS_offset->type, 'OVERDUE_INCREASE', 'Adjust type stored correctly' ); + + is( $schema->resultset('ActionLog')->count(), $action_logs + 0, 'No log was added' ); + + # Update fine to partially paid + my $debits = Koha::Account::Lines->search({ accountlines_id => $debit_2->id }); + $credit->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } ); + + $debit_2->discard_changes; + is( $debit_2->amount * 1, 150, 'Fine amount unaffected by partial payment' ); + is( $debit_2->amountoutstanding * 1, 110, 'Fine amountoutstanding updated by partial payment' ); + + # Enable logs + t::lib::Mocks::mock_preference( 'FinesLog', 1 ); + + # Increment the partially paid fine + $debit_2->adjust( { amount => 160, type => 'overdue_update', interface => 'commandline' } )->discard_changes; + + is( $debit_2->amount * 1, 160, 'Fine amount was updated in full' ); + is( $debit_2->amountoutstanding * 1, 120, 'Fine amountoutstanding was updated by difference' ); + + $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } ); + is( $offsets->count, 3, 'An offset is generated for the increment' ); + $THIS_offset = $offsets->last; + is( $THIS_offset->amount * 1, 10, 'Amount was calculated correctly (increment by 10)' ); + is( $THIS_offset->type, 'OVERDUE_INCREASE', 'Adjust type stored correctly' ); + + is( $schema->resultset('ActionLog')->count(), $action_logs + 1, 'Log was added' ); + + # Decrement the partially paid fine, less than what was paid + $debit_2->adjust( { amount => 50, type => 'overdue_update', interface => 'commandline' } )->discard_changes; + + is( $debit_2->amount * 1, 50, 'Fine amount was updated in full' ); + is( $debit_2->amountoutstanding * 1, 10, 'Fine amountoutstanding was updated by difference' ); + + $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } ); + is( $offsets->count, 4, 'An offset is generated for the decrement' ); + $THIS_offset = $offsets->last; + is( $THIS_offset->amount * 1, -110, 'Amount was calculated correctly (decrement by 110)' ); + is( $THIS_offset->type, 'OVERDUE_DECREASE', 'Adjust type stored correctly' ); + + # Decrement the partially paid fine, more than what was paid + $debit_2->adjust( { amount => 30, type => 'overdue_update', interface => 'commandline' } )->discard_changes; + is( $debit_2->amount * 1, 30, 'Fine amount was updated in full' ); + is( $debit_2->amountoutstanding * 1, 0, 'Fine amountoutstanding was zeroed (payment was 40)' ); + + $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } ); + is( $offsets->count, 5, 'An offset is generated for the decrement' ); + $THIS_offset = $offsets->last; + is( $THIS_offset->amount * 1, -20, 'Amount was calculated correctly (decrement by 20)' ); + is( $THIS_offset->type, 'OVERDUE_DECREASE', 'Adjust type stored correctly' ); + + my $overpayment_refund = $account->lines->last; + is( $overpayment_refund->amount * 1, -10, 'A new credit has been added' ); + is( $overpayment_refund->description, 'Overpayment refund', 'Credit generated with the expected description' ); + + $schema->storage->txn_rollback; +}; + +subtest 'checkout() tests' => sub { + plan tests => 6; + + $schema->storage->txn_begin; + + my $library = $builder->build_object( { class => 'Koha::Libraries' } ); + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $item = $builder->build_sample_item; + my $account = $patron->account; + + t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode }); + my $checkout = AddIssue( $patron->unblessed, $item->barcode ); + + my $line = $account->add_debit({ + amount => 10, + interface => 'commandline', + item_id => $item->itemnumber, + issue_id => $checkout->issue_id, + type => 'OVERDUE', + }); + + my $line_checkout = $line->checkout; + is( ref($line_checkout), 'Koha::Checkout', 'Result type is correct' ); + is( $line_checkout->issue_id, $checkout->issue_id, 'Koha::Account::Line->checkout should return the correct checkout'); + + my ( $returned, undef, $old_checkout) = C4::Circulation::AddReturn( $item->barcode, $library->branchcode ); + is( $returned, 1, 'The item should have been returned' ); + + $line = $line->get_from_storage; + my $old_line_checkout = $line->checkout; + is( ref($old_line_checkout), 'Koha::Old::Checkout', 'Result type is correct' ); + is( $old_line_checkout->issue_id, $old_checkout->issue_id, 'Koha::Account::Line->checkout should return the correct old_checkout' ); + + $line->issue_id(undef)->store; + is( $line->checkout, undef, 'Koha::Account::Line->checkout should return undef if no checkout linked' ); + + $schema->storage->txn_rollback; +}; + +subtest 'credits() and debits() tests' => sub { + plan tests => 10; + + $schema->storage->txn_begin; + + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $account = $patron->account; + + my $debit1 = $account->add_debit({ + amount => 8, + interface => 'commandline', + type => 'ACCOUNT', + }); + my $debit2 = $account->add_debit({ + amount => 12, + interface => 'commandline', + type => 'ACCOUNT', + }); + my $credit1 = $account->add_credit({ + amount => 5, + interface => 'commandline', + type => 'CREDIT', + }); + my $credit2 = $account->add_credit({ + amount => 10, + interface => 'commandline', + type => 'CREDIT', + }); + + $credit1->apply({ debits => [ $debit1 ] }); + $credit2->apply({ debits => [ $debit1, $debit2 ] }); + + my $credits = $debit1->credits; + is($credits->count, 2, '2 Credits applied to debit 1'); + my $credit = $credits->next; + is($credit->amount + 0, -5, 'Correct first credit'); + $credit = $credits->next; + is($credit->amount + 0, -10, 'Correct second credit'); + + $credits = $debit2->credits; + is($credits->count, 1, '1 Credits applied to debit 2'); + $credit = $credits->next; + is($credit->amount + 0, -10, 'Correct first credit'); + + my $debits = $credit1->debits; + is($debits->count, 1, 'Credit 1 applied to 1 debit'); + my $debit = $debits->next; + is($debit->amount + 0, 8, 'Correct first debit'); + + $debits = $credit2->debits; + is($debits->count, 2, 'Credit 2 applied to 2 debits'); + $debit = $debits->next; + is($debit->amount + 0, 8, 'Correct first debit'); + $debit = $debits->next; + is($debit->amount + 0, 12, 'Correct second debit'); + + $schema->storage->txn_rollback; +}; + +subtest "void() tests" => sub { + + plan tests => 16; + + $schema->storage->txn_begin; + + # Create a borrower + my $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; + my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; + + my $borrower = Koha::Patron->new( { + cardnumber => 'dariahall', + surname => 'Hall', + firstname => 'Daria', + } ); + $borrower->categorycode( $categorycode ); + $borrower->branchcode( $branchcode ); + $borrower->store; + + my $account = Koha::Account->new({ patron_id => $borrower->id }); + + my $line1 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => 10, + amountoutstanding => 10, + interface => 'commandline', + debit_type_code => 'OVERDUE' + } + )->store(); + my $line2 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => 20, + amountoutstanding => 20, + interface => 'commandline', + debit_type_code => 'OVERDUE' + } + )->store(); + + is( $account->balance(), 30, "Account balance is 30" ); + is( $line1->amountoutstanding, 10, 'First fee has amount outstanding of 10' ); + is( $line2->amountoutstanding, 20, 'Second fee has amount outstanding of 20' ); + + my $id = $account->pay( + { + lines => [$line1, $line2], + amount => 30, + } + ); + + my $account_payment = Koha::Account::Lines->find( $id ); + + is( $account->balance(), 0, "Account balance is 0" ); + + $line1->_result->discard_changes(); + $line2->_result->discard_changes(); + is( $line1->amountoutstanding+0, 0, 'First fee has amount outstanding of 0' ); + is( $line2->amountoutstanding+0, 0, 'Second fee has amount outstanding of 0' ); + + my $ret = $account_payment->void(); + + is( ref($ret), 'Koha::Account::Line', 'Void returns the account line' ); + is( $account->balance(), 30, "Account balance is again 30" ); + + $account_payment->_result->discard_changes(); + $line1->_result->discard_changes(); + $line2->_result->discard_changes(); + + is( $account_payment->credit_type_code, 'PAYMENT', 'Voided payment credit_type_code is still PAYMENT' ); + is( $account_payment->status, 'VOID', 'Voided payment status is VOID' ); + is( $account_payment->amount+0, 0, 'Voided payment amount is 0' ); + is( $account_payment->amountoutstanding+0, 0, 'Voided payment amount outstanding is 0' ); + + is( $line1->amountoutstanding+0, 10, 'First fee again has amount outstanding of 10' ); + is( $line2->amountoutstanding+0, 20, 'Second fee again has amount outstanding of 20' ); + + # Accountlines that are not credits should be un-voidable + my $line1_pre = $line1->unblessed(); + $ret = $line1->void(); + $line1->_result->discard_changes(); + my $line1_post = $line1->unblessed(); + is( $ret, undef, 'Attempted void on non-credit returns undef' ); + is_deeply( $line1_pre, $line1_post, 'Non-credit account line cannot be voided' ); + + $schema->storage->txn_rollback; +}; + +subtest "payout() tests" => sub { + + plan tests => 18; + + $schema->storage->txn_begin; + + # Create a borrower + my $categorycode = + $builder->build( { source => 'Category' } )->{categorycode}; + my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; + + my $borrower = Koha::Patron->new( + { + cardnumber => 'dariahall', + surname => 'Hall', + firstname => 'Daria', + } + ); + $borrower->categorycode($categorycode); + $borrower->branchcode($branchcode); + $borrower->store; + + my $staff = Koha::Patron->new( + { + cardnumber => 'bobby', + surname => 'Bloggs', + firstname => 'Bobby', + } + ); + $staff->categorycode($categorycode); + $staff->branchcode($branchcode); + $staff->store; + + my $account = Koha::Account->new( { patron_id => $borrower->id } ); + + my $debit1 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => 10, + amountoutstanding => 10, + interface => 'commandline', + debit_type_code => 'OVERDUE' + } + )->store(); + my $credit1 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => -20, + amountoutstanding => -20, + interface => 'commandline', + credit_type_code => 'CREDIT' + } + )->store(); + + is( $account->balance(), -10, "Account balance is -10" ); + is( $debit1->amountoutstanding + 0, + 10, 'Overdue fee has an amount outstanding of 10' ); + is( $credit1->amountoutstanding + 0, + -20, 'Credit has an amount outstanding of -20' ); + + my $pay_params = { + interface => 'intranet', + staff_id => $staff->borrowernumber, + branch => $branchcode, + payout_type => 'CASH', + amount => 20 + }; + + throws_ok { $debit1->payout($pay_params); } + 'Koha::Exceptions::Account::IsNotCredit', + '->payout() can only be used with credits'; + + my @required = + ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' ); + for my $required (@required) { + my $params = {%$pay_params}; + delete( $params->{$required} ); + throws_ok { + $credit1->payout($params); + } + 'Koha::Exceptions::MissingParameter', + "->payout() requires the `$required` parameter is passed"; + } + + throws_ok { + $credit1->payout( + { + interface => 'intranet', + staff_id => $staff->borrowernumber, + branch => $branchcode, + payout_type => 'CASH', + amount => 25 + } + ); + } + 'Koha::Exceptions::ParameterTooHigh', + '->payout() cannot pay out more than the amountoutstanding'; + + t::lib::Mocks::mock_preference( 'UseCashRegisters', 1 ); + throws_ok { + $credit1->payout( + { + interface => 'intranet', + staff_id => $staff->borrowernumber, + branch => $branchcode, + payout_type => 'CASH', + amount => 10 + } + ); + } + 'Koha::Exceptions::Account::RegisterRequired', + '->payout() requires a cash_register if payout_type is `CASH`'; + + t::lib::Mocks::mock_preference( 'UseCashRegisters', 0 ); + my $payout = $credit1->payout( + { + interface => 'intranet', + staff_id => $staff->borrowernumber, + branch => $branchcode, + payout_type => 'CASH', + amount => 10 + } + ); + + is( ref($payout), 'Koha::Account::Line', + '->payout() returns a Koha::Account::Line' ); + is( $payout->amount() + 0, 10, "Payout amount is 10" ); + is( $payout->amountoutstanding() + 0, 0, "Payout amountoutstanding is 0" ); + is( $account->balance() + 0, 0, "Account balance is 0" ); + is( $debit1->amountoutstanding + 0, + 10, 'Overdue fee still has an amount outstanding of 10' ); + is( $credit1->amountoutstanding + 0, + -10, 'Credit has an new amount outstanding of -10' ); + is( $credit1->status(), 'PAID', "Credit has a new status of PAID" ); + + $schema->storage->txn_rollback; +}; + +subtest "reduce() tests" => sub { + + plan tests => 27; + + $schema->storage->txn_begin; + + # Create a borrower + my $categorycode = + $builder->build( { source => 'Category' } )->{categorycode}; + my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; + + my $borrower = Koha::Patron->new( + { + cardnumber => 'dariahall', + surname => 'Hall', + firstname => 'Daria', + } + ); + $borrower->categorycode($categorycode); + $borrower->branchcode($branchcode); + $borrower->store; + + my $staff = Koha::Patron->new( + { + cardnumber => 'bobby', + surname => 'Bloggs', + firstname => 'Bobby', + } + ); + $staff->categorycode($categorycode); + $staff->branchcode($branchcode); + $staff->store; + + my $account = Koha::Account->new( { patron_id => $borrower->id } ); + + my $debit1 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => 20, + amountoutstanding => 20, + interface => 'commandline', + debit_type_code => 'LOST' + } + )->store(); + my $credit1 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => -20, + amountoutstanding => -20, + interface => 'commandline', + credit_type_code => 'CREDIT' + } + )->store(); + + is( $account->balance(), 0, "Account balance is 0" ); + is( $debit1->amountoutstanding, + 20, 'Overdue fee has an amount outstanding of 20' ); + is( $credit1->amountoutstanding, + -20, 'Credit has an amount outstanding of -20' ); + + my $reduce_params = { + interface => 'commandline', + reduction_type => 'REFUND', + amount => 5, + staff_id => $staff->borrowernumber, + branch => $branchcode + }; + + throws_ok { $credit1->reduce($reduce_params); } + 'Koha::Exceptions::Account::IsNotDebit', + '->reduce() can only be used with debits'; + + my @required = ( 'interface', 'reduction_type', 'amount' ); + for my $required (@required) { + my $params = {%$reduce_params}; + delete( $params->{$required} ); + throws_ok { + $debit1->reduce($params); + } + 'Koha::Exceptions::MissingParameter', + "->reduce() requires the `$required` parameter is passed"; + } + + $reduce_params->{interface} = 'intranet'; + my @dependant_required = ( 'staff_id', 'branch' ); + for my $d (@dependant_required) { + my $params = {%$reduce_params}; + delete( $params->{$d} ); + throws_ok { + $debit1->reduce($params); + } + 'Koha::Exceptions::MissingParameter', +"->reduce() requires the `$d` parameter is passed when interface is intranet"; + } + + throws_ok { + $debit1->reduce( + { + interface => 'intranet', + staff_id => $staff->borrowernumber, + branch => $branchcode, + reduction_type => 'REFUND', + amount => 25 + } + ); + } + 'Koha::Exceptions::ParameterTooHigh', + '->reduce() cannot reduce more than original amount'; + + # Partial Reduction + # (Refund 5 on debt of 20) + my $reduction = $debit1->reduce($reduce_params); + + is( ref($reduction), 'Koha::Account::Line', + '->reduce() returns a Koha::Account::Line' ); + is( $reduction->amount() * 1, -5, "Reduce amount is -5" ); + is( $reduction->amountoutstanding() * 1, + 0, "Reduce amountoutstanding is 0" ); + is( $debit1->amountoutstanding() * 1, + 15, "Debit amountoutstanding reduced by 5 to 15" ); + is( $account->balance() * 1, -5, "Account balance is -5" ); + is( $reduction->status(), 'APPLIED', "Reduction status is 'APPLIED'" ); + + my $offsets = Koha::Account::Offsets->search( + { credit_id => $reduction->id, debit_id => $debit1->id } ); + is( $offsets->count, 1, 'Only one offset is generated' ); + my $THE_offset = $offsets->next; + is( $THE_offset->amount * 1, + -5, 'Correct amount was applied against debit' ); + is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" ); + + # Zero offset created when zero outstanding + # (Refund another 5 on paid debt of 20) + $credit1->apply( { debits => [$debit1] } ); + is( $debit1->amountoutstanding + 0, + 0, 'Debit1 amountoutstanding reduced to 0' ); + $reduction = $debit1->reduce($reduce_params); + is( $reduction->amount() * 1, -5, "Reduce amount is -5" ); + is( $reduction->amountoutstanding() * 1, + -5, "Reduce amountoutstanding is -5" ); + + $offsets = Koha::Account::Offsets->search( + { credit_id => $reduction->id, debit_id => $debit1->id } ); + is( $offsets->count, 1, 'Only one new offset is generated' ); + $THE_offset = $offsets->next; + is( $THE_offset->amount * 1, + 0, 'Zero offset created for already paid off debit' ); + is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" ); + + # Compound reduction should not allow more than original amount + # (Reduction of 5 + 5 + 20 > 20) + $reduce_params->{amount} = 20; + throws_ok { + $debit1->reduce($reduce_params); + } + 'Koha::Exceptions::ParameterTooHigh', +'->reduce cannot reduce more than the original amount (combined reductions test)'; + + # Throw exception if attempting to reduce a payout + my $payout = $reduction->payout( + { + interface => 'intranet', + staff_id => $staff->borrowernumber, + branch => $branchcode, + payout_type => 'CASH', + amount => 5 + } + ); + throws_ok { + $payout->reduce($reduce_params); + } + 'Koha::Exceptions::Account::IsNotDebit', + '->reduce() cannot be used on a payout debit'; + + $schema->storage->txn_rollback; +}; + +1; --- a/t/db_dependent/Koha/Account/Lines.t +++ a/t/db_dependent/Koha/Account/Lines.t @@ -19,14 +19,12 @@ use Modern::Perl; -use Test::More tests => 15; +use Test::More tests => 4; use Test::Exception; -use C4::Circulation qw/AddIssue AddReturn/; use Koha::Account; use Koha::Account::Lines; use Koha::Account::Offsets; -use Koha::Items; use t::lib::Mocks; use t::lib::TestBuilder; @@ -34,74 +32,6 @@ use t::lib::TestBuilder; my $schema = Koha::Database->new->schema; my $builder = t::lib::TestBuilder->new; -subtest 'patron() tests' => sub { - - plan tests => 3; - - $schema->storage->txn_begin; - - my $library = $builder->build( { source => 'Branch' } ); - my $patron = $builder->build( { source => 'Borrower' } ); - - my $line = Koha::Account::Line->new( - { - borrowernumber => $patron->{borrowernumber}, - debit_type_code => "OVERDUE", - status => "RETURNED", - amount => 10, - interface => 'commandline', - })->store; - - my $account_line_patron = $line->patron; - is( ref( $account_line_patron ), 'Koha::Patron', 'Koha::Account::Line->patron should return a Koha::Patron' ); - is( $line->borrowernumber, $account_line_patron->borrowernumber, 'Koha::Account::Line->patron should return the correct borrower' ); - - $line->borrowernumber(undef)->store; - is( $line->patron, undef, 'Koha::Account::Line->patron should return undef if no patron linked' ); - - $schema->storage->txn_rollback; -}; - - -subtest 'item() tests' => sub { - - plan tests => 3; - - $schema->storage->txn_begin; - - my $library = $builder->build( { source => 'Branch' } ); - my $biblioitem = $builder->build( { source => 'Biblioitem' } ); - my $patron = $builder->build( { source => 'Borrower' } ); - my $item = Koha::Item->new( - { - biblionumber => $biblioitem->{biblionumber}, - biblioitemnumber => $biblioitem->{biblioitemnumber}, - homebranch => $library->{branchcode}, - holdingbranch => $library->{branchcode}, - barcode => 'some_barcode_12', - itype => 'BK', - })->store; - - my $line = Koha::Account::Line->new( - { - borrowernumber => $patron->{borrowernumber}, - itemnumber => $item->itemnumber, - debit_type_code => "OVERDUE", - status => "RETURNED", - amount => 10, - interface => 'commandline', - })->store; - - my $account_line_item = $line->item; - is( ref( $account_line_item ), 'Koha::Item', 'Koha::Account::Line->item should return a Koha::Item' ); - is( $line->itemnumber, $account_line_item->itemnumber, 'Koha::Account::Line->item should return the correct item' ); - - $line->itemnumber(undef)->store; - is( $line->item, undef, 'Koha::Account::Line->item should return undef if no item linked' ); - - $schema->storage->txn_rollback; -}; - subtest 'total_outstanding() tests' => sub { plan tests => 5; @@ -115,7 +45,7 @@ subtest 'total_outstanding() tests' => sub { my $debit_1 = Koha::Account::Line->new( { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", + debit_type_code => "OVERDUE", status => "RETURNED", amount => 10, amountoutstanding => 10, @@ -125,7 +55,7 @@ subtest 'total_outstanding() tests' => sub { my $debit_2 = Koha::Account::Line->new( { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", + debit_type_code => "OVERDUE", status => "RETURNED", amount => 10, amountoutstanding => 10, @@ -138,7 +68,7 @@ subtest 'total_outstanding() tests' => sub { my $credit_1 = Koha::Account::Line->new( { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", + debit_type_code => "OVERDUE", status => "RETURNED", amount => -10, amountoutstanding => -10, @@ -151,7 +81,7 @@ subtest 'total_outstanding() tests' => sub { my $credit_2 = Koha::Account::Line->new( { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", + debit_type_code => "OVERDUE", status => "RETURNED", amount => -10, amountoutstanding => -10, @@ -164,7 +94,7 @@ subtest 'total_outstanding() tests' => sub { my $credit_3 = Koha::Account::Line->new( { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", + debit_type_code => "OVERDUE", status => "RETURNED", amount => -100, amountoutstanding => -100, @@ -406,813 +336,4 @@ subtest 'debits_total() tests' => sub { $schema->storage->txn_rollback; }; -subtest 'is_credit() and is_debit() tests' => sub { - - plan tests => 4; - - $schema->storage->txn_begin; - - my $patron = $builder->build_object({ class => 'Koha::Patrons' }); - my $account = $patron->account; - - my $credit = $account->add_credit({ amount => 100, user_id => $patron->id, interface => 'commandline' }); - - ok( $credit->is_credit, 'is_credit detects credits' ); - ok( !$credit->is_debit, 'is_debit detects credits' ); - - my $debit = Koha::Account::Line->new( - { - borrowernumber => $patron->id, - debit_type_code => "OVERDUE", - status => "RETURNED", - amount => 10, - interface => 'commandline', - })->store; - - ok( !$debit->is_credit, 'is_credit detects debits' ); - ok( $debit->is_debit, 'is_debit detects debits'); - - $schema->storage->txn_rollback; -}; - -subtest 'apply() tests' => sub { - - plan tests => 24; - - $schema->storage->txn_begin; - - my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); - my $account = $patron->account; - - my $credit = $account->add_credit( { amount => 100, user_id => $patron->id, interface => 'commandline' } ); - - my $debit_1 = Koha::Account::Line->new( - { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", - status => "RETURNED", - amount => 10, - amountoutstanding => 10, - interface => 'commandline', - } - )->store; - - my $debit_2 = Koha::Account::Line->new( - { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", - status => "RETURNED", - amount => 100, - amountoutstanding => 100, - interface => 'commandline', - } - )->store; - - $credit->discard_changes; - $debit_1->discard_changes; - - my $debits = Koha::Account::Lines->search({ accountlines_id => $debit_1->id }); - my $remaining_credit = $credit->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } ); - is( $remaining_credit * 1, 90, 'Remaining credit is correctly calculated' ); - $credit->discard_changes; - is( $credit->amountoutstanding * -1, $remaining_credit, 'Remaining credit correctly stored' ); - - # re-read debit info - $debit_1->discard_changes; - is( $debit_1->amountoutstanding * 1, 0, 'Debit has been cancelled' ); - - my $offsets = Koha::Account::Offsets->search( { credit_id => $credit->id, debit_id => $debit_1->id } ); - is( $offsets->count, 1, 'Only one offset is generated' ); - my $THE_offset = $offsets->next; - is( $THE_offset->amount * 1, -10, 'Amount was calculated correctly (less than the available credit)' ); - is( $THE_offset->type, 'Manual Credit', 'Passed type stored correctly' ); - - $debits = Koha::Account::Lines->search({ accountlines_id => $debit_2->id }); - $remaining_credit = $credit->apply( { debits => [ $debits->as_list ] } ); - is( $remaining_credit, 0, 'No remaining credit left' ); - $credit->discard_changes; - is( $credit->amountoutstanding * 1, 0, 'No outstanding credit' ); - $debit_2->discard_changes; - is( $debit_2->amountoutstanding * 1, 10, 'Outstanding amount decremented correctly' ); - - $offsets = Koha::Account::Offsets->search( { credit_id => $credit->id, debit_id => $debit_2->id } ); - is( $offsets->count, 1, 'Only one offset is generated' ); - $THE_offset = $offsets->next; - is( $THE_offset->amount * 1, -90, 'Amount was calculated correctly (less than the available credit)' ); - is( $THE_offset->type, 'Credit Applied', 'Defaults to \'Credit Applied\' offset type' ); - - $debits = Koha::Account::Lines->search({ accountlines_id => $debit_1->id }); - throws_ok - { $credit->apply({ debits => [ $debits->as_list ] }); } - 'Koha::Exceptions::Account::NoAvailableCredit', - '->apply() can only be used with outstanding credits'; - - $debits = Koha::Account::Lines->search({ accountlines_id => $credit->id }); - throws_ok - { $debit_1->apply({ debits => [ $debits->as_list ] }); } - 'Koha::Exceptions::Account::IsNotCredit', - '->apply() can only be used with credits'; - - $debits = Koha::Account::Lines->search({ accountlines_id => $credit->id }); - my $credit_3 = $account->add_credit({ amount => 1, interface => 'commandline' }); - throws_ok - { $credit_3->apply({ debits => [ $debits->as_list ] }); } - 'Koha::Exceptions::Account::IsNotDebit', - '->apply() can only be applied to credits'; - - my $credit_2 = $account->add_credit({ amount => 20, interface => 'commandline' }); - my $debit_3 = Koha::Account::Line->new( - { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", - status => "RETURNED", - amount => 100, - amountoutstanding => 100, - interface => 'commandline', - } - )->store; - - $debits = Koha::Account::Lines->search({ accountlines_id => { -in => [ $debit_1->id, $debit_2->id, $debit_3->id, $credit->id ] } }); - throws_ok { - $credit_2->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } ); } - 'Koha::Exceptions::Account::IsNotDebit', - '->apply() rolls back if any of the passed lines is not a debit'; - - is( $debit_1->discard_changes->amountoutstanding * 1, 0, 'No changes to already cancelled debit' ); - is( $debit_2->discard_changes->amountoutstanding * 1, 10, 'Debit cancelled' ); - is( $debit_3->discard_changes->amountoutstanding * 1, 100, 'Outstanding amount correctly calculated' ); - is( $credit_2->discard_changes->amountoutstanding * -1, 20, 'No changes made' ); - - $debits = Koha::Account::Lines->search({ accountlines_id => { -in => [ $debit_1->id, $debit_2->id, $debit_3->id ] } }); - $remaining_credit = $credit_2->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } ); - - is( $debit_1->discard_changes->amountoutstanding * 1, 0, 'No changes to already cancelled debit' ); - is( $debit_2->discard_changes->amountoutstanding * 1, 0, 'Debit cancelled' ); - is( $debit_3->discard_changes->amountoutstanding * 1, 90, 'Outstanding amount correctly calculated' ); - is( $credit_2->discard_changes->amountoutstanding * 1, 0, 'No remaining credit' ); - - $schema->storage->txn_rollback; -}; - -subtest 'Keep account info when related patron, staff, item or cash_register is deleted' => sub { - - plan tests => 4; - - $schema->storage->txn_begin; - - my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); - my $staff = $builder->build_object( { class => 'Koha::Patrons' } ); - my $item = $builder->build_object({ class => 'Koha::Items' }); - my $issue = $builder->build_object( - { - class => 'Koha::Checkouts', - value => { itemnumber => $item->itemnumber } - } - ); - my $register = $builder->build_object({ class => 'Koha::Cash::Registers' }); - - my $line = Koha::Account::Line->new( - { - borrowernumber => $patron->borrowernumber, - manager_id => $staff->borrowernumber, - itemnumber => $item->itemnumber, - debit_type_code => "OVERDUE", - status => "RETURNED", - amount => 10, - interface => 'commandline', - register_id => $register->id - })->store; - - $issue->delete; - $item->delete; - $line = $line->get_from_storage; - is( $line->itemnumber, undef, "The account line should not be deleted when the related item is delete"); - - $staff->delete; - $line = $line->get_from_storage; - is( $line->manager_id, undef, "The account line should not be deleted when the related staff is delete"); - - $patron->delete; - $line = $line->get_from_storage; - is( $line->borrowernumber, undef, "The account line should not be deleted when the related patron is delete"); - - $register->delete; - $line = $line->get_from_storage; - is( $line->register_id, undef, "The account line should not be deleted when the related cash register is delete"); - - $schema->storage->txn_rollback; -}; - -subtest 'adjust() tests' => sub { - - plan tests => 29; - - $schema->storage->txn_begin; - - # count logs before any actions - my $action_logs = $schema->resultset('ActionLog')->search()->count; - - # Disable logs - t::lib::Mocks::mock_preference( 'FinesLog', 0 ); - - my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); - my $account = $patron->account; - - my $debit_1 = Koha::Account::Line->new( - { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", - status => "RETURNED", - amount => 10, - amountoutstanding => 10, - interface => 'commandline', - } - )->store; - - my $debit_2 = Koha::Account::Line->new( - { borrowernumber => $patron->id, - debit_type_code => "OVERDUE", - status => "UNRETURNED", - amount => 100, - amountoutstanding => 100, - interface => 'commandline' - } - )->store; - - my $credit = $account->add_credit( { amount => 40, user_id => $patron->id, interface => 'commandline' } ); - - throws_ok { $debit_1->adjust( { amount => 50, type => 'bad', interface => 'commandline' } ) } - qr/Update type not recognised/, 'Exception thrown for unrecognised type'; - - throws_ok { $debit_1->adjust( { amount => 50, type => 'overdue_update', interface => 'commandline' } ) } - qr/Update type not allowed on this debit_type/, - 'Exception thrown for type conflict'; - - # Increment an unpaid fine - $debit_2->adjust( { amount => 150, type => 'overdue_update', interface => 'commandline' } )->discard_changes; - - is( $debit_2->amount * 1, 150, 'Fine amount was updated in full' ); - is( $debit_2->amountoutstanding * 1, 150, 'Fine amountoutstanding was update in full' ); - isnt( $debit_2->date, undef, 'Date has been set' ); - - my $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } ); - is( $offsets->count, 1, 'An offset is generated for the increment' ); - my $THIS_offset = $offsets->next; - is( $THIS_offset->amount * 1, 50, 'Amount was calculated correctly (increment by 50)' ); - is( $THIS_offset->type, 'OVERDUE_INCREASE', 'Adjust type stored correctly' ); - - is( $schema->resultset('ActionLog')->count(), $action_logs + 0, 'No log was added' ); - - # Update fine to partially paid - my $debits = Koha::Account::Lines->search({ accountlines_id => $debit_2->id }); - $credit->apply( { debits => [ $debits->as_list ], offset_type => 'Manual Credit' } ); - - $debit_2->discard_changes; - is( $debit_2->amount * 1, 150, 'Fine amount unaffected by partial payment' ); - is( $debit_2->amountoutstanding * 1, 110, 'Fine amountoutstanding updated by partial payment' ); - - # Enable logs - t::lib::Mocks::mock_preference( 'FinesLog', 1 ); - - # Increment the partially paid fine - $debit_2->adjust( { amount => 160, type => 'overdue_update', interface => 'commandline' } )->discard_changes; - - is( $debit_2->amount * 1, 160, 'Fine amount was updated in full' ); - is( $debit_2->amountoutstanding * 1, 120, 'Fine amountoutstanding was updated by difference' ); - - $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } ); - is( $offsets->count, 3, 'An offset is generated for the increment' ); - $THIS_offset = $offsets->last; - is( $THIS_offset->amount * 1, 10, 'Amount was calculated correctly (increment by 10)' ); - is( $THIS_offset->type, 'OVERDUE_INCREASE', 'Adjust type stored correctly' ); - - is( $schema->resultset('ActionLog')->count(), $action_logs + 1, 'Log was added' ); - - # Decrement the partially paid fine, less than what was paid - $debit_2->adjust( { amount => 50, type => 'overdue_update', interface => 'commandline' } )->discard_changes; - - is( $debit_2->amount * 1, 50, 'Fine amount was updated in full' ); - is( $debit_2->amountoutstanding * 1, 10, 'Fine amountoutstanding was updated by difference' ); - - $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } ); - is( $offsets->count, 4, 'An offset is generated for the decrement' ); - $THIS_offset = $offsets->last; - is( $THIS_offset->amount * 1, -110, 'Amount was calculated correctly (decrement by 110)' ); - is( $THIS_offset->type, 'OVERDUE_DECREASE', 'Adjust type stored correctly' ); - - # Decrement the partially paid fine, more than what was paid - $debit_2->adjust( { amount => 30, type => 'overdue_update', interface => 'commandline' } )->discard_changes; - is( $debit_2->amount * 1, 30, 'Fine amount was updated in full' ); - is( $debit_2->amountoutstanding * 1, 0, 'Fine amountoutstanding was zeroed (payment was 40)' ); - - $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } ); - is( $offsets->count, 5, 'An offset is generated for the decrement' ); - $THIS_offset = $offsets->last; - is( $THIS_offset->amount * 1, -20, 'Amount was calculated correctly (decrement by 20)' ); - is( $THIS_offset->type, 'OVERDUE_DECREASE', 'Adjust type stored correctly' ); - - my $overpayment_refund = $account->lines->last; - is( $overpayment_refund->amount * 1, -10, 'A new credit has been added' ); - is( $overpayment_refund->description, 'Overpayment refund', 'Credit generated with the expected description' ); - - $schema->storage->txn_rollback; -}; - -subtest 'checkout() tests' => sub { - plan tests => 6; - - $schema->storage->txn_begin; - - my $library = $builder->build_object( { class => 'Koha::Libraries' } ); - my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); - my $item = $builder->build_sample_item; - my $account = $patron->account; - - t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode }); - my $checkout = AddIssue( $patron->unblessed, $item->barcode ); - - my $line = $account->add_debit({ - amount => 10, - interface => 'commandline', - item_id => $item->itemnumber, - issue_id => $checkout->issue_id, - type => 'OVERDUE', - }); - - my $line_checkout = $line->checkout; - is( ref($line_checkout), 'Koha::Checkout', 'Result type is correct' ); - is( $line_checkout->issue_id, $checkout->issue_id, 'Koha::Account::Line->checkout should return the correct checkout'); - - my ( $returned, undef, $old_checkout) = C4::Circulation::AddReturn( $item->barcode, $library->branchcode ); - is( $returned, 1, 'The item should have been returned' ); - - $line = $line->get_from_storage; - my $old_line_checkout = $line->checkout; - is( ref($old_line_checkout), 'Koha::Old::Checkout', 'Result type is correct' ); - is( $old_line_checkout->issue_id, $old_checkout->issue_id, 'Koha::Account::Line->checkout should return the correct old_checkout' ); - - $line->issue_id(undef)->store; - is( $line->checkout, undef, 'Koha::Account::Line->checkout should return undef if no checkout linked' ); - - $schema->storage->txn_rollback; -}; - -subtest 'credits() and debits() tests' => sub { - plan tests => 10; - - $schema->storage->txn_begin; - - my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); - my $account = $patron->account; - - my $debit1 = $account->add_debit({ - amount => 8, - interface => 'commandline', - type => 'ACCOUNT', - }); - my $debit2 = $account->add_debit({ - amount => 12, - interface => 'commandline', - type => 'ACCOUNT', - }); - my $credit1 = $account->add_credit({ - amount => 5, - interface => 'commandline', - type => 'CREDIT', - }); - my $credit2 = $account->add_credit({ - amount => 10, - interface => 'commandline', - type => 'CREDIT', - }); - - $credit1->apply({ debits => [ $debit1 ] }); - $credit2->apply({ debits => [ $debit1, $debit2 ] }); - - my $credits = $debit1->credits; - is($credits->count, 2, '2 Credits applied to debit 1'); - my $credit = $credits->next; - is($credit->amount + 0, -5, 'Correct first credit'); - $credit = $credits->next; - is($credit->amount + 0, -10, 'Correct second credit'); - - $credits = $debit2->credits; - is($credits->count, 1, '1 Credits applied to debit 2'); - $credit = $credits->next; - is($credit->amount + 0, -10, 'Correct first credit'); - - my $debits = $credit1->debits; - is($debits->count, 1, 'Credit 1 applied to 1 debit'); - my $debit = $debits->next; - is($debit->amount + 0, 8, 'Correct first debit'); - - $debits = $credit2->debits; - is($debits->count, 2, 'Credit 2 applied to 2 debits'); - $debit = $debits->next; - is($debit->amount + 0, 8, 'Correct first debit'); - $debit = $debits->next; - is($debit->amount + 0, 12, 'Correct second debit'); - - $schema->storage->txn_rollback; -}; - -subtest "void() tests" => sub { - - plan tests => 16; - - $schema->storage->txn_begin; - - # Create a borrower - my $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; - my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; - - my $borrower = Koha::Patron->new( { - cardnumber => 'dariahall', - surname => 'Hall', - firstname => 'Daria', - } ); - $borrower->categorycode( $categorycode ); - $borrower->branchcode( $branchcode ); - $borrower->store; - - my $account = Koha::Account->new({ patron_id => $borrower->id }); - - my $line1 = Koha::Account::Line->new( - { - borrowernumber => $borrower->borrowernumber, - amount => 10, - amountoutstanding => 10, - interface => 'commandline', - debit_type_code => 'OVERDUE' - } - )->store(); - my $line2 = Koha::Account::Line->new( - { - borrowernumber => $borrower->borrowernumber, - amount => 20, - amountoutstanding => 20, - interface => 'commandline', - debit_type_code => 'OVERDUE' - } - )->store(); - - is( $account->balance(), 30, "Account balance is 30" ); - is( $line1->amountoutstanding, 10, 'First fee has amount outstanding of 10' ); - is( $line2->amountoutstanding, 20, 'Second fee has amount outstanding of 20' ); - - my $id = $account->pay( - { - lines => [$line1, $line2], - amount => 30, - } - ); - - my $account_payment = Koha::Account::Lines->find( $id ); - - is( $account->balance(), 0, "Account balance is 0" ); - - $line1->_result->discard_changes(); - $line2->_result->discard_changes(); - is( $line1->amountoutstanding+0, 0, 'First fee has amount outstanding of 0' ); - is( $line2->amountoutstanding+0, 0, 'Second fee has amount outstanding of 0' ); - - my $ret = $account_payment->void(); - - is( ref($ret), 'Koha::Account::Line', 'Void returns the account line' ); - is( $account->balance(), 30, "Account balance is again 30" ); - - $account_payment->_result->discard_changes(); - $line1->_result->discard_changes(); - $line2->_result->discard_changes(); - - is( $account_payment->credit_type_code, 'PAYMENT', 'Voided payment credit_type_code is still PAYMENT' ); - is( $account_payment->status, 'VOID', 'Voided payment status is VOID' ); - is( $account_payment->amount+0, 0, 'Voided payment amount is 0' ); - is( $account_payment->amountoutstanding+0, 0, 'Voided payment amount outstanding is 0' ); - - is( $line1->amountoutstanding+0, 10, 'First fee again has amount outstanding of 10' ); - is( $line2->amountoutstanding+0, 20, 'Second fee again has amount outstanding of 20' ); - - # Accountlines that are not credits should be un-voidable - my $line1_pre = $line1->unblessed(); - $ret = $line1->void(); - $line1->_result->discard_changes(); - my $line1_post = $line1->unblessed(); - is( $ret, undef, 'Attempted void on non-credit returns undef' ); - is_deeply( $line1_pre, $line1_post, 'Non-credit account line cannot be voided' ); - - $schema->storage->txn_rollback; -}; - -subtest "payout() tests" => sub { - - plan tests => 18; - - $schema->storage->txn_begin; - - # Create a borrower - my $categorycode = - $builder->build( { source => 'Category' } )->{categorycode}; - my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; - - my $borrower = Koha::Patron->new( - { - cardnumber => 'dariahall', - surname => 'Hall', - firstname => 'Daria', - } - ); - $borrower->categorycode($categorycode); - $borrower->branchcode($branchcode); - $borrower->store; - - my $staff = Koha::Patron->new( - { - cardnumber => 'bobby', - surname => 'Bloggs', - firstname => 'Bobby', - } - ); - $staff->categorycode($categorycode); - $staff->branchcode($branchcode); - $staff->store; - - my $account = Koha::Account->new( { patron_id => $borrower->id } ); - - my $debit1 = Koha::Account::Line->new( - { - borrowernumber => $borrower->borrowernumber, - amount => 10, - amountoutstanding => 10, - interface => 'commandline', - debit_type_code => 'OVERDUE' - } - )->store(); - my $credit1 = Koha::Account::Line->new( - { - borrowernumber => $borrower->borrowernumber, - amount => -20, - amountoutstanding => -20, - interface => 'commandline', - credit_type_code => 'CREDIT' - } - )->store(); - - is( $account->balance(), -10, "Account balance is -10" ); - is( $debit1->amountoutstanding + 0, - 10, 'Overdue fee has an amount outstanding of 10' ); - is( $credit1->amountoutstanding + 0, - -20, 'Credit has an amount outstanding of -20' ); - - my $pay_params = { - interface => 'intranet', - staff_id => $staff->borrowernumber, - branch => $branchcode, - payout_type => 'CASH', - amount => 20 - }; - - throws_ok { $debit1->payout($pay_params); } - 'Koha::Exceptions::Account::IsNotCredit', - '->payout() can only be used with credits'; - - my @required = - ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' ); - for my $required (@required) { - my $params = {%$pay_params}; - delete( $params->{$required} ); - throws_ok { - $credit1->payout($params); - } - 'Koha::Exceptions::MissingParameter', - "->payout() requires the `$required` parameter is passed"; - } - - throws_ok { - $credit1->payout( - { - interface => 'intranet', - staff_id => $staff->borrowernumber, - branch => $branchcode, - payout_type => 'CASH', - amount => 25 - } - ); - } - 'Koha::Exceptions::ParameterTooHigh', - '->payout() cannot pay out more than the amountoutstanding'; - - t::lib::Mocks::mock_preference( 'UseCashRegisters', 1 ); - throws_ok { - $credit1->payout( - { - interface => 'intranet', - staff_id => $staff->borrowernumber, - branch => $branchcode, - payout_type => 'CASH', - amount => 10 - } - ); - } - 'Koha::Exceptions::Account::RegisterRequired', - '->payout() requires a cash_register if payout_type is `CASH`'; - - t::lib::Mocks::mock_preference( 'UseCashRegisters', 0 ); - my $payout = $credit1->payout( - { - interface => 'intranet', - staff_id => $staff->borrowernumber, - branch => $branchcode, - payout_type => 'CASH', - amount => 10 - } - ); - - is( ref($payout), 'Koha::Account::Line', - '->payout() returns a Koha::Account::Line' ); - is( $payout->amount() + 0, 10, "Payout amount is 10" ); - is( $payout->amountoutstanding() + 0, 0, "Payout amountoutstanding is 0" ); - is( $account->balance() + 0, 0, "Account balance is 0" ); - is( $debit1->amountoutstanding + 0, - 10, 'Overdue fee still has an amount outstanding of 10' ); - is( $credit1->amountoutstanding + 0, - -10, 'Credit has an new amount outstanding of -10' ); - is( $credit1->status(), 'PAID', "Credit has a new status of PAID" ); - - $schema->storage->txn_rollback; -}; - -subtest "reduce() tests" => sub { - - plan tests => 27; - - $schema->storage->txn_begin; - - # Create a borrower - my $categorycode = - $builder->build( { source => 'Category' } )->{categorycode}; - my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; - - my $borrower = Koha::Patron->new( - { - cardnumber => 'dariahall', - surname => 'Hall', - firstname => 'Daria', - } - ); - $borrower->categorycode($categorycode); - $borrower->branchcode($branchcode); - $borrower->store; - - my $staff = Koha::Patron->new( - { - cardnumber => 'bobby', - surname => 'Bloggs', - firstname => 'Bobby', - } - ); - $staff->categorycode($categorycode); - $staff->branchcode($branchcode); - $staff->store; - - my $account = Koha::Account->new( { patron_id => $borrower->id } ); - - my $debit1 = Koha::Account::Line->new( - { - borrowernumber => $borrower->borrowernumber, - amount => 20, - amountoutstanding => 20, - interface => 'commandline', - debit_type_code => 'LOST' - } - )->store(); - my $credit1 = Koha::Account::Line->new( - { - borrowernumber => $borrower->borrowernumber, - amount => -20, - amountoutstanding => -20, - interface => 'commandline', - credit_type_code => 'CREDIT' - } - )->store(); - - is( $account->balance(), 0, "Account balance is 0" ); - is( $debit1->amountoutstanding, - 20, 'Overdue fee has an amount outstanding of 20' ); - is( $credit1->amountoutstanding, - -20, 'Credit has an amount outstanding of -20' ); - - my $reduce_params = { - interface => 'commandline', - reduction_type => 'REFUND', - amount => 5, - staff_id => $staff->borrowernumber, - branch => $branchcode - }; - - throws_ok { $credit1->reduce($reduce_params); } - 'Koha::Exceptions::Account::IsNotDebit', - '->reduce() can only be used with debits'; - - my @required = ( 'interface', 'reduction_type', 'amount' ); - for my $required (@required) { - my $params = {%$reduce_params}; - delete( $params->{$required} ); - throws_ok { - $debit1->reduce($params); - } - 'Koha::Exceptions::MissingParameter', - "->reduce() requires the `$required` parameter is passed"; - } - - $reduce_params->{interface} = 'intranet'; - my @dependant_required = ( 'staff_id', 'branch' ); - for my $d (@dependant_required) { - my $params = {%$reduce_params}; - delete( $params->{$d} ); - throws_ok { - $debit1->reduce($params); - } - 'Koha::Exceptions::MissingParameter', -"->reduce() requires the `$d` parameter is passed when interface is intranet"; - } - - throws_ok { - $debit1->reduce( - { - interface => 'intranet', - staff_id => $staff->borrowernumber, - branch => $branchcode, - reduction_type => 'REFUND', - amount => 25 - } - ); - } - 'Koha::Exceptions::ParameterTooHigh', - '->reduce() cannot reduce more than original amount'; - - # Partial Reduction - # (Refund 5 on debt of 20) - my $reduction = $debit1->reduce($reduce_params); - - is( ref($reduction), 'Koha::Account::Line', - '->reduce() returns a Koha::Account::Line' ); - is( $reduction->amount() * 1, -5, "Reduce amount is -5" ); - is( $reduction->amountoutstanding() * 1, - 0, "Reduce amountoutstanding is 0" ); - is( $debit1->amountoutstanding() * 1, - 15, "Debit amountoutstanding reduced by 5 to 15" ); - is( $account->balance() * 1, -5, "Account balance is -5" ); - is( $reduction->status(), 'APPLIED', "Reduction status is 'APPLIED'" ); - - my $offsets = Koha::Account::Offsets->search( - { credit_id => $reduction->id, debit_id => $debit1->id } ); - is( $offsets->count, 1, 'Only one offset is generated' ); - my $THE_offset = $offsets->next; - is( $THE_offset->amount * 1, - -5, 'Correct amount was applied against debit' ); - is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" ); - - # Zero offset created when zero outstanding - # (Refund another 5 on paid debt of 20) - $credit1->apply( { debits => [$debit1] } ); - is( $debit1->amountoutstanding + 0, - 0, 'Debit1 amountoutstanding reduced to 0' ); - $reduction = $debit1->reduce($reduce_params); - is( $reduction->amount() * 1, -5, "Reduce amount is -5" ); - is( $reduction->amountoutstanding() * 1, - -5, "Reduce amountoutstanding is -5" ); - - $offsets = Koha::Account::Offsets->search( - { credit_id => $reduction->id, debit_id => $debit1->id } ); - is( $offsets->count, 1, 'Only one new offset is generated' ); - $THE_offset = $offsets->next; - is( $THE_offset->amount * 1, - 0, 'Zero offset created for already paid off debit' ); - is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" ); - - # Compound reduction should not allow more than original amount - # (Reduction of 5 + 5 + 20 > 20) - $reduce_params->{amount} = 20; - throws_ok { - $debit1->reduce($reduce_params); - } - 'Koha::Exceptions::ParameterTooHigh', -'->reduce cannot reduce more than the original amount (combined reductions test)'; - - # Throw exception if attempting to reduce a payout - my $payout = $reduction->payout( - { - interface => 'intranet', - staff_id => $staff->borrowernumber, - branch => $branchcode, - payout_type => 'CASH', - amount => 5 - } - ); - throws_ok { - $payout->reduce($reduce_params); - } - 'Koha::Exceptions::Account::IsNotDebit', - '->reduce() cannot be used on a payout debit'; - - $schema->storage->txn_rollback; -}; - 1; --