diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientNomineeRepository.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientNomineeRepository.java index 09fd880a9..eaf88fd92 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientNomineeRepository.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientNomineeRepository.java @@ -53,6 +53,17 @@ public interface ClientNomineeRepository extends JpaRepository 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); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientNomineeWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientNomineeWritePlatformService.java index fb3151aad..7cd1fa989 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientNomineeWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientNomineeWritePlatformService.java @@ -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); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientNomineeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientNomineeWritePlatformServiceImpl.java index a196e06a0..2a899d6d6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientNomineeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientNomineeWritePlatformServiceImpl.java @@ -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); + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountDataValidator.java index 60901bc7d..992bd698f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountDataValidator.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java index f82f56b1b..298f484cd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index ef393c6ea..db58f4288 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -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)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index f1e3c8a28..ff73b4c16 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -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(); + } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 00f3fe253..57f32baee 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -313,4 +313,6 @@ + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0297_fix_unique_nominee_per_account.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0297_fix_unique_nominee_per_account.xml new file mode 100644 index 000000000..af5be1757 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0297_fix_unique_nominee_per_account.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + 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; + + + + 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; + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0298_unique_external_id_on_current_account.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0298_unique_external_id_on_current_account.xml new file mode 100644 index 000000000..68b986924 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0298_unique_external_id_on_current_account.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java index 7ea652bed..6cad12d87 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java @@ -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)); diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java index f29475234..993b4af69 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java @@ -56,6 +56,9 @@ public interface SavingsAccountRepository extends JpaRepository 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 findByGroupId(@Param("groupId") Long groupId); diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java index d5e0332fa..252f85c98 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java @@ -181,4 +181,8 @@ public class SavingsAccountRepositoryWrapper { public List findLoanIdsByStatusId(Integer status) { return null; } + + public long countActiveAccountsByClientAndProduct(final Long clientId, final Long productId) { + return this.repository.countActiveAccountsByClientAndProduct(clientId, productId); + } } diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml index 3db8cc422..e3a40fc1b 100644 --- a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml @@ -36,4 +36,5 @@ + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2014_add_is_mandatory_to_joint_holders.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2014_add_is_mandatory_to_joint_holders.xml new file mode 100644 index 000000000..2b5ae4100 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2014_add_is_mandatory_to_joint_holders.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + +