diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java index 03c685961..2ca00fc8c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java @@ -213,6 +213,14 @@ public final class DepositsApiConstants { public static final String transferDescriptionParamName = "transferDescription"; public static final String toSavingsAccountIdParamName = "toSavingsAccountId"; public static final String reinvestAmountParamName = "reinvestAmount"; + // Lets a maker waive the product's configured pre-closure penalty for one premature closure -- + // see FixedDepositAccount#waivePreClosurePenaltyForThisRequest for why this is a transient + // per-request flag rather than a stored account field. + public static final String waivePreClosurePenaltyParamName = "waivePreClosurePenalty"; + // Why the deposit was closed before maturity -- see PrematureClosureReasonType. Persisted (unlike + // waivePreClosurePenaltyParamName above) as the RBI audit trail of which closures waived their + // penalty and why. + public static final String prematureClosureReasonIdParamName = "prematureClosureReasonId"; public static final String savingsAccounts = "savingsAccounts"; public static final String maturityInstructionIdParamName = "maturityInstructionId"; public static final String transferToSavingsIdParamName = "transferToSavingsId"; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/PrematureClosureReasonType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/PrematureClosureReasonType.java new file mode 100644 index 000000000..4c6708970 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/PrematureClosureReasonType.java @@ -0,0 +1,70 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings; + +import lombok.Getter; + +/** + * Why a Fixed/Recurring Deposit was closed before maturity -- captured on every premature closure, not + * just the ones that waive the penalty, so the audit trail always shows a reason rather than only + * recording one when it happens to matter. {@link #isAutoWaived()} is the RBI-driven business rule this + * type exists to encode: a maker cannot leave the platform's own penalty-waiver decision to memory or a + * free-text note for these four reasons -- picking the reason IS the waiver decision, enforced on the + * server regardless of what a client happened to also send for the plain waive-penalty flag. + */ +@Getter +public enum PrematureClosureReasonType { + + INVALID(0, "prematureClosureReasonType.invalid", false), // + NORMAL(100, "prematureClosureReasonType.normal", false), // + PREMATURE_EXTENSION(200, "prematureClosureReasonType.prematureExtension", true), // + CONVERSION_TO_ANOTHER_TD(300, "prematureClosureReasonType.conversionToAnotherTd", true), // + DECEASED_SETTLEMENT(400, "prematureClosureReasonType.deceasedSettlement", true), // + NOMINEE_LEGAL_HEIR_CLOSURE(500, "prematureClosureReasonType.nomineeLegalHeirClosure", true); // + + private final Integer value; + private final String code; + /** True when this reason, by itself, waives the pre-closure penalty regardless of any manual + * waive-penalty flag also sent -- see the class javadoc. */ + private final boolean autoWaived; + + PrematureClosureReasonType(final Integer value, final String code, final boolean autoWaived) { + this.value = value; + this.code = code; + this.autoWaived = autoWaived; + } + + public static PrematureClosureReasonType fromInt(final Integer v) { + if (v == null) { + return INVALID; + } + return switch (v) { + case 100 -> NORMAL; + case 200 -> PREMATURE_EXTENSION; + case 300 -> CONVERSION_TO_ANOTHER_TD; + case 400 -> DECEASED_SETTLEMENT; + case 500 -> NOMINEE_LEGAL_HEIR_CLOSURE; + default -> INVALID; + }; + } + + public boolean isInvalid() { + return this.equals(INVALID); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java index 841776e50..2aeaa146d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java @@ -77,10 +77,12 @@ public class DepositAccountTransactionDataValidator { Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName, DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, - bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName)); + bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName, + DepositsApiConstants.waivePreClosurePenaltyParamName, DepositsApiConstants.prematureClosureReasonIdParamName)); - private static final Set DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName)); + private static final Set DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList( + DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName, + DepositsApiConstants.waivePreClosurePenaltyParamName, DepositsApiConstants.prematureClosureReasonIdParamName)); private static final Set DEPOSIT_ACCOUNT_PAYMENT_REQUEST_DATA_PARAMETERS = new HashSet<>( Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java index acdd50fdc..6e4102a71 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java @@ -19,9 +19,11 @@ package org.apache.fineract.portfolio.savings.domain; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.prematureClosureReasonIdParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.reinvestAmountParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferDescriptionParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.waivePreClosurePenaltyParamName; import java.math.BigDecimal; import java.math.MathContext; @@ -65,6 +67,7 @@ import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.DepositsApiConstants; +import org.apache.fineract.portfolio.savings.PrematureClosureReasonType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType; import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; @@ -812,6 +815,18 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); Long savingsTransactionId = null; + // Maker's own "waive penalty" choice on the Premature Close dialog, not the product's stored + // preClosurePenalApplicable config -- this is a one-time override for this closure only, read + // fresh here since the preview and the actual submit are two separate requests. + final boolean waivePreClosurePenalty = command.hasParameter(waivePreClosurePenaltyParamName) + && command.booleanPrimitiveValueOfParameterNamed(waivePreClosurePenaltyParamName); + // RBI-mandated waiver reasons waive the penalty regardless of the manual checkbox above -- see + // PrematureClosureReasonType. Enforced here (not just trusted from the client) so the actual + // closure always matches what the preview showed for the same reason. + final PrematureClosureReasonType prematureClosureReason = PrematureClosureReasonType + .fromInt(command.integerValueOfParameterNamed(prematureClosureReasonIdParamName)); + account.setWaivePreClosurePenaltyForThisRequest(waivePreClosurePenalty || prematureClosureReason.isAutoWaived()); + // post interest account.postPreMaturityInterest(closedDate, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index d1bbdc50c..788ade745 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -58,7 +58,7 @@ import org.apache.fineract.portfolio.interestratechart.service.InterestRateChart import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.DepositsApiConstants; -import org.apache.fineract.portfolio.savings.PreClosurePenalInterestOnType; +import org.apache.fineract.portfolio.savings.PrematureClosureReasonType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType; @@ -84,6 +84,16 @@ public class FixedDepositAccount extends SavingsAccount { protected InterestRateChartAssembler chartAssembler; @Transient private ConfigurationDomainService configurationDomainService; + /** + * Set once per request, immediately before a premature-closure preview or the real closure, from the + * maker's own "waive penalty" checkbox on the Premature Close dialog -- never persisted, since it is + * a one-time decision for this specific closure rather than a product/account setting. The product's + * own {@code accountTermAndPreClosure.isPreClosurePenalApplicable()} configuration is untouched, so + * every other premature closure of this same product still applies its penalty as normal; this only + * suppresses it for the one request that explicitly asked to waive it. + */ + @Transient + private boolean waivePreClosurePenaltyForThisRequest; protected FixedDepositAccount() { // @@ -177,18 +187,17 @@ public class FixedDepositAccount extends SavingsAccount { if (this.chart != null) { boolean applyPreMaturePenalty = false; BigDecimal penalInterest = BigDecimal.ZERO; - LocalDate depositCloseDate = calculateMaturityDate(); + // Rate-selection date is always calculateMaturityDate() below -- premature closure prices off + // the deposit's own CONTRACTED rate, the same rate the FD was created with and the same one + // every regular scheduled interest posting this account ever makes (see this method's 2-arg + // overload), so a closure's freshly-recomputed total always matches what the account was + // already accruing all along. No separate "actual period held" slab lookup -- deliberately + // simple, one rate for the account's whole life. + final LocalDate depositCloseDate = calculateMaturityDate(); if (isPreMatureClosure) { - if (this.accountTermAndPreClosure.isPreClosurePenalApplicable()) { + if (this.accountTermAndPreClosure.isPreClosurePenalApplicable() && !this.waivePreClosurePenaltyForThisRequest) { applyPreMaturePenalty = true; penalInterest = this.accountTermAndPreClosure.depositPreClosureDetail().preClosurePenalInterest(); - final PreClosurePenalInterestOnType preClosurePenalInterestOnType = this.accountTermAndPreClosure - .depositPreClosureDetail().preClosurePenalInterestOnType(); - if (preClosurePenalInterestOnType.isWholeTerm()) { - depositCloseDate = interestCalculatedUpto(); - } else if (preClosurePenalInterestOnType.isTillPrematureWithdrawal()) { - depositCloseDate = interestPostingUpToDate; - } } } @@ -353,7 +362,7 @@ public class FixedDepositAccount extends SavingsAccount { boolean isInterestTransfer = false; final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); List savingsAccountTransactionDetailsForPostingPeriodList = toSavingsAccountTransactionDetailsForPostingPeriodList( - transactions); + transactionsForCurrentTermInterestCalculation(transactions)); for (final LocalDateInterval periodInterval : postingPeriodIntervals) { boolean isUserPosting = false; if (postedAsOnTransactionDates.contains(periodInterval.endDate())) { @@ -439,7 +448,7 @@ public class FixedDepositAccount extends SavingsAccount { final boolean isInterestTransfer = false; final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); final List savingsAccountTransactionDetailsForPostingPeriodList = toSavingsAccountTransactionDetailsForPostingPeriodList( - transactions); + transactionsForCurrentTermInterestCalculation(transactions)); for (final LocalDateInterval periodInterval : postingPeriodIntervals) { final boolean isUserPosting = postedAsOnTransactionDates.contains(periodInterval.endDate()); @@ -525,6 +534,12 @@ public class FixedDepositAccount extends SavingsAccount { final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); + // RBI audit trail: record why, not just that, this closure happened -- see + // PrematureClosureReasonType. Absent entirely on a request from before this field existed, + // which resolves to INVALID/null here rather than a misleading "Normal". + final Integer prematureClosureReasonId = command.integerValueOfParameterNamed(DepositsApiConstants.prematureClosureReasonIdParamName); + this.accountTermAndPreClosure.updatePrematureClosureReason(PrematureClosureReasonType.fromInt(prematureClosureReasonId)); + /* * // withdraw deposit amount before closing the account final Money transactionAmountMoney = * Money.of(this.currency, this.getAccountBalance()); final SavingsAccountTransaction withdraw = @@ -789,6 +804,13 @@ public class FixedDepositAccount extends SavingsAccount { } + /** See {@link #waivePreClosurePenaltyForThisRequest}. Call before {@link #calculatePreMatureAmount} (preview) + * or {@link #postPreMaturityInterest} (the real closure) -- both read the flag at the same point in the + * same interest calculation this waives a part of. */ + public void setWaivePreClosurePenaltyForThisRequest(final boolean waive) { + this.waivePreClosurePenaltyForThisRequest = waive; + } + public BigDecimal calculatePreMatureAmount(final LocalDate preMatureDate, final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { @@ -909,10 +931,26 @@ public class FixedDepositAccount extends SavingsAccount { return this.product instanceof FixedDepositProduct ? ((FixedDepositProduct) this.product).getFdType() : null; } + /** + * Interest already posted for the term currently running, NOT the account's whole lifetime. + *

+ * A renewed account keeps every prior term's transactions in {@code this.transactions} -- including + * whatever interest was posted (and already paid out, via the renewal's own withdrawal/transfer leg) + * for a term that has since closed. Summing all of them here double-counted that stale interest: a + * premature closure of the CURRENT term subtracted it from the maturity amount + * ({@code accountBalance - interestPostedToDate + interestEarnedTillDate}) even though it was never + * part of this term's balance to begin with, silently shortchanging the customer by exactly that + * amount -- enough to make a freshly-renewed FD's premature-closure amount come out BELOW its + * deposit amount, penalty or no penalty. Scoped to {@link #depositStartDate()} for the same reason + * {@link #isRenewedTerm()} and the posting-period calculation already are: interest posted before + * the term restarted belongs to a term that has already been settled. + */ private Money totalInterestPosted() { + final LocalDate currentTermStartDate = depositStartDate(); Money interestPostedToDate = Money.zero(this.currency); for (final SavingsAccountTransaction transaction : this.transactions) { - if (transaction.isInterestPostingAndNotReversed()) { + if (transaction.isInterestPostingAndNotReversed() + && !DateUtils.isBefore(transaction.getTransactionDate(), currentTermStartDate)) { interestPostedToDate = interestPostedToDate.plus(transaction.getAmount(currency)); } } @@ -960,6 +998,33 @@ public class FixedDepositAccount extends SavingsAccount { return termStart != null && getActivationDate() != null && !termStart.equals(getActivationDate()); } + /** + * A renewed term's opening balance is seeded straight to the term's own principal (see + * calculateInterestPayable/projectInterestSchedule) -- which is already the balance AS OF + * depositStartDate(), i.e. net of whatever transaction closed out the previous term. That closing + * transaction is dated the SAME day the new term starts (a renewal is atomic: the old term is closed + * and the new one opened in the same stroke), so without this filter it still falls inside the new + * term's posting-period walk and gets applied a SECOND time on top of the already-net seed -- + * understating the very first day's balance by the old term's entire payout, which silently corrupts + * that day's interest for as long as the term keeps re-including it (every premature-closure preview, + * and any later maturity recalculation). Scoping to transactions strictly after the term start avoids + * the double-count; a genuine new transaction can't land on the renewal day itself for an FD anyway. + */ + private List transactionsForCurrentTermInterestCalculation( + final List transactions) { + if (!isRenewedTerm()) { + return transactions; + } + final LocalDate currentTermStartDate = depositStartDate(); + final List scoped = new ArrayList<>(); + for (final SavingsAccountTransaction transaction : transactions) { + if (DateUtils.isAfter(transaction.getTransactionDate(), currentTermStartDate)) { + scoped.add(transaction); + } + } + return scoped; + } + private LocalDate interestCalculatedUpto() { LocalDate uptoMaturityDate = calculateMaturityDate(); if (uptoMaturityDate != null) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountPreMatureCalculationPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountPreMatureCalculationPlatformServiceImpl.java index eacefbdec..822d6bcb8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountPreMatureCalculationPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountPreMatureCalculationPlatformServiceImpl.java @@ -19,8 +19,11 @@ package org.apache.fineract.portfolio.savings.service; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closedOnDateParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.prematureClosureReasonIdParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.waivePreClosurePenaltyParamName; import com.google.gson.JsonElement; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; import lombok.RequiredArgsConstructor; @@ -32,6 +35,7 @@ import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService; import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.PrematureClosureReasonType; import org.apache.fineract.portfolio.savings.data.DepositAccountData; import org.apache.fineract.portfolio.savings.data.DepositAccountTransactionDataValidator; import org.apache.fineract.portfolio.savings.data.FixedDepositAccountData; @@ -77,13 +81,32 @@ public class DepositAccountPreMatureCalculationPlatformServiceImpl implements De // calculate interest before one day of closure date final LocalDate interestCalculatedToDate = preMaturityDate.minusDays(1); final boolean isPreMatureClosure = true; + // Preview-time only -- the real closure re-reads this same box from its own request when it + // actually happens (see DepositAccountDomainServiceJpa#handleFDAccountPreMatureClosure), so a + // maker previewing with the box checked and then submitting without it (or vice versa) gets + // exactly what they last confirmed, not what an earlier preview computed. + final boolean waivePreClosurePenalty = this.fromJsonHelper.parameterExists(waivePreClosurePenaltyParamName, element) + && this.fromJsonHelper.extractBooleanNamed(waivePreClosurePenaltyParamName, element); + // RBI-mandated waiver reasons (premature extension, conversion to another TD, deceased + // settlement, nominee/legal heir closure) waive the penalty regardless of the manual checkbox + // above -- see PrematureClosureReasonType. This is enforced here, not just on the client, so + // the preview a maker sees always matches what the actual closure will compute. + final Integer prematureClosureReasonId = this.fromJsonHelper.extractIntegerSansLocaleNamed(prematureClosureReasonIdParamName, + element); + final PrematureClosureReasonType prematureClosureReason = PrematureClosureReasonType.fromInt(prematureClosureReasonId); + final boolean effectiveWaivePenalty = waivePreClosurePenalty || prematureClosureReason.isAutoWaived(); if (depositAccountType.isFixedDeposit()) { final FixedDepositAccount fd = (FixedDepositAccount) account; - accountData = FixedDepositAccountData.preClosureDetails( - account.getId(), fd.calculatePreMatureAmount(interestCalculatedToDate, isPreMatureClosure, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth), - onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas); + fd.setWaivePreClosurePenaltyForThisRequest(effectiveWaivePenalty); + final BigDecimal maturityAmount = fd.calculatePreMatureAmount(interestCalculatedToDate, isPreMatureClosure, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + // calculatePreMatureAmount() (via getEffectiveInterestRateAsFraction()) leaves the account's + // own nominalAnnualInterestRate set to whichever rate it just resolved for THIS preview -- + // the deposit's own contracted rate, minus any pre-closure penalty -- so read it back here + // rather than re-deriving it, to show the maker exactly what applied. + accountData = FixedDepositAccountData.preClosureDetails(account.getId(), maturityAmount, onAccountClosureOptions, + paymentTypeOptions, savingsAccountDatas, fd.getNominalAnnualInterestRate()); } else if (depositAccountType.isRecurringDeposit()) { final RecurringDepositAccount rd = (RecurringDepositAccount) account; accountData = RecurringDepositAccountData.preClosureDetails( diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index d1385c84d..9b2f6e419 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -156,4 +156,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0588_add_premature_closure_reason.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0588_add_premature_closure_reason.xml new file mode 100644 index 000000000..e7e35d6b0 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0588_add_premature_closure_reason.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java index edb6c2d02..2758c91da 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java @@ -674,7 +674,7 @@ public final class FixedDepositAccountData extends DepositAccountData { public static FixedDepositAccountData preClosureDetails(final Long accountId, BigDecimal maturityAmount, final Collection onAccountClosureOptions, final Collection paymentTypeOptions, - final Collection savingsAccountDatas) { + final Collection 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; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java index 0490f3d09..136c60a45 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java @@ -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