feat(accounts): backend fixes for Modify Application feature (Current/Savings/FD/RD)
Current Account: - CurrentAccountReadPlatformServiceImpl: add retrieveJointHolders() and retrievePartners() - CurrentAccountWritePlatformServiceImpl: full modifyApplication() - updates all CBS fields, joint holders, partners, nominees, facility config, dates, introducer - Fix cbsAccountType save: parse string account type from 'accountType' JSON field Nominees: - ClientNomineeWritePlatformService: add replaceNomineesForAccount() for atomic replace - Savings/FD/RD modifyApplication: replace nominees on modify FD/RD Read (DepositAccountReadPlatformServiceImpl): - Add isSeniorCitizen, seniorCitizenInterestRate to BASE SQL SELECT - Add setCbsAccountFields() call in FD/RD mappers reading accountStatus, modeOfOperation, introducerClientId, guardianFields, linkedAccountId, isSeniorCitizen from ResultSet - Fix retrieveOne() to also fetch joint holders from m_savings_account_joint_holders - Use explicit LinkedHashMap in joint holder query (camelCase keys for Angular) FD/RD Data Classes: - FixedDepositAccountData: add accountStatus, modeOfOperation, introducerClientId, linkedAccountId, jointHolders, isSeniorCitizen, seniorCitizenInterestRate fields - RecurringDepositAccountData: same fields added - Fix all factory methods (withInterestChartAndRecurringDetails, withClosureTemplateDetails, withTemplateOptions, associationsAndTemplate) to call copyCbsFieldsFrom() - Fix SavingsAccountReadPlatformServiceImpl joint holders: use camelCase keys FD Calculator: - FixedDepositAccountInterestCalculationServiceImpl: add calculateExact() with actual calendar-based calculation matching PostingPeriod engine (exact same as account creation) - Add submittedOnDate, daysInYear, interestPostingPeriodInMonths optional params - Support interestCompoundingPeriodInMonths=0 (daily compounding) in both validator and calc - DepositsApiConstants: add submittedOnDate/dateFormat/locale/daysInYear to allowed params - DepositAccountDataValidator: allow 0 (daily) in interestCompoundingPeriodInMonths Misc: - SavingsProductData: null-check in isCashBased/isAccrualBased methods (NPE fix) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
7c4cf6260c
commit
a051faba5d
+5
@@ -512,6 +512,11 @@ public final class DepositsApiConstants {
|
||||
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
|
||||
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
|
||||
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
|
||||
// Exact calendar-based calculation params
|
||||
fixedDepositInterestCalculationData.add("submittedOnDate");
|
||||
fixedDepositInterestCalculationData.add("dateFormat");
|
||||
fixedDepositInterestCalculationData.add("locale");
|
||||
fixedDepositInterestCalculationData.add("daysInYear");
|
||||
|
||||
return fixedDepositInterestCalculationData;
|
||||
|
||||
|
||||
+717
-714
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
+40386
-37900
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+47
-41
@@ -1,41 +1,47 @@
|
||||
/**
|
||||
* 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.client.service;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface ClientNomineeWritePlatformService {
|
||||
|
||||
CommandProcessingResult addNominee(Long clientId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult updateNominee(Long clientId, Long nomineeId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult deleteNominee(Long clientId, Long nomineeId);
|
||||
|
||||
/**
|
||||
* Saves nominee from a JSON element already extracted from the account creation payload.
|
||||
* Called internally during account creation (SB/CB/FD/RD) so nominee is persisted
|
||||
* in the same transaction without a separate API call.
|
||||
*/
|
||||
void saveNomineeIfPresent(Long clientId, String accountNo, String accountType, JsonElement nomineeJson);
|
||||
|
||||
void saveNomineesIfPresent(Long clientId, String accountNo, String accountType, JsonElement nomineesJson);
|
||||
}
|
||||
/**
|
||||
* 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.client.service;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface ClientNomineeWritePlatformService {
|
||||
|
||||
CommandProcessingResult addNominee(Long clientId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult updateNominee(Long clientId, Long nomineeId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult deleteNominee(Long clientId, Long nomineeId);
|
||||
|
||||
/**
|
||||
* Saves nominee from a JSON element already extracted from the account creation payload.
|
||||
* Called internally during account creation (SB/CB/FD/RD) so nominee is persisted
|
||||
* in the same transaction without a separate API call.
|
||||
*/
|
||||
void saveNomineeIfPresent(Long clientId, String accountNo, String accountType, JsonElement nomineeJson);
|
||||
|
||||
void saveNomineesIfPresent(Long clientId, String accountNo, String accountType, JsonElement nomineesJson);
|
||||
|
||||
/**
|
||||
* Soft-deletes all existing nominees for the given account, then saves the new nominees.
|
||||
* Used during modifyApplication to replace nominees atomically.
|
||||
*/
|
||||
void replaceNomineesForAccount(Long clientId, String accountNo, String accountType, JsonElement nomineesJson);
|
||||
}
|
||||
|
||||
+16
@@ -465,4 +465,20 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
|
||||
saveNomineeIfPresent(clientId, accountNo, accountType, nomineesJson);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replaceNomineesForAccount(final Long clientId, final String accountNo, final String accountType, final JsonElement nomineesJson) {
|
||||
// Soft-delete all existing nominees for this account
|
||||
final List<org.apache.fineract.portfolio.client.domain.ClientNominee> existing =
|
||||
nomineeRepository.findByClientIdAndAccountNoAndAccountType(clientId, accountNo, accountType);
|
||||
for (org.apache.fineract.portfolio.client.domain.ClientNominee n : existing) {
|
||||
n.setIsDeleted(true);
|
||||
n.setIsActive(false);
|
||||
}
|
||||
nomineeRepository.saveAll(existing);
|
||||
// Save new nominees
|
||||
if (nomineesJson != null && !nomineesJson.isJsonNull()) {
|
||||
saveNomineesIfPresent(clientId, accountNo, accountType, nomineesJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-6
@@ -257,24 +257,34 @@ public class FixedDepositAccountsApiResource {
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.CalculateFixedDepositInterestResponse.class)))
|
||||
public String calculateFixedDepositInterest(@Context final UriInfo uriInfo,
|
||||
@QueryParam("principalAmount") @Parameter(description = "BigDecimal principalAmount") final BigDecimal principalAmount,
|
||||
@QueryParam("annualInterestRate") @Parameter(description = "annualInterestRate") final BigDecimal annualInterestRate,
|
||||
@QueryParam("tenureInMonths") @Parameter(description = "tenureInMonths") final Long tenureInMonths,
|
||||
@QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "interestCompoundingPeriodInMonths") final Long interestCompoundingPeriodInMonths,
|
||||
@QueryParam("interestPostingPeriodInMonths") @Parameter(description = "interestPostingPeriodInMonths") final Long interestPostingPeriodInMonths) {
|
||||
@QueryParam("principalAmount") @Parameter(description = "Deposit principal amount") final BigDecimal principalAmount,
|
||||
@QueryParam("annualInterestRate") @Parameter(description = "Annual interest rate (%)") final BigDecimal annualInterestRate,
|
||||
@QueryParam("tenureInMonths") @Parameter(description = "Tenure in months") final Long tenureInMonths,
|
||||
@QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "Compounding period in months (1,3,6,12)") final Long interestCompoundingPeriodInMonths,
|
||||
@QueryParam("interestPostingPeriodInMonths") @Parameter(description = "Posting period in months (1,3,6,12)") final Long interestPostingPeriodInMonths,
|
||||
@QueryParam("submittedOnDate") @Parameter(description = "Submission date (yyyy-MM-dd) for exact calendar calculation") final String submittedOnDate,
|
||||
@QueryParam("daysInYear") @Parameter(description = "Days in year (360 or 365)") final Integer daysInYear) {
|
||||
HashMap request = new HashMap<>();
|
||||
request.put("annualInterestRate", annualInterestRate);
|
||||
request.put("tenureInMonths", tenureInMonths);
|
||||
request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths);
|
||||
request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths);
|
||||
request.put("principalAmount", principalAmount);
|
||||
// Pass exact calculation params if provided
|
||||
if (submittedOnDate != null && !submittedOnDate.isBlank()) {
|
||||
request.put("submittedOnDate", submittedOnDate); // pass as string, extracted in service
|
||||
request.put("dateFormat", "yyyy-MM-dd");
|
||||
request.put("locale", "en");
|
||||
}
|
||||
if (daysInYear != null) {
|
||||
request.put("daysInYear", daysInYear);
|
||||
}
|
||||
String apiRequestBodyAsJson = toApiJsonSerializer.serialize(request);
|
||||
JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson);
|
||||
HashMap result = fixedDepositAccountInterestCalculationService
|
||||
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper));
|
||||
|
||||
return toApiJsonSerializer.serializeResult(result);
|
||||
|
||||
}
|
||||
|
||||
private BigDecimal getActivationCharge(Long accountId) {
|
||||
|
||||
+820
-818
File diff suppressed because it is too large
Load Diff
+42
@@ -66,11 +66,53 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
|
||||
throw new CurrentAccountNotFoundException(accountId);
|
||||
}
|
||||
final Map<String, Object> response = buildStructuredResponse(rows.get(0));
|
||||
response.put("jointHolders", retrieveJointHolders(accountId));
|
||||
response.put("partners", retrievePartners(accountId));
|
||||
response.put("charges", retrieveCharges(accountId));
|
||||
response.put("transactions", retrieveTransactions(accountId));
|
||||
return response;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> retrieveJointHolders(Long accountId) {
|
||||
final String sql =
|
||||
"SELECT jh.id, jh.client_id, jh.cif_number, jh.display_name, "
|
||||
+ "jh.share_percentage, jh.signing_authority, jh.role, jh.is_primary, jh.is_mandatory "
|
||||
+ "FROM m_current_account_joint_holder jh "
|
||||
+ "WHERE jh.current_account_id = ? "
|
||||
+ "ORDER BY jh.is_primary DESC, jh.id ASC";
|
||||
return jdbcTemplate.query(sql, (rs, rowNum) -> {
|
||||
final Map<String, Object> h = new LinkedHashMap<>();
|
||||
h.put("id", rs.getLong("id"));
|
||||
h.put("clientId", rs.getLong("client_id"));
|
||||
h.put("cifNumber", rs.getString("cif_number"));
|
||||
h.put("displayName", rs.getString("display_name"));
|
||||
h.put("sharePercentage", rs.getBigDecimal("share_percentage"));
|
||||
h.put("signingAuthority", rs.getString("signing_authority"));
|
||||
h.put("role", rs.getString("role"));
|
||||
h.put("isPrimary", rs.getBoolean("is_primary"));
|
||||
h.put("isMandatory", rs.getBoolean("is_mandatory"));
|
||||
return h;
|
||||
}, accountId);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> retrievePartners(Long accountId) {
|
||||
final String sql =
|
||||
"SELECT p.id, p.client_id, p.client_cif, p.client_name, p.share_percentage, p.is_mandatory "
|
||||
+ "FROM m_current_account_partner p "
|
||||
+ "WHERE p.current_account_id = ? "
|
||||
+ "ORDER BY p.id ASC";
|
||||
return jdbcTemplate.query(sql, (rs, rowNum) -> {
|
||||
final Map<String, Object> p = new LinkedHashMap<>();
|
||||
p.put("id", rs.getLong("id"));
|
||||
p.put("clientId", rs.getLong("client_id"));
|
||||
p.put("clientCif", rs.getString("client_cif"));
|
||||
p.put("clientName", rs.getString("client_name"));
|
||||
p.put("sharePercentage", rs.getBigDecimal("share_percentage"));
|
||||
p.put("isMandatory", rs.getBoolean("is_mandatory"));
|
||||
return p;
|
||||
}, accountId);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> retrieveTransactions(Long accountId) {
|
||||
final String sql =
|
||||
"SELECT t.id, t.transaction_type_enum, t.transaction_date, t.amount, "
|
||||
|
||||
+775
-702
File diff suppressed because it is too large
Load Diff
+48
-1
@@ -309,13 +309,44 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
||||
sqlBuilder.append(depositAccountMapper.schema());
|
||||
sqlBuilder.append(" where sa.id = ? and sa.deposit_type_enum = ? ");
|
||||
|
||||
return this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId, depositAccountType.getValue());
|
||||
final DepositAccountData account = this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId, depositAccountType.getValue());
|
||||
if (account != null) {
|
||||
final java.util.List<java.util.Map<String, Object>> jointHolders = retrieveJointHolders(accountId);
|
||||
if (!jointHolders.isEmpty()) {
|
||||
if (account instanceof org.apache.fineract.portfolio.savings.data.FixedDepositAccountData) {
|
||||
((org.apache.fineract.portfolio.savings.data.FixedDepositAccountData) account).setJointHolders(jointHolders);
|
||||
} else if (account instanceof org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData) {
|
||||
((org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData) account).setJointHolders(jointHolders);
|
||||
}
|
||||
}
|
||||
}
|
||||
return account;
|
||||
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new DepositAccountNotFoundException(depositAccountType, accountId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private java.util.List<java.util.Map<String, Object>> retrieveJointHolders(final Long accountId) {
|
||||
final String sql = "SELECT jh.id, jh.client_id, jh.cif_number, jh.display_name, "
|
||||
+ "jh.share_percentage, jh.signing_authority, jh.role, jh.is_primary, jh.is_mandatory "
|
||||
+ "FROM m_savings_account_joint_holders jh WHERE jh.savings_account_id = ? "
|
||||
+ "ORDER BY jh.is_primary DESC, jh.id ASC";
|
||||
return this.jdbcTemplate.query(sql, (rs, rowNum) -> {
|
||||
final java.util.Map<String, Object> h = new java.util.LinkedHashMap<>();
|
||||
h.put("id", rs.getLong("id"));
|
||||
h.put("clientId", rs.getLong("client_id"));
|
||||
h.put("cifNumber", rs.getString("cif_number"));
|
||||
h.put("displayName", rs.getString("display_name"));
|
||||
h.put("sharePercentage", rs.getBigDecimal("share_percentage"));
|
||||
h.put("signingAuthority", rs.getString("signing_authority"));
|
||||
h.put("role", rs.getString("role"));
|
||||
h.put("isPrimary", rs.getBoolean("is_primary"));
|
||||
h.put("isMandatory", rs.getBoolean("is_mandatory"));
|
||||
return h;
|
||||
}, accountId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DepositAccountData retrieveOneWithClosureTemplate(final DepositAccountType depositAccountType, final Long accountId) {
|
||||
try {
|
||||
@@ -812,6 +843,8 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
||||
selectFieldsSqlBuilder.append("sa.introducer_cif_number as introducerCifNumber, ");
|
||||
selectFieldsSqlBuilder.append("sa.linked_account_id as linkedAccountId, sa.linked_account_type as linkedAccountType, ");
|
||||
selectFieldsSqlBuilder.append("sa.linked_account_no as linkedAccountNo, ");
|
||||
selectFieldsSqlBuilder.append("COALESCE(sa.is_senior_citizen, false) as isSeniorCitizen, ");
|
||||
selectFieldsSqlBuilder.append("sa.senior_citizen_interest_rate as seniorCitizenInterestRate, ");
|
||||
selectFieldsSqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation, ");
|
||||
selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,");
|
||||
selectFieldsSqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName ");
|
||||
@@ -1111,6 +1144,13 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
||||
rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"),
|
||||
rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"),
|
||||
rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate"));
|
||||
result.setCbsAccountFields(rs.getString("accountStatus"), rs.getString("modeOfOperation"),
|
||||
JdbcSupport.getLong(rs, "introducerClientId"), rs.getString("introducerName"),
|
||||
rs.getString("introducerCifNumber"), rs.getString("specialCategory"), rs.getString("weakerSection"),
|
||||
rs.getString("guardianName"), rs.getString("guardianCifNumber"), rs.getString("guardianRelationship"),
|
||||
JdbcSupport.getLong(rs, "guardianClientId"), JdbcSupport.getLocalDate(rs, "minorDateOfBirth"),
|
||||
JdbcSupport.getLong(rs, "linkedAccountId"),
|
||||
rs.getBoolean("isSeniorCitizen"), JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "seniorCitizenInterestRate"));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1268,6 +1308,13 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
||||
rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"),
|
||||
rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"),
|
||||
rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate"));
|
||||
result.setCbsAccountFields(rs.getString("accountStatus"), rs.getString("modeOfOperation"),
|
||||
JdbcSupport.getLong(rs, "introducerClientId"), rs.getString("introducerName"),
|
||||
rs.getString("introducerCifNumber"), rs.getString("specialCategory"), rs.getString("weakerSection"),
|
||||
rs.getString("guardianName"), rs.getString("guardianCifNumber"), rs.getString("guardianRelationship"),
|
||||
JdbcSupport.getLong(rs, "guardianClientId"), JdbcSupport.getLocalDate(rs, "minorDateOfBirth"),
|
||||
JdbcSupport.getLong(rs, "linkedAccountId"),
|
||||
rs.getBoolean("isSeniorCitizen"), JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "seniorCitizenInterestRate"));
|
||||
|
||||
return result;
|
||||
|
||||
|
||||
+832
-812
File diff suppressed because it is too large
Load Diff
+147
-74
@@ -1,74 +1,147 @@
|
||||
/**
|
||||
* 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.interestCompoundingPeriodInMonthsParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
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.apache.fineract.portfolio.savings.data.DepositAccountDataValidator;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FixedDepositAccountInterestCalculationServiceImpl implements FixedDepositAccountInterestCalculationService {
|
||||
|
||||
private final DepositAccountDataValidator depositAccountDataValidator;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Override
|
||||
public HashMap calculateInterest(JsonQuery query) {
|
||||
depositAccountDataValidator.validateFixedDepositForInterestCalculation(query.json());
|
||||
JsonElement element = query.parsedJson();
|
||||
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
|
||||
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
|
||||
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
||||
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName,
|
||||
element);
|
||||
BigDecimal maturityAmount = this.calculateInterestInternal(principalAmount, annualInterestRate, tenureInMonths,
|
||||
interestCompoundingPeriodInMonths);
|
||||
String warning = "This is an approximate calculated amount - it may vary slightly when the account is created";
|
||||
|
||||
HashMap result = new HashMap<>();
|
||||
result.put("maturityAmount", maturityAmount);
|
||||
result.put("warning", warning);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public BigDecimal calculateInterestInternal(BigDecimal principalAmount, BigDecimal annualInterestRate, Long tenureInMonths,
|
||||
Long interestCompoundingPeriodInMonths) {
|
||||
BigDecimal numberOfCompoundingsPerAnnum = BigDecimal.valueOf(12).divide(BigDecimal.valueOf(interestCompoundingPeriodInMonths));
|
||||
Long totalNumberOfCompoundings = tenureInMonths / interestCompoundingPeriodInMonths;
|
||||
MathContext mc = MoneyHelper.getMathContext();
|
||||
BigDecimal exponentialTerm = annualInterestRate.divide(numberOfCompoundingsPerAnnum, mc).divide(BigDecimal.valueOf(100), mc)
|
||||
.add(BigDecimal.valueOf(1)).pow(Math.toIntExact(totalNumberOfCompoundings));
|
||||
return principalAmount.multiply(exponentialTerm);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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.interestCompoundingPeriodInMonthsParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
||||
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.time.temporal.ChronoUnit;
|
||||
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.apache.fineract.portfolio.savings.data.DepositAccountDataValidator;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FixedDepositAccountInterestCalculationServiceImpl implements FixedDepositAccountInterestCalculationService {
|
||||
|
||||
private final DepositAccountDataValidator depositAccountDataValidator;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Override
|
||||
public HashMap calculateInterest(JsonQuery query) {
|
||||
depositAccountDataValidator.validateFixedDepositForInterestCalculation(query.json());
|
||||
JsonElement element = query.parsedJson();
|
||||
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
|
||||
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
|
||||
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
||||
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName, element);
|
||||
|
||||
// Optional params for exact calendar-based calculation
|
||||
LocalDate submittedOnDate = null;
|
||||
try {
|
||||
final String dateStr = this.fromApiJsonHelper.extractStringNamed("submittedOnDate", element);
|
||||
final String dateFormat = this.fromApiJsonHelper.extractStringNamed("dateFormat", element);
|
||||
if (dateStr != null && dateFormat != null) {
|
||||
submittedOnDate = LocalDate.parse(dateStr, java.time.format.DateTimeFormatter.ofPattern(dateFormat));
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
Integer daysInYear = null;
|
||||
final Long daysInYearLong = this.fromApiJsonHelper.extractLongNamed("daysInYear", element);
|
||||
if (daysInYearLong != null) daysInYear = daysInYearLong.intValue();
|
||||
Long interestPostingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed("interestPostingPeriodInMonths", element);
|
||||
|
||||
BigDecimal maturityAmount;
|
||||
if (submittedOnDate != null && daysInYear != null && interestPostingPeriodInMonths != null) {
|
||||
// Exact calendar-based calculation — same algorithm as account creation
|
||||
final LocalDate maturityDate = submittedOnDate.plusMonths(tenureInMonths);
|
||||
maturityAmount = this.calculateExact(principalAmount, annualInterestRate, submittedOnDate, maturityDate,
|
||||
interestCompoundingPeriodInMonths, interestPostingPeriodInMonths, daysInYear);
|
||||
} else {
|
||||
maturityAmount = this.calculateInterestInternal(principalAmount, annualInterestRate, tenureInMonths,
|
||||
interestCompoundingPeriodInMonths);
|
||||
}
|
||||
|
||||
HashMap result = new HashMap<>();
|
||||
result.put("maturityAmount", maturityAmount);
|
||||
result.put("totalInterest", maturityAmount.subtract(principalAmount).setScale(2, RoundingMode.HALF_UP));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact calendar-based FD maturity calculation — replicates the PostingPeriod engine
|
||||
* used during actual account creation/activation. Uses actual days per month.
|
||||
*/
|
||||
public BigDecimal calculateExact(final BigDecimal principal, final BigDecimal annualRate,
|
||||
final LocalDate submittedOnDate, final LocalDate maturityDate,
|
||||
final long compoundingMonths, final long postingMonths, final int daysInYear) {
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
BigDecimal balance = principal;
|
||||
LocalDate periodStart = submittedOnDate;
|
||||
|
||||
while (periodStart.isBefore(maturityDate)) {
|
||||
// Determine end of this posting period (based on actual calendar)
|
||||
LocalDate periodEnd;
|
||||
if (postingMonths == 0) {
|
||||
periodEnd = periodStart.plusDays(1);
|
||||
} else {
|
||||
periodEnd = periodStart.plusMonths(postingMonths);
|
||||
}
|
||||
if (periodEnd.isAfter(maturityDate)) {
|
||||
periodEnd = maturityDate;
|
||||
}
|
||||
|
||||
final long daysInPeriod = ChronoUnit.DAYS.between(periodStart, periodEnd);
|
||||
|
||||
if (compoundingMonths == 0) {
|
||||
// Daily compounding — exact: balance × (1 + r/D)^days
|
||||
final double dailyRate = annualRate.doubleValue() / (daysInYear * 100.0);
|
||||
final double multiplier = Math.pow(1.0 + dailyRate, daysInPeriod);
|
||||
balance = balance.multiply(BigDecimal.valueOf(multiplier), mc);
|
||||
} else {
|
||||
// Periodic compounding — interest = balance × rate × days / (daysInYear × 100)
|
||||
final BigDecimal interest = balance
|
||||
.multiply(annualRate, mc)
|
||||
.multiply(BigDecimal.valueOf(daysInPeriod), mc)
|
||||
.divide(BigDecimal.valueOf((long) daysInYear * 100), mc);
|
||||
balance = balance.add(interest);
|
||||
}
|
||||
|
||||
periodStart = periodEnd;
|
||||
}
|
||||
|
||||
return balance.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
public BigDecimal calculateInterestInternal(BigDecimal principalAmount, BigDecimal annualInterestRate, Long tenureInMonths,
|
||||
Long interestCompoundingPeriodInMonths) {
|
||||
if (interestCompoundingPeriodInMonths == null || interestCompoundingPeriodInMonths <= 0) {
|
||||
// Daily compounding fallback: approximate with monthly
|
||||
interestCompoundingPeriodInMonths = 1L;
|
||||
}
|
||||
BigDecimal numberOfCompoundingsPerAnnum = BigDecimal.valueOf(12).divide(BigDecimal.valueOf(interestCompoundingPeriodInMonths));
|
||||
Long totalNumberOfCompoundings = tenureInMonths / interestCompoundingPeriodInMonths;
|
||||
MathContext mc = MoneyHelper.getMathContext();
|
||||
BigDecimal exponentialTerm = annualInterestRate.divide(numberOfCompoundingsPerAnnum, mc).divide(BigDecimal.valueOf(100), mc)
|
||||
.add(BigDecimal.valueOf(1)).pow(Math.toIntExact(totalNumberOfCompoundings));
|
||||
return principalAmount.multiply(exponentialTerm);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-6
@@ -267,13 +267,25 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
|
||||
final SavingsAccountData accountData = this.jdbcTemplate.queryForObject(sql, this.savingAccountMapper,
|
||||
new Object[] { accountId }); // NOSONAR
|
||||
|
||||
// Load joint holders
|
||||
// Load joint holders with explicit camelCase keys (queryForList returns lowercase from MySQL)
|
||||
if (accountData != null) {
|
||||
final String jointSql = "SELECT jh.id as id, jh.client_id as clientId, jh.cif_number as cifNumber, "
|
||||
+ "jh.display_name as displayName, jh.share_percentage as sharePercentage, "
|
||||
+ "jh.signing_authority as signingAuthority, jh.role as role, jh.is_primary as isPrimary "
|
||||
+ "FROM m_savings_account_joint_holders jh WHERE jh.savings_account_id = ?";
|
||||
final List<java.util.Map<String, Object>> jointHolders = this.jdbcTemplate.queryForList(jointSql, accountId);
|
||||
final String jointSql = "SELECT jh.id, jh.client_id, jh.cif_number, jh.display_name, "
|
||||
+ "jh.share_percentage, jh.signing_authority, jh.role, jh.is_primary, jh.is_mandatory "
|
||||
+ "FROM m_savings_account_joint_holders jh WHERE jh.savings_account_id = ? "
|
||||
+ "ORDER BY jh.is_primary DESC, jh.id ASC";
|
||||
final List<java.util.Map<String, Object>> jointHolders = this.jdbcTemplate.query(jointSql, (rs, rowNum) -> {
|
||||
final java.util.Map<String, Object> h = new java.util.LinkedHashMap<>();
|
||||
h.put("id", rs.getLong("id"));
|
||||
h.put("clientId", rs.getLong("client_id"));
|
||||
h.put("cifNumber", rs.getString("cif_number"));
|
||||
h.put("displayName", rs.getString("display_name"));
|
||||
h.put("sharePercentage", rs.getBigDecimal("share_percentage"));
|
||||
h.put("signingAuthority", rs.getString("signing_authority"));
|
||||
h.put("role", rs.getString("role"));
|
||||
h.put("isPrimary", rs.getBoolean("is_primary"));
|
||||
h.put("isMandatory", rs.getBoolean("is_mandatory"));
|
||||
return h;
|
||||
}, accountId);
|
||||
if (!jointHolders.isEmpty()) {
|
||||
accountData.setJointHolders(jointHolders);
|
||||
}
|
||||
|
||||
+762
-752
File diff suppressed because it is too large
Load Diff
+784
-715
File diff suppressed because it is too large
Load Diff
+833
-758
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user