feat: multi-nominee support, duplicate validation, and FD/RD required fields
Account Creation Enhancements: - Added multi-nominee support for SB, CA, FD, RD accounts (nominees[] array) - Backend: saveNomineesIfPresent() validates total share <= 100% before saving - Added 'nominees' to parameter whitelists (SavingsAccountConstant, DepositsApiConstants) Duplicate Validation Fixes: - SB/FD/RD: Fixed duplicate externalId error handler to walk full exception cause chain (EclipseLink wraps PSQLException deep) — now matches constraint 'm_savings_account_external_id_key' and returns proper 400 error - CA: Added try-catch around both saveAndFlush calls for duplicate externalId (constraint: ca_unq_external_id) with throwIfDuplicateExternalId() helper - CA: Added duplicate product check — blocks creating same product twice for client - SB/FD/RD: Added JpaSystemException handling via getAllCauseMessages() helper Current Account (CA) Improvements: - CurrentAccount entity: added @UniqueConstraint for external_id (ca_unq_external_id) - CurrentAccountRepository: added countActiveByClientAndProduct() JPQL query - Liquibase 0298: unique constraint on m_current_account.external_id FD/RD Required Field Validation (Backend): - DepositAccountDataValidator: depositPeriod now required for RD (was FD-only) - DepositAccountDataValidator: mandatoryRecommendedDepositAmount now notNull+positive for RD - DepositAccountDataValidator: customerSegment now required (notBlank) for both FD and RD Liquibase Migrations: - 0297: fix unique nominee per account constraint - 0298: unique external_id on m_current_account - 2014: add is_mandatory column to m_savings_account_joint_holders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9d67afec3e
commit
cc0ccbb461
+11
@@ -53,6 +53,17 @@ public interface ClientNomineeRepository extends JpaRepository<ClientNominee, Lo
|
||||
@Param("nomineeClientId") Long nomineeClientId,
|
||||
@Param("accountType") String accountType);
|
||||
|
||||
@Query("SELECT COUNT(n) FROM ClientNominee n "
|
||||
+ "WHERE n.client.id = :clientId "
|
||||
+ "AND n.nomineeClient.id = :nomineeClientId "
|
||||
+ "AND n.accountNo = :accountNo "
|
||||
+ "AND n.accountType = :accountType "
|
||||
+ "AND n.isDeleted = false")
|
||||
long countActiveByClientNomineeAndAccountNoAndType(@Param("clientId") Long clientId,
|
||||
@Param("nomineeClientId") Long nomineeClientId,
|
||||
@Param("accountNo") String accountNo,
|
||||
@Param("accountType") String accountType);
|
||||
|
||||
@Query("SELECT COUNT(n) FROM ClientNominee n "
|
||||
+ "WHERE n.client.id = :clientId "
|
||||
+ "AND n.nomineeClient.id = :nomineeClientId "
|
||||
|
||||
+2
@@ -537,6 +537,7 @@ public final class DepositsApiConstants {
|
||||
fixedDepositRequestData.add(SavingsApiConstants.introducerNameParamName);
|
||||
fixedDepositRequestData.add(SavingsApiConstants.introducerCifNumberParamName);
|
||||
fixedDepositRequestData.add(SavingsApiConstants.nomineeParamName);
|
||||
fixedDepositRequestData.add("nominees");
|
||||
fixedDepositRequestData.add(SavingsApiConstants.linkedAccountIdParamName);
|
||||
fixedDepositRequestData.add(SavingsApiConstants.linkedAccountTypeParamName);
|
||||
fixedDepositRequestData.add(SavingsApiConstants.linkedAccountNoParamName);
|
||||
@@ -603,6 +604,7 @@ public final class DepositsApiConstants {
|
||||
recurringDepositRequestData.add(SavingsApiConstants.introducerNameParamName);
|
||||
recurringDepositRequestData.add(SavingsApiConstants.introducerCifNumberParamName);
|
||||
recurringDepositRequestData.add(SavingsApiConstants.nomineeParamName);
|
||||
recurringDepositRequestData.add("nominees");
|
||||
recurringDepositRequestData.add(SavingsApiConstants.linkedAccountIdParamName);
|
||||
recurringDepositRequestData.add(SavingsApiConstants.linkedAccountTypeParamName);
|
||||
recurringDepositRequestData.add(SavingsApiConstants.linkedAccountNoParamName);
|
||||
|
||||
+4
-1
@@ -41,7 +41,10 @@ import org.apache.fineract.organisation.staff.domain.Staff;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_current_account")
|
||||
@Table(name = "m_current_account", uniqueConstraints = {
|
||||
@jakarta.persistence.UniqueConstraint(columnNames = { "account_no" }, name = "ca_account_no_UNIQUE"),
|
||||
@jakarta.persistence.UniqueConstraint(columnNames = { "external_id" }, name = "ca_unq_external_id")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
public class CurrentAccount extends AbstractAuditableCustom {
|
||||
|
||||
+3
@@ -37,4 +37,7 @@ public interface CurrentAccountRepository extends JpaRepository<CurrentAccount,
|
||||
|
||||
@Query("SELECT ca FROM CurrentAccount ca WHERE ca.client.id = :clientId AND ca.statusEnum != 600")
|
||||
List<CurrentAccount> findActiveByClientId(@Param("clientId") Long clientId);
|
||||
|
||||
@Query("SELECT COUNT(ca) FROM CurrentAccount ca WHERE ca.client.id = :clientId AND ca.productId = :productId AND ca.statusEnum != 600")
|
||||
long countActiveByClientAndProduct(@Param("clientId") Long clientId, @Param("productId") Long productId);
|
||||
}
|
||||
|
||||
+2
@@ -36,4 +36,6 @@ public interface ClientNomineeWritePlatformService {
|
||||
* 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);
|
||||
}
|
||||
|
||||
+36
@@ -351,6 +351,13 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
|
||||
|
||||
if (nomineeClientId.equals(clientId)) return; // self-nomination guard
|
||||
|
||||
// Skip if nominee already exists for this exact account
|
||||
if (nomineeRepository.countActiveByClientNomineeAndAccountNoAndType(clientId, nomineeClientId, accountNo, accountType) > 0) {
|
||||
log.info("Nominee already exists for clientId={}, nomineeClientId={}, accountNo={}, accountType={} — skipping duplicate insert",
|
||||
clientId, nomineeClientId, accountNo, accountType);
|
||||
return;
|
||||
}
|
||||
|
||||
final Client nomineeClient = clientRepository.findOneWithNotFoundDetection(nomineeClientId);
|
||||
|
||||
final BigDecimal share = n.has("percentageShare") && !n.get("percentageShare").isJsonNull()
|
||||
@@ -392,4 +399,33 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
|
||||
log.warn("Failed to save nominee during account creation for clientId={}, accountNo={}: {}", clientId, accountNo, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveNomineesIfPresent(final Long clientId, final String accountNo, final String accountType, final JsonElement nomineesJson) {
|
||||
if (nomineesJson == null || nomineesJson.isJsonNull()) return;
|
||||
if (nomineesJson.isJsonArray()) {
|
||||
final com.google.gson.JsonArray arr = nomineesJson.getAsJsonArray();
|
||||
BigDecimal totalShare = BigDecimal.ZERO;
|
||||
for (com.google.gson.JsonElement el : arr) {
|
||||
if (!el.isJsonObject()) continue;
|
||||
final com.google.gson.JsonObject n = el.getAsJsonObject();
|
||||
if (!n.has("nomineeClientId") || n.get("nomineeClientId").isJsonNull()) continue;
|
||||
final BigDecimal share = n.has("percentageShare") && !n.get("percentageShare").isJsonNull()
|
||||
? n.get("percentageShare").getAsBigDecimal() : BigDecimal.ZERO;
|
||||
totalShare = totalShare.add(share);
|
||||
}
|
||||
if (totalShare.compareTo(new BigDecimal("100")) > 0) {
|
||||
throw new PlatformDataIntegrityException(
|
||||
"error.msg.nominee.total.share.exceeds.100",
|
||||
"Total nominee percentage share cannot exceed 100%. Provided total: " + totalShare + "%.",
|
||||
"percentageShare", totalShare);
|
||||
}
|
||||
for (com.google.gson.JsonElement el : arr) {
|
||||
if (el.isJsonObject()) saveNomineeIfPresent(clientId, accountNo, accountType, el);
|
||||
}
|
||||
} else if (nomineesJson.isJsonObject()) {
|
||||
// fallback: single nominee object
|
||||
saveNomineeIfPresent(clientId, accountNo, accountType, nomineesJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -47,6 +47,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureI
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferToSavingsIdParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountNoParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName;
|
||||
@@ -260,6 +261,9 @@ public class DepositAccountDataValidator {
|
||||
baseDataValidator.reset().parameter(externalIdParamName).value(externalId).notExceedingLengthOf(100);
|
||||
}
|
||||
|
||||
final String customerSegment = this.fromApiJsonHelper.extractStringNamed(customerSegmentParamName, element);
|
||||
baseDataValidator.reset().parameter(customerSegmentParamName).value(customerSegment).notBlank();
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(nominalAnnualInterestRateParamName, element)) {
|
||||
final BigDecimal interestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(nominalAnnualInterestRateParamName,
|
||||
element);
|
||||
@@ -567,7 +571,7 @@ public class DepositAccountDataValidator {
|
||||
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).notNull().positiveAmount();
|
||||
}
|
||||
|
||||
if (depositType.isFixedDeposit() || fromApiJsonHelper.parameterExists(depositPeriodParamName, element)) {
|
||||
if (depositType.isFixedDeposit() || depositType.isRecurringDeposit() || fromApiJsonHelper.parameterExists(depositPeriodParamName, element)) {
|
||||
final Integer depositPeriod = fromApiJsonHelper.extractIntegerSansLocaleNamed(depositPeriodParamName, element);
|
||||
baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull().integerGreaterThanZero();
|
||||
}
|
||||
@@ -643,7 +647,7 @@ public class DepositAccountDataValidator {
|
||||
final BigDecimal mandatoryRecommendedDepositAmount = fromApiJsonHelper
|
||||
.extractBigDecimalWithLocaleNamed(mandatoryRecommendedDepositAmountParamName, element);
|
||||
baseDataValidator.reset().parameter(mandatoryRecommendedDepositAmountParamName).value(mandatoryRecommendedDepositAmount)
|
||||
.ignoreIfNull().zeroOrPositiveAmount();
|
||||
.notNull().positiveAmount();
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(isMandatoryDepositParamName, element)) {
|
||||
final Boolean isMandatoryDeposit = this.fromApiJsonHelper.extractBooleanNamed(isMandatoryDepositParamName, element);
|
||||
|
||||
+38
-4
@@ -29,6 +29,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.office.domain.Office;
|
||||
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
|
||||
@@ -88,6 +89,13 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||
final CurrentProduct product = currentProductRepository.findById(productId)
|
||||
.orElseThrow(() -> new SavingsProductNotFoundException(productId));
|
||||
|
||||
if (currentAccountRepository.countActiveByClientAndProduct(clientId, productId) > 0) {
|
||||
throw new PlatformDataIntegrityException(
|
||||
"error.msg.savings.account.duplicate.product.for.client",
|
||||
"Client already has an active Current Account with the same product.",
|
||||
"productId", productId);
|
||||
}
|
||||
final Office office = officeId != null
|
||||
? officeRepository.findOneWithNotFoundDetection(officeId)
|
||||
: client.getOffice();
|
||||
@@ -185,14 +193,21 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
}
|
||||
|
||||
// Save first to get ID
|
||||
currentAccountRepository.saveAndFlush(account);
|
||||
try {
|
||||
currentAccountRepository.saveAndFlush(account);
|
||||
} catch (RuntimeException e) {
|
||||
throwIfDuplicateExternalId(e, account.getExternalId());
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Generate account number: 00 + GL_CODE(6) + OFFICE_ID(2) + ACCOUNT_ID(5)
|
||||
final String accountNo = accountNumberGenerator.generate(account);
|
||||
account.updateAccountNo(accountNo);
|
||||
|
||||
// Save nominee if provided in payload (same transaction)
|
||||
if (json.has("nominee") && !json.get("nominee").isJsonNull()) {
|
||||
// Save nominees if provided in payload (same transaction)
|
||||
if (json.has("nominees") && !json.get("nominees").isJsonNull()) {
|
||||
clientNomineeWritePlatformService.saveNomineesIfPresent(clientId, accountNo, "CB", json.get("nominees"));
|
||||
} else if (json.has("nominee") && !json.get("nominee").isJsonNull()) {
|
||||
clientNomineeWritePlatformService.saveNomineeIfPresent(clientId, accountNo, "CB", json.get("nominee"));
|
||||
}
|
||||
|
||||
@@ -213,7 +228,12 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
assemblePartners(account, json.getAsJsonArray("partners"));
|
||||
}
|
||||
|
||||
currentAccountRepository.saveAndFlush(account);
|
||||
try {
|
||||
currentAccountRepository.saveAndFlush(account);
|
||||
} catch (RuntimeException e) {
|
||||
throwIfDuplicateExternalId(e, account.getExternalId());
|
||||
throw e;
|
||||
}
|
||||
|
||||
return new CommandProcessingResultBuilder()
|
||||
.withEntityId(account.getId())
|
||||
@@ -645,6 +665,20 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
journalEntryRepository.save(entry);
|
||||
}
|
||||
|
||||
private void throwIfDuplicateExternalId(final RuntimeException e, final String externalId) {
|
||||
Throwable cause = e;
|
||||
while (cause != null) {
|
||||
final String msg = cause.getMessage();
|
||||
if (msg != null && (msg.contains("ca_external_id_UNIQUE") || msg.contains("ca_unq_external_id"))) {
|
||||
throw new PlatformDataIntegrityException(
|
||||
"error.msg.savings.account.duplicate.externalid",
|
||||
"Current Account with this External ID already exists.",
|
||||
"externalId", externalId);
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMapped(final CurrentAccount account, final boolean enabled, final String facilityType) {
|
||||
if (!enabled) {
|
||||
log.debug("[FacilityCharge] {} disabled — skip", facilityType);
|
||||
|
||||
+53
-12
@@ -48,6 +48,7 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.deposit.FixedDepositAccountCreateBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.deposit.RecurringDepositAccountCreateBusinessEvent;
|
||||
@@ -134,16 +135,16 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
String msg = "Unknown data integrity issue with savings account.";
|
||||
String param = null;
|
||||
Object[] msgArgs;
|
||||
Throwable checkEx = realCause == null ? dve : realCause;
|
||||
if (checkEx.getMessage().contains("sa_account_no_UNIQUE")) {
|
||||
final String causeMsg = getAllCauseMessages(realCause, dve);
|
||||
if (causeMsg.contains("sa_account_no_UNIQUE")) {
|
||||
final String accountNo = command.stringValueOfParameterNamed("accountNo");
|
||||
msgCode += ".duplicate.accountNo";
|
||||
msg = "Savings account with accountNo " + accountNo + " already exists";
|
||||
param = "accountNo";
|
||||
msgArgs = new Object[] { accountNo, dve };
|
||||
} else if (checkEx.getMessage().contains("sa_external_id_UNIQUE")) {
|
||||
} else if (causeMsg.contains("sa_external_id_UNIQUE") || causeMsg.contains("m_savings_account_external_id_key")) {
|
||||
final String externalId = command.stringValueOfParameterNamed("externalId");
|
||||
msgCode += ".duplicate.externalId";
|
||||
msgCode = "error.msg.savingsaccount.duplicate.externalId";
|
||||
msg = "Savings account with externalId " + externalId + " already exists";
|
||||
param = "externalId";
|
||||
msgArgs = new Object[] { externalId, dve };
|
||||
@@ -155,6 +156,18 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
throw ErrorHandler.getMappable(dve, msgCode, msg, param, msgArgs);
|
||||
}
|
||||
|
||||
private String getAllCauseMessages(final Throwable realCause, final Exception dve) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
Throwable t = realCause != null ? realCause : dve;
|
||||
while (t != null) {
|
||||
if (t.getMessage() != null) {
|
||||
sb.append(t.getMessage()).append(' ');
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult submitFDApplication(final JsonCommand command) {
|
||||
@@ -169,6 +182,17 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(command, submittedBy,
|
||||
DepositAccountType.FIXED_DEPOSIT);
|
||||
|
||||
if (account.getClient() != null && account.productId() != null) {
|
||||
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
|
||||
account.getClient().getId(), account.productId());
|
||||
if (existingCount > 0) {
|
||||
throw new PlatformDataIntegrityException(
|
||||
"error.msg.savings.account.duplicate.product.for.client",
|
||||
"Client already has an active Fixed Deposit account with the same product.",
|
||||
"productId", account.productId());
|
||||
}
|
||||
}
|
||||
|
||||
final MathContext mc = MathContext.DECIMAL64;
|
||||
final boolean isPreMatureClosure = false;
|
||||
|
||||
@@ -198,11 +222,14 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
this.accountAssociationsRepository.save(accountAssociations);
|
||||
}
|
||||
|
||||
// Save nominee if provided in payload (same transaction)
|
||||
if (command.parameterExists("nominee")) {
|
||||
// Save nominees if provided in payload (same transaction)
|
||||
final com.google.gson.JsonObject fdRoot = command.parsedJson().getAsJsonObject();
|
||||
if (fdRoot.has("nominees") && !fdRoot.get("nominees").isJsonNull()) {
|
||||
clientNomineeWritePlatformService.saveNomineesIfPresent(
|
||||
account.clientId(), account.getAccountNumber(), "FD", fdRoot.get("nominees"));
|
||||
} else if (fdRoot.has("nominee") && !fdRoot.get("nominee").isJsonNull()) {
|
||||
clientNomineeWritePlatformService.saveNomineeIfPresent(
|
||||
account.clientId(), account.getAccountNumber(), "FD",
|
||||
command.parsedJson().getAsJsonObject().get("nominee"));
|
||||
account.clientId(), account.getAccountNumber(), "FD", fdRoot.get("nominee"));
|
||||
}
|
||||
|
||||
final Long savingsId = account.getId();
|
||||
@@ -240,6 +267,17 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(command,
|
||||
submittedBy, DepositAccountType.RECURRING_DEPOSIT);
|
||||
|
||||
if (account.getClient() != null && account.productId() != null) {
|
||||
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
|
||||
account.getClient().getId(), account.productId());
|
||||
if (existingCount > 0) {
|
||||
throw new PlatformDataIntegrityException(
|
||||
"error.msg.savings.account.duplicate.product.for.client",
|
||||
"Client already has an active Recurring Deposit account with the same product.",
|
||||
"productId", account.productId());
|
||||
}
|
||||
}
|
||||
|
||||
this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account);
|
||||
|
||||
this.recurringDepositAccountRepository.save(account);
|
||||
@@ -268,11 +306,14 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
account.validateApplicableInterestRate();
|
||||
savingAccountRepository.save(account);
|
||||
|
||||
// Save nominee if provided in payload (same transaction)
|
||||
if (command.parameterExists("nominee")) {
|
||||
// Save nominees if provided in payload (same transaction)
|
||||
final com.google.gson.JsonObject rdRoot = command.parsedJson().getAsJsonObject();
|
||||
if (rdRoot.has("nominees") && !rdRoot.get("nominees").isJsonNull()) {
|
||||
clientNomineeWritePlatformService.saveNomineesIfPresent(
|
||||
account.clientId(), account.getAccountNumber(), "RD", rdRoot.get("nominees"));
|
||||
} else if (rdRoot.has("nominee") && !rdRoot.get("nominee").isJsonNull()) {
|
||||
clientNomineeWritePlatformService.saveNomineeIfPresent(
|
||||
account.clientId(), account.getAccountNumber(), "RD",
|
||||
command.parsedJson().getAsJsonObject().get("nominee"));
|
||||
account.clientId(), account.getAccountNumber(), "RD", rdRoot.get("nominee"));
|
||||
}
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new RecurringDepositAccountCreateBusinessEvent(account));
|
||||
|
||||
+36
-8
@@ -47,6 +47,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum;
|
||||
import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService;
|
||||
@@ -145,6 +146,18 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
final AppUser submittedBy = this.context.authenticatedUser();
|
||||
|
||||
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(command, submittedBy);
|
||||
|
||||
if (account.getClient() != null && account.productId() != null) {
|
||||
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
|
||||
account.getClient().getId(), account.productId());
|
||||
if (existingCount > 0) {
|
||||
throw new PlatformDataIntegrityException(
|
||||
"error.msg.savings.account.duplicate.product.for.client",
|
||||
"Client already has an active account with the same product.",
|
||||
"productId", account.productId());
|
||||
}
|
||||
}
|
||||
|
||||
this.savingAccountRepository.save(account);
|
||||
String accountNumber = "";
|
||||
GroupSavingsIndividualMonitoring gsimAccount = null;
|
||||
@@ -233,11 +246,14 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
}
|
||||
}
|
||||
// end of gsim
|
||||
// Save nominee if provided in payload (same transaction)
|
||||
if (command.parameterExists("nominee")) {
|
||||
// Save nominees if provided in payload (same transaction)
|
||||
final com.google.gson.JsonObject root = command.parsedJson().getAsJsonObject();
|
||||
if (root.has("nominees") && !root.get("nominees").isJsonNull()) {
|
||||
clientNomineeWritePlatformService.saveNomineesIfPresent(
|
||||
account.clientId(), account.getAccountNumber(), "SB", root.get("nominees"));
|
||||
} else if (root.has("nominee") && !root.get("nominee").isJsonNull()) {
|
||||
clientNomineeWritePlatformService.saveNomineeIfPresent(
|
||||
account.clientId(), account.getAccountNumber(), "SB",
|
||||
command.parsedJson().getAsJsonObject().get("nominee"));
|
||||
account.clientId(), account.getAccountNumber(), "SB", root.get("nominee"));
|
||||
}
|
||||
|
||||
final Long savingsId = account.getId();
|
||||
@@ -701,16 +717,16 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
String msg = "Unknown data integrity issue with savings account.";
|
||||
String param = null;
|
||||
Object[] msgArgs;
|
||||
Throwable checkEx = realCause == null ? dve : realCause;
|
||||
if (checkEx.getMessage().contains("sa_account_no_UNIQUE")) {
|
||||
final String causeMsg = getRootCauseMessage(realCause, dve);
|
||||
if (causeMsg.contains("sa_account_no_UNIQUE")) {
|
||||
final String accountNo = command.stringValueOfParameterNamed("accountNo");
|
||||
msgCode += ".duplicate.accountNo";
|
||||
msg = "Savings account with accountNo " + accountNo + " already exists";
|
||||
param = "accountNo";
|
||||
msgArgs = new Object[] { accountNo, dve };
|
||||
} else if (checkEx.getMessage().contains("sa_externalid_UNIQUE")) {
|
||||
} else if (causeMsg.contains("sa_external_id_UNIQUE") || causeMsg.contains("m_savings_account_external_id_key")) {
|
||||
final String externalId = command.stringValueOfParameterNamed("externalId");
|
||||
msgCode += ".duplicate.externalId";
|
||||
msgCode = "error.msg.savingsaccount.duplicate.externalId";
|
||||
msg = "Savings account with externalId " + externalId + " already exists";
|
||||
param = "externalId";
|
||||
msgArgs = new Object[] { externalId, dve };
|
||||
@@ -721,4 +737,16 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
log.error("Error occured.", dve);
|
||||
throw ErrorHandler.getMappable(dve, msgCode, msg, param, msgArgs);
|
||||
}
|
||||
|
||||
private String getRootCauseMessage(final Throwable realCause, final Exception dve) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
Throwable t = realCause != null ? realCause : dve;
|
||||
while (t != null) {
|
||||
if (t.getMessage() != null) {
|
||||
sb.append(t.getMessage()).append(' ');
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,4 +313,6 @@
|
||||
<include file="parts/0294_add_tfa_token_to_dsc_challenge.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0295_unique_email_mobile_on_m_appuser.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0296_unique_nominee_per_account_type.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0297_fix_unique_nominee_per_account.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0298_unique_external_id_on_current_account.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Fix: previous constraint uq_client_nominee_per_account_type was on
|
||||
(client_id, nominee_client_id, account_type) — this incorrectly blocked the
|
||||
same nominee from being added to multiple accounts of the same type
|
||||
(e.g. two SB accounts). The correct uniqueness is per individual account:
|
||||
(client_id, nominee_client_id, account_no, account_type).
|
||||
-->
|
||||
<changeSet id="0297_fix_unique_nominee_per_account" author="fineract" dbms="postgresql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<tableExists tableName="m_client_nominee"/>
|
||||
</preConditions>
|
||||
<sql>
|
||||
DROP INDEX IF EXISTS uq_client_nominee_per_account_type;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_client_nominee_per_account
|
||||
ON m_client_nominee (client_id, nominee_client_id, account_no, account_type)
|
||||
WHERE is_deleted = false;
|
||||
</sql>
|
||||
<rollback>
|
||||
<sql>
|
||||
DROP INDEX IF EXISTS uq_client_nominee_per_account;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_client_nominee_per_account_type
|
||||
ON m_client_nominee (client_id, nominee_client_id, account_type)
|
||||
WHERE is_deleted = false;
|
||||
</sql>
|
||||
</rollback>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<changeSet id="0298_unique_external_id_on_current_account" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<tableExists tableName="m_current_account"/>
|
||||
<not><indexExists tableName="m_current_account" indexName="ca_external_id_UNIQUE"/></not>
|
||||
<not><indexExists tableName="m_current_account" indexName="ca_unq_external_id"/></not>
|
||||
</preConditions>
|
||||
<addUniqueConstraint
|
||||
tableName="m_current_account"
|
||||
columnNames="external_id"
|
||||
constraintName="ca_external_id_UNIQUE"/>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+1
-1
@@ -46,7 +46,7 @@ public class SavingsAccountConstant extends SavingsApiConstants {
|
||||
guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName,
|
||||
guardianClientIdParamName, minorDateOfBirthParamName,
|
||||
introducerIdParamName, introducerNameParamName, introducerCifNumberParamName,
|
||||
nomineeParamName, linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName,
|
||||
nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName,
|
||||
accountFacilityConfigParamName, jointHoldersParamName,
|
||||
balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName));
|
||||
|
||||
|
||||
+3
@@ -56,6 +56,9 @@ public interface SavingsAccountRepository extends JpaRepository<SavingsAccount,
|
||||
@Query("select case when (count (saving) > 0) then 'true' else 'false' end from SavingsAccount saving where saving.client.id = :clientId and saving.status in (100,200,300,303,304)")
|
||||
boolean doNonClosedSavingAccountsExistForClient(@Param("clientId") Long clientId);
|
||||
|
||||
@Query("SELECT COUNT(sa) FROM SavingsAccount sa WHERE sa.client.id = :clientId AND sa.product.id = :productId AND sa.status IN (100, 200, 300, 303, 304)")
|
||||
long countActiveAccountsByClientAndProduct(@Param("clientId") Long clientId, @Param("productId") Long productId);
|
||||
|
||||
@Query("select sa from SavingsAccount sa where sa.client.id is null and sa.group.id = :groupId")
|
||||
List<SavingsAccount> findByGroupId(@Param("groupId") Long groupId);
|
||||
|
||||
|
||||
+4
@@ -181,4 +181,8 @@ public class SavingsAccountRepositoryWrapper {
|
||||
public List<Long> findLoanIdsByStatusId(Integer status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public long countActiveAccountsByClientAndProduct(final Long clientId, final Long productId) {
|
||||
return this.repository.countActiveAccountsByClientAndProduct(clientId, productId);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -36,4 +36,5 @@
|
||||
<include file="parts/2011_add_facility_charge_mapping.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2012_add_current_account_permissions.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2013_add_missing_current_account_columns.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2014_add_is_mandatory_to_joint_holders.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<changeSet id="2014_add_is_mandatory_to_joint_holders" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<tableExists tableName="m_savings_account_joint_holders"/>
|
||||
<not><columnExists tableName="m_savings_account_joint_holders" columnName="is_mandatory"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_account_joint_holders">
|
||||
<column name="is_mandatory" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user