update RD Prodcut code

This commit is contained in:
Nitesh Yadav
2026-04-20 12:51:35 +05:30
parent 2facd3744d
commit 800e114a4d
37 changed files with 2327 additions and 42 deletions
@@ -292,6 +292,44 @@ public final class DepositsApiConstants {
recurringDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS);
recurringDepositRequestData.addAll(RECURRING_DETAILS_REQUEST_DATA_PARAMETERS);
recurringDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName);
// Custom CBS fields — Details step
recurringDepositRequestData.add("accountType");
recurringDepositRequestData.add("customerSegment");
recurringDepositRequestData.add("productCode");
recurringDepositRequestData.add("effectiveTo");
recurringDepositRequestData.add("endDate");
recurringDepositRequestData.add("introducerRequired");
recurringDepositRequestData.add("shareLinkingEnabled");
// Custom CBS fields — Currency step
recurringDepositRequestData.add("dormancyAlert");
recurringDepositRequestData.add("balanceSwapOption");
recurringDepositRequestData.add("msme");
recurringDepositRequestData.add("nominationFacility");
recurringDepositRequestData.add("autoDebitFacility");
// Custom CBS fields — Terms step
recurringDepositRequestData.add("effectiveDate");
recurringDepositRequestData.add("interestCalculateSelector");
recurringDepositRequestData.add("averagePeriod");
recurringDepositRequestData.add("autoPost");
recurringDepositRequestData.add("frequencyFixDay");
recurringDepositRequestData.add("lastInterestDate");
// Custom CBS fields — Settings step
recurringDepositRequestData.add("loanFacility");
recurringDepositRequestData.add("prematureWithdrawalOption");
recurringDepositRequestData.add("tenureFlexibility");
recurringDepositRequestData.add("lienMarked");
recurringDepositRequestData.add("withdrawalFeeForTransfers");
recurringDepositRequestData.add("enableLockinPeriod");
recurringDepositRequestData.add("maturityInstructions");
recurringDepositRequestData.add("creditMaturityProceeds");
recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments");
recurringDepositRequestData.add("panelRequestApplicable");
recurringDepositRequestData.add("noPenaltyNomineeHeirClosure");
recurringDepositRequestData.add("deceasedSettlement");
recurringDepositRequestData.add("allowDebitTxnAfterDueDate");
recurringDepositRequestData.add("accountClosedZeroBalanceInDayClosing");
// Custom CBS fields — Interest rate slabs (sent inline but ignored server-side; saved separately)
recurringDepositRequestData.add("rdSlabs");
return recurringDepositRequestData;
}
@@ -302,6 +340,47 @@ public final class DepositsApiConstants {
recurringDepositRequestData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS);
recurringDepositRequestData.addAll(RECURRING_DETAILS_RESPONSE_DATA_PARAMETERS);
recurringDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName);
// CBS extended fields — Options (for template=true)
recurringDepositRequestData.add("interestCalculationTypeOption");
recurringDepositRequestData.add("periodFrequencyOptions");
recurringDepositRequestData.add("yesNoOptions");
recurringDepositRequestData.add("accountTypeOptions");
recurringDepositRequestData.add("customerSegmentOptions");
// CBS extended fields — Details step
recurringDepositRequestData.add("accountType");
recurringDepositRequestData.add("customerSegment");
recurringDepositRequestData.add("productCode");
recurringDepositRequestData.add("effectiveDate");
recurringDepositRequestData.add("effectiveTo");
recurringDepositRequestData.add("endDate");
recurringDepositRequestData.add("introducerRequired");
recurringDepositRequestData.add("shareLinkingEnabled");
recurringDepositRequestData.add("nominationFacility");
recurringDepositRequestData.add("dormancyAlert");
// CBS extended fields — Terms step
recurringDepositRequestData.add("interestCalculateSelector");
recurringDepositRequestData.add("averagePeriod");
recurringDepositRequestData.add("autoPost");
recurringDepositRequestData.add("frequencyFixDay");
recurringDepositRequestData.add("lastInterestDate");
// CBS extended fields — Settings step
recurringDepositRequestData.add("enableLockinPeriod");
recurringDepositRequestData.add("balanceFrequency");
recurringDepositRequestData.add("accountClosedZeroBalanceInDayClosing");
recurringDepositRequestData.add("balanceSwapOption");
recurringDepositRequestData.add("msme");
recurringDepositRequestData.add("autoDebitFacility");
recurringDepositRequestData.add("loanFacility");
recurringDepositRequestData.add("prematureWithdrawalOption");
recurringDepositRequestData.add("tenureFlexibility");
recurringDepositRequestData.add("lienMarked");
recurringDepositRequestData.add("maturityInstructions");
recurringDepositRequestData.add("creditMaturityProceeds");
recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments");
recurringDepositRequestData.add("panelRequestApplicable");
recurringDepositRequestData.add("noPenaltyNomineeHeirClosure");
recurringDepositRequestData.add("deceasedSettlement");
recurringDepositRequestData.add("allowDebitTxnAfterDueDate");
return recurringDepositRequestData;
}
@@ -0,0 +1,53 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.api;
import java.math.BigDecimal;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Request payload for saving a new RD interest rate version.
* JSON structure:
* {
* "versionLabel": "Rate Version 2025-Q2",
* "remarks": "Revised rates",
* "slabs": [
* { "srNo": 1, "description": "15 Days to 29 Days", "periodTypeEnum": 1,
* "fromPeriod": 15, "toPeriod": 29, "annualInterestRate": 3.00 },
* ...
* ]
* }
*/
@Getter
@Setter
@NoArgsConstructor
public class RdInterestRateSaveRequest {
private String versionLabel;
private String remarks;
private List<SlabRequest> slabs;
@Getter
@Setter
@NoArgsConstructor
public static class SlabRequest {
private Integer srNo;
private String description;
private Integer periodTypeEnum;
private Integer fromPeriod;
private Integer toPeriod;
private BigDecimal annualInterestRate;
}
}
@@ -79,6 +79,11 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYea
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;
import java.time.LocalDate;
import org.apache.fineract.portfolio.savings.data.RecurringDepositProductData;
import org.apache.fineract.portfolio.savings.service.DepositProductReadPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositsDropdownReadPlatformService;
@@ -117,6 +122,10 @@ public class RecurringDepositProductsApiResource {
private final TaxReadPlatformService taxReadPlatformService;
private final CodeValueReadPlatformService codeValueReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(LocalDate.class,
(JsonSerializer<LocalDate>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString()))
.create();
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@@ -354,19 +363,21 @@ public class RecurringDepositProductsApiResource {
private String generateNextProductCode(final String tableName, final String prefix) {
try {
String maxCode = jdbcTemplate.queryForObject(
"SELECT MAX(product_code) FROM " + tableName + " WHERE product_code LIKE ? AND deposit_type_enum = 3",
"SELECT MAX(product_code) FROM " + tableName + " WHERE product_code LIKE ? AND deposit_type_enum = 300",
String.class, prefix + "%");
if (maxCode == null || maxCode.isBlank()) {
return prefix + "00";
return prefix + "001";
}
String numStr = maxCode.substring(prefix.length());
int num = Integer.parseInt(numStr);
return prefix + String.format("%02d", num + 1);
return prefix + String.format("%03d", num + 1);
} catch (Exception e) {
return prefix + "00";
return prefix + "001";
}
}
// ── RD Interest Rate Chart endpoints ────────────────────────────────────
@DELETE
@Path("{productId}")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -0,0 +1,84 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.data.RdInterestRateEffectiveData;
import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/recurringdepositproducts/baseinterestrates")
@Component
@RequiredArgsConstructor
public class RecurringDepositProductsBaseInterestRatesApiResource {
private final PlatformSecurityContext context;
private final RdBaseInterestRateReadPlatformService rdBaseInterestRateReadPlatformService;
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(LocalDate.class,
(JsonSerializer<LocalDate>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString()))
.create();
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Get all active RD base interest rate charts", description = "Returns the current CBS base interest-rate version with slabs.")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String retrieveAllActiveInterestRates() {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME);
final List<RdInterestRateEffectiveData> data = this.rdBaseInterestRateReadPlatformService.retrieveAllActiveInterestRates();
return gson.toJson(data);
}
@GET
@Path("history")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Get all RD base interest rate charts", description = "Returns the full CBS base chart history.")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String retrieveHistory() {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME);
final List<RdInterestRateEffectiveData> data = this.rdBaseInterestRateReadPlatformService.retrieveAllInterestRates();
return gson.toJson(data);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save new RD base interest rate version", description = "Creates a new CBS base chart version and deactivates the previous active version.")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String saveInterestRates(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME);
final RdInterestRateSaveRequest request = gson.fromJson(apiRequestBodyAsJson, RdInterestRateSaveRequest.class);
final Long createdById = this.context.authenticatedUser().getId();
final RdInterestRateEffectiveData saved = this.rdBaseInterestRateReadPlatformService.saveInterestRates(request, createdById);
return gson.toJson(saved);
}
}
@@ -170,13 +170,21 @@ public class DepositProductDataValidator {
validatePreClosureDetailForCreate(element, baseDataValidator);
validateDepositTermDeatilForCreate(element, baseDataValidator);
// When custom rdSlabs are provided, minDepositTerm / charts / depositAmount are not required
final boolean hasRdSlabs = this.fromApiJsonHelper.parameterExists("rdSlabs", element)
&& this.fromApiJsonHelper.extractJsonArrayNamed("rdSlabs", element) != null
&& this.fromApiJsonHelper.extractJsonArrayNamed("rdSlabs", element).size() > 0;
if (!hasRdSlabs) {
validateDepositTermDeatilForCreate(element, baseDataValidator);
}
validateRecurringDetailForCreate(element, baseDataValidator);
validateChartsData(element, baseDataValidator);
validateDepositAmountForCreate(element, baseDataValidator);
if (!hasRdSlabs) {
validateChartsData(element, baseDataValidator);
validateDepositAmountForCreate(element, baseDataValidator);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@@ -198,13 +206,20 @@ public class DepositProductDataValidator {
validatePreClosureDetailForUpdate(element, baseDataValidator);
validateDepositTermDetailForUpdate(element, baseDataValidator);
final boolean hasRdSlabsForUpdate = this.fromApiJsonHelper.parameterExists("rdSlabs", element)
&& this.fromApiJsonHelper.extractJsonArrayNamed("rdSlabs", element) != null
&& this.fromApiJsonHelper.extractJsonArrayNamed("rdSlabs", element).size() > 0;
if (!hasRdSlabsForUpdate) {
validateDepositTermDetailForUpdate(element, baseDataValidator);
}
validateRecurringDepositUpdate(element, baseDataValidator);
validateChartsData(element, baseDataValidator);
validateDepositAmountForUpdate(element, baseDataValidator);
if (!hasRdSlabsForUpdate) {
validateChartsData(element, baseDataValidator);
validateDepositAmountForUpdate(element, baseDataValidator);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@@ -0,0 +1,31 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.data;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class RdInterestRateEffectiveData {
private Long id;
private Long productId;
private String versionLabel;
private LocalDate effectiveFromDate;
private LocalDate effectiveToDate;
private Boolean isActive;
private String remarks;
private List<RdInterestRateSlabData> slabs;
}
@@ -0,0 +1,30 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class RdInterestRateSlabData {
private Long id;
private Integer srNo;
private String description;
private Integer periodTypeEnum;
private String periodTypeLabel;
private Integer fromPeriod;
private Integer toPeriod;
private BigDecimal annualInterestRate;
}
@@ -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
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "m_rd_base_interest_rate_effective")
@Getter
@NoArgsConstructor
public class RdBaseInterestRateEffective {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "version_label")
private String versionLabel;
@Column(name = "effective_from_date")
private LocalDate effectiveFromDate;
@Column(name = "effective_to_date")
private LocalDate effectiveToDate;
@Column(name = "is_active")
private Boolean isActive;
@Column(name = "remarks")
private String remarks;
@Column(name = "created_by_id")
private Long createdById;
@Column(name = "created_date")
private LocalDate createdDate;
public static RdBaseInterestRateEffective create(String versionLabel, LocalDate effectiveFromDate, String remarks, Long createdById) {
RdBaseInterestRateEffective e = new RdBaseInterestRateEffective();
e.versionLabel = versionLabel;
e.effectiveFromDate = effectiveFromDate;
e.isActive = true;
e.remarks = remarks;
e.createdById = createdById;
e.createdDate = LocalDate.now();
return e;
}
public void deactivate(LocalDate toDate) {
this.isActive = false;
this.effectiveToDate = toDate;
}
}
@@ -0,0 +1,22 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RdBaseInterestRateEffectiveRepository extends JpaRepository<RdBaseInterestRateEffective, Long> {
List<RdBaseInterestRateEffective> findByIsActiveTrueOrderByEffectiveFromDateDesc();
List<RdBaseInterestRateEffective> findByOrderByEffectiveFromDateDesc();
}
@@ -0,0 +1,67 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "m_rd_base_interest_rate_slab_history")
@Getter
@NoArgsConstructor
public class RdBaseInterestRateSlabHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "effective_id")
private Long effectiveId;
@Column(name = "sr_no")
private Integer srNo;
@Column(name = "description")
private String description;
@Column(name = "period_type_enum")
private Integer periodTypeEnum;
@Column(name = "from_period")
private Integer fromPeriod;
@Column(name = "to_period")
private Integer toPeriod;
@Column(name = "annual_interest_rate")
private BigDecimal annualInterestRate;
public static RdBaseInterestRateSlabHistory create(Long effectiveId, Integer srNo,
String description, Integer periodTypeEnum, Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate) {
RdBaseInterestRateSlabHistory s = new RdBaseInterestRateSlabHistory();
s.effectiveId = effectiveId;
s.srNo = srNo;
s.description = description;
s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1;
s.fromPeriod = fromPeriod;
s.toPeriod = toPeriod;
s.annualInterestRate = annualInterestRate;
return s;
}
}
@@ -0,0 +1,20 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RdBaseInterestRateSlabHistoryRepository extends JpaRepository<RdBaseInterestRateSlabHistory, Long> {
List<RdBaseInterestRateSlabHistory> findByEffectiveIdOrderBySrNo(Long effectiveId);
}
@@ -0,0 +1,80 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* Maps to: m_rd_interest_rate_effective
* Stores one "version" of the bank's RD interest rate chart for a product.
* When a new version is saved the old one is deactivated (is_active=0) but kept for history.
*/
@Entity
@Table(name = "m_rd_interest_rate_effective")
@Getter
@NoArgsConstructor
public class RdInterestRateEffective {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_id")
private Long productId;
@Column(name = "version_label")
private String versionLabel;
@Column(name = "effective_from_date")
private LocalDate effectiveFromDate;
@Column(name = "effective_to_date")
private LocalDate effectiveToDate;
@Column(name = "is_active")
private Boolean isActive;
@Column(name = "remarks")
private String remarks;
@Column(name = "created_by_id")
private Long createdById;
@Column(name = "created_date")
private LocalDate createdDate;
public static RdInterestRateEffective create(Long productId, String versionLabel,
LocalDate effectiveFromDate, String remarks, Long createdById) {
RdInterestRateEffective e = new RdInterestRateEffective();
e.productId = productId;
e.versionLabel = versionLabel;
e.effectiveFromDate = effectiveFromDate;
e.isActive = true;
e.remarks = remarks;
e.createdById = createdById;
e.createdDate = LocalDate.now();
return e;
}
public void deactivate(LocalDate toDate) {
this.isActive = false;
this.effectiveToDate = toDate;
}
}
@@ -0,0 +1,24 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RdInterestRateEffectiveRepository extends JpaRepository<RdInterestRateEffective, Long> {
List<RdInterestRateEffective> findByProductIdOrderByEffectiveFromDateDesc(Long productId);
List<RdInterestRateEffective> findByProductIdAndIsActiveTrueOrderByEffectiveFromDateDesc(Long productId);
List<RdInterestRateEffective> findByIsActiveTrueOrderByEffectiveFromDateDesc();
}
@@ -0,0 +1,72 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* Maps to: m_rd_interest_rate_slab_history
* One row per slab per version. All historical slabs are preserved.
*/
@Entity
@Table(name = "m_rd_interest_rate_slab_history")
@Getter
@NoArgsConstructor
public class RdInterestRateSlabHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "effective_id")
private Long effectiveId;
@Column(name = "sr_no")
private Integer srNo;
@Column(name = "description")
private String description;
@Column(name = "period_type_enum")
private Integer periodTypeEnum;
@Column(name = "from_period")
private Integer fromPeriod;
@Column(name = "to_period")
private Integer toPeriod;
@Column(name = "annual_interest_rate")
private BigDecimal annualInterestRate;
public static RdInterestRateSlabHistory create(Long effectiveId, Integer srNo,
String description, Integer periodTypeEnum,
Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate) {
RdInterestRateSlabHistory s = new RdInterestRateSlabHistory();
s.effectiveId = effectiveId;
s.srNo = srNo;
s.description = description;
s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1;
s.fromPeriod = fromPeriod;
s.toPeriod = toPeriod;
s.annualInterestRate = annualInterestRate;
return s;
}
}
@@ -0,0 +1,20 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RdInterestRateSlabHistoryRepository extends JpaRepository<RdInterestRateSlabHistory, Long> {
List<RdInterestRateSlabHistory> findByEffectiveIdOrderBySrNo(Long effectiveId);
}
@@ -301,9 +301,26 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, ");
sqlBuilder.append("sp.account_type as accountType, sp.customer_segment as customerSegment, ");
sqlBuilder.append("sp.product_code as productCode, ");
sqlBuilder.append("sp.effective_date as effectiveDate, sp.effective_to as effectiveTo, sp.end_date as endDate, ");
sqlBuilder.append("sp.introducer_required as introducerRequired, sp.share_linking_enabled as shareLinkingEnabled, ");
sqlBuilder.append("sp.nomination_facility as nominationFacility, sp.dormancy_alert as dormancyAlert, ");
sqlBuilder.append("sp.interest_calculate_selector as interestCalculateSelector, ");
sqlBuilder.append("sp.average_period as averagePeriod, ");
sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay ");
sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, ");
sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, ");
sqlBuilder.append("sp.balance_frequency as balanceFrequency, ");
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, sp.msme as msme, ");
sqlBuilder.append("sp.auto_debit_facility as autoDebitFacility, sp.loan_facility as loanFacility, ");
sqlBuilder.append("sp.premature_withdrawal_option as prematureWithdrawalOption, sp.tenure_flexibility as tenureFlexibility, ");
sqlBuilder.append("sp.lien_marked as lienMarked, sp.maturity_instructions as maturityInstructions, ");
sqlBuilder.append("sp.credit_maturity_proceeds as creditMaturityProceeds, ");
sqlBuilder.append("sp.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, ");
sqlBuilder.append("sp.panel_request_applicable as panelRequestApplicable, ");
sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, ");
sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, ");
sqlBuilder.append("sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate, ");
sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers ");
sqlBuilder.append("from m_savings_product sp ");
sqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sp.id=dptp.savings_product_id ");
sqlBuilder.append("left join m_deposit_product_recurring_detail dprd on sp.id=dprd.savings_product_id ");
@@ -350,10 +367,36 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
final String accountType = rs.getString("accountType");
final String customerSegment = rs.getString("customerSegment");
final String productCode = rs.getString("productCode");
final java.time.LocalDate effectiveDate = JdbcSupport.getLocalDate(rs, "effectiveDate");
final java.time.LocalDate effectiveTo = JdbcSupport.getLocalDate(rs, "effectiveTo");
final java.time.LocalDate endDate = JdbcSupport.getLocalDate(rs, "endDate");
final boolean introducerRequired = rs.getBoolean("introducerRequired");
final boolean shareLinkingEnabled = rs.getBoolean("shareLinkingEnabled");
final boolean nominationFacility = rs.getBoolean("nominationFacility");
final boolean dormancyAlert = rs.getBoolean("dormancyAlert");
final Integer interestCalculateSelector = JdbcSupport.getInteger(rs, "interestCalculateSelector");
final String averagePeriod = rs.getString("averagePeriod");
final Boolean autoPost = rs.getBoolean("autoPost");
final boolean autoPost = rs.getBoolean("autoPost");
final Integer frequencyFixDay = JdbcSupport.getInteger(rs, "frequencyFixDay");
final java.time.LocalDate lastInterestDate = JdbcSupport.getLocalDate(rs, "lastInterestDate");
final boolean enableLockinPeriod = rs.getBoolean("enableLockinPeriod");
final String balanceFrequency = rs.getString("balanceFrequency");
final boolean accountClosedZeroBalanceInDayClosing = rs.getBoolean("accountClosedZeroBalanceInDayClosing");
final boolean balanceSwapOption = rs.getBoolean("balanceSwapOption");
final boolean msme = rs.getBoolean("msme");
final boolean autoDebitFacility = rs.getBoolean("autoDebitFacility");
final boolean loanFacility = rs.getBoolean("loanFacility");
final boolean prematureWithdrawalOption = rs.getBoolean("prematureWithdrawalOption");
final boolean tenureFlexibility = rs.getBoolean("tenureFlexibility");
final boolean lienMarked = rs.getBoolean("lienMarked");
final boolean maturityInstructions = rs.getBoolean("maturityInstructions");
final boolean creditMaturityProceeds = rs.getBoolean("creditMaturityProceeds");
final boolean penaltyGracePeriodMissedInstallments = rs.getBoolean("penaltyGracePeriodMissedInstallments");
final boolean panelRequestApplicable = rs.getBoolean("panelRequestApplicable");
final boolean noPenaltyNomineeHeirClosure = rs.getBoolean("noPenaltyNomineeHeirClosure");
final boolean deceasedSettlement = rs.getBoolean("deceasedSettlement");
final boolean allowDebitTxnAfterDueDate = rs.getBoolean("allowDebitTxnAfterDueDate");
final boolean withdrawalFeeForTransfers = rs.getBoolean("withdrawalFeeForTransfers");
RecurringDepositProductData result = RecurringDepositProductData.instance(depositProductData,
preClosurePenalApplicable, preClosurePenalInterest,
@@ -363,10 +406,36 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
result.setAccountType(accountType);
result.setCustomerSegment(customerSegment);
result.setProductCode(productCode);
result.setEffectiveDate(effectiveDate);
result.setEffectiveTo(effectiveTo);
result.setEndDate(endDate);
result.setIntroducerRequired(introducerRequired);
result.setShareLinkingEnabled(shareLinkingEnabled);
result.setNominationFacility(nominationFacility);
result.setDormancyAlert(dormancyAlert);
result.setInterestCalculateSelector(interestCalculateSelector);
result.setAveragePeriod(averagePeriod);
result.setAutoPost(autoPost);
result.setFrequencyFixDay(frequencyFixDay);
result.setLastInterestDate(lastInterestDate);
result.setEnableLockinPeriod(enableLockinPeriod);
result.setBalanceFrequency(balanceFrequency);
result.setAccountClosedZeroBalanceInDayClosing(accountClosedZeroBalanceInDayClosing);
result.setBalanceSwapOption(balanceSwapOption);
result.setMsme(msme);
result.setAutoDebitFacility(autoDebitFacility);
result.setLoanFacility(loanFacility);
result.setPrematureWithdrawalOption(prematureWithdrawalOption);
result.setTenureFlexibility(tenureFlexibility);
result.setLienMarked(lienMarked);
result.setMaturityInstructions(maturityInstructions);
result.setCreditMaturityProceeds(creditMaturityProceeds);
result.setPenaltyGracePeriodMissedInstallments(penaltyGracePeriodMissedInstallments);
result.setPanelRequestApplicable(panelRequestApplicable);
result.setNoPenaltyNomineeHeirClosure(noPenaltyNomineeHeirClosure);
result.setDeceasedSettlement(deceasedSettlement);
result.setAllowDebitTxnAfterDueDate(allowDebitTxnAfterDueDate);
result.setWithdrawalFeeForTransfers(withdrawalFeeForTransfers);
return result;
}
}
@@ -0,0 +1,25 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.service;
import java.util.List;
import org.apache.fineract.portfolio.savings.api.RdInterestRateSaveRequest;
import org.apache.fineract.portfolio.savings.data.RdInterestRateEffectiveData;
public interface RdBaseInterestRateReadPlatformService {
List<RdInterestRateEffectiveData> retrieveAllInterestRates();
List<RdInterestRateEffectiveData> retrieveAllActiveInterestRates();
RdInterestRateEffectiveData saveInterestRates(RdInterestRateSaveRequest request, Long createdById);
}
@@ -0,0 +1,202 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.savings.api.RdInterestRateSaveRequest;
import org.apache.fineract.portfolio.savings.data.RdInterestRateEffectiveData;
import org.apache.fineract.portfolio.savings.data.RdInterestRateSlabData;
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateEffective;
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateEffectiveRepository;
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateSlabHistory;
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateSlabHistoryRepository;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class RdBaseInterestRateReadPlatformServiceImpl implements RdBaseInterestRateReadPlatformService {
private final RdBaseInterestRateEffectiveRepository effectiveRepository;
private final RdBaseInterestRateSlabHistoryRepository slabRepository;
private static final String[] PERIOD_LABELS = { "", "Days", "Months", "Years" };
@Override
public List<RdInterestRateEffectiveData> retrieveAllInterestRates() {
return effectiveRepository.findByOrderByEffectiveFromDateDesc().stream().map(this::toData).collect(Collectors.toList());
}
@Override
public List<RdInterestRateEffectiveData> retrieveAllActiveInterestRates() {
return effectiveRepository.findByIsActiveTrueOrderByEffectiveFromDateDesc().stream().map(this::toData).collect(Collectors.toList());
}
@Override
@Transactional
public RdInterestRateEffectiveData saveInterestRates(RdInterestRateSaveRequest request, Long createdById) {
validateRequest(request);
final List<RdInterestRateSaveRequest.SlabRequest> normalizedSlabs = normalizeSlabs(request.getSlabs());
final LocalDate today = LocalDate.now();
final List<RdBaseInterestRateEffective> activeVersions = effectiveRepository.findByIsActiveTrueOrderByEffectiveFromDateDesc();
for (RdBaseInterestRateEffective version : activeVersions) {
version.deactivate(today);
}
if (!activeVersions.isEmpty()) {
effectiveRepository.saveAll(activeVersions);
}
final String label = request.getVersionLabel() != null && !request.getVersionLabel().isBlank()
? request.getVersionLabel()
: "CBS Base Rate Version " + LocalDate.now();
RdBaseInterestRateEffective newVersion = RdBaseInterestRateEffective.create(label, LocalDate.now(), request.getRemarks(), createdById);
newVersion = effectiveRepository.saveAndFlush(newVersion);
final Long versionId = newVersion.getId();
final AtomicInteger srCounter = new AtomicInteger(1);
for (RdInterestRateSaveRequest.SlabRequest slabRequest : normalizedSlabs) {
final int sr = slabRequest.getSrNo() != null ? slabRequest.getSrNo() : srCounter.getAndIncrement();
final RdBaseInterestRateSlabHistory slab = RdBaseInterestRateSlabHistory.create(versionId, sr, slabRequest.getDescription(),
slabRequest.getPeriodTypeEnum(), slabRequest.getFromPeriod(), slabRequest.getToPeriod(),
slabRequest.getAnnualInterestRate());
slabRepository.save(slab);
}
return toData(newVersion);
}
private void validateRequest(final RdInterestRateSaveRequest request) {
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("RD_BASE_INTEREST_RATE_VERSION");
if (request == null) {
validator.failWithCodeNoParameterAddedToErrorCode("request.cannot.be.null");
throwExceptionIfValidationWarningsExist(errors);
return;
}
if (request.getSlabs() == null || request.getSlabs().isEmpty()) {
validator.parameter("slabs").failWithCode("cannot.be.blank");
}
if (request.getVersionLabel() != null && request.getVersionLabel().length() > 100) {
validator.parameter("versionLabel").value(request.getVersionLabel()).failWithCode("exceeds.max.length");
}
throwExceptionIfValidationWarningsExist(errors);
}
private List<RdInterestRateSaveRequest.SlabRequest> normalizeSlabs(final List<RdInterestRateSaveRequest.SlabRequest> slabs) {
final List<RdInterestRateSaveRequest.SlabRequest> sorted = new ArrayList<>(slabs);
sorted.sort(Comparator.comparing((RdInterestRateSaveRequest.SlabRequest slab) -> slab.getSrNo() == null ? Integer.MAX_VALUE : slab.getSrNo())
.thenComparing(slab -> slab.getFromPeriod() == null ? Integer.MAX_VALUE : slab.getFromPeriod()));
final List<RdInterestRateSaveRequest.SlabRequest> normalized = new ArrayList<>(sorted.size());
Integer expectedFrom = 0;
for (int i = 0; i < sorted.size(); i++) {
final RdInterestRateSaveRequest.SlabRequest source = sorted.get(i);
final RdInterestRateSaveRequest.SlabRequest target = new RdInterestRateSaveRequest.SlabRequest();
final Integer fromPeriod = source.getFromPeriod();
if (fromPeriod == null || !fromPeriod.equals(expectedFrom)) {
throwValidationError("fromPeriod", "must.start.from.previous.range.end");
}
target.setFromPeriod(fromPeriod);
final Integer toPeriod = source.getToPeriod();
if (i < sorted.size() - 1) {
if (toPeriod == null) {
throwValidationError("toPeriod", "cannot.be.blank.for.intermediate.slab");
}
if (toPeriod < fromPeriod) {
throwValidationError("toPeriod", "cannot.be.less.than.fromPeriod");
}
expectedFrom = toPeriod + 1;
target.setToPeriod(toPeriod);
} else {
target.setToPeriod(null);
}
target.setSrNo(source.getSrNo() == null ? i + 1 : source.getSrNo());
target.setDescription(source.getDescription());
target.setPeriodTypeEnum(source.getPeriodTypeEnum() == null ? 1 : source.getPeriodTypeEnum());
target.setAnnualInterestRate(source.getAnnualInterestRate());
if (target.getAnnualInterestRate() == null) {
throwValidationError("annualInterestRate", "cannot.be.blank");
}
if (target.getAnnualInterestRate().signum() < 0) {
throwValidationError("annualInterestRate", "must.be.non.negative");
}
normalized.add(target);
}
if (!normalized.isEmpty() && normalized.get(0).getFromPeriod() != 0) {
throwValidationError("fromPeriod", "must.start.from.zero");
}
return normalized;
}
private void throwValidationError(final String parameterName, final String code) {
final List<ApiParameterError> errors = new ArrayList<>();
new DataValidatorBuilder(errors).resource("RD_BASE_INTEREST_RATE_VERSION").parameter(parameterName).failWithCode(code);
throwExceptionIfValidationWarningsExist(errors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
private RdInterestRateEffectiveData toData(RdBaseInterestRateEffective e) {
List<RdInterestRateSlabData> slabs = slabRepository.findByEffectiveIdOrderBySrNo(e.getId()).stream()
.map(s -> RdInterestRateSlabData.builder()
.id(s.getId())
.srNo(s.getSrNo())
.description(s.getDescription())
.periodTypeEnum(s.getPeriodTypeEnum())
.periodTypeLabel(periodLabel(s.getPeriodTypeEnum()))
.fromPeriod(s.getFromPeriod())
.toPeriod(s.getToPeriod())
.annualInterestRate(s.getAnnualInterestRate())
.build())
.collect(Collectors.toList());
return RdInterestRateEffectiveData.builder()
.id(e.getId())
.productId(null)
.versionLabel(e.getVersionLabel())
.effectiveFromDate(e.getEffectiveFromDate())
.effectiveToDate(e.getEffectiveToDate())
.isActive(e.getIsActive())
.remarks(e.getRemarks())
.slabs(slabs)
.build();
}
private String periodLabel(Integer code) {
if (code == null || code < 1 || code >= PERIOD_LABELS.length) return "Days";
return PERIOD_LABELS[code];
}
}
@@ -0,0 +1,32 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.service;
import java.util.List;
import org.apache.fineract.portfolio.savings.api.RdInterestRateSaveRequest;
import org.apache.fineract.portfolio.savings.data.RdInterestRateEffectiveData;
public interface RdInterestRateReadPlatformService {
/** All versions (with slabs) for a specific product, newest first. */
List<RdInterestRateEffectiveData> retrieveInterestRates(Long productId);
/** All active versions across all RD products – used as reference on the create-product page. */
List<RdInterestRateEffectiveData> retrieveAllActiveInterestRates();
/**
* Saves a NEW version of the rate chart for the given product.
* Deactivates the previous active version first (immutable history).
* Returns the newly created version's data.
*/
RdInterestRateEffectiveData saveInterestRates(Long productId, RdInterestRateSaveRequest request, Long createdById);
}
@@ -0,0 +1,216 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.savings.api.RdInterestRateSaveRequest;
import org.apache.fineract.portfolio.savings.data.RdInterestRateEffectiveData;
import org.apache.fineract.portfolio.savings.data.RdInterestRateSlabData;
import org.apache.fineract.portfolio.savings.domain.RdInterestRateEffective;
import org.apache.fineract.portfolio.savings.domain.RdInterestRateEffectiveRepository;
import org.apache.fineract.portfolio.savings.domain.RdInterestRateSlabHistory;
import org.apache.fineract.portfolio.savings.domain.RdInterestRateSlabHistoryRepository;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class RdInterestRateReadPlatformServiceImpl implements RdInterestRateReadPlatformService {
private final RdInterestRateEffectiveRepository effectiveRepository;
private final RdInterestRateSlabHistoryRepository slabRepository;
private static final String[] PERIOD_LABELS = { "", "Days", "Months", "Years" };
@Override
public List<RdInterestRateEffectiveData> retrieveInterestRates(Long productId) {
return effectiveRepository.findByProductIdOrderByEffectiveFromDateDesc(productId)
.stream().map(this::toData).collect(Collectors.toList());
}
@Override
public List<RdInterestRateEffectiveData> retrieveAllActiveInterestRates() {
return effectiveRepository.findByIsActiveTrueOrderByEffectiveFromDateDesc()
.stream().map(this::toData).collect(Collectors.toList());
}
@Override
@Transactional
public RdInterestRateEffectiveData saveInterestRates(Long productId,
RdInterestRateSaveRequest request, Long createdById) {
validateRequest(request);
final List<RdInterestRateSaveRequest.SlabRequest> normalizedSlabs = normalizeSlabs(request.getSlabs());
// 1. Deactivate all current active versions for this product, preserving end dates for audit/history.
final LocalDate today = LocalDate.now();
final List<RdInterestRateEffective> activeVersions = effectiveRepository
.findByProductIdAndIsActiveTrueOrderByEffectiveFromDateDesc(productId);
for (RdInterestRateEffective version : activeVersions) {
version.deactivate(today);
}
if (!activeVersions.isEmpty()) {
effectiveRepository.saveAll(activeVersions);
}
// 2. Create the new version header
String label = request.getVersionLabel() != null && !request.getVersionLabel().isBlank()
? request.getVersionLabel()
: "Rate Version " + LocalDate.now();
RdInterestRateEffective newVersion = RdInterestRateEffective.create(
productId, label, LocalDate.now(), request.getRemarks(), createdById);
newVersion = effectiveRepository.saveAndFlush(newVersion);
// 3. Insert slabs for the new version
final Long versionId = newVersion.getId();
AtomicInteger srCounter = new AtomicInteger(1);
for (RdInterestRateSaveRequest.SlabRequest s : normalizedSlabs) {
int sr = s.getSrNo() != null ? s.getSrNo() : srCounter.getAndIncrement();
RdInterestRateSlabHistory slab = RdInterestRateSlabHistory.create(
versionId, sr, s.getDescription(),
s.getPeriodTypeEnum(), s.getFromPeriod(), s.getToPeriod(),
s.getAnnualInterestRate());
slabRepository.save(slab);
}
return toData(newVersion);
}
private void validateRequest(final RdInterestRateSaveRequest request) {
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("RD_INTEREST_RATE_VERSION");
if (request == null) {
validator.failWithCodeNoParameterAddedToErrorCode("request.cannot.be.null");
throwExceptionIfValidationWarningsExist(errors);
return;
}
if (request.getSlabs() == null || request.getSlabs().isEmpty()) {
validator.parameter("slabs").failWithCode("cannot.be.blank");
}
if (request.getVersionLabel() != null && request.getVersionLabel().length() > 100) {
validator.parameter("versionLabel").value(request.getVersionLabel()).failWithCode("exceeds.max.length");
}
throwExceptionIfValidationWarningsExist(errors);
}
private List<RdInterestRateSaveRequest.SlabRequest> normalizeSlabs(final List<RdInterestRateSaveRequest.SlabRequest> slabs) {
final List<RdInterestRateSaveRequest.SlabRequest> sorted = new ArrayList<>(slabs);
sorted.sort(Comparator.comparing((RdInterestRateSaveRequest.SlabRequest slab) -> slab.getSrNo() == null ? Integer.MAX_VALUE : slab.getSrNo())
.thenComparing(slab -> slab.getFromPeriod() == null ? Integer.MAX_VALUE : slab.getFromPeriod()));
final List<RdInterestRateSaveRequest.SlabRequest> normalized = new ArrayList<>(sorted.size());
Integer expectedFrom = 0;
for (int i = 0; i < sorted.size(); i++) {
final RdInterestRateSaveRequest.SlabRequest source = sorted.get(i);
final RdInterestRateSaveRequest.SlabRequest target = new RdInterestRateSaveRequest.SlabRequest();
final Integer fromPeriod = source.getFromPeriod();
if (fromPeriod == null || !fromPeriod.equals(expectedFrom)) {
throwValidationError("fromPeriod", "must.start.from.previous.range.end");
}
target.setFromPeriod(fromPeriod);
final Integer toPeriod = source.getToPeriod();
if (i < sorted.size() - 1) {
if (toPeriod == null) {
throwValidationError("toPeriod", "cannot.be.blank.for.intermediate.slab");
}
if (toPeriod < fromPeriod) {
throwValidationError("toPeriod", "cannot.be.less.than.fromPeriod");
}
expectedFrom = toPeriod + 1;
target.setToPeriod(toPeriod);
} else {
target.setToPeriod(null);
}
target.setSrNo(source.getSrNo() == null ? i + 1 : source.getSrNo());
target.setDescription(source.getDescription());
target.setPeriodTypeEnum(source.getPeriodTypeEnum() == null ? 1 : source.getPeriodTypeEnum());
target.setAnnualInterestRate(source.getAnnualInterestRate());
if (target.getAnnualInterestRate() == null) {
throwValidationError("annualInterestRate", "cannot.be.blank");
}
if (target.getAnnualInterestRate().signum() < 0) {
throwValidationError("annualInterestRate", "must.be.non.negative");
}
normalized.add(target);
}
if (!normalized.isEmpty() && normalized.get(0).getFromPeriod() != 0) {
throwValidationError("fromPeriod", "must.start.from.zero");
}
return normalized;
}
private void throwValidationError(final String parameterName, final String code) {
final List<ApiParameterError> errors = new ArrayList<>();
new DataValidatorBuilder(errors).resource("RD_INTEREST_RATE_VERSION").parameter(parameterName).failWithCode(code);
throwExceptionIfValidationWarningsExist(errors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
// ── helpers ─────────────────────────────────────────────────────────────
private RdInterestRateEffectiveData toData(RdInterestRateEffective e) {
List<RdInterestRateSlabData> slabs = slabRepository
.findByEffectiveIdOrderBySrNo(e.getId())
.stream()
.map(s -> RdInterestRateSlabData.builder()
.id(s.getId())
.srNo(s.getSrNo())
.description(s.getDescription())
.periodTypeEnum(s.getPeriodTypeEnum())
.periodTypeLabel(periodLabel(s.getPeriodTypeEnum()))
.fromPeriod(s.getFromPeriod())
.toPeriod(s.getToPeriod())
.annualInterestRate(s.getAnnualInterestRate())
.build())
.collect(Collectors.toList());
return RdInterestRateEffectiveData.builder()
.id(e.getId())
.productId(e.getProductId())
.versionLabel(e.getVersionLabel())
.effectiveFromDate(e.getEffectiveFromDate())
.effectiveToDate(e.getEffectiveToDate())
.isActive(e.getIsActive())
.remarks(e.getRemarks())
.slabs(slabs)
.build();
}
private String periodLabel(Integer code) {
if (code == null || code < 1 || code >= PERIOD_LABELS.length) return "Days";
return PERIOD_LABELS[code];
}
}
@@ -144,6 +144,10 @@ import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformS
import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatformService;
import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatformServiceJpaRepositoryImpl;
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateEffectiveRepository;
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateSlabHistoryRepository;
import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformService;
import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPoster;
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPosterTask;
import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService;
@@ -159,6 +163,14 @@ import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class SavingsConfiguration {
@Bean
@ConditionalOnMissingBean(RdBaseInterestRateReadPlatformService.class)
public RdBaseInterestRateReadPlatformService rdBaseInterestRateReadPlatformService(
RdBaseInterestRateEffectiveRepository effectiveRepository,
RdBaseInterestRateSlabHistoryRepository slabRepository) {
return new RdBaseInterestRateReadPlatformServiceImpl(effectiveRepository, slabRepository);
}
@Bean
@ConditionalOnMissingBean(SavingsAccountTransactionSearchService.class)
public SavingsAccountTransactionSearchService savingsAccountTransactionSearchService(PlatformSecurityContext context,
@@ -271,4 +271,9 @@
<include file="parts/0251_add_savings_product_enhanced_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0255_create_current_product_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0256_add_recurring_deposit_product_codes.xml" relativeToChangelogFile="true"/>
<include file="parts/0257_create_rd_interest_rate_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0267_seed_rd_interest_rate_data.xml" relativeToChangelogFile="true"/>
<include file="parts/0270_add_rd_product_extra_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0271_create_rd_base_interest_rate_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0272_seed_rd_base_interest_rate_data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
m_rd_interest_rate_effective — one row per rate-version per product.
m_rd_interest_rate_slab_history — slabs for every version (immutable once written).
Period Type Enum: 1 = Days, 2 = Months, 3 = Years
-->
<changeSet id="0257a_create_rd_interest_rate_effective" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_rd_interest_rate_effective"/></not>
</preConditions>
<createTable tableName="m_rd_interest_rate_effective">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="product_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="version_label" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="effective_from_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="effective_to_date" type="DATE"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="remarks" type="VARCHAR(500)"/>
<column name="created_by_id" type="BIGINT"/>
<column name="created_date" type="DATE"/>
</createTable>
<addForeignKeyConstraint
baseTableName="m_rd_interest_rate_effective"
baseColumnNames="product_id"
referencedTableName="m_savings_product"
referencedColumnNames="id"
constraintName="fk_rd_rate_effective_product"/>
</changeSet>
<changeSet id="0257b_create_rd_interest_rate_slab_history" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_rd_interest_rate_slab_history"/></not>
</preConditions>
<createTable tableName="m_rd_interest_rate_slab_history">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="effective_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="sr_no" type="INT">
<constraints nullable="false"/>
</column>
<column name="description" type="VARCHAR(200)"/>
<column name="period_type_enum" type="INT" defaultValueNumeric="1">
<constraints nullable="false"/>
</column>
<column name="from_period" type="INT">
<constraints nullable="false"/>
</column>
<column name="to_period" type="INT"/>
<column name="annual_interest_rate" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="created_date" type="DATE"/>
</createTable>
<addForeignKeyConstraint
baseTableName="m_rd_interest_rate_slab_history"
baseColumnNames="effective_id"
referencedTableName="m_rd_interest_rate_effective"
referencedColumnNames="id"
constraintName="fk_rd_rate_slab_effective"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Seed RD Interest Rate Data (runs only if both tables are empty)
Sr Particular Rate
1 0 Days to 29 Days 3.00%
2 30 Days to 45 Days 3.00%
3 46 Days to 90 Days 3.00%
4 91 Days to 180 Days 4.50%
5 181 Days to below 1 Year 5.50%
6 1 Year to Below 2 Year 7.00%
7 444 Days 7.00%
8 400 Days 7.00%
9 500 Days 7.00%
10 2 Year to Below 3 Year 7.00%
11 3 Year to Below 5 Year 7.25%
12 5 Year &amp; Above 6.50%
-->
<changeSet id="0267_insert_rd_rate_version" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_rd_interest_rate_effective</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_rd_interest_rate_effective
(product_id, version_label, effective_from_date, effective_to_date, is_active, remarks, created_by_id, created_date)
SELECT id, 'Initial Rate Version', '2025-01-01', NULL, TRUE,
'Standard interest rate card published for RD products', 1, CURRENT_DATE
FROM m_savings_product
WHERE deposit_type_enum = 300
ORDER BY id LIMIT 1;
</sql>
</changeSet>
<changeSet id="0268_insert_rd_rate_slabs" author="fineract">
<validCheckSum>9:e35a0696046940f370f7cfb55c20740b</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_rd_interest_rate_slab_history</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 1, '0 Days to 29 Days', 1, 0, 29, 3.000000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 2, '30 Days to 45 Days', 1, 30, 45, 3.000000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 3, '46 Days to 90 Days', 1, 46, 90, 3.000000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 4, '91 Days to 180 Days', 1, 91, 180, 4.500000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 5, '181 Days to below 1 Year', 1, 181, 364, 5.500000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 6, '1 Year to Below 2 Year', 1, 365, 729, 7.000000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 7, '444 Days', 1, 444, 444, 7.000000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 8, '400 Days', 1, 400, 400, 7.000000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 9, '500 Days', 1, 500, 500, 7.000000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 10, '2 Year to Below 3 Year', 1, 730, 1094, 7.000000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 11, '3 Year to Below 5 Year', 1, 1095, 1824, 7.250000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
INSERT INTO m_rd_interest_rate_slab_history (effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT e.id, 12, '5 Year &amp; Above', 1, 1825, NULL, 6.500000, CURRENT_DATE FROM m_rd_interest_rate_effective e WHERE e.version_label='Initial Rate Version' AND e.product_id=(SELECT id FROM m_savings_product WHERE deposit_type_enum=300 ORDER BY id LIMIT 1);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- balance_frequency — may already exist from a prior manual migration -->
<changeSet id="0270a_add_balance_frequency" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_savings_product" columnName="balance_frequency"/></not>
</preConditions>
<addColumn tableName="m_savings_product">
<column name="balance_frequency" type="varchar(50)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<!-- account_closed_zero_balance_in_day_closing — may already exist -->
<changeSet id="0270b_add_account_closed_zero_balance" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_savings_product" columnName="account_closed_zero_balance_in_day_closing"/></not>
</preConditions>
<addColumn tableName="m_savings_product">
<column name="account_closed_zero_balance_in_day_closing" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<!-- 14 new RD-specific boolean flags -->
<changeSet id="0270c_add_rd_product_flags" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_savings_product" columnName="balance_swap_option"/></not>
</preConditions>
<addColumn tableName="m_savings_product">
<column name="balance_swap_option" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="msme" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="auto_debit_facility" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="loan_facility" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="premature_withdrawal_option" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="tenure_flexibility" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="lien_marked" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="maturity_instructions" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="credit_maturity_proceeds" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="penalty_grace_period_missed_installments" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="panel_request_applicable" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="no_penalty_nominee_heir_closure" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="deceased_settlement" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="allow_debit_txn_after_due_date" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0271a_create_rd_base_interest_rate_effective" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_rd_base_interest_rate_effective"/></not>
</preConditions>
<createTable tableName="m_rd_base_interest_rate_effective">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="version_label" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="effective_from_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="effective_to_date" type="DATE"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="remarks" type="VARCHAR(500)"/>
<column name="created_by_id" type="BIGINT"/>
<column name="created_date" type="DATE"/>
</createTable>
</changeSet>
<changeSet id="0271b_create_rd_base_interest_rate_slab_history" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_rd_base_interest_rate_slab_history"/></not>
</preConditions>
<createTable tableName="m_rd_base_interest_rate_slab_history">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="effective_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="sr_no" type="INT">
<constraints nullable="false"/>
</column>
<column name="description" type="VARCHAR(200)"/>
<column name="period_type_enum" type="INT" defaultValueNumeric="1">
<constraints nullable="false"/>
</column>
<column name="from_period" type="INT">
<constraints nullable="false"/>
</column>
<column name="to_period" type="INT"/>
<column name="annual_interest_rate" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="created_date" type="DATE"/>
</createTable>
<addForeignKeyConstraint
baseTableName="m_rd_base_interest_rate_slab_history"
baseColumnNames="effective_id"
referencedTableName="m_rd_base_interest_rate_effective"
referencedColumnNames="id"
constraintName="fk_rd_base_rate_slab_effective"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0272_insert_rd_base_rate_version" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_rd_base_interest_rate_effective</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_rd_base_interest_rate_effective
(version_label, effective_from_date, effective_to_date, is_active, remarks, created_by_id, created_date)
VALUES
('CBS Base Rate Version', '2025-01-01', NULL, TRUE, 'Central RD base chart', 1, CURRENT_DATE);
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 1, '15 Days to 29 Days', 1, 15, 29, 3.000000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 2, '30 Days to 45 Days', 1, 30, 45, 3.000000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 3, '46 Days to 90 Days', 1, 46, 90, 3.000000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 4, '91 Days to 180 Days', 1, 91, 180, 4.500000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 5, '181 Days to below 1 Year', 1, 181, 364, 5.500000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 6, '1 Year to Below 2 Year', 1, 365, 729, 7.000000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 7, '444 Days', 1, 444, 444, 7.000000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 8, '400 Days', 1, 400, 400, 7.000000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 9, '500 Days', 1, 500, 500, 7.000000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 10, '2 Year to Below 3 Year', 1, 730, 1094, 7.000000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 11, '3 Year to Below 5 Year', 1, 1095, 1824, 7.250000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
INSERT INTO m_rd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, created_date)
SELECT id, 12, '5 Year &amp; Above', 1, 1825, NULL, 6.500000, CURRENT_DATE
FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version';
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,31 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.data;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public final class RdInterestRateEffectiveData {
private final Long id;
private final Long productId;
private final String versionLabel;
private final LocalDate effectiveFromDate;
private final LocalDate effectiveToDate;
private final Boolean isActive;
private final String remarks;
private final List<RdInterestRateSlabData> slabs;
}
@@ -0,0 +1,30 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public final class RdInterestRateSlabData {
private final Long id;
private final Integer srNo;
private final String description;
private final Integer periodTypeEnum;
private final String periodTypeLabel;
private final Integer fromPeriod;
private final Integer toPeriod;
private final BigDecimal annualInterestRate;
}
@@ -65,10 +65,36 @@ public final class RecurringDepositProductData extends DepositProductData {
private String accountType;
private String customerSegment;
private String productCode;
private java.time.LocalDate effectiveDate;
private java.time.LocalDate effectiveTo;
private java.time.LocalDate endDate;
private Boolean introducerRequired;
private Boolean shareLinkingEnabled;
private Boolean nominationFacility;
private Boolean dormancyAlert;
private Integer interestCalculateSelector;
private String averagePeriod;
private Boolean autoPost;
private Integer frequencyFixDay;
private java.time.LocalDate lastInterestDate;
private Boolean enableLockinPeriod;
private String balanceFrequency;
private Boolean accountClosedZeroBalanceInDayClosing;
private Boolean balanceSwapOption;
private Boolean msme;
private Boolean autoDebitFacility;
private Boolean loanFacility;
private Boolean prematureWithdrawalOption;
private Boolean tenureFlexibility;
private Boolean lienMarked;
private Boolean maturityInstructions;
private Boolean creditMaturityProceeds;
private Boolean penaltyGracePeriodMissedInstallments;
private Boolean panelRequestApplicable;
private Boolean noPenaltyNomineeHeirClosure;
private Boolean deceasedSettlement;
private Boolean allowDebitTxnAfterDueDate;
private Boolean withdrawalFeeForTransfers;
public void setInterestCalculationTypeOption(Collection<EnumOptionData> v) { this.interestCalculationTypeOption = v; }
public void setPeriodFrequencyOptions(Collection<EnumOptionData> v) { this.periodFrequencyOptions = v; }
@@ -78,10 +104,84 @@ public final class RecurringDepositProductData extends DepositProductData {
public void setAccountType(String v) { this.accountType = v; }
public void setCustomerSegment(String v) { this.customerSegment = v; }
public void setProductCode(String v) { this.productCode = v; }
public void setEffectiveDate(java.time.LocalDate v) { this.effectiveDate = v; }
public void setEffectiveTo(java.time.LocalDate v) { this.effectiveTo = v; }
public void setEndDate(java.time.LocalDate v) { this.endDate = v; }
public void setIntroducerRequired(Boolean v) { this.introducerRequired = v; }
public void setShareLinkingEnabled(Boolean v) { this.shareLinkingEnabled = v; }
public void setNominationFacility(Boolean v) { this.nominationFacility = v; }
public void setDormancyAlert(Boolean v) { this.dormancyAlert = v; }
public void setInterestCalculateSelector(Integer v) { this.interestCalculateSelector = v; }
public void setAveragePeriod(String v) { this.averagePeriod = v; }
public void setAutoPost(Boolean v) { this.autoPost = v; }
public void setFrequencyFixDay(Integer v) { this.frequencyFixDay = v; }
public void setLastInterestDate(java.time.LocalDate v) { this.lastInterestDate = v; }
public void setEnableLockinPeriod(Boolean v) { this.enableLockinPeriod = v; }
public void setBalanceFrequency(String v) { this.balanceFrequency = v; }
public void setAccountClosedZeroBalanceInDayClosing(Boolean v) { this.accountClosedZeroBalanceInDayClosing = v; }
public void setBalanceSwapOption(Boolean v) { this.balanceSwapOption = v; }
public void setMsme(Boolean v) { this.msme = v; }
public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; }
public void setLoanFacility(Boolean v) { this.loanFacility = v; }
public void setPrematureWithdrawalOption(Boolean v) { this.prematureWithdrawalOption = v; }
public void setTenureFlexibility(Boolean v) { this.tenureFlexibility = v; }
public void setLienMarked(Boolean v) { this.lienMarked = v; }
public void setMaturityInstructions(Boolean v) { this.maturityInstructions = v; }
public void setCreditMaturityProceeds(Boolean v) { this.creditMaturityProceeds = v; }
public void setPenaltyGracePeriodMissedInstallments(Boolean v) { this.penaltyGracePeriodMissedInstallments = v; }
public void setPanelRequestApplicable(Boolean v) { this.panelRequestApplicable = v; }
public void setNoPenaltyNomineeHeirClosure(Boolean v) { this.noPenaltyNomineeHeirClosure = v; }
public void setDeceasedSettlement(Boolean v) { this.deceasedSettlement = v; }
public void setAllowDebitTxnAfterDueDate(Boolean v) { this.allowDebitTxnAfterDueDate = v; }
public void setWithdrawalFeeForTransfers(Boolean v) { this.withdrawalFeeForTransfers = v; }
/** Copy all CBS custom fields from source into this instance. */
public void copyCbsFieldsFrom(final RecurringDepositProductData src) {
if (src == null) return;
this.accountType = src.accountType;
this.customerSegment = src.customerSegment;
this.productCode = src.productCode;
this.effectiveDate = src.effectiveDate;
this.effectiveTo = src.effectiveTo;
this.endDate = src.endDate;
this.introducerRequired = src.introducerRequired;
this.shareLinkingEnabled = src.shareLinkingEnabled;
this.nominationFacility = src.nominationFacility;
this.dormancyAlert = src.dormancyAlert;
this.interestCalculateSelector = src.interestCalculateSelector;
this.averagePeriod = src.averagePeriod;
this.autoPost = src.autoPost;
this.frequencyFixDay = src.frequencyFixDay;
this.lastInterestDate = src.lastInterestDate;
this.enableLockinPeriod = src.enableLockinPeriod;
this.balanceFrequency = src.balanceFrequency;
this.accountClosedZeroBalanceInDayClosing = src.accountClosedZeroBalanceInDayClosing;
this.balanceSwapOption = src.balanceSwapOption;
this.msme = src.msme;
this.autoDebitFacility = src.autoDebitFacility;
this.loanFacility = src.loanFacility;
this.prematureWithdrawalOption = src.prematureWithdrawalOption;
this.tenureFlexibility = src.tenureFlexibility;
this.lienMarked = src.lienMarked;
this.maturityInstructions = src.maturityInstructions;
this.creditMaturityProceeds = src.creditMaturityProceeds;
this.penaltyGracePeriodMissedInstallments = src.penaltyGracePeriodMissedInstallments;
this.panelRequestApplicable = src.panelRequestApplicable;
this.noPenaltyNomineeHeirClosure = src.noPenaltyNomineeHeirClosure;
this.deceasedSettlement = src.deceasedSettlement;
this.allowDebitTxnAfterDueDate = src.allowDebitTxnAfterDueDate;
this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers;
// options
this.interestCalculationTypeOption = src.interestCalculationTypeOption;
this.periodFrequencyOptions = src.periodFrequencyOptions;
this.yesNoOptions = src.yesNoOptions;
this.accountTypeOptions = src.accountTypeOptions;
this.customerSegmentOptions = src.customerSegmentOptions;
}
public String getProductCode() {
return productCode;
}
public static RecurringDepositProductData template(final CurrencyData currency, final EnumOptionData interestCompoundingPeriodType,
final EnumOptionData interestPostingPeriodType, final EnumOptionData interestCalculationType,
@@ -146,7 +246,8 @@ public final class RecurringDepositProductData extends DepositProductData {
public static RecurringDepositProductData withCharges(final RecurringDepositProductData existingProduct,
final Collection<ChargeData> charges) {
return new RecurringDepositProductData(existingProduct.id, existingProduct.name, existingProduct.shortName,
final RecurringDepositProductData result = new RecurringDepositProductData(existingProduct.id, existingProduct.name,
existingProduct.shortName,
existingProduct.description, existingProduct.currency, existingProduct.nominalAnnualInterestRate,
existingProduct.interestCompoundingPeriodType, existingProduct.interestPostingPeriodType,
existingProduct.interestCalculationType, existingProduct.interestCalculationDaysInYearType,
@@ -166,6 +267,8 @@ public final class RecurringDepositProductData extends DepositProductData {
existingProduct.isMandatoryDeposit, existingProduct.allowWithdrawal, existingProduct.adjustAdvanceTowardsFuturePayments,
existingProduct.periodFrequencyTypeOptions, existingProduct.minDepositAmount, existingProduct.depositAmount,
existingProduct.maxDepositAmount, existingProduct.withHoldTax, existingProduct.taxGroup, existingProduct.taxGroupOptions);
result.copyCbsFieldsFrom(existingProduct);
return result;
}
/**
@@ -187,7 +290,8 @@ public final class RecurringDepositProductData extends DepositProductData {
final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions,
final Collection<EnumOptionData> periodFrequencyTypeOptions, final Collection<TaxGroupData> taxGroupOptions) {
return new RecurringDepositProductData(existingProduct.id, existingProduct.name, existingProduct.shortName,
final RecurringDepositProductData result = new RecurringDepositProductData(existingProduct.id, existingProduct.name,
existingProduct.shortName,
existingProduct.description, existingProduct.currency, existingProduct.nominalAnnualInterestRate,
existingProduct.interestCompoundingPeriodType, existingProduct.interestPostingPeriodType,
existingProduct.interestCalculationType, existingProduct.interestCalculationDaysInYearType,
@@ -206,7 +310,8 @@ public final class RecurringDepositProductData extends DepositProductData {
existingProduct.adjustAdvanceTowardsFuturePayments, periodFrequencyTypeOptions, existingProduct.minDepositAmount,
existingProduct.depositAmount, existingProduct.maxDepositAmount, existingProduct.withHoldTax, existingProduct.taxGroup,
taxGroupOptions);
result.copyCbsFieldsFrom(existingProduct);
return result;
}
public static RecurringDepositProductData withAccountingDetails(final RecurringDepositProductData existingProduct,
@@ -227,7 +332,8 @@ public final class RecurringDepositProductData extends DepositProductData {
final Collection<ChargeData> chargeOptions = null;
final Collection<ChargeData> penaltyOptions = null;
return new RecurringDepositProductData(existingProduct.id, existingProduct.name, existingProduct.shortName,
final RecurringDepositProductData result = new RecurringDepositProductData(existingProduct.id, existingProduct.name,
existingProduct.shortName,
existingProduct.description, existingProduct.currency, existingProduct.nominalAnnualInterestRate,
existingProduct.interestCompoundingPeriodType, existingProduct.interestPostingPeriodType,
existingProduct.interestCalculationType, existingProduct.interestCalculationDaysInYearType,
@@ -245,6 +351,8 @@ public final class RecurringDepositProductData extends DepositProductData {
existingProduct.adjustAdvanceTowardsFuturePayments, existingProduct.periodFrequencyTypeOptions,
existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount,
existingProduct.withHoldTax, existingProduct.taxGroup, existingProduct.taxGroupOptions);
result.copyCbsFieldsFrom(existingProduct);
return result;
}
public static RecurringDepositProductData instance(final DepositProductData depositProductData, final boolean preClosurePenalApplicable,
@@ -367,7 +475,8 @@ public final class RecurringDepositProductData extends DepositProductData {
public static RecurringDepositProductData withInterestChart(final RecurringDepositProductData product,
final Collection<InterestRateChartData> interestRateCharts) {
return new RecurringDepositProductData(product.id, product.name, product.shortName, product.description, product.currency,
final RecurringDepositProductData result = new RecurringDepositProductData(product.id, product.name, product.shortName,
product.description, product.currency,
product.nominalAnnualInterestRate, product.interestCompoundingPeriodType, product.interestPostingPeriodType,
product.interestCalculationType, product.interestCalculationDaysInYearType, product.lockinPeriodFrequency,
product.lockinPeriodFrequencyType, product.minBalanceForInterestCalculation, product.accountingRule,
@@ -383,7 +492,8 @@ public final class RecurringDepositProductData extends DepositProductData {
product.inMultiplesOfDepositTermType, product.isMandatoryDeposit, product.allowWithdrawal,
product.adjustAdvanceTowardsFuturePayments, product.periodFrequencyTypeOptions, product.minDepositAmount,
product.depositAmount, product.maxDepositAmount, product.withHoldTax, product.taxGroup, product.taxGroupOptions);
result.copyCbsFieldsFrom(product);
return result;
}
private RecurringDepositProductData(final Long id, final String name, final String shortName, final String description,
@@ -58,9 +58,13 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHold
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.fineract.accounting.common.AccountingRuleType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -277,6 +281,47 @@ public class DepositProductAssembler {
productTermAndPreClosure.updateProductReference(recurringDepositProduct);
productRecurringDetail.updateProductReference(recurringDepositProduct);
// ── CBS Custom Fields (from 0251 migration) ─────────────────────────
recurringDepositProduct.setAccountType(command.stringValueOfParameterNamed("accountType"));
recurringDepositProduct.setCustomerSegment(command.stringValueOfParameterNamed("customerSegment"));
recurringDepositProduct.setProductCode(command.stringValueOfParameterNamed("productCode"));
recurringDepositProduct.setEffectiveDate(parseCbsDate(command.stringValueOfParameterNamed("effectiveDate")));
recurringDepositProduct.setEffectiveTo(parseCbsDate(command.stringValueOfParameterNamed("effectiveTo")));
recurringDepositProduct.setEndDate(parseCbsDate(command.stringValueOfParameterNamed("endDate")));
recurringDepositProduct.setIntroducerRequired(command.booleanPrimitiveValueOfParameterNamed("introducerRequired"));
recurringDepositProduct.setShareLinkingEnabled(command.booleanPrimitiveValueOfParameterNamed("shareLinkingEnabled"));
recurringDepositProduct.setNominationFacility(command.booleanPrimitiveValueOfParameterNamed("nominationFacility"));
recurringDepositProduct.setDormancyAlert(command.booleanPrimitiveValueOfParameterNamed("dormancyAlert"));
recurringDepositProduct.setInterestCalculateSelector(command.integerValueOfParameterNamed("interestCalculateSelector"));
recurringDepositProduct.setAveragePeriod(command.stringValueOfParameterNamed("averagePeriod"));
recurringDepositProduct.setAutoPost(command.booleanPrimitiveValueOfParameterNamed("autoPost"));
recurringDepositProduct.setFrequencyFixDay(command.integerValueOfParameterNamed("frequencyFixDay"));
recurringDepositProduct.setLastInterestDate(parseCbsDate(command.stringValueOfParameterNamed("lastInterestDate")));
recurringDepositProduct.setEnableLockinPeriod(command.booleanPrimitiveValueOfParameterNamed("enableLockinPeriod"));
recurringDepositProduct.setBalanceFrequency(command.stringValueOfParameterNamed("balanceFrequency"));
if (command.parameterExists("accountClosedZeroBalanceInDayClosing")) {
recurringDepositProduct.setAccountClosedZeroBalanceInDayClosing(
command.booleanPrimitiveValueOfParameterNamed("accountClosedZeroBalanceInDayClosing"));
}
// ── RD-specific flags (from 0270 migration) ──────────────────────────
recurringDepositProduct.setBalanceSwapOption(command.booleanPrimitiveValueOfParameterNamed("balanceSwapOption"));
recurringDepositProduct.setMsme(command.booleanPrimitiveValueOfParameterNamed("msme"));
recurringDepositProduct.setAutoDebitFacility(command.booleanPrimitiveValueOfParameterNamed("autoDebitFacility"));
recurringDepositProduct.setLoanFacility(command.booleanPrimitiveValueOfParameterNamed("loanFacility"));
recurringDepositProduct.setPrematureWithdrawalOption(command.booleanPrimitiveValueOfParameterNamed("prematureWithdrawalOption"));
recurringDepositProduct.setTenureFlexibility(command.booleanPrimitiveValueOfParameterNamed("tenureFlexibility"));
recurringDepositProduct.setLienMarked(command.booleanPrimitiveValueOfParameterNamed("lienMarked"));
recurringDepositProduct.setMaturityInstructions(command.booleanPrimitiveValueOfParameterNamed("maturityInstructions"));
recurringDepositProduct.setCreditMaturityProceeds(command.booleanPrimitiveValueOfParameterNamed("creditMaturityProceeds"));
recurringDepositProduct.setPenaltyGracePeriodMissedInstallments(
command.booleanPrimitiveValueOfParameterNamed("penaltyGracePeriodMissedInstallments"));
recurringDepositProduct.setPanelRequestApplicable(command.booleanPrimitiveValueOfParameterNamed("panelRequestApplicable"));
recurringDepositProduct.setNoPenaltyNomineeHeirClosure(
command.booleanPrimitiveValueOfParameterNamed("noPenaltyNomineeHeirClosure"));
recurringDepositProduct.setDeceasedSettlement(command.booleanPrimitiveValueOfParameterNamed("deceasedSettlement"));
recurringDepositProduct.setAllowDebitTxnAfterDueDate(command.booleanPrimitiveValueOfParameterNamed("allowDebitTxnAfterDueDate"));
recurringDepositProduct.validateDomainRules();
return recurringDepositProduct;
@@ -500,6 +545,30 @@ public class DepositProductAssembler {
return new DepositProductAmountDetails(minDepositAmount, depositAmount, maxDepositAmount);
}
/**
* Parses a CBS date string using common formats (e.g. "17 April 2026", "2026-04-17").
* Returns null if the input is null or blank.
*/
private LocalDate parseCbsDate(final String dateStr) {
if (dateStr == null || dateStr.isBlank()) {
return null;
}
DateTimeFormatter[] formatters = {
DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.ENGLISH),
DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH),
DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH),
DateTimeFormatter.ofPattern("dd/MM/yyyy", Locale.ENGLISH)
};
for (DateTimeFormatter fmt : formatters) {
try {
return LocalDate.parse(dateStr, fmt);
} catch (DateTimeParseException ignored) {
// try next format
}
}
return null;
}
public TaxGroup assembleTaxGroup(final JsonCommand command) {
final Long taxGroupId = command.longValueOfParameterNamed(taxGroupIdParamName);
TaxGroup taxGroup = null;
@@ -0,0 +1,72 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_rd_interest_rate_effective")
@Getter
@Setter
@NoArgsConstructor
public class RdInterestRateEffective {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_id", nullable = false)
private Long productId;
@Column(name = "version_label", nullable = false, length = 100)
private String versionLabel;
@Column(name = "effective_from_date", nullable = false)
private LocalDate effectiveFromDate;
@Column(name = "effective_to_date")
private LocalDate effectiveToDate;
@Column(name = "is_active", nullable = false)
private Boolean isActive;
@Column(name = "remarks", length = 500)
private String remarks;
@Column(name = "created_by_id")
private Long createdById;
@Column(name = "created_date")
private LocalDate createdDate;
public static RdInterestRateEffective create(Long productId, String versionLabel,
LocalDate effectiveFromDate, String remarks, Long createdById) {
RdInterestRateEffective e = new RdInterestRateEffective();
e.productId = productId;
e.versionLabel = versionLabel;
e.effectiveFromDate = effectiveFromDate;
e.isActive = true;
e.remarks = remarks;
e.createdById = createdById;
e.createdDate = LocalDate.now();
return e;
}
}
@@ -0,0 +1,31 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface RdInterestRateEffectiveRepository extends JpaRepository<RdInterestRateEffective, Long> {
List<RdInterestRateEffective> findByProductIdOrderByEffectiveFromDateDesc(Long productId);
List<RdInterestRateEffective> findByIsActiveTrueOrderByEffectiveFromDateDesc();
@Modifying
@Query("UPDATE RdInterestRateEffective e SET e.isActive = false WHERE e.productId = :productId AND e.isActive = true")
void deactivateAllForProduct(@Param("productId") Long productId);
}
@@ -0,0 +1,72 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "m_rd_interest_rate_slab_history")
@Getter
@NoArgsConstructor
public class RdInterestRateSlabHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "effective_id", nullable = false)
private Long effectiveId;
@Column(name = "sr_no", nullable = false)
private Integer srNo;
@Column(name = "description", length = 200)
private String description;
@Column(name = "period_type_enum", nullable = false)
private Integer periodTypeEnum;
@Column(name = "from_period", nullable = false)
private Integer fromPeriod;
@Column(name = "to_period")
private Integer toPeriod;
@Column(name = "annual_interest_rate", nullable = false, precision = 19, scale = 6)
private BigDecimal annualInterestRate;
@Column(name = "created_date")
private LocalDate createdDate;
public static RdInterestRateSlabHistory create(Long effectiveId, Integer srNo, String description,
Integer periodTypeEnum, Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate) {
RdInterestRateSlabHistory s = new RdInterestRateSlabHistory();
s.effectiveId = effectiveId;
s.srNo = srNo;
s.description = description;
s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1;
s.fromPeriod = fromPeriod;
s.toPeriod = toPeriod;
s.annualInterestRate = annualInterestRate;
s.createdDate = LocalDate.now();
return s;
}
}
@@ -0,0 +1,22 @@
/**
* 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
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RdInterestRateSlabHistoryRepository extends JpaRepository<RdInterestRateSlabHistory, Long> {
List<RdInterestRateSlabHistory> findByEffectiveIdOrderBySrNo(Long effectiveId);
}
@@ -150,12 +150,16 @@ public class RecurringDepositProduct extends FixedDepositProduct {
}
private void validateDomainRules(final DataValidatorBuilder baseDataValidator) {
final DepositTermDetail termDetails = this.depositProductTermAndPreClosure().depositTermDetail();
final boolean isMinTermGreaterThanMax = termDetails.isMinDepositTermGreaterThanMaxDepositTerm();
if (isMinTermGreaterThanMax) {
final Integer maxTerm = termDetails.maxDepositTerm();
baseDataValidator.reset().parameter(maxDepositTermParamName).value(maxTerm)
.failWithCodeNoParameterAddedToErrorCode("max.term.lessthan.min.term");
final DepositTermDetail termDetails = this.depositProductTermAndPreClosure() != null
? this.depositProductTermAndPreClosure().depositTermDetail()
: null;
if (termDetails != null) {
final boolean isMinTermGreaterThanMax = termDetails.isMinDepositTermGreaterThanMaxDepositTerm();
if (isMinTermGreaterThanMax) {
final Integer maxTerm = termDetails.maxDepositTerm();
baseDataValidator.reset().parameter(maxDepositTermParamName).value(maxTerm)
.failWithCodeNoParameterAddedToErrorCode("max.term.lessthan.min.term");
}
}
if (this.charts != null) {
@@ -90,6 +90,9 @@ import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -290,6 +293,49 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
@Column(name = "account_closed_zero_balance_in_day_closing")
private Boolean accountClosedZeroBalanceInDayClosing = false;
// RD-specific flags
@Column(name = "balance_swap_option")
private boolean balanceSwapOption = false;
@Column(name = "msme")
private boolean msme = false;
@Column(name = "auto_debit_facility")
private boolean autoDebitFacility = false;
@Column(name = "loan_facility")
private boolean loanFacility = false;
@Column(name = "premature_withdrawal_option")
private boolean prematureWithdrawalOption = false;
@Column(name = "tenure_flexibility")
private boolean tenureFlexibility = false;
@Column(name = "lien_marked")
private boolean lienMarked = false;
@Column(name = "maturity_instructions")
private boolean maturityInstructions = false;
@Column(name = "credit_maturity_proceeds")
private boolean creditMaturityProceeds = false;
@Column(name = "penalty_grace_period_missed_installments")
private boolean penaltyGracePeriodMissedInstallments = false;
@Column(name = "panel_request_applicable")
private boolean panelRequestApplicable = false;
@Column(name = "no_penalty_nominee_heir_closure")
private boolean noPenaltyNomineeHeirClosure = false;
@Column(name = "deceased_settlement")
private boolean deceasedSettlement = false;
@Column(name = "allow_debit_txn_after_due_date")
private boolean allowDebitTxnAfterDueDate = false;
// Relationships for complex objects
@OneToOne(cascade = CascadeType.ALL, mappedBy = "savingsProduct", orphanRemoval = true)
private SavingsProductFacilityConfig facilityConfig;
@@ -750,20 +796,26 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
actualChanges.put(productCodeParamName, newValue);
this.productCode = newValue;
}
if (command.isChangeInLocalDateParameterNamed(effectiveDateParamName, this.effectiveDate)) {
final LocalDate newValue = command.localDateValueOfParameterNamed(effectiveDateParamName);
actualChanges.put(effectiveDateParamName, newValue);
this.effectiveDate = newValue;
if (command.parameterExists(effectiveDateParamName)) {
final LocalDate newValue = parseCbsLocalDate(command.stringValueOfParameterNamed(effectiveDateParamName));
if (!Objects.equals(newValue, this.effectiveDate)) {
actualChanges.put(effectiveDateParamName, newValue);
this.effectiveDate = newValue;
}
}
if (command.isChangeInLocalDateParameterNamed(effectiveToParamName, this.effectiveTo)) {
final LocalDate newValue = command.localDateValueOfParameterNamed(effectiveToParamName);
actualChanges.put(effectiveToParamName, newValue);
this.effectiveTo = newValue;
if (command.parameterExists(effectiveToParamName)) {
final LocalDate newValue = parseCbsLocalDate(command.stringValueOfParameterNamed(effectiveToParamName));
if (!Objects.equals(newValue, this.effectiveTo)) {
actualChanges.put(effectiveToParamName, newValue);
this.effectiveTo = newValue;
}
}
if (command.isChangeInLocalDateParameterNamed(endDateParamName, this.endDate)) {
final LocalDate newValue = command.localDateValueOfParameterNamed(endDateParamName);
actualChanges.put(endDateParamName, newValue);
this.endDate = newValue;
if (command.parameterExists(endDateParamName)) {
final LocalDate newValue = parseCbsLocalDate(command.stringValueOfParameterNamed(endDateParamName));
if (!Objects.equals(newValue, this.endDate)) {
actualChanges.put(endDateParamName, newValue);
this.endDate = newValue;
}
}
if (command.isChangeInBooleanParameterNamed(introducerRequiredParamName, this.introducerRequired)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(introducerRequiredParamName);
@@ -805,10 +857,12 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
actualChanges.put(frequencyFixDayParamName, newValue);
this.frequencyFixDay = newValue;
}
if (command.isChangeInLocalDateParameterNamed(lastInterestDateParamName, this.lastInterestDate)) {
final LocalDate newValue = command.localDateValueOfParameterNamed(lastInterestDateParamName);
actualChanges.put(lastInterestDateParamName, newValue);
this.lastInterestDate = newValue;
if (command.parameterExists(lastInterestDateParamName)) {
final LocalDate newValue = parseCbsLocalDate(command.stringValueOfParameterNamed(lastInterestDateParamName));
if (!Objects.equals(newValue, this.lastInterestDate)) {
actualChanges.put(lastInterestDateParamName, newValue);
this.lastInterestDate = newValue;
}
}
if (command.isChangeInBooleanParameterNamed(enableLockinPeriodParamName, this.enableLockinPeriod)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(enableLockinPeriodParamName);
@@ -830,6 +884,64 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
this.accountClosedZeroBalanceInDayClosing = newValue;
}
// Update RD-specific flags (0270 migration fields)
if (command.isChangeInBooleanParameterNamed("balanceSwapOption", this.balanceSwapOption)) {
this.balanceSwapOption = command.booleanPrimitiveValueOfParameterNamed("balanceSwapOption");
actualChanges.put("balanceSwapOption", this.balanceSwapOption);
}
if (command.isChangeInBooleanParameterNamed("msme", this.msme)) {
this.msme = command.booleanPrimitiveValueOfParameterNamed("msme");
actualChanges.put("msme", this.msme);
}
if (command.isChangeInBooleanParameterNamed("autoDebitFacility", this.autoDebitFacility)) {
this.autoDebitFacility = command.booleanPrimitiveValueOfParameterNamed("autoDebitFacility");
actualChanges.put("autoDebitFacility", this.autoDebitFacility);
}
if (command.isChangeInBooleanParameterNamed("loanFacility", this.loanFacility)) {
this.loanFacility = command.booleanPrimitiveValueOfParameterNamed("loanFacility");
actualChanges.put("loanFacility", this.loanFacility);
}
if (command.isChangeInBooleanParameterNamed("prematureWithdrawalOption", this.prematureWithdrawalOption)) {
this.prematureWithdrawalOption = command.booleanPrimitiveValueOfParameterNamed("prematureWithdrawalOption");
actualChanges.put("prematureWithdrawalOption", this.prematureWithdrawalOption);
}
if (command.isChangeInBooleanParameterNamed("tenureFlexibility", this.tenureFlexibility)) {
this.tenureFlexibility = command.booleanPrimitiveValueOfParameterNamed("tenureFlexibility");
actualChanges.put("tenureFlexibility", this.tenureFlexibility);
}
if (command.isChangeInBooleanParameterNamed("lienMarked", this.lienMarked)) {
this.lienMarked = command.booleanPrimitiveValueOfParameterNamed("lienMarked");
actualChanges.put("lienMarked", this.lienMarked);
}
if (command.isChangeInBooleanParameterNamed("maturityInstructions", this.maturityInstructions)) {
this.maturityInstructions = command.booleanPrimitiveValueOfParameterNamed("maturityInstructions");
actualChanges.put("maturityInstructions", this.maturityInstructions);
}
if (command.isChangeInBooleanParameterNamed("creditMaturityProceeds", this.creditMaturityProceeds)) {
this.creditMaturityProceeds = command.booleanPrimitiveValueOfParameterNamed("creditMaturityProceeds");
actualChanges.put("creditMaturityProceeds", this.creditMaturityProceeds);
}
if (command.isChangeInBooleanParameterNamed("penaltyGracePeriodMissedInstallments", this.penaltyGracePeriodMissedInstallments)) {
this.penaltyGracePeriodMissedInstallments = command.booleanPrimitiveValueOfParameterNamed("penaltyGracePeriodMissedInstallments");
actualChanges.put("penaltyGracePeriodMissedInstallments", this.penaltyGracePeriodMissedInstallments);
}
if (command.isChangeInBooleanParameterNamed("panelRequestApplicable", this.panelRequestApplicable)) {
this.panelRequestApplicable = command.booleanPrimitiveValueOfParameterNamed("panelRequestApplicable");
actualChanges.put("panelRequestApplicable", this.panelRequestApplicable);
}
if (command.isChangeInBooleanParameterNamed("noPenaltyNomineeHeirClosure", this.noPenaltyNomineeHeirClosure)) {
this.noPenaltyNomineeHeirClosure = command.booleanPrimitiveValueOfParameterNamed("noPenaltyNomineeHeirClosure");
actualChanges.put("noPenaltyNomineeHeirClosure", this.noPenaltyNomineeHeirClosure);
}
if (command.isChangeInBooleanParameterNamed("deceasedSettlement", this.deceasedSettlement)) {
this.deceasedSettlement = command.booleanPrimitiveValueOfParameterNamed("deceasedSettlement");
actualChanges.put("deceasedSettlement", this.deceasedSettlement);
}
if (command.isChangeInBooleanParameterNamed("allowDebitTxnAfterDueDate", this.allowDebitTxnAfterDueDate)) {
this.allowDebitTxnAfterDueDate = command.booleanPrimitiveValueOfParameterNamed("allowDebitTxnAfterDueDate");
actualChanges.put("allowDebitTxnAfterDueDate", this.allowDebitTxnAfterDueDate);
}
validateLockinDetails();
esnureOverdraftLimitsSetForOverdraftAccounts();
@@ -1142,6 +1254,48 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
this.accountClosedZeroBalanceInDayClosing = accountClosedZeroBalanceInDayClosing;
}
public boolean isBalanceSwapOption() { return balanceSwapOption; }
public void setBalanceSwapOption(boolean v) { this.balanceSwapOption = v; }
public boolean isMsme() { return msme; }
public void setMsme(boolean v) { this.msme = v; }
public boolean isAutoDebitFacility() { return autoDebitFacility; }
public void setAutoDebitFacility(boolean v) { this.autoDebitFacility = v; }
public boolean isLoanFacility() { return loanFacility; }
public void setLoanFacility(boolean v) { this.loanFacility = v; }
public boolean isPrematureWithdrawalOption() { return prematureWithdrawalOption; }
public void setPrematureWithdrawalOption(boolean v) { this.prematureWithdrawalOption = v; }
public boolean isTenureFlexibility() { return tenureFlexibility; }
public void setTenureFlexibility(boolean v) { this.tenureFlexibility = v; }
public boolean isLienMarked() { return lienMarked; }
public void setLienMarked(boolean v) { this.lienMarked = v; }
public boolean isMaturityInstructions() { return maturityInstructions; }
public void setMaturityInstructions(boolean v) { this.maturityInstructions = v; }
public boolean isCreditMaturityProceeds() { return creditMaturityProceeds; }
public void setCreditMaturityProceeds(boolean v) { this.creditMaturityProceeds = v; }
public boolean isPenaltyGracePeriodMissedInstallments() { return penaltyGracePeriodMissedInstallments; }
public void setPenaltyGracePeriodMissedInstallments(boolean v) { this.penaltyGracePeriodMissedInstallments = v; }
public boolean isPanelRequestApplicable() { return panelRequestApplicable; }
public void setPanelRequestApplicable(boolean v) { this.panelRequestApplicable = v; }
public boolean isNoPenaltyNomineeHeirClosure() { return noPenaltyNomineeHeirClosure; }
public void setNoPenaltyNomineeHeirClosure(boolean v) { this.noPenaltyNomineeHeirClosure = v; }
public boolean isDeceasedSettlement() { return deceasedSettlement; }
public void setDeceasedSettlement(boolean v) { this.deceasedSettlement = v; }
public boolean isAllowDebitTxnAfterDueDate() { return allowDebitTxnAfterDueDate; }
public void setAllowDebitTxnAfterDueDate(boolean v) { this.allowDebitTxnAfterDueDate = v; }
public SavingsProductFacilityConfig getFacilityConfig() {
return facilityConfig;
}
@@ -1174,4 +1328,25 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
}
}
/**
* Parses CBS date strings (e.g. "17 April 2026") without requiring a dateFormat parameter.
*/
public static LocalDate parseCbsLocalDate(final String dateStr) {
if (dateStr == null || dateStr.isBlank()) {
return null;
}
for (DateTimeFormatter fmt : Arrays.asList(
DateTimeFormatter.ofPattern("dd MMMM yyyy", java.util.Locale.ENGLISH),
DateTimeFormatter.ofPattern("dd MMM yyyy", java.util.Locale.ENGLISH),
DateTimeFormatter.ofPattern("yyyy-MM-dd", java.util.Locale.ENGLISH),
DateTimeFormatter.ofPattern("dd/MM/yyyy", java.util.Locale.ENGLISH))) {
try {
return LocalDate.parse(dateStr, fmt);
} catch (DateTimeParseException ignored) {
// try next
}
}
return null;
}
}