[
https://issues.apache.org/jira/browse/FINERACT-2797?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Josue Araujo updated FINERACT-2797:
-----------------------------------
Description:
## Summary
When "Force Withdrawal" (allow-force-withdrawal-on-savings-account global
config, FINERACT-2471) is used to push a savings account balance below its
minimum/zero threshold, any *subsequent* regular (non-forced) withdrawal on
that same account throws InsufficientAccountBalanceException — even when the
account's current balance is fully sufficient to cover the new withdrawal.
## Root cause
validateAccountBalanceConstraints() replays the ENTIRE transaction history on
every balance-affecting operation to recompute the running balance at each
historical point (needed to correctly validate backdated transactions).
isForceWithdrawalAllowed(isForceWithdrawal, runningBalance) is passed a
single boolean scoped to the CURRENT call, not to the specific historical
transaction being evaluated in the loop:
private boolean isForceWithdrawalAllowed(final boolean isForceWithdrawal,
final Money runningBalance) {
if (!isForceWithdrawal || this.configurationDomainService == null)
{ return false; }
...
}
So when a later *regular* withdrawal is validated (isForceWithdrawal=false
for that call), the loop re-encounters the earlier forced-withdrawal dip in
the account's history, and — since the flag for the CURRENT call is false —
throws, even though that historical dip was legitimately authorized at the
time it happened. The "force" authorization is never persisted on the
transaction itself, only used transiently at write time.
* Steps to reproduce
1. Create a savings account/product: no interest, overdraft NOT allowed.
2. Global config: allow-force-withdrawal-on-savings-account=true,
force-withdrawal-on-savings-account-limit=1000.
3. Deposit 10.00 (balance: 10.00)
4. Force withdrawal 15.00 (balance: -5.00) — succeeds as expected.
5. Deposit 20.00 (balance: 15.00)
6. Deposit 5.00 (balance: 20.00)
7. Regular withdrawal 3.00 (expected balance: 17.00)
-> ACTUAL: throws InsufficientAccountBalanceException.
## Expected behavior
Step 7 should succeed: the account's current balance (20.00) is more than
sufficient for a 3.00 withdrawal. A previously authorized forced-withdrawal
dip should not retroactively block unrelated future regular transactions.
## Environment
- Branch: develop
- Commit introducing the feature: 8c187f9d17fb839f26fc888f34a64f7c1f57a802
(FINERACT-2471)
- Verified still present as of commit c6328ab3062d8e1cf8395c00c40918a1741134ad
(2026-07-27)
## Suggested fix
Persist an is_force_transaction flag on m_savings_account_transaction at
creation time, and check that per-transaction flag (or the current
transaction's flag) inside the validateAccountBalanceConstraints loop
instead of relying solely on the call-scoped isForceWithdrawal boolean.
Draft PR to follow.
was:
## Summary
When "Force Withdrawal" (allow-force-withdrawal-on-savings-account global
config, FINERACT-2471) is used to push a savings account balance below its
minimum/zero threshold, any *subsequent* regular (non-forced) withdrawal on
that same account throws InsufficientAccountBalanceException — even when the
account's current balance is fully sufficient to cover the new withdrawal.
## Root cause
validateAccountBalanceConstraints() replays the ENTIRE transaction history on
every balance-affecting operation to recompute the running balance at each
historical point (needed to correctly validate backdated transactions).
isForceWithdrawalAllowed(isForceWithdrawal, runningBalance) is passed a
single boolean scoped to the CURRENT call, not to the specific historical
transaction being evaluated in the loop:
private boolean isForceWithdrawalAllowed(final boolean isForceWithdrawal,
final Money runningBalance) {
if (!isForceWithdrawal || this.configurationDomainService == null) {
return false;
}
...
}
So when a later *regular* withdrawal is validated (isForceWithdrawal=false
for that call), the loop re-encounters the earlier forced-withdrawal dip in
the account's history, and — since the flag for the CURRENT call is false —
throws, even though that historical dip was legitimately authorized at the
time it happened. The "force" authorization is never persisted on the
transaction itself, only used transiently at write time.
## Steps to reproduce
1. Create a savings account/product: no interest, overdraft NOT allowed.
2. Global config: allow-force-withdrawal-on-savings-account=true,
force-withdrawal-on-savings-account-limit=1000.
3. Deposit 10.00 (balance: 10.00)
4. Force withdrawal 15.00 (balance: -5.00) — succeeds as expected.
5. Deposit 20.00 (balance: 15.00)
6. Deposit 5.00 (balance: 20.00)
7. Regular withdrawal 3.00 (expected balance: 17.00)
-> ACTUAL: throws InsufficientAccountBalanceException.
## Expected behavior
Step 7 should succeed: the account's current balance (20.00) is more than
sufficient for a 3.00 withdrawal. A previously authorized forced-withdrawal
dip should not retroactively block unrelated future regular transactions.
## Environment
- Branch: develop
- Commit introducing the feature: 8c187f9d17fb839f26fc888f34a64f7c1f57a802
(FINERACT-2471)
- Verified still present as of commit c6328ab3062d8e1cf8395c00c40918a1741134ad
(2026-07-27)
## Suggested fix
Persist an is_force_transaction flag on m_savings_account_transaction at
creation time, and check that per-transaction flag (or the current
transaction's flag) inside the validateAccountBalanceConstraints loop
instead of relying solely on the call-scoped isForceWithdrawal boolean.
Draft PR to follow.
> Force withdrawal balance validation fails on subsequent regular transactions
> ----------------------------------------------------------------------------
>
> Key: FINERACT-2797
> URL: https://issues.apache.org/jira/browse/FINERACT-2797
> Project: Apache Fineract
> Issue Type: Bug
> Reporter: Josue Araujo
> Priority: Major
>
> ## Summary
> When "Force Withdrawal" (allow-force-withdrawal-on-savings-account global
> config, FINERACT-2471) is used to push a savings account balance below its
> minimum/zero threshold, any *subsequent* regular (non-forced) withdrawal on
> that same account throws InsufficientAccountBalanceException — even when the
> account's current balance is fully sufficient to cover the new withdrawal.
> ## Root cause
> validateAccountBalanceConstraints() replays the ENTIRE transaction history on
> every balance-affecting operation to recompute the running balance at each
> historical point (needed to correctly validate backdated transactions).
> isForceWithdrawalAllowed(isForceWithdrawal, runningBalance) is passed a
> single boolean scoped to the CURRENT call, not to the specific historical
> transaction being evaluated in the loop:
> private boolean isForceWithdrawalAllowed(final boolean isForceWithdrawal,
> final Money runningBalance) {
> if (!isForceWithdrawal || this.configurationDomainService == null)
> { return false; }
> ...
> }
> So when a later *regular* withdrawal is validated (isForceWithdrawal=false
> for that call), the loop re-encounters the earlier forced-withdrawal dip in
> the account's history, and — since the flag for the CURRENT call is false —
> throws, even though that historical dip was legitimately authorized at the
> time it happened. The "force" authorization is never persisted on the
> transaction itself, only used transiently at write time.
> * Steps to reproduce
> 1. Create a savings account/product: no interest, overdraft NOT allowed.
> 2. Global config: allow-force-withdrawal-on-savings-account=true,
> force-withdrawal-on-savings-account-limit=1000.
> 3. Deposit 10.00 (balance: 10.00)
> 4. Force withdrawal 15.00 (balance: -5.00) — succeeds as expected.
> 5. Deposit 20.00 (balance: 15.00)
> 6. Deposit 5.00 (balance: 20.00)
> 7. Regular withdrawal 3.00 (expected balance: 17.00)
> -> ACTUAL: throws InsufficientAccountBalanceException.
> ## Expected behavior
> Step 7 should succeed: the account's current balance (20.00) is more than
> sufficient for a 3.00 withdrawal. A previously authorized forced-withdrawal
> dip should not retroactively block unrelated future regular transactions.
> ## Environment
> - Branch: develop
> - Commit introducing the feature: 8c187f9d17fb839f26fc888f34a64f7c1f57a802
> (FINERACT-2471)
> - Verified still present as of commit
> c6328ab3062d8e1cf8395c00c40918a1741134ad
> (2026-07-27)
> ## Suggested fix
> Persist an is_force_transaction flag on m_savings_account_transaction at
> creation time, and check that per-transaction flag (or the current
> transaction's flag) inside the validateAccountBalanceConstraints loop
> instead of relying solely on the call-scoped isForceWithdrawal boolean.
> Draft PR to follow.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)