From 4499b8afad3f61356d8b96d8457d9274099e19dd Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Thu, 1 Oct 2026 12:12:51 +0530 Subject: [PATCH] fix(accounting): post GL entries for a negative (clawback) interest transaction A premature FD closure that reprices interest downward (e.g. a pre-closure penalty) posts the difference as a negative interest-posting transaction -- a clawback of interest already recognised in earlier periods. Both the cash- and accrual-based accounting processors only posted journal entries when the transaction amount was greater than zero, so a clawback updated the FD subledger but silently posted nothing to the General Ledger, leaving the two out of sync. Treats a negative amount as a mirror-image reversal: positive magnitude, debit/credit legs swapped -- the same mechanism already used for isReversal. Co-Authored-By: Claude Sonnet 5 --- ...ualBasedAccountingProcessorForSavings.java | 21 +++++++++++++++---- ...ashBasedAccountingProcessorForSavings.java | 19 +++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java index 748daf5d0..5092daed6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java @@ -173,12 +173,25 @@ public class AccrualBasedAccountingProcessorForSavings implements AccountingProc } else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { - // Post journal entry if earned interest amount is greater than - // zero - if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { + // A premature FD closure that reprices interest downward (see + // FixedDepositAccount#postPreMaturityInterest) posts the difference as a NEGATIVE + // interest-posting transaction -- a clawback of interest already recognised in earlier + // periods, not "zero interest, nothing to post". Skipping it here (the original + // "amount > 0" guard) left the FD subledger showing the clawback while the GL never + // recorded it, silently going out of sync. Treating a negative amount as a mirror-image + // reversal -- positive magnitude, debit/credit legs swapped, same mechanism + // createCashBasedJournalEntriesAndReversalsForSavings already uses for isReversal -- keeps + // the two in sync instead. See CashBasedAccountingProcessorForSavings for the same fix on + // the cash-based path. + final int amountSign = savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO); + if (amountSign != 0) { + final boolean isClawback = amountSign < 0; + final BigDecimal postingAmount = isClawback ? amount.abs() : amount; + final boolean effectiveIsReversal = isReversal ^ isClawback; this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, postingAmount, + effectiveIsReversal); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java index d6a6034e4..1ca1ad249 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java @@ -167,15 +167,26 @@ public class CashBasedAccountingProcessorForSavings implements AccountingProcess } else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { - // Post journal entry if earned interest amount is greater than - // zero - if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { + // A premature FD closure that reprices interest downward (see + // FixedDepositAccount#postPreMaturityInterest) posts the difference as a NEGATIVE + // interest-posting transaction -- a clawback of interest already recognised in earlier + // periods, not "zero interest, nothing to post". Skipping it here (the original + // "amount > 0" guard) left the FD subledger showing the clawback while the GL never + // recorded it, silently going out of sync. Treating a negative amount as a mirror-image + // reversal -- positive magnitude, debit/credit legs swapped, same mechanism + // createInterestPostingJournalEntriesForSavings already uses for isReversal -- keeps the two + // in sync instead. + final int amountSign = savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO); + if (amountSign != 0) { + final boolean isClawback = amountSign < 0; + final BigDecimal postingAmount = isClawback ? amount.abs() : amount; + final boolean effectiveIsReversal = isReversal ^ isClawback; // Routed through the payable and paid accounts the product names, so the society // can read interest declared but not yet credited as its own balance. Collapses // to the plain Dr Interest On Savings / Cr Savings Control pair for a product // that names neither. this.helper.createInterestPostingJournalEntriesForSavings(office, currencyCode, savingsProductId, paymentTypeId, - savingsId, transactionId, transactionDate, amount, isReversal); + savingsId, transactionId, transactionDate, postingAmount, effectiveIsReversal); } }