From 5281c7b17d705c78fcd9199e536f9590b68c90a6 Mon Sep 17 00:00:00 2001 From: "adam.magyari" Date: Thu, 29 May 2025 19:16:21 +0200 Subject: [PATCH] FINERACT-2232: Validations and cob step fix - Add validation for capitalized income transaction not being in the future - CapitalizedIncomeAmortizationBusinessStep add filter against too early amortization --- .../features/LoanCapitalizedIncome.feature | 2 +- ...ogressiveLoanTransactionValidatorImpl.java | 15 +++- ...talizedIncomeAmortizationBusinessStep.java | 14 ++-- .../LoanCapitalizedIncomeTest.java | 79 +++++++++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature index 4ec3ca072..17686b769 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature @@ -73,7 +73,7 @@ Feature: Capitalized Income Then Loan status will be "ACTIVE" When Admin sets the business date to "2 January 2024" And Admin successfully disburse the loan on "2 January 2024" with "200" EUR transaction amount - When Admin sets the business date to "2 January 2024" + When Admin sets the business date to "3 January 2024" And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "3 January 2024" with "200" EUR transaction amount Then Loan Repayment schedule has 6 periods, with the following data for periods: | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java index dc3d42e6c..483763405 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java @@ -36,6 +36,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.workingdays.domain.WorkingDays; @@ -108,6 +109,12 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT "Transaction date cannot be before disbursement date"); } + // Validate transaction date is not in the future + if (transactionDate != null && transactionDate.isAfter(DateUtils.getBusinessLocalDate())) { + baseDataValidator.reset().parameter("transactionDate").failWithCode("cannot.be.in.the.future", + "Transaction date cannot be in the future"); + } + final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount(); @@ -142,7 +149,7 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT final Type typeOfMap = new TypeToken>() {}.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getCapitalizedIncomeAdjustmentParameters()); - Validator.validateOrThrow("loan.capitalized.incomeAdjustment", baseDataValidator -> { + Validator.validateOrThrow("loan.capitalizedIncomeAdjustment", baseDataValidator -> { final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); validateLoanClientIsActive(loan); validateLoanGroupIsActive(loan); @@ -171,6 +178,12 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT "Transaction date cannot be before disbursement date"); } + // Validate transaction date is not in the future + if (transactionDate != null && transactionDate.isAfter(DateUtils.getBusinessLocalDate())) { + baseDataValidator.reset().parameter("transactionDate").failWithCode("cannot.be.in.the.future", + "Transaction date cannot be in the future"); + } + final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CapitalizedIncomeAmortizationBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CapitalizedIncomeAmortizationBusinessStep.java index b8e1cae23..0fb1686b8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CapitalizedIncomeAmortizationBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CapitalizedIncomeAmortizationBusinessStep.java @@ -66,12 +66,14 @@ public class CapitalizedIncomeAmortizationBusinessStep implements LoanCOBBusines BigDecimal totalAmortizationAmount = BigDecimal.ZERO; for (LoanCapitalizedIncomeBalance balance : balances) { - long daysUntilMaturity = businessDate.until(loan.getMaturityDate(), ChronoUnit.DAYS); - Money dailyAmortization = CapitalizedIncomeAmortizationUtil.calculateDailyAmortization( - loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), daysUntilMaturity, balance.getUnrecognizedAmount(), - loan.getCurrency().toData()); - totalAmortizationAmount = totalAmortizationAmount.add(dailyAmortization.getAmount(), mc); - balance.setUnrecognizedAmount(balance.getUnrecognizedAmount().subtract(dailyAmortization.getAmount(), mc)); + if (DateUtils.isAfterInclusive(businessDate, balance.getDate())) { + long daysUntilMaturity = businessDate.until(loan.getMaturityDate(), ChronoUnit.DAYS); + Money dailyAmortization = CapitalizedIncomeAmortizationUtil.calculateDailyAmortization( + loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), daysUntilMaturity, + balance.getUnrecognizedAmount(), loan.getCurrency().toData()); + totalAmortizationAmount = totalAmortizationAmount.add(dailyAmortization.getAmount(), mc); + balance.setUnrecognizedAmount(balance.getUnrecognizedAmount().subtract(dailyAmortization.getAmount(), mc)); + } } capitalizedIncomeBalanceRepository.saveAll(balances); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java index 087bde1af..92134ad75 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java @@ -339,4 +339,83 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest { ); }); } + + @Test + public void testCapitalizedIncomeTransactionsNotInFuture() { + final AtomicReference loanIdRef = new AtomicReference<>(); + + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true) + .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT) + .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION) + .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue()) + .incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue()) + .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE)); + + runAt("1 January 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024", + 500.0, 7.0, 3, null); + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024"); + + // Capitalized income cannot be in the future + Assertions.assertThrows(RuntimeException.class, + () -> loanTransactionHelper.addCapitalizedIncome(loanId, "1 February 2024", 100.0)); + + Long capitalizedIncomeId = loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 2024", 100.0).getResourceId(); + + // Capitalized income adjustment cannot be in the future + Assertions.assertThrows(RuntimeException.class, + () -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeId, "1 February 2024", 10.0)); + }); + } + + @Test + public void testCapitalizedIncomeAmortizationShouldNotHappensForFutureBalances() { + final AtomicReference loanIdRef = new AtomicReference<>(); + final AtomicReference capitalizedIncomeIdRef = new AtomicReference<>(); + + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true) + .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT) + .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION) + .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue()) + .incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue()) + .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE)); + + runAt("1 January 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024", + 500.0, 7.0, 3, null); + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024"); + PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = loanTransactionHelper.addCapitalizedIncome(loanId, + "1 January 2024", 100.0); + capitalizedIncomeIdRef.set(capitalizedIncomeResponse.getResourceId()); + + // random midday COB run + executeInlineCOB(loanId); + + // verify no early amortization was created + verifyTransactions(loanId, // + transaction(100.0, "Disbursement", "01 January 2024"), // + transaction(100.0, "Capitalized Income", "01 January 2024") // + ); + }); + runAt("2 January 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + verifyTransactions(loanId, // + transaction(100.0, "Disbursement", "01 January 2024"), // + transaction(100.0, "Capitalized Income", "01 January 2024"), // + transaction(1.10, "Capitalized Income Amortization", "01 January 2024") // + ); + }); + } }