diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java index a825a0d98..1fd3be897 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java @@ -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; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RdInterestRateSaveRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RdInterestRateSaveRequest.java new file mode 100644 index 000000000..2674c73ee --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RdInterestRateSaveRequest.java @@ -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 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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java index 3d80158ea..92bfa6035 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java @@ -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) (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 }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsBaseInterestRatesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsBaseInterestRatesApiResource.java new file mode 100644 index 000000000..0c0615936 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsBaseInterestRatesApiResource.java @@ -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) (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 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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java index 33069eae6..5416065c6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java @@ -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); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateEffectiveData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateEffectiveData.java new file mode 100644 index 000000000..3a37eb66c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateEffectiveData.java @@ -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 slabs; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateSlabData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateSlabData.java new file mode 100644 index 000000000..58045cb1a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateSlabData.java @@ -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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateEffective.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateEffective.java new file mode 100644 index 000000000..5bf874c92 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateEffective.java @@ -0,0 +1,70 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateEffectiveRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateEffectiveRepository.java new file mode 100644 index 000000000..b96e779be --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateEffectiveRepository.java @@ -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 { + + List findByIsActiveTrueOrderByEffectiveFromDateDesc(); + + List findByOrderByEffectiveFromDateDesc(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateSlabHistory.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateSlabHistory.java new file mode 100644 index 000000000..daa1e5ecd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateSlabHistory.java @@ -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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateSlabHistoryRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateSlabHistoryRepository.java new file mode 100644 index 000000000..3c16edf15 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdBaseInterestRateSlabHistoryRepository.java @@ -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 { + + List findByEffectiveIdOrderBySrNo(Long effectiveId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffective.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffective.java new file mode 100644 index 000000000..11836f623 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffective.java @@ -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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffectiveRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffectiveRepository.java new file mode 100644 index 000000000..050040cbd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffectiveRepository.java @@ -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 { + + List findByProductIdOrderByEffectiveFromDateDesc(Long productId); + + List findByProductIdAndIsActiveTrueOrderByEffectiveFromDateDesc(Long productId); + + List findByIsActiveTrueOrderByEffectiveFromDateDesc(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistory.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistory.java new file mode 100644 index 000000000..99ce41525 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistory.java @@ -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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistoryRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistoryRepository.java new file mode 100644 index 000000000..c92bb7c61 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistoryRepository.java @@ -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 { + + List findByEffectiveIdOrderBySrNo(Long effectiveId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java index 17aa99f92..0ced40350 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java @@ -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; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdBaseInterestRateReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdBaseInterestRateReadPlatformService.java new file mode 100644 index 000000000..26a5b16cf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdBaseInterestRateReadPlatformService.java @@ -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 retrieveAllInterestRates(); + + List retrieveAllActiveInterestRates(); + + RdInterestRateEffectiveData saveInterestRates(RdInterestRateSaveRequest request, Long createdById); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdBaseInterestRateReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdBaseInterestRateReadPlatformServiceImpl.java new file mode 100644 index 000000000..ea622e2d3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdBaseInterestRateReadPlatformServiceImpl.java @@ -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 retrieveAllInterestRates() { + return effectiveRepository.findByOrderByEffectiveFromDateDesc().stream().map(this::toData).collect(Collectors.toList()); + } + + @Override + public List retrieveAllActiveInterestRates() { + return effectiveRepository.findByIsActiveTrueOrderByEffectiveFromDateDesc().stream().map(this::toData).collect(Collectors.toList()); + } + + @Override + @Transactional + public RdInterestRateEffectiveData saveInterestRates(RdInterestRateSaveRequest request, Long createdById) { + validateRequest(request); + final List normalizedSlabs = normalizeSlabs(request.getSlabs()); + + final LocalDate today = LocalDate.now(); + final List 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 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 normalizeSlabs(final List slabs) { + final List 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 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 errors = new ArrayList<>(); + new DataValidatorBuilder(errors).resource("RD_BASE_INTEREST_RATE_VERSION").parameter(parameterName).failWithCode(code); + throwExceptionIfValidationWarningsExist(errors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + private RdInterestRateEffectiveData toData(RdBaseInterestRateEffective e) { + List 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]; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdInterestRateReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdInterestRateReadPlatformService.java new file mode 100644 index 000000000..ab1d01a1f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdInterestRateReadPlatformService.java @@ -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 retrieveInterestRates(Long productId); + + /** All active versions across all RD products – used as reference on the create-product page. */ + List 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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdInterestRateReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdInterestRateReadPlatformServiceImpl.java new file mode 100644 index 000000000..830ddb9f3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RdInterestRateReadPlatformServiceImpl.java @@ -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 retrieveInterestRates(Long productId) { + return effectiveRepository.findByProductIdOrderByEffectiveFromDateDesc(productId) + .stream().map(this::toData).collect(Collectors.toList()); + } + + @Override + public List 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 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 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 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 normalizeSlabs(final List slabs) { + final List 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 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 errors = new ArrayList<>(); + new DataValidatorBuilder(errors).resource("RD_INTEREST_RATE_VERSION").parameter(parameterName).failWithCode(code); + throwExceptionIfValidationWarningsExist(errors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + // ── helpers ───────────────────────────────────────────────────────────── + + private RdInterestRateEffectiveData toData(RdInterestRateEffective e) { + List 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]; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index 75d2acf9d..ff5559526 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -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, diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index bfc448817..a1e6f7931 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -271,4 +271,9 @@ + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0257_create_rd_interest_rate_tables.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0257_create_rd_interest_rate_tables.xml new file mode 100644 index 000000000..9cb66f274 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0257_create_rd_interest_rate_tables.xml @@ -0,0 +1,95 @@ + + + + + + + + ANY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ANY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0267_seed_rd_interest_rate_data.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0267_seed_rd_interest_rate_data.xml new file mode 100644 index 000000000..6986740f5 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0267_seed_rd_interest_rate_data.xml @@ -0,0 +1,94 @@ + + + + + + + + + SELECT COUNT(*) FROM m_rd_interest_rate_effective + + + 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; + + + + + 9:e35a0696046940f370f7cfb55c20740b + + SELECT COUNT(*) FROM m_rd_interest_rate_slab_history + + + 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 & 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); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0270_add_rd_product_extra_fields.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0270_add_rd_product_extra_fields.xml new file mode 100644 index 000000000..99430745f --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0270_add_rd_product_extra_fields.xml @@ -0,0 +1,96 @@ + + + + + + + ANY + + + + + + + + + + + + + ANY + + + + + + + + + + + + + ANY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0271_create_rd_base_interest_rate_tables.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0271_create_rd_base_interest_rate_tables.xml new file mode 100644 index 000000000..5bcf95e4d --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0271_create_rd_base_interest_rate_tables.xml @@ -0,0 +1,67 @@ + + + + + ANY + + + + + + + + + + + + + + + + + + + + + + + + + ANY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0272_seed_rd_base_interest_rate_data.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0272_seed_rd_base_interest_rate_data.xml new file mode 100644 index 000000000..4e0b72399 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0272_seed_rd_base_interest_rate_data.xml @@ -0,0 +1,78 @@ + + + + + + SELECT COUNT(*) FROM m_rd_base_interest_rate_effective + + + 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 & Above', 1, 1825, NULL, 6.500000, CURRENT_DATE + FROM m_rd_base_interest_rate_effective WHERE version_label = 'CBS Base Rate Version'; + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateEffectiveData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateEffectiveData.java new file mode 100644 index 000000000..8eddb748d --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateEffectiveData.java @@ -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 slabs; +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateSlabData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateSlabData.java new file mode 100644 index 000000000..1eb944297 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RdInterestRateSlabData.java @@ -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; +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java index b1258d248..a3de5a7bb 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java @@ -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 v) { this.interestCalculationTypeOption = v; } public void setPeriodFrequencyOptions(Collection 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 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 preClosurePenalInterestOnTypeOptions, final Collection periodFrequencyTypeOptions, final Collection 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 chargeOptions = null; final Collection 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 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, diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositProductAssembler.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositProductAssembler.java index 5d4a29d34..f8e36da01 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositProductAssembler.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositProductAssembler.java @@ -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; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffective.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffective.java new file mode 100644 index 000000000..90c50ab02 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffective.java @@ -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; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffectiveRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffectiveRepository.java new file mode 100644 index 000000000..80dd7fd20 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateEffectiveRepository.java @@ -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 { + + List findByProductIdOrderByEffectiveFromDateDesc(Long productId); + + List findByIsActiveTrueOrderByEffectiveFromDateDesc(); + + @Modifying + @Query("UPDATE RdInterestRateEffective e SET e.isActive = false WHERE e.productId = :productId AND e.isActive = true") + void deactivateAllForProduct(@Param("productId") Long productId); +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistory.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistory.java new file mode 100644 index 000000000..4bc6caeeb --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistory.java @@ -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; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistoryRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistoryRepository.java new file mode 100644 index 000000000..3fb205a29 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RdInterestRateSlabHistoryRepository.java @@ -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 { + + List findByEffectiveIdOrderBySrNo(Long effectiveId); +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositProduct.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositProduct.java index ede63a24f..3843fe1cd 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositProduct.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositProduct.java @@ -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) { diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java index 7e39ebe24..71be4baf3 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java @@ -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 { @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 { 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 { 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 { 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 { 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 { } } + /** + * 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; + } + }