feat: recurring deposit product rules, lien, auto debit, missed installment penalty, and jobs

This commit is contained in:
2026-10-12 10:25:12 +05:30
parent cca4ae583f
commit 9df8e924a1
65 changed files with 4445 additions and 1412 deletions
@@ -163,6 +163,7 @@ public class CommandSourceService {
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "RECURRINGDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())) {
return false;
@@ -3157,6 +3157,15 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder extendTenureRecurringDepositAccount(final Long accountId) {
this.actionName = "EXTENDTENURE";
this.entityName = "RECURRINGDEPOSITACCOUNT";
this.entityId = accountId;
this.savingsId = accountId;
this.href = "/recurringdepositaccounts/" + accountId + "?command=extendTenure";
return this;
}
public CommandWrapperBuilder prematureCloseRecurringDepositAccount(final Long accountId) {
this.actionName = "PREMATURECLOSE";
this.entityName = "RECURRINGDEPOSITACCOUNT";
@@ -75,6 +75,8 @@ public enum JobName {
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
CLIENT_BEHAVIOUR_WINDOW("Client Behaviour Window Computation"), //
SAVE_RBI_FORM_A_RETURN("Save RBI Form A Return"), //
EXECUTE_RD_AUTO_DEBIT("Execute RD Auto Debit"), //
APPLY_RD_MISSED_INSTALLMENT_PENALTY("Apply RD Missed Installment Penalty"), //
; //
private final String name;
@@ -432,6 +432,10 @@ public final class DepositsApiConstants {
recurringDepositRequestData.add("prematureWithdrawalOption");
recurringDepositRequestData.add("tenureFlexibility");
recurringDepositRequestData.add("lienMarked");
recurringDepositRequestData.add("lienPercentage");
recurringDepositRequestData.add("missedInstallmentGraceDays");
recurringDepositRequestData.add("missedInstallmentPenaltyType");
recurringDepositRequestData.add("missedInstallmentPenaltyAmount");
recurringDepositRequestData.add("withdrawalFeeForTransfers");
recurringDepositRequestData.add("enableLockinPeriod");
recurringDepositRequestData.add("maturityInstructions");
@@ -491,6 +495,10 @@ public final class DepositsApiConstants {
recurringDepositRequestData.add("prematureWithdrawalOption");
recurringDepositRequestData.add("tenureFlexibility");
recurringDepositRequestData.add("lienMarked");
recurringDepositRequestData.add("lienPercentage");
recurringDepositRequestData.add("missedInstallmentGraceDays");
recurringDepositRequestData.add("missedInstallmentPenaltyType");
recurringDepositRequestData.add("missedInstallmentPenaltyAmount");
recurringDepositRequestData.add("maturityInstructions");
recurringDepositRequestData.add("creditMaturityProceeds");
recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments");
@@ -648,6 +656,8 @@ public final class DepositsApiConstants {
recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName);
recurringDepositRequestData.add(SavingsApiConstants.msmeParamName);
recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
// Lien Marked: how much of this deposit may be given as loan, capped by its product's percentage.
recurringDepositRequestData.add("lienPercentage");
recurringDepositRequestData.add(isSeniorCitizenParamName);
recurringDepositRequestData.add(seniorCitizenInterestRateParamName);
return recurringDepositRequestData;
@@ -234,6 +234,20 @@ public final class SavingsAccountData implements Serializable {
return this.transferInterestToLinkedAccount;
}
/**
* True for a recurring deposit opened under a product whose Interest Calculation Type is "Simple": its interest
* never compounds. Transient because it is only read by the batch interest-posting engine.
*/
private transient boolean simpleInterest;
public void setSimpleInterest(final boolean simpleInterest) {
this.simpleInterest = simpleInterest;
}
public boolean isSimpleInterest() {
return this.simpleInterest;
}
public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate,
BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum,
EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum,
@@ -180,6 +180,12 @@ public final class SavingsHelper {
immediateWithdrawalOfInterest);
}
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
LocalDate accountLockedUntil, Boolean immediateWithdrawalOfInterest, final boolean simpleInterest) {
return COMPOUND_INTEREST_HELPER.calculateInterestForAllPostingPeriods(currency, allPeriods, accountLockedUntil,
immediateWithdrawalOfInterest, simpleInterest);
}
public Collection<Long> fetchPostInterestTransactionIds(Long accountId) {
return this.accountTransfersReadPlatformService.fetchPostInterestTransactionIds(accountId);
}
@@ -40,6 +40,18 @@ public class CompoundInterestHelper {
*/
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
LocalDate lockUntil, Boolean interestTransferEnabled) {
return calculateInterestForAllPostingPeriods(currency, allPeriods, lockUntil, interestTransferEnabled, false);
}
/**
* @param simpleInterest
* - when true, earned interest is never added back to the balance it is earned on, so every period
* earns on the principal alone (a recurring deposit whose product's Interest Calculation Type is
* "Simple"). Compounding and posting periods still decide when interest is recognised and posted;
* they just stop changing how much is earned.
*/
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
LocalDate lockUntil, Boolean interestTransferEnabled, final boolean simpleInterest) {
// sum up the 'rounded' values that are posted each posting period
Money interestEarned = Money.zero(currency);
@@ -47,7 +59,8 @@ public class CompoundInterestHelper {
// total interest earned in previous periods but not yet recognised
BigDecimal compoundedInterest = BigDecimal.ZERO;
BigDecimal unCompoundedInterest = BigDecimal.ZERO;
final CompoundInterestValues compoundInterestValues = new CompoundInterestValues(compoundedInterest, unCompoundedInterest);
final CompoundInterestValues compoundInterestValues = simpleInterest ? new SimpleInterestValues()
: new CompoundInterestValues(compoundedInterest, unCompoundedInterest);
for (final PostingPeriod postingPeriod : allPeriods) {
final BigDecimal interestEarnedThisPeriod = postingPeriod.calculateInterest(compoundInterestValues);
@@ -67,4 +80,25 @@ public class CompoundInterestHelper {
return interestEarned;
}
/**
* Interest values that refuse to compound: whatever a compounding period hands over to be compounded is dropped,
* so the balance interest is earned on never includes interest already earned.
*/
private static final class SimpleInterestValues extends CompoundInterestValues {
SimpleInterestValues() {
super(BigDecimal.ZERO, BigDecimal.ZERO);
}
@Override
public void setCompoundedInterest(final BigDecimal interestToBeCompounded) {
super.setCompoundedInterest(BigDecimal.ZERO);
}
@Override
public boolean isSimpleInterest() {
return true;
}
}
}
@@ -50,4 +50,9 @@ public class CompoundInterestValues {
this.uncompoundedInterest = BigDecimal.ZERO;
}
/** True when earned interest must never earn interest itself - not across periods, and not within one either. */
public boolean isSimpleInterest() {
return false;
}
}
@@ -94,6 +94,18 @@ public final class DailyCompoundingPeriod implements CompoundingPeriod {
return interestEarned;
}
/** Interest on each day's balance alone, none of it added to the balance - for simple interest. */
public BigDecimal calculateSimpleInterest(final BigDecimal interestRateAsFraction, final long daysInYear,
final BigDecimal minBalanceForInterestCalculation, final BigDecimal overdraftInterestRateAsFraction,
final BigDecimal minOverdraftForInterestCalculation) {
BigDecimal interestEarned = BigDecimal.ZERO;
for (final EndOfDayBalance balance : this.endOfDayBalances) {
interestEarned = interestEarned.add(balance.calculateInterestOnBalance(BigDecimal.ZERO, interestRateAsFraction, daysInYear,
minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation));
}
return interestEarned;
}
@Override
public LocalDateInterval getPeriodInterval() {
return this.periodInterval;
@@ -301,10 +301,17 @@ public final class PostingPeriod {
// to be applied to the balanced for interest calculation
for (final CompoundingPeriod compoundingPeriod : this.compoundingPeriods) {
final BigDecimal interestUnrounded = compoundingPeriod.calculateInterest(this.interestCompoundingType,
this.interestCalculationType, compoundInterestValues.getcompoundedInterest(), this.interestRateAsFraction,
this.daysInYear, this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
this.minOverdraftForInterestCalculation.getAmount());
// A daily compounding period compounds day by day inside itself, so simple interest has to bypass it - zeroing
// what is carried between periods alone still left a month's interest earning interest within the month.
final BigDecimal interestUnrounded = compoundInterestValues.isSimpleInterest()
&& compoundingPeriod instanceof DailyCompoundingPeriod dailyPeriod
? dailyPeriod.calculateSimpleInterest(this.interestRateAsFraction, this.daysInYear,
this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
this.minOverdraftForInterestCalculation.getAmount())
: compoundingPeriod.calculateInterest(this.interestCompoundingType, this.interestCalculationType,
compoundInterestValues.getcompoundedInterest(), this.interestRateAsFraction, this.daysInYear,
this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
this.minOverdraftForInterestCalculation.getAmount());
BigDecimal unCompoundedInterest = compoundInterestValues.getuncompoundedInterest().add(interestUnrounded);
compoundInterestValues.setuncompoundedInterest(unCompoundedInterest);
LocalDate compoundingPeriodEndDate = compoundingPeriod.getPeriodInterval().endDate();