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:
Jaanu423
2026-06-03 10:32:59 +05:30
co-authored by Claude Sonnet 4.6
parent 7c4cf6260c
commit a051faba5d
19 changed files with 68620 additions and 45192 deletions
@@ -512,6 +512,11 @@ public final class DepositsApiConstants {
fixedDepositInterestCalculationData.add(tenureInMonthsParamName); fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName); fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName); fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
// Exact calendar-based calculation params
fixedDepositInterestCalculationData.add("submittedOnDate");
fixedDepositInterestCalculationData.add("dateFormat");
fixedDepositInterestCalculationData.add("locale");
fixedDepositInterestCalculationData.add("daysInYear");
return fixedDepositInterestCalculationData; return fixedDepositInterestCalculationData;
@@ -692,6 +692,7 @@ public final class SavingsProductData implements Serializable {
} }
public boolean isCashBasedAccountingEnabled() { public boolean isCashBasedAccountingEnabled() {
if (this.accountingRule == null) return false;
return AccountingRuleType.CASH_BASED.getValue().toString().equals(this.accountingRule.getValue()); return AccountingRuleType.CASH_BASED.getValue().toString().equals(this.accountingRule.getValue());
} }
@@ -700,10 +701,12 @@ public final class SavingsProductData implements Serializable {
} }
public boolean isUpfrontAccrualAccounting() { public boolean isUpfrontAccrualAccounting() {
if (this.accountingRule == null) return false;
return AccountingRuleType.ACCRUAL_UPFRONT.getValue().toString().equals(this.accountingRule.getValue()); return AccountingRuleType.ACCRUAL_UPFRONT.getValue().toString().equals(this.accountingRule.getValue());
} }
public boolean isPeriodicAccrualAccounting() { public boolean isPeriodicAccrualAccounting() {
if (this.accountingRule == null) return false;
return AccountingRuleType.ACCRUAL_PERIODIC.getValue().toString().equals(this.accountingRule.getValue()); return AccountingRuleType.ACCRUAL_PERIODIC.getValue().toString().equals(this.accountingRule.getValue());
} }
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
@@ -38,4 +38,10 @@ public interface ClientNomineeWritePlatformService {
void saveNomineeIfPresent(Long clientId, String accountNo, String accountType, JsonElement nomineeJson); void saveNomineeIfPresent(Long clientId, String accountNo, String accountType, JsonElement nomineeJson);
void saveNomineesIfPresent(Long clientId, String accountNo, String accountType, JsonElement nomineesJson); 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);
} }
@@ -465,4 +465,20 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
saveNomineeIfPresent(clientId, accountNo, accountType, nomineesJson); 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);
}
}
} }
@@ -257,24 +257,34 @@ public class FixedDepositAccountsApiResource {
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.CalculateFixedDepositInterestResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.CalculateFixedDepositInterestResponse.class)))
public String calculateFixedDepositInterest(@Context final UriInfo uriInfo, public String calculateFixedDepositInterest(@Context final UriInfo uriInfo,
@QueryParam("principalAmount") @Parameter(description = "BigDecimal principalAmount") final BigDecimal principalAmount, @QueryParam("principalAmount") @Parameter(description = "Deposit principal amount") final BigDecimal principalAmount,
@QueryParam("annualInterestRate") @Parameter(description = "annualInterestRate") final BigDecimal annualInterestRate, @QueryParam("annualInterestRate") @Parameter(description = "Annual interest rate (%)") final BigDecimal annualInterestRate,
@QueryParam("tenureInMonths") @Parameter(description = "tenureInMonths") final Long tenureInMonths, @QueryParam("tenureInMonths") @Parameter(description = "Tenure in months") final Long tenureInMonths,
@QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "interestCompoundingPeriodInMonths") final Long interestCompoundingPeriodInMonths, @QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "Compounding period in months (1,3,6,12)") final Long interestCompoundingPeriodInMonths,
@QueryParam("interestPostingPeriodInMonths") @Parameter(description = "interestPostingPeriodInMonths") final Long interestPostingPeriodInMonths) { @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<>(); HashMap request = new HashMap<>();
request.put("annualInterestRate", annualInterestRate); request.put("annualInterestRate", annualInterestRate);
request.put("tenureInMonths", tenureInMonths); request.put("tenureInMonths", tenureInMonths);
request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths); request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths);
request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths); request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths);
request.put("principalAmount", principalAmount); 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); String apiRequestBodyAsJson = toApiJsonSerializer.serialize(request);
JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson); JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson);
HashMap result = fixedDepositAccountInterestCalculationService HashMap result = fixedDepositAccountInterestCalculationService
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper)); .calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper));
return toApiJsonSerializer.serializeResult(result); return toApiJsonSerializer.serializeResult(result);
} }
private BigDecimal getActivationCharge(Long accountId) { private BigDecimal getActivationCharge(Long accountId) {
@@ -628,13 +628,15 @@ public class DepositAccountDataValidator {
element); element);
baseDataValidator.reset().parameter(interestCompoundingPeriodInMonthsParamName).value(interestCompoundingPeriodInMonths).notNull(); baseDataValidator.reset().parameter(interestCompoundingPeriodInMonthsParamName).value(interestCompoundingPeriodInMonths).notNull();
List<Long> allowedValues = List.of(1L, 2L, 3L, 4L, 6L, 12L); // 0 = daily compounding (handled by calculateExact); 1,2,3,4,6,12 = periodic
List<Long> allowedValues = List.of(0L, 1L, 2L, 3L, 4L, 6L, 12L);
if (interestCompoundingPeriodInMonths != null && !allowedValues.contains(interestCompoundingPeriodInMonths)) { if (interestCompoundingPeriodInMonths != null && !allowedValues.contains(interestCompoundingPeriodInMonths)) {
baseDataValidator.reset().parameter(interestCompoundingPeriodInMonthsParamName).value(interestCompoundingPeriodInMonths) baseDataValidator.reset().parameter(interestCompoundingPeriodInMonthsParamName).value(interestCompoundingPeriodInMonths)
.failWithCode("parameter.value.error", "interestCompoundingPeriodInMonths can only be one {1,2,3,4,6,12}"); .failWithCode("parameter.value.error", "interestCompoundingPeriodInMonths can only be one {0,1,2,3,4,6,12}");
} }
if (interestCompoundingPeriodInMonths != null && tenureInMonths != null // Divisibility check only for periodic (non-daily) compounding
&& tenureInMonths % interestCompoundingPeriodInMonths != 0) { if (interestCompoundingPeriodInMonths != null && interestCompoundingPeriodInMonths > 0
&& tenureInMonths != null && tenureInMonths % interestCompoundingPeriodInMonths != 0) {
baseDataValidator.reset().parameter(interestCompoundingPeriodInMonthsParamName).value(interestCompoundingPeriodInMonths) baseDataValidator.reset().parameter(interestCompoundingPeriodInMonthsParamName).value(interestCompoundingPeriodInMonths)
.failWithCode("parameter.relational.validation", .failWithCode("parameter.relational.validation",
"tenureInMonths must be perfectly divisible by interestCompoundingPeriodInMonths"); "tenureInMonths must be perfectly divisible by interestCompoundingPeriodInMonths");
@@ -66,11 +66,53 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
throw new CurrentAccountNotFoundException(accountId); throw new CurrentAccountNotFoundException(accountId);
} }
final Map<String, Object> response = buildStructuredResponse(rows.get(0)); 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("charges", retrieveCharges(accountId));
response.put("transactions", retrieveTransactions(accountId)); response.put("transactions", retrieveTransactions(accountId));
return response; 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) { private List<Map<String, Object>> retrieveTransactions(Long accountId) {
final String sql = final String sql =
"SELECT t.id, t.transaction_type_enum, t.transaction_date, t.amount, " "SELECT t.id, t.transaction_type_enum, t.transaction_date, t.amount, "
@@ -106,12 +106,18 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
LocalDate submittedOnDate = parseDate(getString(json, "submittedOnDate"), dateFormat, locale); LocalDate submittedOnDate = parseDate(getString(json, "submittedOnDate"), dateFormat, locale);
if (submittedOnDate == null) submittedOnDate = LocalDate.now(); if (submittedOnDate == null) submittedOnDate = LocalDate.now();
// accountType may arrive as integer or descriptive string — store null if unparseable // accountType (Fineract enum: 1=individual, 2=group) — try int, else default 1
Integer accountTypeEnum = null; Integer accountTypeEnum = 1;
// cbsAccountType — descriptive string e.g. "Standard Account" sent as "accountType"
String cbsAccountType = null;
if (json.has("accountType") && !json.get("accountType").isJsonNull()) { if (json.has("accountType") && !json.get("accountType").isJsonNull()) {
final String rawAccountType = json.get("accountType").getAsString();
try { try {
accountTypeEnum = json.get("accountType").getAsInt(); accountTypeEnum = Integer.parseInt(rawAccountType);
} catch (Exception ignored) { } } catch (Exception ignored) {
// It's a descriptive CBS account type string, not an enum int
cbsAccountType = rawAccountType;
}
} }
boolean msme = json.has("msme") && !json.get("msme").isJsonNull() boolean msme = json.has("msme") && !json.get("msme").isJsonNull()
@@ -134,6 +140,9 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
product.getInterestCalculateSelector(), product.getAveragePeriod(), product.getInterestCalculateSelector(), product.getAveragePeriod(),
product.getAvgBalanceCalcPeriod()); product.getAvgBalanceCalcPeriod());
// Save CBS account type (descriptive name: "Standard Account", "Basic Account", etc.)
if (cbsAccountType != null) account.setCbsAccountType(cbsAccountType);
// Copy product-level fields that are not in createNew() // Copy product-level fields that are not in createNew()
account.setMinRequiredOpeningBalance(product.getMinRequiredOpeningBalance()); account.setMinRequiredOpeningBalance(product.getMinRequiredOpeningBalance());
account.setMinRequiredBalance(product.getMinRequiredBalance()); account.setMinRequiredBalance(product.getMinRequiredBalance());
@@ -389,16 +398,80 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
context.authenticatedUser(); context.authenticatedUser();
final CurrentAccount account = findAccount(accountId); final CurrentAccount account = findAccount(accountId);
final JsonObject json = command.parsedJson().getAsJsonObject(); final JsonObject json = command.parsedJson().getAsJsonObject();
if (json.has("msme") && !json.get("msme").isJsonNull()) {
account.setMsme(json.get("msme").getAsBoolean()); if (json.has("externalId") && !json.get("externalId").isJsonNull())
account.setExternalId(json.get("externalId").getAsString());
if (json.has("accountStatus") && !json.get("accountStatus").isJsonNull())
account.setAccountStatus(json.get("accountStatus").getAsString());
if (json.has("accountType") && !json.get("accountType").isJsonNull()) {
final String rawAt = json.get("accountType").getAsString();
try { Integer.parseInt(rawAt); } catch (Exception ignored) {
// descriptive CBS type — save it
account.setCbsAccountType(rawAt);
}
} }
if (json.has("signatoryRights") && !json.get("signatoryRights").isJsonNull()) { if (json.has("customerSegment") && !json.get("customerSegment").isJsonNull())
account.setSignatoryRights(json.get("signatoryRights").getAsString()); account.setCustomerSegment(json.get("customerSegment").getAsString());
} if (json.has("modeOfOperation") && !json.get("modeOfOperation").isJsonNull())
if (json.has("modeOfOperation") && !json.get("modeOfOperation").isJsonNull()) {
account.setModeOfOperation(json.get("modeOfOperation").getAsString()); account.setModeOfOperation(json.get("modeOfOperation").getAsString());
if (json.has("signatoryRights") && !json.get("signatoryRights").isJsonNull())
account.setSignatoryRights(json.get("signatoryRights").getAsString());
if (json.has("msme") && !json.get("msme").isJsonNull())
account.setMsme(json.get("msme").getAsBoolean());
if (json.has("balanceSwapOption") && !json.get("balanceSwapOption").isJsonNull())
account.setBalanceSwapOption(json.get("balanceSwapOption").getAsBoolean());
if (json.has("autoDebitFacility") && !json.get("autoDebitFacility").isJsonNull())
account.setAutoDebitFacility(json.get("autoDebitFacility").getAsBoolean());
if (json.has("introducerClientId") && !json.get("introducerClientId").isJsonNull())
account.setIntroducerClientId(json.get("introducerClientId").getAsLong());
if (json.has("introducerName") && !json.get("introducerName").isJsonNull())
account.setIntroducerName(json.get("introducerName").getAsString());
if (json.has("introducerCifNumber") && !json.get("introducerCifNumber").isJsonNull())
account.setIntroducerCifNumber(json.get("introducerCifNumber").getAsString());
// dates
final String dateFormat = json.has("dateFormat") && !json.get("dateFormat").isJsonNull()
? json.get("dateFormat").getAsString() : "dd MMMM yyyy";
if (json.has("establishmentDate") && !json.get("establishmentDate").isJsonNull()) {
try {
account.setEstablishmentDate(LocalDate.parse(json.get("establishmentDate").getAsString(),
DateTimeFormatter.ofPattern(dateFormat)));
} catch (Exception ignored) {}
} }
if (json.has("startOperationDate") && !json.get("startOperationDate").isJsonNull()) {
try {
account.setStartOperationDate(LocalDate.parse(json.get("startOperationDate").getAsString(),
DateTimeFormatter.ofPattern(dateFormat)));
} catch (Exception ignored) {}
}
// facility config
JsonObject fcJson = json.has("accountFacilityConfig") && !json.get("accountFacilityConfig").isJsonNull()
? json.getAsJsonObject("accountFacilityConfig") : null;
if (fcJson == null && json.has("facilityConfig") && !json.get("facilityConfig").isJsonNull())
fcJson = json.getAsJsonObject("facilityConfig");
if (fcJson != null) account.setFacilityConfig(assembleFacilityConfig(account, fcJson));
// joint holders — clear and re-add
if (json.has("jointHolders") && json.get("jointHolders").isJsonArray()) {
account.getJointHolders().clear();
assembleJointHolders(account, json.getAsJsonArray("jointHolders"));
}
// partners — clear and re-add
if (json.has("partners") && json.get("partners").isJsonArray()) {
account.getPartners().clear();
assemblePartners(account, json.getAsJsonArray("partners"));
}
currentAccountRepository.saveAndFlush(account); currentAccountRepository.saveAndFlush(account);
// nominees — replace existing with new (done after flush to ensure accountNo is stable)
if (json.has("nominees") && !json.get("nominees").isJsonNull()) {
clientNomineeWritePlatformService.replaceNomineesForAccount(
account.getClient().getId(), account.getAccountNo(), "CB", json.get("nominees"));
}
return new CommandProcessingResultBuilder().withEntityId(accountId).build(); return new CommandProcessingResultBuilder().withEntityId(accountId).build();
} }
@@ -309,13 +309,44 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
sqlBuilder.append(depositAccountMapper.schema()); sqlBuilder.append(depositAccountMapper.schema());
sqlBuilder.append(" where sa.id = ? and sa.deposit_type_enum = ? "); 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) { } catch (final EmptyResultDataAccessException e) {
throw new DepositAccountNotFoundException(depositAccountType, accountId, 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 @Override
public DepositAccountData retrieveOneWithClosureTemplate(final DepositAccountType depositAccountType, final Long accountId) { public DepositAccountData retrieveOneWithClosureTemplate(final DepositAccountType depositAccountType, final Long accountId) {
try { try {
@@ -812,6 +843,8 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
selectFieldsSqlBuilder.append("sa.introducer_cif_number as introducerCifNumber, "); 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_id as linkedAccountId, sa.linked_account_type as linkedAccountType, ");
selectFieldsSqlBuilder.append("sa.linked_account_no as linkedAccountNo, "); 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.min_balance_for_interest_calculation as minBalanceForInterestCalculation, ");
selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,"); selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,");
selectFieldsSqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName "); 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("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"),
rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"),
rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); 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; return result;
} }
} }
@@ -1268,6 +1308,13 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"),
rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"),
rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); 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; return result;
@@ -467,6 +467,16 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
} }
} }
// Replace nominees if provided
final com.google.gson.JsonElement parsedJsonFd = command.parsedJson();
if (parsedJsonFd != null && parsedJsonFd.isJsonObject()) {
final com.google.gson.JsonObject jsonObjFd = parsedJsonFd.getAsJsonObject();
if (jsonObjFd.has("nominees") && !jsonObjFd.get("nominees").isJsonNull()) {
clientNomineeWritePlatformService.replaceNomineesForAccount(
account.clientId(), account.getAccountNumber(), "FD", jsonObjFd.get("nominees"));
}
}
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(accountId) // .withEntityId(accountId) //
@@ -539,6 +549,16 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
this.calendarInstanceRepository.save(calendarInstance); this.calendarInstanceRepository.save(calendarInstance);
} }
// Replace nominees if provided
final com.google.gson.JsonElement parsedJsonRd = command.parsedJson();
if (parsedJsonRd != null && parsedJsonRd.isJsonObject()) {
final com.google.gson.JsonObject jsonObjRd = parsedJsonRd.getAsJsonObject();
if (jsonObjRd.has("nominees") && !jsonObjRd.get("nominees").isJsonNull()) {
clientNomineeWritePlatformService.replaceNomineesForAccount(
account.clientId(), account.getAccountNumber(), "RD", jsonObjRd.get("nominees"));
}
}
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(accountId) // .withEntityId(accountId) //
@@ -27,6 +27,9 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureI
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.HashMap; import java.util.HashMap;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonQuery; import org.apache.fineract.infrastructure.core.api.JsonQuery;
@@ -49,21 +52,91 @@ public class FixedDepositAccountInterestCalculationServiceImpl implements FixedD
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element); BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element); BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element); Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName, Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName, element);
element);
BigDecimal maturityAmount = this.calculateInterestInternal(principalAmount, annualInterestRate, tenureInMonths, // Optional params for exact calendar-based calculation
interestCompoundingPeriodInMonths); LocalDate submittedOnDate = null;
String warning = "This is an approximate calculated amount - it may vary slightly when the account is created"; 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<>(); HashMap result = new HashMap<>();
result.put("maturityAmount", maturityAmount); result.put("maturityAmount", maturityAmount);
result.put("warning", warning); result.put("totalInterest", maturityAmount.subtract(principalAmount).setScale(2, RoundingMode.HALF_UP));
return result; 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, public BigDecimal calculateInterestInternal(BigDecimal principalAmount, BigDecimal annualInterestRate, Long tenureInMonths,
Long interestCompoundingPeriodInMonths) { Long interestCompoundingPeriodInMonths) {
if (interestCompoundingPeriodInMonths == null || interestCompoundingPeriodInMonths <= 0) {
// Daily compounding fallback: approximate with monthly
interestCompoundingPeriodInMonths = 1L;
}
BigDecimal numberOfCompoundingsPerAnnum = BigDecimal.valueOf(12).divide(BigDecimal.valueOf(interestCompoundingPeriodInMonths)); BigDecimal numberOfCompoundingsPerAnnum = BigDecimal.valueOf(12).divide(BigDecimal.valueOf(interestCompoundingPeriodInMonths));
Long totalNumberOfCompoundings = tenureInMonths / interestCompoundingPeriodInMonths; Long totalNumberOfCompoundings = tenureInMonths / interestCompoundingPeriodInMonths;
MathContext mc = MoneyHelper.getMathContext(); MathContext mc = MoneyHelper.getMathContext();
@@ -267,13 +267,25 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
final SavingsAccountData accountData = this.jdbcTemplate.queryForObject(sql, this.savingAccountMapper, final SavingsAccountData accountData = this.jdbcTemplate.queryForObject(sql, this.savingAccountMapper,
new Object[] { accountId }); // NOSONAR new Object[] { accountId }); // NOSONAR
// Load joint holders // Load joint holders with explicit camelCase keys (queryForList returns lowercase from MySQL)
if (accountData != null) { if (accountData != null) {
final String jointSql = "SELECT jh.id as id, jh.client_id as clientId, jh.cif_number as cifNumber, " final String jointSql = "SELECT jh.id, jh.client_id, jh.cif_number, jh.display_name, "
+ "jh.display_name as displayName, jh.share_percentage as sharePercentage, " + "jh.share_percentage, jh.signing_authority, jh.role, jh.is_primary, jh.is_mandatory "
+ "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 = ? "
+ "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.queryForList(jointSql, accountId); 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()) { if (!jointHolders.isEmpty()) {
accountData.setJointHolders(jointHolders); accountData.setJointHolders(jointHolders);
} }
@@ -386,6 +386,16 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
this.savingAccountRepository.saveAndFlush(account); this.savingAccountRepository.saveAndFlush(account);
} }
// Replace nominees if provided in the modify payload
final com.google.gson.JsonElement parsedJson = command.parsedJson();
if (parsedJson != null && parsedJson.isJsonObject()) {
final com.google.gson.JsonObject jsonObj = parsedJson.getAsJsonObject();
if (jsonObj.has("nominees") && !jsonObj.get("nominees").isJsonNull()) {
clientNomineeWritePlatformService.replaceNomineesForAccount(
account.clientId(), account.getAccountNumber(), "SB", jsonObj.get("nominees"));
}
}
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(savingsId) // .withEntityId(savingsId) //
@@ -116,6 +116,28 @@ public final class FixedDepositAccountData extends DepositAccountData {
private Boolean deceasedSettlement; private Boolean deceasedSettlement;
private Boolean allowDebitTxnAfterDueDate; private Boolean allowDebitTxnAfterDueDate;
// Joint holders for JOINT account status
private java.util.List<java.util.Map<String, Object>> jointHolders;
// Senior citizen
private Boolean isSeniorCitizen;
private java.math.BigDecimal seniorCitizenInterestRate;
// CBS account-level fields (stored per-account in m_savings_account)
private String accountStatus;
private String modeOfOperation;
private Long introducerClientId;
private String introducerName;
private String introducerCifNumber;
private String specialCategory;
private String weakerSection;
private String guardianName;
private String guardianCifNumber;
private String guardianRelationship;
private Long guardianClientId;
private java.time.LocalDate minorDateOfBirth;
private Long linkedAccountId;
// import fields // import fields
private transient Integer rowIndex; private transient Integer rowIndex;
private String dateFormat; private String dateFormat;
@@ -206,6 +228,31 @@ public final class FixedDepositAccountData extends DepositAccountData {
this.allowDebitTxnAfterDueDate = allowDebitTxnAfterDueDate; this.allowDebitTxnAfterDueDate = allowDebitTxnAfterDueDate;
} }
public void setJointHolders(java.util.List<java.util.Map<String, Object>> v) { this.jointHolders = v; }
public void setCbsAccountFields(final String accountStatus, final String modeOfOperation,
final Long introducerClientId, final String introducerName, final String introducerCifNumber,
final String specialCategory, final String weakerSection,
final String guardianName, final String guardianCifNumber, final String guardianRelationship,
final Long guardianClientId, final java.time.LocalDate minorDateOfBirth, final Long linkedAccountId,
final Boolean isSeniorCitizen, final java.math.BigDecimal seniorCitizenInterestRate) {
this.accountStatus = accountStatus;
this.modeOfOperation = modeOfOperation;
this.introducerClientId = introducerClientId;
this.introducerName = introducerName;
this.introducerCifNumber = introducerCifNumber;
this.specialCategory = specialCategory;
this.weakerSection = weakerSection;
this.guardianName = guardianName;
this.guardianCifNumber = guardianCifNumber;
this.guardianRelationship = guardianRelationship;
this.guardianClientId = guardianClientId;
this.minorDateOfBirth = minorDateOfBirth;
this.linkedAccountId = linkedAccountId;
this.isSeniorCitizen = isSeniorCitizen;
this.seniorCitizenInterestRate = seniorCitizenInterestRate;
}
public void copyCbsFieldsFrom(final FixedDepositAccountData src) { public void copyCbsFieldsFrom(final FixedDepositAccountData src) {
if (src == null) { if (src == null) {
return; return;
@@ -250,6 +297,23 @@ public final class FixedDepositAccountData extends DepositAccountData {
this.noPenaltyNomineeHeirClosure = src.noPenaltyNomineeHeirClosure; this.noPenaltyNomineeHeirClosure = src.noPenaltyNomineeHeirClosure;
this.deceasedSettlement = src.deceasedSettlement; this.deceasedSettlement = src.deceasedSettlement;
this.allowDebitTxnAfterDueDate = src.allowDebitTxnAfterDueDate; this.allowDebitTxnAfterDueDate = src.allowDebitTxnAfterDueDate;
// CBS account-level fields (per-account, not product defaults)
this.accountStatus = src.accountStatus;
this.modeOfOperation = src.modeOfOperation;
this.introducerClientId = src.introducerClientId;
this.introducerName = src.introducerName;
this.introducerCifNumber = src.introducerCifNumber;
this.specialCategory = src.specialCategory;
this.weakerSection = src.weakerSection;
this.guardianName = src.guardianName;
this.guardianCifNumber = src.guardianCifNumber;
this.guardianRelationship = src.guardianRelationship;
this.guardianClientId = src.guardianClientId;
this.minorDateOfBirth = src.minorDateOfBirth;
this.linkedAccountId = src.linkedAccountId;
this.jointHolders = src.jointHolders;
this.isSeniorCitizen = src.isSeniorCitizen;
this.seniorCitizenInterestRate = src.seniorCitizenInterestRate;
} }
private FixedDepositAccountData(Long clientId, Long productId, Long fieldofficerId, LocalDate submittedOnDate, private FixedDepositAccountData(Long clientId, Long productId, Long fieldofficerId, LocalDate submittedOnDate,
@@ -387,6 +451,11 @@ public final class FixedDepositAccountData extends DepositAccountData {
linkedAccount, account.transferInterestToSavings, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions, linkedAccount, account.transferInterestToSavings, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions,
account.transferToSavingsId, transferToSavingsAccount); account.transferToSavingsId, transferToSavingsAccount);
result.copyCbsFieldsFrom(account); result.copyCbsFieldsFrom(account);
// Merge accountTypeOptions and customerSegmentOptions from template (not in account data)
if (template != null && template != account) {
if (template.getAccountTypeOptions() != null) result.setAccountTypeOptions(template.getAccountTypeOptions());
if (template.getCustomerSegmentOptions() != null) result.setCustomerSegmentOptions(template.getCustomerSegmentOptions());
}
return result; return result;
} }
@@ -84,6 +84,28 @@ public final class RecurringDepositAccountData extends DepositAccountData {
private LocalDate submittedOnDate; private LocalDate submittedOnDate;
private Long depositPeriodFrequencyId; private Long depositPeriodFrequencyId;
// Joint holders for JOINT account status
private java.util.List<java.util.Map<String, Object>> jointHolders;
// Senior citizen
private Boolean isSeniorCitizen;
private java.math.BigDecimal seniorCitizenInterestRate;
// CBS account-level fields (per-account values)
private String accountStatus;
private String modeOfOperation;
private Long introducerClientId;
private String introducerName;
private String introducerCifNumber;
private String specialCategory;
private String weakerSection;
private String guardianName;
private String guardianCifNumber;
private String guardianRelationship;
private Long guardianClientId;
private java.time.LocalDate minorDateOfBirth;
private Long linkedAccountId;
// CBS product fields (populated from product template) // CBS product fields (populated from product template)
private String accountType; private String accountType;
private String customerSegment; private String customerSegment;
@@ -133,6 +155,31 @@ public final class RecurringDepositAccountData extends DepositAccountData {
this.customerSegmentOptions = customerSegmentOptions; this.customerSegmentOptions = customerSegmentOptions;
} }
public void setJointHolders(java.util.List<java.util.Map<String, Object>> v) { this.jointHolders = v; }
public void setCbsAccountFields(final String accountStatus, final String modeOfOperation,
final Long introducerClientId, final String introducerName, final String introducerCifNumber,
final String specialCategory, final String weakerSection,
final String guardianName, final String guardianCifNumber, final String guardianRelationship,
final Long guardianClientId, final java.time.LocalDate minorDateOfBirth, final Long linkedAccountId,
final Boolean isSeniorCitizen, final java.math.BigDecimal seniorCitizenInterestRate) {
this.accountStatus = accountStatus;
this.modeOfOperation = modeOfOperation;
this.introducerClientId = introducerClientId;
this.introducerName = introducerName;
this.introducerCifNumber = introducerCifNumber;
this.specialCategory = specialCategory;
this.weakerSection = weakerSection;
this.guardianName = guardianName;
this.guardianCifNumber = guardianCifNumber;
this.guardianRelationship = guardianRelationship;
this.guardianClientId = guardianClientId;
this.minorDateOfBirth = minorDateOfBirth;
this.linkedAccountId = linkedAccountId;
this.isSeniorCitizen = isSeniorCitizen;
this.seniorCitizenInterestRate = seniorCitizenInterestRate;
}
public void copyCbsFieldsFrom(final RecurringDepositAccountData src) { public void copyCbsFieldsFrom(final RecurringDepositAccountData src) {
if (src == null) return; if (src == null) return;
this.accountType = src.accountType; this.accountType = src.accountType;
@@ -174,6 +221,23 @@ public final class RecurringDepositAccountData extends DepositAccountData {
this.noPenaltyNomineeHeirClosure = src.noPenaltyNomineeHeirClosure; this.noPenaltyNomineeHeirClosure = src.noPenaltyNomineeHeirClosure;
this.deceasedSettlement = src.deceasedSettlement; this.deceasedSettlement = src.deceasedSettlement;
this.allowDebitTxnAfterDueDate = src.allowDebitTxnAfterDueDate; this.allowDebitTxnAfterDueDate = src.allowDebitTxnAfterDueDate;
// CBS account-level fields (per-account, not product defaults)
this.accountStatus = src.accountStatus;
this.modeOfOperation = src.modeOfOperation;
this.introducerClientId = src.introducerClientId;
this.introducerName = src.introducerName;
this.introducerCifNumber = src.introducerCifNumber;
this.specialCategory = src.specialCategory;
this.weakerSection = src.weakerSection;
this.guardianName = src.guardianName;
this.guardianCifNumber = src.guardianCifNumber;
this.guardianRelationship = src.guardianRelationship;
this.guardianClientId = src.guardianClientId;
this.minorDateOfBirth = src.minorDateOfBirth;
this.linkedAccountId = src.linkedAccountId;
this.jointHolders = src.jointHolders;
this.isSeniorCitizen = src.isSeniorCitizen;
this.seniorCitizenInterestRate = src.seniorCitizenInterestRate;
} }
public void setCbsProductFields(String accountType, String customerSegment, String productCode, public void setCbsProductFields(String accountType, String customerSegment, String productCode,
@@ -340,7 +404,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
public static RecurringDepositAccountData withInterestChartAndRecurringDetails(final RecurringDepositAccountData account, public static RecurringDepositAccountData withInterestChartAndRecurringDetails(final RecurringDepositAccountData account,
final DepositAccountInterestRateChartData accountChart, final Integer recurringFrequency, final DepositAccountInterestRateChartData accountChart, final Integer recurringFrequency,
final EnumOptionData recurringFrequencyType) { final EnumOptionData recurringFrequencyType) {
return new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName, final RecurringDepositAccountData result = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId, account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate, account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate,
account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType, account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType,
@@ -360,6 +424,8 @@ public final class RecurringDepositAccountData extends DepositAccountData {
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, recurringFrequency, recurringFrequencyType, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, recurringFrequency, recurringFrequencyType,
account.withHoldTax, account.taxGroup); account.withHoldTax, account.taxGroup);
result.copyCbsFieldsFrom(account);
return result;
} }
public static RecurringDepositAccountData withTemplateOptions(final RecurringDepositAccountData account, public static RecurringDepositAccountData withTemplateOptions(final RecurringDepositAccountData account,
@@ -386,7 +452,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions); preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions);
} }
return new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName, final RecurringDepositAccountData result = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId, account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate, account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate,
account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType, account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType,
@@ -406,6 +472,11 @@ public final class RecurringDepositAccountData extends DepositAccountData {
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
account.recurringFrequencyType, account.withHoldTax, account.taxGroup); account.recurringFrequencyType, account.withHoldTax, account.taxGroup);
result.copyCbsFieldsFrom(account);
// Merge accountTypeOptions and customerSegmentOptions from template
if (template.getAccountTypeOptions() != null) result.setAccountTypeOptions(template.getAccountTypeOptions());
if (template.getCustomerSegmentOptions() != null) result.setCustomerSegmentOptions(template.getCustomerSegmentOptions());
return result;
} }
@@ -420,7 +491,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
final Collection<ChargeData> chargeOptions, final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions, final Collection<ChargeData> chargeOptions, final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions,
final Collection<EnumOptionData> periodFrequencyTypeOptions) { final Collection<EnumOptionData> periodFrequencyTypeOptions) {
return new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName, final RecurringDepositAccountData r2 = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId, account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate, account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate,
account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType, account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType,
@@ -439,6 +510,8 @@ public final class RecurringDepositAccountData extends DepositAccountData {
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
account.recurringFrequencyType, account.withHoldTax, account.taxGroup); account.recurringFrequencyType, account.withHoldTax, account.taxGroup);
r2.copyCbsFieldsFrom(account);
return r2;
} }
public static RecurringDepositAccountData withClientTemplate(final Long clientId, final String clientName, final Long groupId, public static RecurringDepositAccountData withClientTemplate(final Long clientId, final String clientName, final Long groupId,
@@ -629,7 +702,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
final Collection<EnumOptionData> onAccountClosureOptions, final Collection<PaymentTypeData> paymentTypeOptions, final Collection<EnumOptionData> onAccountClosureOptions, final Collection<PaymentTypeData> paymentTypeOptions,
final Collection<SavingsAccountData> savingsAccountDatas) { final Collection<SavingsAccountData> savingsAccountDatas) {
return new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName, final RecurringDepositAccountData closureResult = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId, account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate, account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate,
account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType, account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType,
@@ -649,6 +722,8 @@ public final class RecurringDepositAccountData extends DepositAccountData {
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
account.recurringFrequencyType, account.withHoldTax, account.taxGroup); account.recurringFrequencyType, account.withHoldTax, account.taxGroup);
closureResult.copyCbsFieldsFrom(account);
return closureResult;
} }
private RecurringDepositAccountData(final Long id, final String accountNo, final String externalId, final Long groupId, private RecurringDepositAccountData(final Long id, final String accountNo, final String externalId, final Long groupId,