diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java index b8468ebc6..69bea8cf0 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java @@ -130,6 +130,19 @@ public class DataValidatorBuilder { this.dataValidationErrors.add(error); } + /** + * Like {@link #failWithCode(String, Object...)}, but the caller's own message is what's actually shown to the + * user (that method always renders the generic "Failed data validation due to: {errorCode}." regardless of the + * args passed to it). + */ + public void failWithCodeAndMessage(final String errorCode, final String defaultUserMessage, + final Object... defaultUserMessageArgs) { + String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + "." + errorCode; + final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultUserMessage, this.parameter, + this.value, defaultUserMessageArgs); + this.dataValidationErrors.add(error); + } + public void failWithCodeNoParameterAddedToErrorCode(final String errorCode, final Object... defaultUserMessageArgs) { String validationErrorCode = "validation.msg." + this.resource + "." + errorCode; String defaultEnglishMessage = "Failed data validation due to: " + errorCode + "."; diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index c2ffffe6d..857530526 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -1263,6 +1263,11 @@ public class AccountingProcessorHelper { accountMapping = paymentChannelSpecificAccountMapping; } } + + if (accountMapping == null) { + throw new ProductToGLAccountMappingNotFoundException(PortfolioProductType.SAVING, savingsProductId, + CashAccountsForSavings.fromInt(accountMappingTypeId).toString()); + } glAccount = accountMapping.getGlAccount(); } return glAccount; 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 19a9188e7..995811ca1 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 @@ -97,6 +97,7 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -105,11 +106,14 @@ public class DepositAccountDataValidator { private final FromJsonHelper fromApiJsonHelper; private final DepositProductDataValidator productDataValidator; + private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper; @Autowired - public DepositAccountDataValidator(final FromJsonHelper fromApiJsonHelper, final DepositProductDataValidator productDataValidator) { + public DepositAccountDataValidator(final FromJsonHelper fromApiJsonHelper, final DepositProductDataValidator productDataValidator, + final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper) { this.fromApiJsonHelper = fromApiJsonHelper; this.productDataValidator = productDataValidator; + this.savingsAccountRepositoryWrapper = savingsAccountRepositoryWrapper; } public void validateFixedDepositForSubmit(final String json) { @@ -351,10 +355,31 @@ public class DepositAccountDataValidator { baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull() .isOneOfTheseValues(DepositAccountOnClosureType.integerValues()); + // Withdraw Deposit books the maturity proceeds straight out of the bank's cash/vault GL the + // moment the account matures -- before the customer has actually come in to collect it. For + // maturities processed automatically by the scheduled job (not a live counter transaction), + // that books cash as disbursed while it is still physically in the vault, so this option is + // disabled: every account must resolve to Transfer to Savings or a Reinvest option instead. + if (depositRolloverId.equals(DepositAccountOnClosureType.WITHDRAW_DEPOSIT.getValue())) { + baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage( + "maturity.instruction.withdraw.deposit.not.allowed", + "Withdraw Deposit is not a valid maturity instruction -- choose Transfer to Savings or a Reinvest option."); + } + if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) { final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element); baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull() .longGreaterThanZero(); + + // Transfer to Savings can only ever post into a genuine Savings account -- Current accounts + // live in a separate ledger/module that this maturity-closure transfer cannot write into. + if (transferToSavingsId != null && transferToSavingsId > 0 + && !this.savingsAccountRepositoryWrapper.existsWithDepositAccountType(transferToSavingsId, + DepositAccountType.SAVINGS_DEPOSIT)) { + baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId) + .failWithCodeAndMessage("transfer.to.savings.id.is.not.a.savings.account", + "The selected account is not a Savings account -- Transfer to Savings can only target a Savings account."); + } } } } @@ -493,10 +518,31 @@ public class DepositAccountDataValidator { baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull() .isOneOfTheseValues(DepositAccountOnClosureType.integerValues()); + // Withdraw Deposit books the maturity proceeds straight out of the bank's cash/vault GL the + // moment the account matures -- before the customer has actually come in to collect it. For + // maturities processed automatically by the scheduled job (not a live counter transaction), + // that books cash as disbursed while it is still physically in the vault, so this option is + // disabled: every account must resolve to Transfer to Savings or a Reinvest option instead. + if (depositRolloverId.equals(DepositAccountOnClosureType.WITHDRAW_DEPOSIT.getValue())) { + baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage( + "maturity.instruction.withdraw.deposit.not.allowed", + "Withdraw Deposit is not a valid maturity instruction -- choose Transfer to Savings or a Reinvest option."); + } + if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) { final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element); baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull() .longGreaterThanZero(); + + // Transfer to Savings can only ever post into a genuine Savings account -- Current accounts + // live in a separate ledger/module that this maturity-closure transfer cannot write into. + if (transferToSavingsId != null && transferToSavingsId > 0 + && !this.savingsAccountRepositoryWrapper.existsWithDepositAccountType(transferToSavingsId, + DepositAccountType.SAVINGS_DEPOSIT)) { + baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId) + .failWithCodeAndMessage("transfer.to.savings.id.is.not.a.savings.account", + "The selected account is not a Savings account -- Transfer to Savings can only target a Savings account."); + } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java index 7d0ad1121..2ae1124a7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java @@ -555,14 +555,19 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi } private void updateAlreadyPostedTransactions(final Set existingTransactionIds, final SavingsAccount savingsAccount) { - List transactions = savingsAccount.getTransactions(); - int size = transactions.size(); - for (int i = size - 1;; i--) { - SavingsAccountTransaction transaction = transactions.get(i); - if (transaction.isWithdrawal() || transaction.isWithdrawalFee()) { + // accountTransfersWritePlatformService.transferFunds() already posts accounting for the + // withdrawal it creates on this account (via SavingsAccountDomainServiceJpa#handleWithdrawal), + // so that withdrawal must be added to existingTransactionIds here or the caller's own + // postJournalEntries() re-derives and re-posts it a second time. The previous implementation + // found it by walking the transaction list backwards from the end while the type stayed + // withdrawal/withdrawal-fee -- but @OneToMany collections only guarantee @OrderBy ordering on + // a fresh fetch, not after an in-memory append, so a same-day interest posting or any other + // ordering quirk could put the new withdrawal anywhere in the list and break the walk + // silently. Scanning every transaction and checking against the original snapshot is correct + // regardless of collection order. + for (final SavingsAccountTransaction transaction : savingsAccount.getTransactions()) { + if ((transaction.isWithdrawal() || transaction.isWithdrawalFee()) && !existingTransactionIds.contains(transaction.getId())) { existingTransactionIds.add(transaction.getId()); - } else { - break; } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java index 9f8b28e2f..61d8afdf2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java @@ -554,7 +554,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, - periodFrequencyTypeOptions); + periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions); template = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) template, accountChart, null, null); // Re-apply CBS product fields lost during factory method object creation @@ -626,7 +626,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, - periodFrequencyTypeOptions); + periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index ae121f8dd..79a9e8aa8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1393,6 +1393,15 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(), fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes); + // Every maturity-instruction branch takes the balance out of this account through + // SavingsAccountDomainServiceJpa#handleWithdrawal, which posts that withdrawal's journal + // entries itself. existingTransactionIds was snapshotted at the top of this method -- + // before that withdrawal existed -- so the postJournalEntries() call at the end would + // treat it as new and post it a second time. Interest postings created by + // updateMaturityStatus() above are still genuinely unposted at this point and must stay + // out of the snapshot so that final call can post them exactly once. + markWithdrawalsAsAlreadyPosted(account, existingTransactionIds); + if (changes.get("reinvestedDepositId") != null) { Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId"); Money amountForDeposit = account.activateWithBalance(); @@ -1421,6 +1430,19 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); } + /** + * Marks the account's withdrawal (and withdrawal-fee) transactions as already accounted for, so a + * subsequent postJournalEntries() built on an older snapshot does not post them again. Used after a + * maturity closure, whose withdrawal leg posts its own journal entries. + */ + private void markWithdrawalsAsAlreadyPosted(final SavingsAccount account, final Set existingTransactionIds) { + for (final SavingsAccountTransaction transaction : account.getTransactions()) { + if (transaction.getId() != null && (transaction.isWithdrawal() || transaction.isWithdrawalFee())) { + existingTransactionIds.add(transaction.getId()); + } + } + } + private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, final Set existingReversedTransactionIds) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 88dd3727f..aad5ddcf4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1676,6 +1676,15 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @Override public void setSubStatusInactive(Long savingsId) { final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + // Same snapshot-vs-execution gap as escheat() below: the dormant-accounts batch selects + // pending-inactive ids from a query that requires status = ACTIVE, but the account can close + // through another path (e.g. deposit maturity) before this call runs. Re-deriving accounting + // off the full transaction list at that point re-posts entries that closure already posted. + if (!account.isActive()) { + log.warn("Skipping setSubStatusInactive for savings account {} -- no longer active (already closed by another process)", + savingsId); + return; + } final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); @@ -1687,6 +1696,11 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @Override public void setSubStatusDormant(Long savingsId) { final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + if (!account.isActive()) { + log.warn("Skipping setSubStatusDormant for savings account {} -- no longer active (already closed by another process)", + savingsId); + return; + } account.setSubStatusDormant(); this.savingAccountRepositoryWrapper.saveAndFlush(account); } @@ -1694,6 +1708,16 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @Override public void escheat(Long savingsId) { final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + // The dormant-accounts batch selects escheat-eligible ids from a snapshot query, so an + // account can legitimately close through another path (e.g. deposit maturity) in the gap + // between that selection and this call. escheat() unconditionally forces status/sub-status + // to CLOSED/ESCHEAT and re-derives accounting off the account's full transaction list, so + // running it against an already-closed account both mislabels the closure reason and + // re-posts journal entries for transactions that were already posted by the real closure. + if (!account.isActive()) { + log.warn("Skipping escheat for savings account {} -- no longer active (already closed by another process)", savingsId); + return; + } final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositAccountData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositAccountData.java index 690f4c275..78b18da87 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositAccountData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositAccountData.java @@ -76,6 +76,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { // for account close private final Collection onAccountClosureOptions; private final Collection paymentTypeOptions; + private final Collection maturityInstructionOptions; // import fields private transient Integer rowIndex; @@ -364,6 +365,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { this.savingsAccounts = null; this.onAccountClosureOptions = null; this.paymentTypeOptions = null; + this.maturityInstructionOptions = null; this.rowIndex = rowIndex; this.dateFormat = dateFormat; this.locale = locale; @@ -394,6 +396,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { final Collection savingsAccountDatas = null; final Integer recurringFrequency = null; final EnumOptionData recurringFrequencyType = null; + final Collection maturityInstructionOptions = null; return new RecurringDepositAccountData(depositAccountData.id, depositAccountData.accountNo, depositAccountData.externalId, depositAccountData.groupId, depositAccountData.groupName, depositAccountData.clientId, depositAccountData.clientName, @@ -416,7 +419,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, depositAccountData.withHoldTax, - depositAccountData.taxGroup); + depositAccountData.taxGroup, maturityInstructionOptions); } public static RecurringDepositAccountData withInterestChartAndRecurringDetails(final RecurringDepositAccountData account, @@ -441,7 +444,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, recurringFrequency, recurringFrequencyType, - account.withHoldTax, account.taxGroup); + account.withHoldTax, account.taxGroup, account.maturityInstructionOptions); result.copyCbsFieldsFrom(account); return result; } @@ -463,11 +466,13 @@ public final class RecurringDepositAccountData extends DepositAccountData { final Collection preClosurePenalInterestOnTypeOptions = null; final Collection periodFrequencyTypeOptions = null; + final Collection savingsAccountDatas = account.savingsAccounts; + final Collection maturityInstructionOptions = account.maturityInstructionOptions; return withTemplateOptions(account, productOptions, fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, transactions, charges, chargeOptions, - preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions); + preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions); } final RecurringDepositAccountData result = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName, @@ -486,10 +491,10 @@ public final class RecurringDepositAccountData extends DepositAccountData { account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate, account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount, template.periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions, - account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount, + account.paymentTypeOptions, template.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency, - account.recurringFrequencyType, account.withHoldTax, account.taxGroup); + account.recurringFrequencyType, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions); result.copyCbsFieldsFrom(account); // Merge accountTypeOptions and customerSegmentOptions from template if (template.getAccountTypeOptions() != null) result.setAccountTypeOptions(template.getAccountTypeOptions()); @@ -507,7 +512,8 @@ public final class RecurringDepositAccountData extends DepositAccountData { final Collection lockinPeriodFrequencyTypeOptions, final Collection withdrawalFeeTypeOptions, final Collection transactions, final Collection charges, final Collection chargeOptions, final Collection preClosurePenalInterestOnTypeOptions, - final Collection periodFrequencyTypeOptions) { + final Collection periodFrequencyTypeOptions, final Collection savingsAccountDatas, + final Collection maturityInstructionOptions) { 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, @@ -524,10 +530,10 @@ public final class RecurringDepositAccountData extends DepositAccountData { account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate, account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount, periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions, - account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount, + account.paymentTypeOptions, savingsAccountDatas, account.expectedFirstDepositOnDate, account.totalOverdueAmount, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency, - account.recurringFrequencyType, account.withHoldTax, account.taxGroup); + account.recurringFrequencyType, account.withHoldTax, account.taxGroup, maturityInstructionOptions); r2.copyCbsFieldsFrom(account); return r2; } @@ -609,6 +615,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { final EnumOptionData recurringFrequencyType = null; final boolean withHoldTax = false; final TaxGroupData taxGroup = null; + final Collection maturityInstructionOptions = null; return new RecurringDepositAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType, @@ -622,7 +629,8 @@ public final class RecurringDepositAccountData extends DepositAccountData { depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount, periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, - adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup); + adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup, + maturityInstructionOptions); } public static RecurringDepositAccountData preClosureDetails(final Long accountId, final BigDecimal maturityAmount, @@ -700,6 +708,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { final EnumOptionData recurringFrequencyType = null; final boolean withHoldTax = false; final TaxGroupData taxGroup = null; + final Collection maturityInstructionOptions = null; return new RecurringDepositAccountData(accountId, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType, @@ -713,7 +722,8 @@ public final class RecurringDepositAccountData extends DepositAccountData { depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount, periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, - adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup); + adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup, + maturityInstructionOptions); } public static RecurringDepositAccountData withClosureTemplateDetails(final RecurringDepositAccountData account, @@ -739,7 +749,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { paymentTypeOptions, savingsAccountDatas, account.expectedFirstDepositOnDate, account.totalOverdueAmount, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency, - account.recurringFrequencyType, account.withHoldTax, account.taxGroup); + account.recurringFrequencyType, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions); closureResult.copyCbsFieldsFrom(account); return closureResult; } @@ -774,7 +784,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { final BigDecimal totalOverdueAmount, final Integer noOfOverdueInstallments, final boolean isMandatoryDeposit, final boolean allowWithdrawal, final boolean adjustAdvanceTowardsFuturePayments, final boolean isCalendarInherited, final Integer recurringFrequency, final EnumOptionData recurringFrequencyType, final boolean withHoldTax, - final TaxGroupData taxGroup) { + final TaxGroupData taxGroup, final Collection maturityInstructionOptions) { super(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, fieldofficerId, fieldofficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType, interestPostingPeriodType, @@ -818,6 +828,7 @@ public final class RecurringDepositAccountData extends DepositAccountData { // account close template options this.onAccountClosureOptions = onAccountClosureOptions; this.paymentTypeOptions = paymentTypeOptions; + this.maturityInstructionOptions = maturityInstructionOptions; } @Override 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 252f85c98..0e52dfd1e 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 @@ -82,6 +82,11 @@ public class SavingsAccountRepositoryWrapper { return account; } + @Transactional(readOnly = true) + public boolean existsWithDepositAccountType(final Long savingsId, final DepositAccountType depositAccountType) { + return this.repository.findByIdAndDepositAccountType(savingsId, depositAccountType.getValue()) != null; + } + @Transactional(readOnly = true) public List findSavingAccountByClientId(@Param("clientId") Long clientId) { List accounts = this.repository.findSavingAccountByClientId(clientId); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositTest.java index a55e312cc..0367baacd 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositTest.java @@ -172,7 +172,7 @@ public class FixedDepositTest extends IntegrationTest { moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP)); fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl( - new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper()); + new DepositAccountDataValidator(new FromJsonHelper(), null, null), new FromJsonHelper()); try { HashMap h = fixedDepositAccountInterestCalculationServiceImpl .calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())); @@ -198,7 +198,7 @@ public class FixedDepositTest extends IntegrationTest { moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP)); fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl( - new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper()); + new DepositAccountDataValidator(new FromJsonHelper(), null, null), new FromJsonHelper()); try { HashMap h = fixedDepositAccountInterestCalculationServiceImpl .calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())); @@ -224,7 +224,7 @@ public class FixedDepositTest extends IntegrationTest { moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP)); fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl( - new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper()); + new DepositAccountDataValidator(new FromJsonHelper(), null, null), new FromJsonHelper()); BigDecimal expectedResult = new BigDecimal("10506.250000"); BigDecimal actualResult = new BigDecimal(fixedDepositAccountInterestCalculationServiceImpl .calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())).get("maturityAmount").toString());