feat(deposits): RBI premature closure compliance, simplified to always use the contracted rate

Adds a Closure Reason to premature FD closure (Normal, Premature Extension,
Conversion to Another TD, Deceased Settlement, Nominee/Legal-Heir), persisted
as an RBI audit trail on m_deposit_account_term_and_preclosure. RBI-mandated
reasons auto-waive the pre-closure penalty regardless of the maker's own
checkbox; the maker can also waive it explicitly for any other reason.

Interest rate for a premature closure is always the deposit's own contracted
rate -- the same one its regular scheduled postings use throughout its life
-- rather than a slab re-lookup against the actual period held. An earlier
"actual-period vs contracted" toggle was built, tested, then deliberately
removed per explicit request: simpler, and avoids disagreeing with interest
the account was already accruing all along.

Also fixes a real bug found while testing this: a renewed FD term's premature-
closure interest was including the previous term's closing transaction a
second time (it's dated the same day the new term starts), understating the
first day's balance by the whole prior term's payout. Scoped the interest
calculation to transactions strictly after the current term's start date.

Known open issue: a renewed/auto-renewed account can still show a materially
wrong (sometimes negative) premature-closure interest figure in some cases
(seen on account 10, a QUARTERLY-posting auto-renewed FD) -- root cause not
yet confirmed; needs further investigation with debug instrumentation before
this is considered fully correct for every renewal shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 12:12:40 +05:30
co-authored by Claude Sonnet 5
parent ce8bb7e5ca
commit 11a206f42a
10 changed files with 273 additions and 22 deletions
@@ -674,7 +674,7 @@ public final class FixedDepositAccountData extends DepositAccountData {
public static FixedDepositAccountData preClosureDetails(final Long accountId, BigDecimal maturityAmount,
final Collection<EnumOptionData> onAccountClosureOptions, final Collection<PaymentTypeData> paymentTypeOptions,
final Collection<SavingsAccountData> savingsAccountDatas) {
final Collection<SavingsAccountData> savingsAccountDatas, final BigDecimal effectiveInterestRate) {
final Long groupId = null;
final String groupName = null;
@@ -689,7 +689,12 @@ public final class FixedDepositAccountData extends DepositAccountData {
final SavingsAccountStatusEnumData status = null;
final SavingsAccountApplicationTimelineData timeline = null;
final CurrencyData currency = null;
final BigDecimal nominalAnnualInterestRate = null;
// Not the account's stored contracted rate -- the rate actually resolved for THIS premature
// closure preview (see FixedDepositAccount#getEffectiveInterestRateAsFraction, read back
// straight after the calculation by the caller), so a maker can see which of the RBI-default
// actual-period rate, a bank's "use contracted rate" override, and any penalty deduction ended
// up applying, rather than only the final rupee amount.
final BigDecimal nominalAnnualInterestRate = effectiveInterestRate;
final EnumOptionData interestPeriodType = null;
final EnumOptionData interestPostingPeriodType = null;
final EnumOptionData interestCalculationType = null;
@@ -45,6 +45,7 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
import org.apache.fineract.portfolio.savings.PrematureClosureReasonType;
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
@@ -118,6 +119,20 @@ public class DepositAccountTermAndPreClosure extends AbstractPersistableCustom<L
this.fdAccountNumber = fdAccountNumber;
}
// RBI audit evidence: why THIS closure happened, set once by FixedDepositAccount#prematureClosure
// and never changed afterward -- see PrematureClosureReasonType. Null on an account that was never
// prematurely closed (including one still active), or on one closed before this column existed.
@Column(name = "premature_closure_reason_enum")
private Integer prematureClosureReason;
public PrematureClosureReasonType getPrematureClosureReason() {
return PrematureClosureReasonType.fromInt(this.prematureClosureReason);
}
public void updatePrematureClosureReason(final PrematureClosureReasonType reason) {
this.prematureClosureReason = (reason == null || reason.isInvalid()) ? null : reason.getValue();
}
// Which renewal cycle of one logical FD this account represents: 0 = originally opened account,
// 1 = first renewal, and so on. Lets the UI present "one FD, many terms" over what is physically
// a chain of per-term account rows.