diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index 2ce19027e..788177150 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1512,6 +1512,23 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo if (depositAccountType.isFixedDeposit()) { ((FixedDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); FixedDepositAccount fdAccount = ((FixedDepositAccount) account); + + // Post the interest transaction(s) updateMaturityStatus() just created BEFORE any maturity + // closure below runs. The closure's own withdrawal (handleWithdrawal -> postJournalEntries) + // snapshots "existing" transactions the moment IT starts, which by then already includes + // this interest posting -- so its own posting treats the interest as already accounted for + // and skips it, while this method's own final postJournalEntries() call (previously relying + // on the interest staying out of existingTransactionIds so it would post there instead) + // never actually saw it get posted either: for an FD whose entire term is a single posting + // period -- interest posts and the account matures in this same run -- that left the + // interest transaction with a real ledger balance but no matching GL entry at all, forever. + // Posting it now, while it is still the only new thing on the account, gives it exactly one + // correct journal entry, and folding it into existingTransactionIds below (the same way + // markWithdrawalsAsAlreadyPosted() already does for the withdrawal) keeps the closure branch + // and the final call from ever trying to post it again. + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); // handle maturity instructions if (fdAccount.isMatured() && (fdAccount.isReinvestOnClosure() || fdAccount.isTransferToSavingsOnClosure())) { @@ -1524,11 +1541,9 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo // Every maturity-instruction branch takes the balance out of this account through // SavingsAccountDomainServiceJpa#handleWithdrawal, which posts that withdrawal's journal - // entries itself. existingTransactionIds was snapshotted at the top of this method -- - // before that withdrawal existed -- so the postJournalEntries() call at the end would - // treat it as new and post it a second time. Interest postings created by - // updateMaturityStatus() above are still genuinely unposted at this point and must stay - // out of the snapshot so that final call can post them exactly once. + // entries itself. existingTransactionIds was refreshed just above (after the interest was + // posted) -- before that withdrawal existed -- so the postJournalEntries() call at the end + // would treat it as new and post it a second time without this. markWithdrawalsAsAlreadyPosted(account, existingTransactionIds); if (changes.get("reinvestedDepositId") != null) {