feat(fd): add Renewal Grace Period product field, both storage paths

New per-FD-product "Renewal Grace Period (Days)" term field: null/0 (every existing product)
keeps today's instant, same-night auto-renewal; a positive value is later read by the nightly
maturity job and the manual Renew action to decide whether/how a matured FD waits for staff to
act instead of renewing itself.

Wired into the live product path end-to-end -- DepositsApiConstants param, DepositTermDetail
column + diffing, DepositProductAssembler (create and update), DepositProductReadPlatformServiceImpl,
FixedDepositProductData -- migration 0572 adds the column.

Also wired into the maker-checker pending-edit mirror (m_saving_product_temp /
FixedDepositEditFields / ProductPendingEditFieldMapper), a second, independent storage path a
governed product edit uses -- without this the field silently dropped out of a checker's
"Proposed changes" review even though it was submitted. Migration 0573 adds its column.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 15:55:11 +05:30
co-authored by Claude Sonnet 5
parent a7cb09f15c
commit ecfe8aa76a
9 changed files with 117 additions and 8 deletions
@@ -496,6 +496,7 @@ public final class FixedDepositProductData extends DepositProductData {
private Boolean accountClosedZeroBalanceInDayClosing;
private String balanceFrequency;
private Boolean isPendingApproval;
private Integer renewalGracePeriodDays;
// Template option setters
public void setInterestCalculationTypeOption(Collection<org.apache.fineract.infrastructure.core.data.EnumOptionData> v) { this.interestCalculationTypeOption = v; }
@@ -543,6 +544,7 @@ public final class FixedDepositProductData extends DepositProductData {
public void setAccountClosedZeroBalanceInDayClosing(Boolean v) { this.accountClosedZeroBalanceInDayClosing = v; }
public void setBalanceFrequency(String v) { this.balanceFrequency = v; }
public void setIsPendingApproval(Boolean v) { this.isPendingApproval = v; }
public void setRenewalGracePeriodDays(Integer v) { this.renewalGracePeriodDays = v; }
// CBS field getters
public String getProductCode() { return productCode; }
@@ -559,6 +561,7 @@ public final class FixedDepositProductData extends DepositProductData {
public Long getDaysToInactive() { return daysToInactive; }
public Long getDaysToDormancy() { return daysToDormancy; }
public Long getDaysToEscheat() { return daysToEscheat; }
public Integer getRenewalGracePeriodDays() { return renewalGracePeriodDays; }
public void copyCbsFieldsFrom(FixedDepositProductData src) {
if (src == null) return;
@@ -605,6 +608,7 @@ public final class FixedDepositProductData extends DepositProductData {
this.accountClosedZeroBalanceInDayClosing = src.accountClosedZeroBalanceInDayClosing;
this.balanceFrequency = src.balanceFrequency;
this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers;
this.renewalGracePeriodDays = src.renewalGracePeriodDays;
}
private Boolean withdrawalFeeForTransfers;
@@ -36,6 +36,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.minDepo
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalApplicableParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalInterestOnTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalInterestParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.renewalGracePeriodDaysParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.currencyCodeParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.descriptionParamName;
@@ -473,8 +474,10 @@ public class DepositProductAssembler {
final SavingsPeriodFrequencyType inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null
: SavingsPeriodFrequencyType.fromInt(inMultiplesOfDepositTermTypeId);
return DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
final DepositTermDetail depositTermDetail = DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType,
maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
depositTermDetail.setRenewalGracePeriodDays(command.integerValueOfParameterNamed(renewalGracePeriodDaysParamName));
return depositTermDetail;
}
public DepositTermDetail assembleDepositTermDetail(final JsonCommand command, final DepositTermDetail prodDepositTermDetail) {
@@ -531,8 +534,17 @@ public class DepositProductAssembler {
final SavingsPeriodFrequencyType inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null
: SavingsPeriodFrequencyType.fromInt(inMultiplesOfDepositTermTypeId);
return DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
Integer renewalGracePeriodDays = null;
if (command.parameterExists(renewalGracePeriodDaysParamName)) {
renewalGracePeriodDays = command.integerValueOfParameterNamed(renewalGracePeriodDaysParamName);
} else if (prodDepositTermDetail != null) {
renewalGracePeriodDays = prodDepositTermDetail.renewalGracePeriodDays();
}
final DepositTermDetail depositTermDetail = DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType,
maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
depositTermDetail.setRenewalGracePeriodDays(renewalGracePeriodDays);
return depositTermDetail;
}
public DepositRecurringDetail assembleRecurringDetail(final JsonCommand command) {
@@ -24,6 +24,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepo
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.minDepositTermParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.minDepositTermTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.renewalGracePeriodDaysParamName;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
@@ -61,6 +62,14 @@ public class DepositTermDetail {
@Column(name = "in_multiples_of_deposit_term_type_enum", nullable = true)
private Integer inMultiplesOfDepositTermType;
// Days after maturity a depositor can still walk in and renew backdated to the original maturity
// date, at the currently active rate. Null/0 means this product keeps the instant, same-night
// auto-renewal every FD product had before this field existed -- see
// DepositAccountWritePlatformServiceJpaRepositoryImpl#updateMaturityDetails, which only skips the
// automatic renewal when this is a positive value.
@Column(name = "renewal_grace_period_days", nullable = true)
private Integer renewalGracePeriodDays;
public static DepositTermDetail createFrom(final Integer minDepositTerm, final Integer maxDepositTerm,
final SavingsPeriodFrequencyType minDepositTermType, final SavingsPeriodFrequencyType maxDepositTermType,
final Integer inMultiplesOfDepositTerm, final SavingsPeriodFrequencyType inMultiplesOfDepositTermType) {
@@ -123,6 +132,12 @@ public class DepositTermDetail {
this.inMultiplesOfDepositTermType = newValue;
}
if (command.isChangeInIntegerParameterNamed(renewalGracePeriodDaysParamName, this.renewalGracePeriodDays)) {
final Integer newValue = command.integerValueOfParameterNamed(renewalGracePeriodDaysParamName);
actualChanges.put(renewalGracePeriodDaysParamName, newValue);
this.renewalGracePeriodDays = newValue;
}
if (isMinDepositTermGreaterThanMaxDepositTerm()) {
baseDataValidator.parameter(minDepositTermParamName).value(this.minDepositTerm).failWithCode(".greater.than.maxDepositTerm",
this.minDepositTerm, this.maxDepositTerm);
@@ -166,6 +181,14 @@ public class DepositTermDetail {
return this.inMultiplesOfDepositTermType;
}
public Integer renewalGracePeriodDays() {
return this.renewalGracePeriodDays;
}
public void setRenewalGracePeriodDays(final Integer renewalGracePeriodDays) {
this.renewalGracePeriodDays = renewalGracePeriodDays;
}
public boolean isDepositBetweenMinAndMax(LocalDate depositStartDate, LocalDate depositEndDate) {
return isEqualOrGreaterThanMin(depositStartDate, depositEndDate) && isEqualOrLessThanMax(depositStartDate, depositEndDate);
}
@@ -260,7 +283,9 @@ public class DepositTermDetail {
final SavingsPeriodFrequencyType inMultiplesOfDepositTermType = SavingsPeriodFrequencyType
.fromInt(this.inMultiplesOfDepositTermType());
return DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
final DepositTermDetail copy = DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType,
maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
copy.renewalGracePeriodDays = this.renewalGracePeriodDays;
return copy;
}
}
@@ -68,4 +68,7 @@ public class FixedDepositEditFields {
@Column(name = "deposit_amount")
private BigDecimal depositAmount;
@Column(name = "renewal_grace_period_days")
private Integer renewalGracePeriodDays;
}
@@ -942,6 +942,10 @@ public final class ProductPendingEditFieldMapper {
f.setDepositAmount(command.bigDecimalValueOfParameterNamed("depositAmount"));
present.add("depositAmount");
}
if (command.hasParameter("renewalGracePeriodDays")) {
f.setRenewalGracePeriodDays(command.integerValueOfParameterNamed("renewalGracePeriodDays"));
present.add("renewalGracePeriodDays");
}
return present;
}
@@ -962,6 +966,7 @@ public final class ProductPendingEditFieldMapper {
if (f.getMinDepositAmount() != null) putNumber(root, submitted, "minDepositAmount", f.getMinDepositAmount());
if (f.getMaxDepositAmount() != null) putNumber(root, submitted, "maxDepositAmount", f.getMaxDepositAmount());
if (f.getDepositAmount() != null) putNumber(root, submitted, "depositAmount", f.getDepositAmount());
if (f.getRenewalGracePeriodDays() != null) putNumber(root, submitted, "renewalGracePeriodDays", f.getRenewalGracePeriodDays());
}
public static Set<String> populateRecurringDepositFields(final JsonCommand command, final RecurringDepositEditFields f) {