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 a992682a9..5f4b58c09 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 @@ -1704,9 +1704,11 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo /** * Read-only preview of what a Renew from the FD Dashboard is about to do, shown in the Payment * dialog before the staff member confirms anything -- the actual current balance, the linked - * account's rate, the gap-day count, the computed gap-interest amount, and the balance the FD will - * carry into its new term once that interest lands. Mirrors {@link #creditGapInterest} exactly but - * never posts anything; a within-grace account gets zero for the gap fields since nothing is owed. + * account's rate, the gap-day count, and the computed gap-interest amount. The gap interest is paid + * onto the LINKED savings account, not folded into the FD (see {@link #creditGapInterest}), so + * projectedBalance is just the FD's own current balance, unchanged by it -- what actually renews. + * Mirrors {@link #creditGapInterest} exactly but never posts anything; a within-grace account gets + * zero for the gap fields since nothing is owed. */ @Transactional(readOnly = true) @Override @@ -1750,12 +1752,15 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo result.put("projectedBalance", null); return result; } + result.put("linkedAccountNo", resolveLinkedAccountNo(linkedAccountId)); final BigDecimal linkedAccountRate = resolveLinkedAccountRate(linkedAccountId); final BigDecimal gapInterestAmount = computeGapInterestAmount(fdAccount, linkedAccountRate, gapDays); result.put("linkedAccountRate", linkedAccountRate); result.put("gapInterestAmount", gapInterestAmount); - result.put("projectedBalance", currentBalance.add(gapInterestAmount)); + // Paid onto the linked savings account, not this FD -- what renews is still just the FD's own + // current balance. + result.put("projectedBalance", currentBalance); return result; } @@ -1790,6 +1795,13 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo """, BigDecimal.class, linkedAccountId); } + /** Read fresh off the target account itself, same reasoning as {@link #resolveLinkedAccountId} -- + * shown in the Renewal Preview so staff can see exactly which account the gap interest is about + * to land on, not just that "a linked account" exists. */ + private String resolveLinkedAccountNo(final Long linkedAccountId) { + return this.jdbcTemplate.queryForObject("SELECT account_no FROM m_savings_account WHERE id = ?", String.class, linkedAccountId); + } + /** Simple interest on the FD's own current balance, for the stretch between its maturity date and * today, at the linked account's product rate -- the pure calculation, shared by the posting path * ({@link #creditGapInterest}) and the read-only preview ({@link #previewFdRenewal}) so the number @@ -1805,23 +1817,30 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo /** * Simple interest for the stretch between the FD's own maturity date and today, at the rate of its - * linked Savings/Current account's product -- credited onto the FD itself as a genuine - * INTEREST_POSTING transaction (not a DEPOSIT, which would wrongly count as cash received in the - * cashier/vault reconciliation) before whatever the caller does next (a manual renewal, or the FD - * Dashboard's generic Payment command) reads the account's balance. Shared by - * {@link #renewMaturedFixedDeposit} and {@link #creditGapInterestIfPastGrace} -- the FD Dashboard's - * Renew action now calls the latter as a pre-step before submitting the same generic `payment` - * command the account's own Account Actions menu already uses, so every closure type (Transfer to - * Savings, both Reinvest options, Partial Reinvest) is available from the Dashboard too, not just the - * two this method's sibling endpoint originally supported. + * linked Savings account's product -- credited onto the LINKED SAVINGS ACCOUNT, as a genuine + * INTEREST_POSTING transaction there (not a DEPOSIT, which would wrongly count as cash received in + * the cashier/vault reconciliation), rather than onto the FD itself. This interest was earned at + * the savings account's rate, not the FD's, so it belongs on that account's own ledger -- folding + * it into the FD's balance instead would mean "Re-Invest Maturity Amount" silently carries savings + * interest into the new FD term as if it were FD interest, and "Re-Invest Principal Only" would pay + * it out through the FD's own closure math instead of it already being where it belongs. The FD's + * own balance is left untouched by this call, so whatever the caller does next (a manual renewal, or + * the FD Dashboard's generic Payment command) reads the FD's real maturity balance, with the gap + * interest already sitting separately in the linked account. Shared by {@link #renewMaturedFixedDeposit} + * and {@link #creditGapInterestIfPastGrace} -- the FD Dashboard's Renew action now calls the latter as + * a pre-step before submitting the same generic `payment` command the account's own Account Actions + * menu already uses, so every closure type (Transfer to Savings, both Reinvest options, Partial + * Reinvest) is available from the Dashboard too, not just the two this method's sibling endpoint + * originally supported. */ private BigDecimal creditGapInterest(final FixedDepositAccount fdAccount, final LocalDate maturityDate, final LocalDate today) { final Long linkedAccountId = resolveLinkedAccountId(fdAccount.getId()); if (linkedAccountId == null) { throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.renewal.linked.account.required", - "This FD's grace period has passed and it has no Deposit Account Link -- link a Savings/Current account to it " - + "before renewing late, so the gap-period interest has a rate to use."); + "This FD's grace period has passed and it has no Deposit Account Link -- link a Savings account to it " + + "before renewing late, so the gap-period interest has somewhere to be paid."); } + final SavingsAccount linkedAccount = this.depositAccountAssembler.assembleFrom(linkedAccountId, DepositAccountType.SAVINGS_DEPOSIT); final BigDecimal linkedAccountRate = resolveLinkedAccountRate(linkedAccountId); final long gapDays = ChronoUnit.DAYS.between(maturityDate, today); final BigDecimal gapInterestAmount = computeGapInterestAmount(fdAccount, linkedAccountRate, gapDays); @@ -1829,14 +1848,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo if (gapInterestAmount.compareTo(BigDecimal.ZERO) > 0) { final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(fdAccount, existingTransactionIds, existingReversedTransactionIds); + updateExistingTransactionsDetails(linkedAccount, existingTransactionIds, existingReversedTransactionIds); - final SavingsAccountTransaction gapInterestTxn = SavingsAccountTransaction.interestPosting(fdAccount, fdAccount.office(), - today, Money.of(fdAccount.getCurrency(), gapInterestAmount), true); - fdAccount.addTransaction(gapInterestTxn); - fdAccount.updateSavingsAccountSummary(fdAccount.getTransactions()); - this.savingAccountRepositoryWrapper.saveAndFlush(fdAccount); - postJournalEntries(fdAccount, existingTransactionIds, existingReversedTransactionIds); + final SavingsAccountTransaction gapInterestTxn = SavingsAccountTransaction.interestPosting(linkedAccount, + linkedAccount.office(), today, Money.of(linkedAccount.getCurrency(), gapInterestAmount), true); + linkedAccount.addTransaction(gapInterestTxn); + linkedAccount.updateSavingsAccountSummary(linkedAccount.getTransactions()); + this.savingAccountRepositoryWrapper.saveAndFlush(linkedAccount); + postJournalEntries(linkedAccount, existingTransactionIds, existingReversedTransactionIds); } return gapInterestAmount; } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformService.java index b588f565e..3c18db4df 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformService.java @@ -105,12 +105,15 @@ public interface DepositAccountWritePlatformService { Map renewMaturedFixedDeposit(Long fdAccountId, Integer reinvestType); /** - * Credits gap-period interest (simple interest, at the linked Savings/Current account's product - * rate) onto a matured FD if its product's renewal grace window has passed -- a no-op, returning - * zero, if still within it. Called as a pre-step before the FD Dashboard's Renew action submits the - * generic `payment` command (the same one the account's own Account Actions menu uses), so every - * closure type is available from the Dashboard, not just the two {@link #renewMaturedFixedDeposit} - * itself supports. Returns {@code withinGrace} (boolean) and {@code gapInterestAmount} (BigDecimal). + * Credits gap-period interest (simple interest, at the linked Savings account's product rate) onto + * that LINKED SAVINGS ACCOUNT -- not the FD -- if the FD's product's renewal grace window has + * passed; a no-op, returning zero, if still within it. Paid to the linked account because it was + * earned at that account's rate, not the FD's own; the FD's own balance is left untouched, so it + * renews from its real maturity amount regardless of which closure type follows. Called as a + * pre-step before the FD Dashboard's Renew action submits the generic `payment` command (the same + * one the account's own Account Actions menu uses), so every closure type is available from the + * Dashboard, not just the two {@link #renewMaturedFixedDeposit} itself supports. Returns + * {@code withinGrace} (boolean) and {@code gapInterestAmount} (BigDecimal). */ Map creditGapInterestIfPastGrace(Long fdAccountId); @@ -118,8 +121,10 @@ public interface DepositAccountWritePlatformService { * Read-only preview of what renewing this matured FD right now would do -- shown in the FD * Dashboard's Payment dialog before staff confirm anything, never posts anything itself. Returns * isMatured (false short-circuits the rest), maturityDate, renewalGracePeriodDays, withinGrace, - * currentBalance, gapDays, and -- only once past grace -- linkedAccountId, linkedAccountRate, - * gapInterestAmount and projectedBalance (the balance the new term would start from). + * currentBalance, gapDays, and -- only once past grace -- linkedAccountId, linkedAccountNo, + * linkedAccountRate, gapInterestAmount (paid to the linked account, not this FD) and + * projectedBalance (the FD's own current balance, unchanged by the gap interest -- what actually + * renews). */ Map previewFdRenewal(Long fdAccountId);