fix(deposits): FD Single Rate Calculator crashed for tenures under 30 days
SYMPTOM
Opening the FD account create screen for a "SINGLE" (Days < 1 Year)
product, entering a Deposit Tenure under 30 days (e.g. 27), and
clicking "Find Rate" returned a 400:
"The parameter `tenureInMonths` must be greater than 0."
ROOT CAUSE
The Rate Calculator preview always converted the entered tenure to
months before calling GET /fixeddepositaccounts/calculate-fd-interest
(fixed-deposits.service.ts -> fixed-deposit-account-interest-rate-
chart-step.component.ts, calculateRate()):
tenureInMonths = Math.floor(enteredDays / 30)
For any tenure under 30 days this floors to 0, and
DepositAccountDataValidator.validateForInterestCalc() requires
tenureInMonths > 0 unconditionally, so the request was rejected
before it ever reached the calculation service.
FIX
Added `tenureInDays` as an alternative to `tenureInMonths` for this
one calculator endpoint only (DepositsApiConstants.
FIXED_DEPOSIT_ACCOUNT_INTEREST_CALCULATION_PARAMETERS) - not a
general-purpose FD field, not touched anywhere else:
- DepositAccountDataValidator: tenureInMonths is required unless
tenureInDays is a positive value (either satisfies the check).
- FixedDepositAccountsApiResource#calculateFixedDepositInterest:
accepts the optional tenureInDays query param.
- FixedDepositAccountInterestCalculationServiceImpl#calculateInterest:
when tenureInDays > 0, the exact-calendar maturityDate is computed
via submittedOnDate.plusDays(tenureInDays) instead of
plusMonths(tenureInMonths) (which would silently compute a 0-day
maturity window for a sub-month tenure).
- Frontend (separate commit) now sends tenureInDays instead of a
truncated-to-zero tenureInMonths whenever the entered tenure is
under 30 days.
SCOPE / WHAT THIS DOES NOT TOUCH
Only the interest-preview calculator endpoint above. Real FD/RD
account submission still uses depositPeriod + depositPeriodFrequencyId
(Days=0) exactly as before; this commit does not change how an actual
account's maturity date/amount gets computed at activation.
IF THIS BREAKS SOMETHING LATER
- "tenureInMonths must be greater than 0" reappearing for a SHORT FD
tenure -> check the frontend is actually sending tenureInDays (network
tab on the Find Rate call); the validator's OR-logic is in
DepositAccountDataValidator.validateForInterestCalc().
- A wrong preview maturity amount for a sub-month FD Single -> check
FixedDepositAccountInterestCalculationServiceImpl#calculateInterest's
branch choosing plusDays vs plusMonths for maturityDate.
- This bug and fix were previously implemented once, then fully
reverted mid-session at the user's request (unrelated Deposit Account
Link regression got tangled up with it), then reapplied cleanly here
as a standalone, narrowly-scoped fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+2
@@ -123,6 +123,7 @@ public final class DepositsApiConstants {
|
||||
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
||||
public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate";
|
||||
public static final String tenureInMonthsParamName = "tenureInMonths";
|
||||
public static final String tenureInDaysParamName = "tenureInDays";
|
||||
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
|
||||
|
||||
// Preclosure parameters
|
||||
@@ -511,6 +512,7 @@ public final class DepositsApiConstants {
|
||||
fixedDepositInterestCalculationData.add(principalAmountParamName);
|
||||
fixedDepositInterestCalculationData.add(annualInterestRateParamName);
|
||||
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
|
||||
fixedDepositInterestCalculationData.add(tenureInDaysParamName);
|
||||
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
|
||||
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
|
||||
// Exact calendar-based calculation params
|
||||
|
||||
+4
@@ -284,6 +284,7 @@ public class FixedDepositAccountsApiResource {
|
||||
@QueryParam("principalAmount") @Parameter(description = "Deposit principal amount") final BigDecimal principalAmount,
|
||||
@QueryParam("annualInterestRate") @Parameter(description = "Annual interest rate (%)") final BigDecimal annualInterestRate,
|
||||
@QueryParam("tenureInMonths") @Parameter(description = "Tenure in months") final Long tenureInMonths,
|
||||
@QueryParam("tenureInDays") @Parameter(description = "Tenure in days (alternative to tenureInMonths, for sub-month FD Single tenures)") final Long tenureInDays,
|
||||
@QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "Compounding period in months (1,3,6,12)") final Long interestCompoundingPeriodInMonths,
|
||||
@QueryParam("interestPostingPeriodInMonths") @Parameter(description = "Posting period in months (1,3,6,12)") final Long interestPostingPeriodInMonths,
|
||||
@QueryParam("submittedOnDate") @Parameter(description = "Submission date (yyyy-MM-dd) for exact calendar calculation") final String submittedOnDate,
|
||||
@@ -291,6 +292,9 @@ public class FixedDepositAccountsApiResource {
|
||||
HashMap request = new HashMap<>();
|
||||
request.put("annualInterestRate", annualInterestRate);
|
||||
request.put("tenureInMonths", tenureInMonths);
|
||||
if (tenureInDays != null) {
|
||||
request.put("tenureInDays", tenureInDays);
|
||||
}
|
||||
request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths);
|
||||
request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths);
|
||||
request.put("principalAmount", principalAmount);
|
||||
|
||||
+9
@@ -43,6 +43,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClos
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyTypeParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInDaysParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferToSavingsIdParamName;
|
||||
@@ -681,8 +682,16 @@ public class DepositAccountDataValidator {
|
||||
baseDataValidator.reset().parameter(annualInterestRateParamName).value(annualInterestRate).notNull()
|
||||
.notLessThanMin(BigDecimal.valueOf(0));
|
||||
|
||||
// FD Single accounts under 30 days are entered in whole days rather than months (a whole-month
|
||||
// tenure would round down to 0 months); tenureInDays is an alternative to tenureInMonths, not
|
||||
// an addition, so only one of the two needs to be a positive value.
|
||||
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
||||
Long tenureInDays = this.fromApiJsonHelper.extractLongNamed(tenureInDaysParamName, element);
|
||||
if (tenureInDays != null && tenureInDays > 0) {
|
||||
baseDataValidator.reset().parameter(tenureInMonthsParamName).value(tenureInMonths).ignoreIfNull().longGreaterThanZero();
|
||||
} else {
|
||||
baseDataValidator.reset().parameter(tenureInMonthsParamName).value(tenureInMonths).notNull().longGreaterThanZero();
|
||||
}
|
||||
|
||||
Long interestPostingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestPostingPeriodInMonthsParamName, element);
|
||||
baseDataValidator.reset().parameter(interestPostingPeriodInMonthsParamName).value(interestPostingPeriodInMonths).notNull()
|
||||
|
||||
+4
-1
@@ -22,6 +22,7 @@ package org.apache.fineract.portfolio.savings.service;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.annualInterestRateParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestCompoundingPeriodInMonthsParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInDaysParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
@@ -52,6 +53,7 @@ public class FixedDepositAccountInterestCalculationServiceImpl implements FixedD
|
||||
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
|
||||
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
|
||||
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
||||
Long tenureInDays = this.fromApiJsonHelper.extractLongNamed(tenureInDaysParamName, element);
|
||||
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName, element);
|
||||
|
||||
// Optional params for exact calendar-based calculation
|
||||
@@ -73,7 +75,8 @@ public class FixedDepositAccountInterestCalculationServiceImpl implements FixedD
|
||||
BigDecimal maturityAmount;
|
||||
if (submittedOnDate != null && daysInYear != null && interestPostingPeriodInMonths != null) {
|
||||
// Exact calendar-based calculation — same algorithm as account creation
|
||||
final LocalDate maturityDate = submittedOnDate.plusMonths(tenureInMonths);
|
||||
final LocalDate maturityDate = (tenureInDays != null && tenureInDays > 0) ? submittedOnDate.plusDays(tenureInDays)
|
||||
: submittedOnDate.plusMonths(tenureInMonths);
|
||||
maturityAmount = this.calculateExact(principalAmount, annualInterestRate, submittedOnDate, maturityDate,
|
||||
interestCompoundingPeriodInMonths, interestPostingPeriodInMonths, daysInYear);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user