rd account and rd product code update

This commit is contained in:
Nitesh Yadav
2026-05-08 13:09:07 +05:30
parent f1535a40c8
commit 44878377d8
14 changed files with 45082 additions and 36273 deletions
@@ -102,21 +102,6 @@ public class CurrentProductsApiResource {
private final CodeValueReadPlatformService codeValueReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private String generateNextProductCode(final String tableName, final String prefix) {
try {
String maxCode = jdbcTemplate.queryForObject(
"SELECT MAX(product_code) FROM " + tableName + " WHERE product_code LIKE ?",
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";
}
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@@ -306,7 +291,7 @@ public class CurrentProductsApiResource {
interestCalculationTypeOption, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions,
periodFrequencyOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, chargeOptions,
penaltyOptions, taxGroupOptions, yesNoOptions, accountTypeOptions, customerSegmentOptions, accountMappingForPayment);
savingsProductToReturn.setProductCode(generateNextProductCode("m_current_product", "CA-IND-"));
// product code removed
}
return savingsProductToReturn;
@@ -345,7 +345,7 @@ public class FixedDepositProductsApiResource {
interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions,
paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, chargeOptions, penaltyOptions, chartTemplate,
preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions, taxGroupOptions);
fixedDepositProductToReturn.setProductCode(generateNextProductCode("m_savings_product", "FD-IND-"));
// product code removed
}
fixedDepositProductToReturn.setInterestCalculationTypeOption(interestCalculationTypeOption);
@@ -372,21 +372,6 @@ public class FixedDepositProductsApiResource {
.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}")
@@ -45,6 +45,7 @@ import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
@@ -77,6 +78,7 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
import org.apache.fineract.portfolio.savings.service.RecurringDepositAccountInterestCalculationService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
@@ -99,6 +101,7 @@ public class RecurringDepositAccountsApiResource {
private final DepositAccountPreMatureCalculationPlatformService accountPreMatureCalculationPlatformService;
private final BulkImportWorkbookService bulkImportWorkbookService;
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
private final RecurringDepositAccountInterestCalculationService recurringDepositAccountInterestCalculationService;
private final SqlValidator sqlValidator;
@GET
@@ -369,6 +372,18 @@ public class RecurringDepositAccountsApiResource {
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("calculate-rd-interest")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String calculateRecurringDepositInterest(@Context final UriInfo uriInfo, final String apiRequestBodyAsJson) {
JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson);
HashMap<String, Object> result = recurringDepositAccountInterestCalculationService
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper));
return toApiJsonSerializer.serializeResult(result);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
@@ -333,7 +333,7 @@ public class RecurringDepositProductsApiResource {
interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions,
paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, chargeOptions, penaltyOptions, chartTemplate,
preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions, taxGroupOptions);
recurringDepositProductToReturn.setProductCode(generateNextProductCode("m_savings_product", "RD-IND-"));
// product code removed
}
recurringDepositProductToReturn.setInterestCalculationTypeOption(interestCalculationTypeOption);
@@ -360,21 +360,6 @@ public class RecurringDepositProductsApiResource {
.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 = 300",
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";
}
}
// ── RD Interest Rate Chart endpoints ────────────────────────────────────
@@ -106,21 +106,6 @@ public class SavingsProductsApiResource {
private final ConfigurationDomainService configurationDomainService;
private final JdbcTemplate jdbcTemplate;
private String generateNextProductCode(final String tableName, final String prefix) {
try {
String maxCode = jdbcTemplate.queryForObject(
"SELECT MAX(product_code) FROM " + tableName + " WHERE product_code LIKE ?",
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";
}
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@@ -315,7 +300,7 @@ public class SavingsProductsApiResource {
periodFrequencyOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, chargeOptions,
penaltyOptions, taxGroupOptions, yesNoOptions, accountTypeOptions, customerSegmentOptions,
accountMappingForPayment);
savingsProductToReturn.setProductCode(generateNextProductCode("m_savings_product", "SB-IND-"));
// product code generation removed
}
return savingsProductToReturn;
@@ -0,0 +1,28 @@
/**
* 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
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.service;
import java.util.HashMap;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
public interface RecurringDepositAccountInterestCalculationService {
HashMap<String, Object> calculateInterest(JsonQuery query);
}
@@ -0,0 +1,138 @@
/**
* 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
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.service;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.annualInterestRateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestCompoundingPeriodInMonthsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
import com.google.gson.JsonElement;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.HashMap;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RecurringDepositAccountInterestCalculationServiceImpl implements RecurringDepositAccountInterestCalculationService {
private final FromJsonHelper fromApiJsonHelper;
@Override
public HashMap<String, Object> calculateInterest(JsonQuery query) {
JsonElement element = query.parsedJson();
BigDecimal depositAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(depositAmountParamName, element);
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName,
element);
LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed("submittedOnDate", element);
if (submittedOnDate == null) {
submittedOnDate = LocalDate.now();
}
BigDecimal maturityAmount = this.calculateInterestInternal(depositAmount, annualInterestRate, tenureInMonths,
interestCompoundingPeriodInMonths, submittedOnDate);
BigDecimal totalPrincipal = depositAmount.multiply(BigDecimal.valueOf(tenureInMonths));
BigDecimal totalInterest = maturityAmount.subtract(totalPrincipal);
String warning = "This is an approximate calculated amount - it may vary slightly when the account is created";
HashMap<String, Object> result = new HashMap<>();
result.put("maturityAmount", maturityAmount);
result.put("totalPrincipal", totalPrincipal);
result.put("totalInterest", totalInterest);
result.put("daysInYear", 365); // Standard for this product as per image
result.put("warning", warning);
return result;
}
public BigDecimal calculateInterestInternal(BigDecimal installmentAmount, BigDecimal annualInterestRate, Long tenureInMonths,
Long interestCompoundingPeriodInMonths, LocalDate submittedOnDate) {
MathContext mc = MoneyHelper.getMathContext();
BigDecimal maturityAmount = BigDecimal.ZERO;
int totalMonths = Math.toIntExact(tenureInMonths);
if (interestCompoundingPeriodInMonths == 0) {
// Standard Banking Approach for Daily Compounding RD using Actual Calendar Days
BigDecimal currentBalance = BigDecimal.ZERO;
double dailyRate = annualInterestRate.doubleValue() / 36500.0;
LocalDate date = submittedOnDate;
for (int m = 1; m <= totalMonths; m++) {
// Add monthly installment at the start of the month
currentBalance = currentBalance.add(installmentAmount);
// Get actual days in the current month
int daysInMonth = date.lengthOfMonth();
// Calculate interest for the month using daily compounding
double multiplier = Math.pow(1.0 + dailyRate, daysInMonth);
currentBalance = currentBalance.multiply(BigDecimal.valueOf(multiplier), mc);
// Move to next month
date = date.plusMonths(1);
}
maturityAmount = currentBalance;
} else {
// Periodic Compounding (Monthly, Quarterly, etc.)
BigDecimal monthlyRate = annualInterestRate.divide(BigDecimal.valueOf(1200), mc);
BigDecimal i = monthlyRate;
int n = totalMonths;
// Standard RD formula for monthly compounding:
// M = P * ((1+i)^n - 1) / i * (1+i)
// For other periods, we can approximate or use a loop.
// Using loop for consistency with various compounding periods.
BigDecimal compoundingFrequencyMonths = BigDecimal.valueOf(interestCompoundingPeriodInMonths);
BigDecimal ratePerCompounding = annualInterestRate.divide(BigDecimal.valueOf(100), mc)
.multiply(compoundingFrequencyMonths).divide(BigDecimal.valueOf(12), mc);
for (int m = 1; m <= totalMonths; m++) {
BigDecimal monthsRemaining = BigDecimal.valueOf(totalMonths - m + 1);
BigDecimal numberOfCompoundings = monthsRemaining.divide(compoundingFrequencyMonths, mc);
BigDecimal installmentMaturity = installmentAmount.multiply(
BigDecimal.ONE.add(ratePerCompounding).pow(numberOfCompoundings.intValue(), mc), mc);
// Add remaining partial compounding period interest (simple interest for the rest)
BigDecimal remainingMonths = monthsRemaining.remainder(compoundingFrequencyMonths);
if (remainingMonths.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal partialInterest = installmentMaturity.multiply(annualInterestRate).multiply(remainingMonths)
.divide(BigDecimal.valueOf(1200), mc);
installmentMaturity = installmentMaturity.add(partialInterest);
}
maturityAmount = maturityAmount.add(installmentMaturity);
}
}
return maturityAmount.setScale(2, RoundingMode.HALF_UP);
}
}
@@ -288,5 +288,6 @@
<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/0284_remove_product_code_unique_constraints.xml" relativeToChangelogFile="true"/>
<include file="parts/0281_update_fd_base_interest_rate_seed_data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,25 @@
<?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="0284_remove_product_code_unique_constraints" author="system">
<!-- For Savings/RD/FD products -->
<dropUniqueConstraint tableName="m_savings_product" constraintName="m_savings_product_product_code_key"/>
<!-- For Current products -->
<dropUniqueConstraint tableName="m_current_product" constraintName="cp_unq_product_code"/>
<!-- Set existing empty or blank product_codes to NULL to avoid future issues -->
<update tableName="m_savings_product">
<column name="product_code" valueComputed="NULL"/>
<where>product_code = '' OR product_code IS NULL</where>
</update>
<update tableName="m_current_product">
<column name="product_code" valueComputed="NULL"/>
<where>product_code = '' OR product_code IS NULL</where>
</update>
</changeSet>
</databaseChangeLog>