fd product releated update code

This commit is contained in:
Nitesh Yadav
2026-05-05 15:18:35 +05:30
parent be220d8778
commit 4735111a4f
33 changed files with 63380 additions and 44790 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
/**
* 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;
@Getter
@Setter
@NoArgsConstructor
public class FdInterestRateSaveRequest {
private String fdType;
private String versionLabel;
private String remarks;
private List<SlabRequest> slabs;
@Getter
@Setter
@NoArgsConstructor
public static class SlabRequest {
private Integer srNo;
private String description;
private Integer periodTypeEnum;
private Integer fromPeriod;
private Integer toPeriod;
private BigDecimal annualInterestRate;
private BigDecimal seniorCitizenRate;
}
}
@@ -54,9 +54,13 @@ import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import java.util.Locale;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
@@ -110,6 +114,8 @@ public class FixedDepositProductsApiResource {
private final DropdownReadPlatformService dropdownReadPlatformService;
private final PaymentTypeReadService paymentTypeReadPlatformService;
private final TaxReadPlatformService taxReadPlatformService;
private final CodeValueReadPlatformService codeValueReadPlatformService;
private final org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@@ -315,6 +321,16 @@ public class FixedDepositProductsApiResource {
// interest rate chart template
final InterestRateChartData chartTemplate = this.interestRateChartReadPlatformService.template();
// CBS custom options
final Collection<EnumOptionData> interestCalculationTypeOption = retrieveInterestCalculationTypeOption();
final Collection<EnumOptionData> periodFrequencyOptions = retrievePeriodFrequencyOptions();
final List<CodeValueData> yesNoOptions = new ArrayList<>(
this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.YES_NO));
final List<CodeValueData> accountTypeOptions = new ArrayList<>(
this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_ACCOUNT_TYPE));
final List<CodeValueData> customerSegmentOptions = new ArrayList<>(
this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_CUSTOMER_SEGMENT));
FixedDepositProductData fixedDepositProductToReturn = null;
if (savingsProduct != null) {
fixedDepositProductToReturn = FixedDepositProductData.withTemplate(savingsProduct, currencyOptions,
@@ -329,11 +345,49 @@ public class FixedDepositProductsApiResource {
interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions,
paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, chargeOptions, penaltyOptions, chartTemplate,
preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions, taxGroupOptions);
fixedDepositProductToReturn.setProductCode(generateNextProductCode("m_savings_product", "FD-IND-"));
}
fixedDepositProductToReturn.setInterestCalculationTypeOption(interestCalculationTypeOption);
fixedDepositProductToReturn.setPeriodFrequencyOptions(periodFrequencyOptions);
fixedDepositProductToReturn.setYesNoOptions(yesNoOptions);
fixedDepositProductToReturn.setAccountTypeOptions(accountTypeOptions);
fixedDepositProductToReturn.setCustomerSegmentOptions(customerSegmentOptions);
return fixedDepositProductToReturn;
}
private Collection<EnumOptionData> retrievePeriodFrequencyOptions() {
return this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.PERIOD_FREQUENCY).stream()
.map(codeValue -> new EnumOptionData(codeValue.getId(),
"periodFrequencyType." + codeValue.getName().toLowerCase(Locale.ROOT).replace(' ', '_'), codeValue.getName()))
.toList();
}
private Collection<EnumOptionData> retrieveInterestCalculationTypeOption() {
return this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.INTEREST_CALCULATION_TYPE_OPTION).stream()
.map(codeValue -> new EnumOptionData(codeValue.getId(),
"interestCalculationTypeOption." + codeValue.getName().toLowerCase(Locale.ROOT).replace(' ', '_'),
codeValue.getName()))
.toList();
}
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 = 200",
String.class, prefix + "%");
if (maxCode == null || maxCode.isBlank()) {
return prefix + "001";
}
String numStr = maxCode.substring(prefix.length());
int num = Integer.parseInt(numStr);
return prefix + String.format("%03d", num + 1);
} catch (Exception e) {
return prefix + "001";
}
}
@DELETE
@Path("{productId}")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -342,6 +342,7 @@ final class FixedDepositProductsApiResourceSwagger {
public GetFixedDepositProductsGlAccount interestOnSavingsAccount;
public GetFixedDepositProductsGlAccount savingsControlAccount;
public GetFixedDepositProductsGlAccount transfersInSuspenseAccount;
public GetFixedDepositProductsGlAccount escheatLiabilityAccount;
public GetFixedDepositProductsGlAccount interestPayableAccount;
}
@@ -0,0 +1,87 @@
/**
* 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.QueryParam;
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.FdInterestRateEffectiveData;
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/fixeddepositproducts/baseinterestrates")
@Component
@RequiredArgsConstructor
public class FixedDepositProductsBaseInterestRatesApiResource {
private final PlatformSecurityContext context;
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(LocalDate.class,
(JsonSerializer<LocalDate>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString()))
.create();
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Get all active FD base interest rate charts", description = "Returns active CBS FD base interest-rate versions with slabs. Optionally filter by fdType (SINGLE, DOUBLE, SPECIAL).")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String retrieveAllActiveInterestRates(@QueryParam("fdType") final String fdType) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
final List<FdInterestRateEffectiveData> data = fdType != null && !fdType.isBlank()
? this.fdBaseInterestRateReadPlatformService.retrieveActiveInterestRatesByType(fdType)
: this.fdBaseInterestRateReadPlatformService.retrieveAllActiveInterestRates();
return gson.toJson(data);
}
@GET
@Path("history")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Get all FD base interest rate charts", description = "Returns the full FD base chart history.")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String retrieveHistory() {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
final List<FdInterestRateEffectiveData> data = this.fdBaseInterestRateReadPlatformService.retrieveAllInterestRates();
return gson.toJson(data);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save new FD base interest rate version", description = "Creates a new FD CBS base chart version for the given fdType 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.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
final FdInterestRateSaveRequest request = gson.fromJson(apiRequestBodyAsJson, FdInterestRateSaveRequest.class);
final Long createdById = this.context.authenticatedUser().getId();
final FdInterestRateEffectiveData saved = this.fdBaseInterestRateReadPlatformService.saveInterestRates(request, createdById);
return gson.toJson(saved);
}
}
@@ -360,6 +360,7 @@ final class RecurringDepositProductsApiResourceSwagger {
public GetRecurringDepositProductsGlAccount interestOnSavingsAccount;
public GetRecurringDepositProductsGlAccount savingsControlAccount;
public GetRecurringDepositProductsGlAccount transfersInSuspenseAccount;
public GetRecurringDepositProductsGlAccount escheatLiabilityAccount;
public GetRecurringDepositProductsGlAccount feeReceivableAccount;
public GetRecurringDepositProductsGlAccount penaltyReceivableAccount;
public GetRecurringDepositProductsGlAccount interestPayableAccount;
@@ -285,6 +285,7 @@ final class SavingsProductsApiResourceSwagger {
public GetSavingsProductsGlAccount writeOffAccount;
public GetSavingsProductsGlAccount savingsControlAccount;
public GetSavingsProductsGlAccount transfersInSuspenseAccount;
public GetSavingsProductsGlAccount escheatLiabilityAccount;
public GetSavingsProductsGlAccount interestPayableAccount;
}
@@ -632,8 +632,12 @@ public class DepositProductDataValidator {
}
private void validateDepositAmountForCreate(JsonElement element, DataValidatorBuilder baseDataValidator) {
final BigDecimal depositAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(depositAmountParamName, element);
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).notNull().positiveAmount();
final BigDecimal depositAmount = this.fromApiJsonHelper.parameterExists(depositAmountParamName, element)
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(depositAmountParamName, element)
: null;
if (depositAmount != null) {
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).positiveAmount();
}
BigDecimal depositMinAmount = null;
if (this.fromApiJsonHelper.parameterExists(depositMinAmountParamName, element)) {
@@ -667,7 +671,7 @@ public class DepositProductDataValidator {
if (this.fromApiJsonHelper.parameterExists(depositAmountParamName, element)) {
depositAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(depositAmountParamName, element);
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).notNull().positiveAmount();
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).positiveAmount();
}
BigDecimal depositMinAmount = null;
@@ -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 FdInterestRateEffectiveData {
private Long id;
private String fdType;
private String versionLabel;
private LocalDate effectiveFromDate;
private LocalDate effectiveToDate;
private Boolean isActive;
private String remarks;
private List<FdInterestRateSlabData> slabs;
}
@@ -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.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class FdInterestRateSlabData {
private Long id;
private Integer srNo;
private String description;
private Integer periodTypeEnum;
private String periodTypeLabel;
private Integer fromPeriod;
private Integer toPeriod;
private BigDecimal annualInterestRate;
private BigDecimal seniorCitizenRate;
}
@@ -0,0 +1,75 @@
/**
* 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_fd_base_interest_rate_effective")
@Getter
@NoArgsConstructor
public class FdBaseInterestRateEffective {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "fd_type", length = 20)
private String fdType;
@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 FdBaseInterestRateEffective create(String fdType, String versionLabel, LocalDate effectiveFromDate, String remarks,
Long createdById) {
FdBaseInterestRateEffective e = new FdBaseInterestRateEffective();
e.fdType = fdType;
e.versionLabel = versionLabel;
e.effectiveFromDate = effectiveFromDate;
e.isActive = true;
e.remarks = remarks;
e.createdById = createdById;
e.createdDate = LocalDate.now();
return e;
}
public void deactivate(LocalDate toDate) {
this.isActive = false;
this.effectiveToDate = toDate;
}
}
@@ -0,0 +1,26 @@
/**
* 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 FdBaseInterestRateEffectiveRepository extends JpaRepository<FdBaseInterestRateEffective, Long> {
List<FdBaseInterestRateEffective> findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc(String fdType);
List<FdBaseInterestRateEffective> findByFdTypeOrderByEffectiveFromDateDesc(String fdType);
List<FdBaseInterestRateEffective> findByIsActiveTrueOrderByEffectiveFromDateDesc();
List<FdBaseInterestRateEffective> findByOrderByEffectiveFromDateDesc();
}
@@ -0,0 +1,76 @@
/**
* 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_fd_base_interest_rate_slab_history")
@Getter
@NoArgsConstructor
public class FdBaseInterestRateSlabHistory {
@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;
@Column(name = "senior_citizen_rate")
private BigDecimal seniorCitizenRate;
@Column(name = "created_date")
private LocalDate createdDate;
public static FdBaseInterestRateSlabHistory create(Long effectiveId, Integer srNo, String description, Integer periodTypeEnum,
Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate, BigDecimal seniorCitizenRate) {
FdBaseInterestRateSlabHistory s = new FdBaseInterestRateSlabHistory();
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.seniorCitizenRate = seniorCitizenRate;
s.createdDate = LocalDate.now();
return s;
}
}
@@ -0,0 +1,20 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface FdBaseInterestRateSlabHistoryRepository extends JpaRepository<FdBaseInterestRateSlabHistory, Long> {
List<FdBaseInterestRateSlabHistory> findByEffectiveIdOrderBySrNo(Long effectiveId);
}
@@ -230,11 +230,37 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
sqlBuilder.append("dptp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, ");
sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, ");
sqlBuilder.append("dptp.min_deposit_amount as minDepositAmount, dptp.deposit_amount as depositAmount, ");
sqlBuilder.append("dptp.max_deposit_amount as maxDepositAmount ");
sqlBuilder.append("dptp.max_deposit_amount as maxDepositAmount, ");
sqlBuilder.append("sp.account_type as accountType, sp.fd_type as fdType, sp.product_code as productCode, ");
sqlBuilder.append("sp.customer_segment as customerSegment, ");
sqlBuilder.append("sp.gl_account_id as glAccountId, gla.name as glAccountName, ");
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.is_dormancy_tracking_active as isDormancyTrackingActive, ");
sqlBuilder.append("sp.days_to_inactive as daysToInactive, sp.days_to_dormancy as daysToDormancy, ");
sqlBuilder.append("sp.days_to_escheat as daysToEscheat, ");
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.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.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("join m_currency curr on curr.code = sp.currency_code ");
sqlBuilder.append(" left join m_tax_group tg on tg.id = sp.tax_group_id ");
sqlBuilder.append("left join m_tax_group tg on tg.id = sp.tax_group_id ");
sqlBuilder.append("left join acc_gl_account gla on gla.id = sp.gl_account_id ");
this.schemaSql = sqlBuilder.toString();
}
@@ -271,9 +297,55 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
final BigDecimal depositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "depositAmount");
final BigDecimal maxDepositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "maxDepositAmount");
return FixedDepositProductData.instance(depositProductData, preClosurePenalApplicable, preClosurePenalInterest,
preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, minDepositAmount, depositAmount, maxDepositAmount);
final FixedDepositProductData result = FixedDepositProductData.instance(depositProductData, preClosurePenalApplicable,
preClosurePenalInterest, preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType,
maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, minDepositAmount, depositAmount,
maxDepositAmount);
// CBS Custom Fields
result.setAccountType(rs.getString("accountType"));
result.setFdType(rs.getString("fdType"));
result.setProductCode(rs.getString("productCode"));
result.setCustomerSegment(rs.getString("customerSegment"));
result.setGlAccountId(JdbcSupport.getLong(rs, "glAccountId"));
result.setGlAccountName(rs.getString("glAccountName"));
final java.time.LocalDate effectiveDate = JdbcSupport.getLocalDate(rs, "effectiveDate");
result.setEffectiveDate(effectiveDate != null ? effectiveDate.toString() : null);
final java.time.LocalDate effectiveTo = JdbcSupport.getLocalDate(rs, "effectiveTo");
result.setEffectiveTo(effectiveTo != null ? effectiveTo.toString() : null);
final java.time.LocalDate endDate = JdbcSupport.getLocalDate(rs, "endDate");
result.setEndDate(endDate != null ? endDate.toString() : null);
result.setIntroducerRequired(rs.getBoolean("introducerRequired"));
result.setShareLinkingEnabled(rs.getBoolean("shareLinkingEnabled"));
result.setNominationFacility(rs.getBoolean("nominationFacility"));
result.setDormancyAlert(rs.getBoolean("dormancyAlert"));
result.setIsDormancyTrackingActive(rs.getBoolean("isDormancyTrackingActive"));
result.setDaysToInactive(JdbcSupport.getLong(rs, "daysToInactive"));
result.setDaysToDormancy(JdbcSupport.getLong(rs, "daysToDormancy"));
result.setDaysToEscheat(JdbcSupport.getLong(rs, "daysToEscheat"));
result.setInterestCalculateSelector(JdbcSupport.getInteger(rs, "interestCalculateSelector"));
result.setAveragePeriod(rs.getString("averagePeriod"));
result.setAutoPost(rs.getBoolean("autoPost"));
result.setFrequencyFixDay(JdbcSupport.getInteger(rs, "frequencyFixDay"));
final java.time.LocalDate lastInterestDate = JdbcSupport.getLocalDate(rs, "lastInterestDate");
result.setLastInterestDate(lastInterestDate != null ? lastInterestDate.toString() : null);
result.setEnableLockinPeriod(rs.getBoolean("enableLockinPeriod"));
result.setBalanceFrequency(rs.getString("balanceFrequency"));
result.setAccountClosedZeroBalanceInDayClosing(rs.getBoolean("accountClosedZeroBalanceInDayClosing"));
result.setAutoDebitFacility(rs.getBoolean("autoDebitFacility"));
result.setLoanFacility(rs.getBoolean("loanFacility"));
result.setPrematureWithdrawalOption(rs.getBoolean("prematureWithdrawalOption"));
result.setTenureFlexibility(rs.getBoolean("tenureFlexibility"));
result.setLienMarked(rs.getBoolean("lienMarked"));
result.setMaturityInstructions(rs.getBoolean("maturityInstructions"));
result.setCreditMaturityProceeds(rs.getBoolean("creditMaturityProceeds"));
result.setPenaltyGracePeriodMissedInstallments(rs.getBoolean("penaltyGracePeriodMissedInstallments"));
result.setPanelRequestApplicable(rs.getBoolean("panelRequestApplicable"));
result.setNoPenaltyNomineeHeirClosure(rs.getBoolean("noPenaltyNomineeHeirClosure"));
result.setDeceasedSettlement(rs.getBoolean("deceasedSettlement"));
result.setAllowDebitTxnAfterDueDate(rs.getBoolean("allowDebitTxnAfterDueDate"));
result.setWithdrawalFeeForTransfers(rs.getBoolean("withdrawalFeeForTransfers"));
return result;
}
}
@@ -0,0 +1,27 @@
/**
* 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.FdInterestRateSaveRequest;
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
public interface FdBaseInterestRateReadPlatformService {
List<FdInterestRateEffectiveData> retrieveAllInterestRates();
List<FdInterestRateEffectiveData> retrieveAllActiveInterestRates();
List<FdInterestRateEffectiveData> retrieveActiveInterestRatesByType(String fdType);
FdInterestRateEffectiveData saveInterestRates(FdInterestRateSaveRequest request, Long createdById);
}
@@ -0,0 +1,128 @@
/**
* 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.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest;
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
import org.apache.fineract.portfolio.savings.data.FdInterestRateSlabData;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffective;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffectiveRepository;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateSlabHistory;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateSlabHistoryRepository;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class FdBaseInterestRateReadPlatformServiceImpl implements FdBaseInterestRateReadPlatformService {
private final FdBaseInterestRateEffectiveRepository effectiveRepository;
private final FdBaseInterestRateSlabHistoryRepository slabRepository;
@Override
public List<FdInterestRateEffectiveData> retrieveAllInterestRates() {
return effectiveRepository.findByOrderByEffectiveFromDateDesc().stream().map(this::toData).collect(Collectors.toList());
}
@Override
public List<FdInterestRateEffectiveData> retrieveAllActiveInterestRates() {
return effectiveRepository.findByIsActiveTrueOrderByEffectiveFromDateDesc().stream().map(this::toData)
.collect(Collectors.toList());
}
@Override
public List<FdInterestRateEffectiveData> retrieveActiveInterestRatesByType(String fdType) {
return effectiveRepository.findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc(fdType).stream().map(this::toData)
.collect(Collectors.toList());
}
@Override
@Transactional
public FdInterestRateEffectiveData saveInterestRates(FdInterestRateSaveRequest request, Long createdById) {
final String fdType = request.getFdType();
final LocalDate today = LocalDate.now();
final List<FdBaseInterestRateEffective> activeVersions = effectiveRepository
.findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc(fdType);
for (FdBaseInterestRateEffective version : activeVersions) {
version.deactivate(today);
}
if (!activeVersions.isEmpty()) {
effectiveRepository.saveAll(activeVersions);
}
final String label = request.getVersionLabel() != null && !request.getVersionLabel().isBlank()
? request.getVersionLabel()
: "FD " + fdType + " Rate Version " + LocalDate.now();
FdBaseInterestRateEffective newVersion = FdBaseInterestRateEffective.create(fdType, label, today, request.getRemarks(), createdById);
newVersion = effectiveRepository.saveAndFlush(newVersion);
final Long versionId = newVersion.getId();
int srCounter = 1;
if (request.getSlabs() != null) {
for (FdInterestRateSaveRequest.SlabRequest slabRequest : request.getSlabs()) {
final int sr = slabRequest.getSrNo() != null ? slabRequest.getSrNo() : srCounter++;
final FdBaseInterestRateSlabHistory slab = FdBaseInterestRateSlabHistory.create(versionId, sr,
slabRequest.getDescription(), slabRequest.getPeriodTypeEnum(), slabRequest.getFromPeriod(),
slabRequest.getToPeriod(), slabRequest.getAnnualInterestRate(), slabRequest.getSeniorCitizenRate());
slabRepository.save(slab);
}
}
return toData(newVersion);
}
private FdInterestRateEffectiveData toData(FdBaseInterestRateEffective e) {
List<FdInterestRateSlabData> slabs = slabRepository.findByEffectiveIdOrderBySrNo(e.getId()).stream()
.map(s -> FdInterestRateSlabData.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())
.seniorCitizenRate(s.getSeniorCitizenRate())
.build())
.collect(Collectors.toList());
return FdInterestRateEffectiveData.builder()
.id(e.getId())
.fdType(e.getFdType())
.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) {
return "Days";
}
return switch (code) {
case 0 -> "Days";
case 1 -> "Weeks";
case 2 -> "Months";
case 3 -> "Years";
default -> "Days";
};
}
}
@@ -1391,8 +1391,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
public SavingsAccountData fallbackPostInterest(SavingsAccountData savingsAccountData, boolean postInterestAs, LocalDate transactionDate,
boolean backdatedTxnsAllowedTill, Throwable t) {
// NOTE: allow caller to catch the exceptions
// NOTE: wrap throwable only if really necessary
throw errorHandler.getMappable(t, null, null, "savings.postinterest");
throw org.apache.fineract.infrastructure.core.exception.ErrorHandler.getMappable(t, null, null, "savings.postinterest");
}
@Transactional
@@ -146,8 +146,12 @@ import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatform
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.domain.FdBaseInterestRateEffectiveRepository;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateSlabHistoryRepository;
import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformService;
import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformServiceImpl;
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;
@@ -171,6 +175,14 @@ public class SavingsConfiguration {
return new RdBaseInterestRateReadPlatformServiceImpl(effectiveRepository, slabRepository);
}
@Bean
@ConditionalOnMissingBean(FdBaseInterestRateReadPlatformService.class)
public FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService(
FdBaseInterestRateEffectiveRepository effectiveRepository,
FdBaseInterestRateSlabHistoryRepository slabRepository) {
return new FdBaseInterestRateReadPlatformServiceImpl(effectiveRepository, slabRepository);
}
@Bean
@ConditionalOnMissingBean(SavingsAccountTransactionSearchService.class)
public SavingsAccountTransactionSearchService savingsAccountTransactionSearchService(PlatformSecurityContext context,
@@ -286,4 +286,7 @@
<include file="parts/0276_create_current_account_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0277_fix_current_account_audit_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0278_add_is_mandatory_to_joint_holder.xml" relativeToChangelogFile="true"/>
<include file="parts/0279_add_fd_type_and_base_interest_rate_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0280_add_fixed_deposit_product_codes.xml" relativeToChangelogFile="true"/>
<include file="parts/0281_update_fd_base_interest_rate_seed_data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,300 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- 1. Add fd_type column to m_savings_product -->
<changeSet id="0279a_add_fd_type_to_savings_product" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_savings_product" columnName="fd_type"/></not>
</preConditions>
<addColumn tableName="m_savings_product">
<column name="fd_type" type="VARCHAR(20)" defaultValue="SINGLE">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<!-- 2. Create FD base interest rate effective (version) table -->
<changeSet id="0279b_create_fd_base_interest_rate_effective" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_fd_base_interest_rate_effective"/></not>
</preConditions>
<createTable tableName="m_fd_base_interest_rate_effective">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="fd_type" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="version_label" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="effective_from_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="effective_to_date" type="DATE"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="remarks" type="VARCHAR(500)"/>
<column name="created_by_id" type="BIGINT"/>
<column name="created_date" type="DATE"/>
</createTable>
</changeSet>
<!-- 3. Create FD base interest rate slab history table -->
<changeSet id="0279c_create_fd_base_interest_rate_slab_history" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_fd_base_interest_rate_slab_history"/></not>
</preConditions>
<createTable tableName="m_fd_base_interest_rate_slab_history">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="effective_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="sr_no" type="INT">
<constraints nullable="false"/>
</column>
<column name="description" type="VARCHAR(200)"/>
<!-- period_type_enum: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS -->
<column name="period_type_enum" type="INT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="from_period" type="INT">
<constraints nullable="false"/>
</column>
<column name="to_period" type="INT"/>
<column name="annual_interest_rate" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<!-- senior_citizen_rate: additional rate for senior citizens -->
<column name="senior_citizen_rate" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
<column name="created_date" type="DATE"/>
</createTable>
<addForeignKeyConstraint
baseTableName="m_fd_base_interest_rate_slab_history"
baseColumnNames="effective_id"
referencedTableName="m_fd_base_interest_rate_effective"
referencedColumnNames="id"
constraintName="fk_fd_base_rate_slab_effective"/>
</changeSet>
<!-- 4. Seed initial FD base interest rate data -->
<changeSet id="0279d_seed_fd_base_interest_rate_single" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE'</sqlCheck>
</preConditions>
<!-- FD SINGLE: &lt; 1 year, period in DAYS -->
<insert tableName="m_fd_base_interest_rate_effective">
<column name="fd_type" value="SINGLE"/>
<column name="version_label" value="FD-SINGLE-V1"/>
<column name="effective_from_date" value="2025-01-01"/>
<column name="is_active" valueBoolean="true"/>
<column name="remarks" value="Initial FD Single (Short Term) rates"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279e_seed_fd_single_slabs" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_slab_history sh JOIN m_fd_base_interest_rate_effective e ON sh.effective_id = e.id WHERE e.fd_type = 'SINGLE'</sqlCheck>
</preConditions>
<!-- 1-29 days -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SINGLE' AND version_label='FD-SINGLE-V1')"/>
<column name="sr_no" valueNumeric="1"/>
<column name="description" value="1 to 29 Days"/>
<column name="period_type_enum" valueNumeric="0"/>
<column name="from_period" valueNumeric="1"/>
<column name="to_period" valueNumeric="29"/>
<column name="annual_interest_rate" valueNumeric="4.50"/>
<column name="senior_citizen_rate" valueNumeric="5.00"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 30-89 days -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SINGLE' AND version_label='FD-SINGLE-V1')"/>
<column name="sr_no" valueNumeric="2"/>
<column name="description" value="30 to 89 Days"/>
<column name="period_type_enum" valueNumeric="0"/>
<column name="from_period" valueNumeric="30"/>
<column name="to_period" valueNumeric="89"/>
<column name="annual_interest_rate" valueNumeric="5.00"/>
<column name="senior_citizen_rate" valueNumeric="5.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 90-179 days -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SINGLE' AND version_label='FD-SINGLE-V1')"/>
<column name="sr_no" valueNumeric="3"/>
<column name="description" value="90 to 179 Days"/>
<column name="period_type_enum" valueNumeric="0"/>
<column name="from_period" valueNumeric="90"/>
<column name="to_period" valueNumeric="179"/>
<column name="annual_interest_rate" valueNumeric="5.50"/>
<column name="senior_citizen_rate" valueNumeric="6.00"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 180+ days -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SINGLE' AND version_label='FD-SINGLE-V1')"/>
<column name="sr_no" valueNumeric="4"/>
<column name="description" value="180+ Days"/>
<column name="period_type_enum" valueNumeric="0"/>
<column name="from_period" valueNumeric="180"/>
<column name="annual_interest_rate" valueNumeric="6.00"/>
<column name="senior_citizen_rate" valueNumeric="6.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279f_seed_fd_base_interest_rate_double" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE'</sqlCheck>
</preConditions>
<!-- FD DOUBLE: >= 1 year, period in YEARS -->
<insert tableName="m_fd_base_interest_rate_effective">
<column name="fd_type" value="DOUBLE"/>
<column name="version_label" value="FD-DOUBLE-V1"/>
<column name="effective_from_date" value="2025-01-01"/>
<column name="is_active" valueBoolean="true"/>
<column name="remarks" value="Initial FD Double (Long Term) rates"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279g_seed_fd_double_slabs" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_slab_history sh JOIN m_fd_base_interest_rate_effective e ON sh.effective_id = e.id WHERE e.fd_type = 'DOUBLE'</sqlCheck>
</preConditions>
<!-- 1-2 years -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='DOUBLE' AND version_label='FD-DOUBLE-V1')"/>
<column name="sr_no" valueNumeric="1"/>
<column name="description" value="1 to 2 Years"/>
<column name="period_type_enum" valueNumeric="3"/>
<column name="from_period" valueNumeric="1"/>
<column name="to_period" valueNumeric="2"/>
<column name="annual_interest_rate" valueNumeric="6.50"/>
<column name="senior_citizen_rate" valueNumeric="7.00"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 3-5 years -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='DOUBLE' AND version_label='FD-DOUBLE-V1')"/>
<column name="sr_no" valueNumeric="2"/>
<column name="description" value="3 to 5 Years"/>
<column name="period_type_enum" valueNumeric="3"/>
<column name="from_period" valueNumeric="3"/>
<column name="to_period" valueNumeric="5"/>
<column name="annual_interest_rate" valueNumeric="6.75"/>
<column name="senior_citizen_rate" valueNumeric="7.25"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 6-10 years -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='DOUBLE' AND version_label='FD-DOUBLE-V1')"/>
<column name="sr_no" valueNumeric="3"/>
<column name="description" value="6 to 10 Years"/>
<column name="period_type_enum" valueNumeric="3"/>
<column name="from_period" valueNumeric="6"/>
<column name="to_period" valueNumeric="10"/>
<column name="annual_interest_rate" valueNumeric="7.00"/>
<column name="senior_citizen_rate" valueNumeric="7.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 11+ years -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='DOUBLE' AND version_label='FD-DOUBLE-V1')"/>
<column name="sr_no" valueNumeric="4"/>
<column name="description" value="11+ Years"/>
<column name="period_type_enum" valueNumeric="3"/>
<column name="from_period" valueNumeric="11"/>
<column name="annual_interest_rate" valueNumeric="7.25"/>
<column name="senior_citizen_rate" valueNumeric="7.75"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279h_seed_fd_base_interest_rate_special" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SPECIAL'</sqlCheck>
</preConditions>
<!-- FD SPECIAL: monthly interest payout, period in MONTHS -->
<insert tableName="m_fd_base_interest_rate_effective">
<column name="fd_type" value="SPECIAL"/>
<column name="version_label" value="FD-SPECIAL-V1"/>
<column name="effective_from_date" value="2025-01-01"/>
<column name="is_active" valueBoolean="true"/>
<column name="remarks" value="Initial FD Special (Monthly Payout) rates"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279i_seed_fd_special_slabs" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_slab_history sh JOIN m_fd_base_interest_rate_effective e ON sh.effective_id = e.id WHERE e.fd_type = 'SPECIAL'</sqlCheck>
</preConditions>
<!-- 1-12 months -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SPECIAL' AND version_label='FD-SPECIAL-V1')"/>
<column name="sr_no" valueNumeric="1"/>
<column name="description" value="1 to 12 Months"/>
<column name="period_type_enum" valueNumeric="2"/>
<column name="from_period" valueNumeric="1"/>
<column name="to_period" valueNumeric="12"/>
<column name="annual_interest_rate" valueNumeric="6.00"/>
<column name="senior_citizen_rate" valueNumeric="6.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 13-24 months -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SPECIAL' AND version_label='FD-SPECIAL-V1')"/>
<column name="sr_no" valueNumeric="2"/>
<column name="description" value="13 to 24 Months"/>
<column name="period_type_enum" valueNumeric="2"/>
<column name="from_period" valueNumeric="13"/>
<column name="to_period" valueNumeric="24"/>
<column name="annual_interest_rate" valueNumeric="6.50"/>
<column name="senior_citizen_rate" valueNumeric="7.00"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 25-60 months -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SPECIAL' AND version_label='FD-SPECIAL-V1')"/>
<column name="sr_no" valueNumeric="3"/>
<column name="description" value="25 to 60 Months"/>
<column name="period_type_enum" valueNumeric="2"/>
<column name="from_period" valueNumeric="25"/>
<column name="to_period" valueNumeric="60"/>
<column name="annual_interest_rate" valueNumeric="7.00"/>
<column name="senior_citizen_rate" valueNumeric="7.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 61+ months -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SPECIAL' AND version_label='FD-SPECIAL-V1')"/>
<column name="sr_no" valueNumeric="4"/>
<column name="description" value="61+ Months"/>
<column name="period_type_enum" valueNumeric="2"/>
<column name="from_period" valueNumeric="61"/>
<column name="annual_interest_rate" valueNumeric="7.25"/>
<column name="senior_citizen_rate" valueNumeric="7.75"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- FixedDepositProductAccountType code and values -->
<changeSet id="0280a_add_fd_product_account_type_code" author="system">
<validCheckSum>ANY</validCheckSum>
<sql>
INSERT INTO m_code (code_name, is_system_defined)
SELECT 'FixedDepositProductAccountType', false
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'FixedDepositProductAccountType');
</sql>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT c.id, v.code_value, v.order_position, true, false
FROM m_code c,
(VALUES
('Fd Account', 1)
) AS v(code_value, order_position)
WHERE c.code_name = 'FixedDepositProductAccountType'
AND NOT EXISTS (
SELECT 1 FROM m_code_value cv
WHERE cv.code_id = c.id AND cv.code_value = v.code_value
);
</sql>
</changeSet>
<!-- FixedDepositProductCustomerSegment code and values -->
<changeSet id="0280b_add_fd_product_customer_segment_code" author="system">
<validCheckSum>ANY</validCheckSum>
<sql>
INSERT INTO m_code (code_name, is_system_defined)
SELECT 'FixedDepositProductCustomerSegment', false
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'FixedDepositProductCustomerSegment');
</sql>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT c.id, v.code_value, v.order_position, true, false
FROM m_code c,
(VALUES
('Individual', 1),
('Non-Individual', 2)
) AS v(code_value, order_position)
WHERE c.code_name = 'FixedDepositProductCustomerSegment'
AND NOT EXISTS (
SELECT 1 FROM m_code_value cv
WHERE cv.code_id = c.id AND cv.code_value = v.code_value
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0281_update_fd_base_interest_rate_seed_data" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_fd_base_interest_rate_effective"/>
</preConditions>
<sql>
UPDATE m_fd_base_interest_rate_slab_history
SET description = '1 to 29 Days',
from_period = 1,
to_period = 29
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE' AND version_label = 'FD-SINGLE-V1')
AND sr_no = 1;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '30 to 89 Days',
from_period = 30,
to_period = 89
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE' AND version_label = 'FD-SINGLE-V1')
AND sr_no = 2;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '90 to 179 Days',
from_period = 90,
to_period = 179
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE' AND version_label = 'FD-SINGLE-V1')
AND sr_no = 3;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '180+ Days',
from_period = 180,
to_period = NULL
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE' AND version_label = 'FD-SINGLE-V1')
AND sr_no = 4;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '1 to 2 Years',
from_period = 1,
to_period = 2
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE' AND version_label = 'FD-DOUBLE-V1')
AND sr_no = 1;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '3 to 5 Years',
from_period = 3,
to_period = 5
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE' AND version_label = 'FD-DOUBLE-V1')
AND sr_no = 2;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '6 to 10 Years',
from_period = 6,
to_period = 10
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE' AND version_label = 'FD-DOUBLE-V1')
AND sr_no = 3;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '11+ Years',
from_period = 11,
to_period = NULL
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE' AND version_label = 'FD-DOUBLE-V1')
AND sr_no = 4;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '1 to 12 Months',
from_period = 1,
to_period = 12
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SPECIAL' AND version_label = 'FD-SPECIAL-V1')
AND sr_no = 1;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '13 to 24 Months',
from_period = 13,
to_period = 24
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SPECIAL' AND version_label = 'FD-SPECIAL-V1')
AND sr_no = 2;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '25 to 60 Months',
from_period = 25,
to_period = 60
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SPECIAL' AND version_label = 'FD-SPECIAL-V1')
AND sr_no = 3;
INSERT INTO m_fd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, senior_citizen_rate, created_date)
SELECT e.id, 4, '61+ Months', 2, 61, NULL, 7.250000, 7.750000, CURRENT_DATE
FROM m_fd_base_interest_rate_effective e
WHERE e.fd_type = 'SPECIAL' AND e.version_label = 'FD-SPECIAL-V1'
AND NOT EXISTS (
SELECT 1
FROM m_fd_base_interest_rate_slab_history sh
WHERE sh.effective_id = e.id
AND sh.sr_no = 4
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -45,6 +45,7 @@ import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccount;
import org.apache.fineract.portfolio.shareproducts.domain.ShareProduct;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
public class AccountNumberGeneratorTest {
@@ -54,6 +55,7 @@ public class AccountNumberGeneratorTest {
private SavingsAccountRepository savingsRepo;
private GLAccountRepository glAccountRepo;
private SavingsProductRepository savingsProductRepo;
private JdbcTemplate jdbcTemplate;
private AccountNumberGenerator generator;
@@ -65,8 +67,9 @@ public class AccountNumberGeneratorTest {
savingsRepo = mock(SavingsAccountRepository.class);
glAccountRepo = mock(GLAccountRepository.class);
savingsProductRepo = mock(SavingsProductRepository.class);
jdbcTemplate = mock(JdbcTemplate.class);
generator = new AccountNumberGenerator(configService, clientRepo, loanRepo, savingsRepo, glAccountRepo, savingsProductRepo);
generator = new AccountNumberGenerator(configService, clientRepo, loanRepo, savingsRepo, glAccountRepo, savingsProductRepo, jdbcTemplate);
GlobalConfigurationPropertyData accountLengthConfig = mock(GlobalConfigurationPropertyData.class);
when(accountLengthConfig.getValue()).thenReturn(Long.valueOf("9"));