Ralph Hopman created FINERACT-2852:
--------------------------------------
Summary: Savings amount holds can cause incorrect scheduled
interest calculation
Key: FINERACT-2852
URL: https://issues.apache.org/jira/browse/FINERACT-2852
Project: Apache Fineract
Issue Type: Bug
Components: Savings
Affects Versions: 1.15.0, 1.14.0
Reporter: Ralph Hopman
Assignee: Ralph Hopman
Fix For: 1.16.0
h2. Description
Savings amount holds can incorrectly affect the balance used by the
{*}scheduled savings interest calculation{*}.
An amount hold should reduce the available balance but should not change the
account's ledger/book balance. However, the savings transaction
{{runningBalance}} currently includes holds and releases.
The scheduled interest calculation operates on {{SavingsAccountData}} /
{{SavingsAccountTransactionData}} DTOs and recalculates these running balances.
In certain scenarios it then uses the resulting {{runningBalance}} directly as
the end-of-day balance for interest calculation.
As a result, when a normal deposit or withdrawal occurs while an amount is on
hold, the held amount can incorrectly affect the principal on which interest is
calculated.
h2. Example
Consider the following transaction history:
{code:java}
01 Jan: Deposit 1,000
10 Jan: Hold 200
15 Jan: Withdrawal 100
20 Jan: Release 200
{code}
The correct ledger/book balances are:
{code:java}
01 Jan: 1000
10 Jan: 1000 (available balance = 800)
15 Jan: 900 (available balance = 700)
20 Jan: 900
{code}
However, the transaction running balances are recalculated approximately as:
{code:java}
Deposit +1000 runningBalance = 1000
Amount Hold -200 runningBalance = 800
Withdrawal -100 runningBalance = 700
Amount Release +200 runningBalance = 900
{code}
The {{SavingsAccountTransactionData.toEndOfDayBalance()}} implementation uses
{{runningBalance}} directly for ordinary withdrawals:
{code:java}
if (isWithdrawal()) {
endOfDayBalance = Money.of(currency, this.runningBalance);
}
{code}
Therefore, after the withdrawal on 15 Jan the scheduled interest calculation
can use:
{code:java}
endOfDayBalance = 700
{code}
instead of the correct ledger balance:
{code:java}
endOfDayBalance = 900
{code}
The held 200 is therefore incorrectly excluded from the balance used for
interest calculation.
h2. Why the Hold Transaction Alone Does Not Expose the Problem
The hold transaction itself is not treated as a deposit or withdrawal when
converting transactions to end-of-day balances. Therefore, a hold without any
subsequent ledger-changing transaction may leave the interest-bearing balance
unchanged.
The incorrect balance becomes observable when a later transaction, such as a
withdrawal or deposit, uses the hold-adjusted {{runningBalance}} as the new
end-of-day balance.
This also means that releasing the hold does not necessarily repair the
interest balance, because {{AMOUNT_RELEASE}} itself is correctly not considered
a ledger credit.
h2. Scheduled Interest Path
The scheduled interest job uses the DTO-based calculation path:
{code:java}
SavingsSchedularInterestPoster
-> SavingsAccountWritePlatformService.postInterest(SavingsAccountData, ...)
-> SavingsAccountInterestPostingServiceImpl
-> recalculateDailyBalances(...)
-> PostingPeriod.createFromDTO(...)
-> SavingsAccountTransactionData.toEndOfDayBalance(...)
-> EndOfDayBalance
-> interest calculation
{code}
During {{{}recalculateDailyBalances(){}}}, holds and releases explicitly change
the DTO running balance:
{code:java}
if (transaction.isCredit() || transaction.isAmountRelease()) {
transactionAmount = transactionAmount.plus(transaction.getAmount());
} else if (transaction.isDebit() || transaction.isAmountOnHold()) {
transactionAmount = transactionAmount.minus(transaction.getAmount());
}
runningBalance = runningBalance.plus(transactionAmount);
transaction.updateRunningBalance(runningBalance);
{code}
The resulting value can subsequently be used as the interest-bearing end-of-day
balance.
h2. Inconsistent Behavior Between Interest Calculation Paths
The entity-based interest calculation reconstructs balances from the previous
ledger balance and transaction amount.
Conceptually:
{code:java}
Deposit -> openingBalance + amount
Hold -> no balance change
Withdrawal -> openingBalance - amount
Release -> no balance change
{code}
For the example above, this produces the correct ledger balance of 900 after
the withdrawal.
The DTO-based scheduled interest calculation can instead use the hold-adjusted
{{runningBalance}} and produce 700.
This creates a risk that manual/entity-based interest posting and
scheduled/DTO-based interest posting produce different results for the same
account history.
h2. Overdraft Accounts
The DTO-based interest service also classifies transactions as positive-balance
or overdraft transactions based directly on
{{{}transaction.getRunningBalance(){}}}.
A hold can therefore make the derived running balance negative even while the
ledger balance remains positive.
Example:
{code:java}
Ledger balance before hold: 100
Hold: 80
Available balance: 20
Withdrawal: 30
Ledger balance after withdrawal: 70
Available balance: -10
Derived runningBalance: -10
{code}
This may incorrectly classify a positive ledger balance as an overdraft balance
for interest calculation.
h2. Expected Behavior
Savings interest must be calculated using the ledger/book balance. Amount holds
and releases affect funds availability but must not change the balance used for
interest calculation.
For the example above, interest after 15 Jan should be calculated on 900, not
700.
h2. Acceptance Criteria
* {{AMOUNT_HOLD}} and {{AMOUNT_RELEASE}} do not affect the ledger balance used
for interest calculation.
* A withdrawal made while a hold is active is applied to the ledger balance,
not to the available balance.
* A deposit made while a hold is active is applied to the ledger balance, not
to the available balance.
* Releasing a hold does not change the interest-bearing balance.
* Scheduled and manual interest posting produce identical results for the same
transaction history.
* Positive ledger balances are not classified as overdraft balances solely
because an active hold makes the available balance negative.
* Regression tests cover at least:
** deposit -> hold -> interest posting
** deposit -> hold -> withdrawal -> interest posting
** deposit -> hold -> deposit -> interest posting
** deposit -> hold -> withdrawal -> release -> interest posting
** a hold which causes available balance, but not ledger balance, to become
negative
* Tests verify both end-of-day balances and the final posted interest amount.
h2. Suggested Regression Scenario
A deterministic regression test could use:
{code:java}
Nominal annual interest rate: 36.5%
Interest calculation: daily balance
Days in year: 365
Interest posting: monthly
01 Jan: Deposit 1000
10 Jan: Hold 200
15 Jan: Withdrawal 100
20 Jan: Release 200
{code}
With a 36.5% annual rate, the daily rate is 0.1%, which makes differences in
the expected interest easy to verify.
The ledger balance used for interest should be:
{code:java}
01-14 Jan: 1000, except for the exact account-activation/date semantics of the
test
15 Jan onward: 900
{code}
The hold and release should not introduce an 800 or 700 ledger balance into the
interest calculation.
h2. Related Issue
Related to FINERACT-2851, describing that savings {{AMOUNT_HOLD}} and
{{AMOUNT_RELEASE}} transactions incorrectly affect transaction
{{{}runningBalance{}}}.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)