From 442290ef921270c2495b0245ae2ecd9c51d45dfb Mon Sep 17 00:00:00 2001 From: Attila Budai Date: Mon, 12 Jan 2026 19:22:43 +0100 Subject: [PATCH] FINERACT-2354: handle reamortization error code 500 issue on same disbursement day --- .../loan/LoanReAmortizationStepDef.java | 39 ++++++ .../features/LoanReAmortization.feature | 46 +------ .../LoanReAmortizationPreview.feature | 16 +-- .../LoanReAmortizationValidator.java | 9 ++ .../LoanReAmortizationValidatorTest.java | 38 ++++++ .../LoanReAmortizationIntegrationTest.java | 125 ++++++++++++++++++ 6 files changed, 219 insertions(+), 54 deletions(-) diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAmortizationStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAmortizationStepDef.java index 833b43ff6..f28fc9268 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAmortizationStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAmortizationStepDef.java @@ -105,6 +105,31 @@ public class LoanReAmortizationStepDef extends AbstractStepDef { testContext().set(TestContextKey.LOAN_REAMORTIZATION_UNDO_RESPONSE, response); } + @When("Admin creates a Loan re-amortization transaction on current business date but fails with {int} error") + public void createLoanReAmortizationFailsWithError(int errorCode) { + final PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final Long loanId = loanResponse.getLoanId(); + + final PostLoansLoanIdTransactionsRequest reAmortizationRequest = LoanRequestFactory.defaultLoanReAmortizationRequest(); + + CallFailedRuntimeException exception = fail(() -> fineractClient.loanTransactions().executeLoanTransaction(loanId, + reAmortizationRequest, Map.of("command", "reAmortize"))); + assertThat(exception.getStatus()).as(ErrorMessageHelper.dateFailureErrorCodeMsg()).isEqualTo(errorCode); + } + + @When("Admin creates a Loan re-amortization transaction on current business date with reAmortizationInterestHandling {string} but fails with {int} error") + public void createLoanReAmortizationWithInterestHandlingFailsWithError(final String reAmortizationInterestHandling, int errorCode) { + final PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final Long loanId = loanResponse.getLoanId(); + + final PostLoansLoanIdTransactionsRequest reAmortizationRequest = LoanRequestFactory.defaultLoanReAmortizationRequest() + .reAmortizationInterestHandling(reAmortizationInterestHandling); + + CallFailedRuntimeException exception = fail(() -> fineractClient.loanTransactions().executeLoanTransaction(loanId, + reAmortizationRequest, Map.of("command", "reAmortize"))); + assertThat(exception.getStatus()).as(ErrorMessageHelper.dateFailureErrorCodeMsg()).isEqualTo(errorCode); + } + @When("Admin creates a Loan re-amortization transaction on current business date is forbidden as loan was charged-off") public void reAmortizationChargedOffLoanFailure() { reAmortizationFailure(ErrorMessageHelper.reAmortizeChargedOffLoanFailure()); @@ -170,6 +195,20 @@ public class LoanReAmortizationStepDef extends AbstractStepDef { testContext().set(TestContextKey.LOAN_REAMORTIZATION_PREVIEW_RESPONSE, response); } + @When("Admin creates a Loan re-amortization preview by Loan external ID with the following data, but fails with {int} error code:") + public void createReAmortizedPreviewByLoanExternalIdFailsWithErrorCode(int errorCode, final DataTable table) { + final PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final String loanExternalId = loanResponse.getResourceExternalId(); + + final List data = table.asLists().get(1); + final String reAmortizationInterestHandling = data.getFirst(); + + final Map queryParams = Map.of("reAmortizationInterestHandling", reAmortizationInterestHandling); + CallFailedRuntimeException exception = fail( + () -> fineractClient.loanTransactions().previewReAmortizationSchedule1(loanExternalId, queryParams)); + assertThat(exception.getStatus()).isEqualTo(errorCode); + } + @Then("Loan Re-Amortization Repayment schedule preview has the following data in Total row:") public void loanRepaymentDScheduleAmountCheck(final DataTable table) { final List> data = table.asLists(); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAmortization.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAmortization.feature index ae35ab537..d8280e1ff 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAmortization.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAmortization.feature @@ -5101,9 +5101,8 @@ Feature: LoanReAmortization When Loan Pay-off is made on "02 April 2024" Then Loan is closed with zero outstanding balance and it's all installments have obligations met - @Skip @TestRailId:C4510 @AdvancedPaymentAllocation - Scenario: Verify allowing re-amortization on same day as disbursement - interest bearing loan with default interest handling - UC5.1 + Scenario: Verify re-amortization on same day as disbursement is rejected when no overdue exists - interest bearing loan with default interest handling - UC5.1 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -5127,31 +5126,14 @@ Feature: LoanReAmortization Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | -# --- re-amortization transaction --- 500 error!! - And Admin creates a Loan re-amortization transaction on current business date - 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 | - | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 01 February 2024 | | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 15 March 2024 | Re-amortize | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | +# --- re-amortization transaction - fails with 403 because no overdue installments exist on disbursement day --- + And Admin creates a Loan re-amortization transaction on current business date but fails with 403 error # --- close the loan --- # When Loan Pay-off is made on "01 January 2024" Then Loan is closed with zero outstanding balance and it's all installments have obligations met - @Skip @TestRailId:C4511 @AdvancedPaymentAllocation - Scenario: Verify allowing re-amortization on same day as disbursement - interest bearing loan with equal amortization + interest split - UC5.2 + Scenario: Verify re-amortization on same day as disbursement is rejected when no overdue exists - interest bearing loan with equal amortization + interest split - UC5.2 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -5175,24 +5157,8 @@ Feature: LoanReAmortization Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | -# --- re-amortization transaction --- 500 error!! - And Admin creates a Loan re-amortization transaction on current business date with reAmortizationInterestHandling "EQUAL_AMORTIZATION_INTEREST_SPLIT" - 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 | - | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 01 February 2024 | | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 15 March 2024 | Re-amortize | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | +# --- re-amortization transaction - fails with 403 because no overdue installments exist on disbursement day --- + And Admin creates a Loan re-amortization transaction on current business date with reAmortizationInterestHandling "EQUAL_AMORTIZATION_INTEREST_SPLIT" but fails with 403 error # --- close the loan --- # When Loan Pay-off is made on "01 January 2024" Then Loan is closed with zero outstanding balance and it's all installments have obligations met diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAmortizationPreview.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAmortizationPreview.feature index 839014301..af53d1182 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAmortizationPreview.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAmortizationPreview.feature @@ -705,21 +705,9 @@ Feature: LoanReAmortizationPreview | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | | 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false | - # --- Re-amortization repayment schedule preview --- - And Admin creates a Loan re-amortization preview by Loan external ID with the following data: + # --- Re-amortization repayment schedule preview - should fail because no overdue principal exists --- + And Admin creates a Loan re-amortization preview by Loan external ID with the following data, but fails with 403 error code: | reAmortizationInterestHandling | | DEFAULT | - Then Loan Re-Amortization Repayment schedule preview 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 | - | | | 01 January 2024 | | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | | | | - | 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | - | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 10.0 | 27.01 | 17.01 | 0.0 | 0.0 | 10.0 | - | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - And Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 10.0 | 112.05 | 34.02 | 0.0 | 0.0 | 78.03 | When Loan Pay-off is made on "15 March 2024" Then Loan is closed with zero outstanding balance and it's all installments have obligations met \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java index 940cd114d..5faeed914 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java @@ -131,6 +131,15 @@ public class LoanReAmortizationValidator { throw new GeneralPlatformDomainRuleException("error.msg.loan.reamortize.not.allowed.on.contract.terminated", "Loan re-amortization is not allowed on contract terminated loan.", loan.getId()); } + + // validate there are overdue installments to re-amortize + boolean hasOverdueInstallments = loan.getRepaymentScheduleInstallments().stream() + .anyMatch(installment -> installment.getDueDate().isBefore(getBusinessLocalDate()) + && installment.getPrincipalOutstanding(loan.getCurrency()).isGreaterThanZero()); + if (!hasOverdueInstallments) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.reamortize.no.overdue.amount", + "Re-amortization cannot be executed: no overdue amount exists.", loan.getId()); + } } public LoanTransaction findAndValidateReAmortizeTransactionForUndo(Loan loan) { diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidatorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidatorTest.java index d5c1441ad..010923c8d 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidatorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidatorTest.java @@ -42,7 +42,10 @@ import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRu import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; @@ -179,6 +182,19 @@ class LoanReAmortizationValidatorTest { .isEqualTo("error.msg.loan.reamortize.reamortize.transaction.already.present.for.today"); } + @Test + public void testValidateReAmortize_ShouldThrowException_WhenNoOverdueInstallmentsExist() { + // given + Loan loan = loan(false); + JsonCommand command = jsonCommand(); + // when + GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class, + () -> underTest.validateReAmortize(loan, command)); + // then + assertThat(result).isNotNull(); + assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reamortize.no.overdue.amount"); + } + @Test public void testValidateUndoReAmortize_ShouldThrowException_WhenLoanDoesntHaveReAmortization() { // given @@ -238,6 +254,10 @@ class LoanReAmortizationValidatorTest { } private Loan loan() { + return loan(true); + } + + private Loan loan(boolean withOverdueInstallments) { Loan loan = mock(Loan.class); given(loan.getStatus()).willReturn(LoanStatus.ACTIVE); given(loan.getMaturityDate()).willReturn(actualDate.plusDays(30)); @@ -248,6 +268,24 @@ class LoanReAmortizationValidatorTest { given(loanProductRelatedDetail.getLoanScheduleType()).willReturn(LoanScheduleType.PROGRESSIVE); given(loan.isInterestBearing()).willReturn(false); given(loan.getLoanTransactions()).willReturn(List.of()); + + MonetaryCurrency currency = new MonetaryCurrency("USD", 2, null); + given(loan.getCurrency()).willReturn(currency); + + Money principalOutstanding = mock(Money.class); + given(principalOutstanding.isGreaterThanZero()).willReturn(true); + + if (withOverdueInstallments) { + LoanRepaymentScheduleInstallment overdueInstallment = mock(LoanRepaymentScheduleInstallment.class); + given(overdueInstallment.getDueDate()).willReturn(actualDate.minusDays(5)); + given(overdueInstallment.getPrincipalOutstanding(currency)).willReturn(principalOutstanding); + given(loan.getRepaymentScheduleInstallments()).willReturn(List.of(overdueInstallment)); + } else { + LoanRepaymentScheduleInstallment futureInstallment = mock(LoanRepaymentScheduleInstallment.class); + given(futureInstallment.getDueDate()).willReturn(actualDate.plusDays(10)); + given(futureInstallment.getPrincipalOutstanding(currency)).willReturn(principalOutstanding); + given(loan.getRepaymentScheduleInstallments()).willReturn(List.of(futureInstallment)); + } return loan; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java index b6ac4b30a..5236d0184 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java @@ -18,7 +18,10 @@ */ package org.apache.fineract.integrationtests.loan.reamortization; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.util.concurrent.atomic.AtomicLong; @@ -27,6 +30,7 @@ import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.integrationtests.BaseLoanIntegrationTest; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; @@ -894,6 +898,127 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest { } + @Test + public void reAmortizationOnDisbursementDayInterestBearingLoanTest() { + runAt("01 January 2024", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + int numberOfRepayments = 6; + int repaymentEvery = 1; + + // Create Interest-Bearing Loan Product with 7% interest (progressive schedule) + Long loanProductId = createInterestBearingProgressiveLoanProduct(numberOfRepayments, repaymentEvery); + + // Apply for loan with 200 amount + double applyAmount = 200.0; + double approveAmount = 100.0; + double disburseAmount = 100.0; + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2024", applyAmount, + numberOfRepayments)// + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)// + .repaymentEvery(repaymentEvery)// + .loanTermFrequency(numberOfRepayments)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// + .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)// + .interestRatePerPeriod(BigDecimal.valueOf(7.0))// + .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY); + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + loanId.set(postLoansResponse.getLoanId()); + + // Approve with 100 (partial approval) + loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), approveLoanRequest(approveAmount, "01 January 2024")); + + // Disburse 100 on Jan 1, 2024 + disburseLoan(loanId.get(), BigDecimal.valueOf(disburseAmount), "01 January 2024"); + + // Verify disbursement transaction exists + verifyTransactions(loanId.get(), // + transaction(100.0, "Disbursement", "01 January 2024") // + ); + + // Re-amortize loan on the same day as disbursement should throw exception + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name())); + + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.reamortize.no.overdue.amount")); + }); + } + + @Test + public void reAmortizationOnDisbursementDayEqualInterestSplitTest() { + runAt("01 January 2024", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + int numberOfRepayments = 6; + int repaymentEvery = 1; + + // Create Interest-Bearing Loan Product with 7% interest (progressive schedule) + Long loanProductId = createInterestBearingProgressiveLoanProduct(numberOfRepayments, repaymentEvery); + + // Apply for loan with 200 amount + double applyAmount = 200.0; + double approveAmount = 100.0; + double disburseAmount = 100.0; + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2024", applyAmount, + numberOfRepayments)// + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)// + .repaymentEvery(repaymentEvery)// + .loanTermFrequency(numberOfRepayments)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// + .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)// + .interestRatePerPeriod(BigDecimal.valueOf(7.0))// + .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY); + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + loanId.set(postLoansResponse.getLoanId()); + + // Approve with 100 (partial approval) + loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), approveLoanRequest(approveAmount, "01 January 2024")); + + // Disburse 100 on Jan 1, 2024 + disburseLoan(loanId.get(), BigDecimal.valueOf(disburseAmount), "01 January 2024"); + + // Verify disbursement transaction exists + verifyTransactions(loanId.get(), // + transaction(100.0, "Disbursement", "01 January 2024") // + ); + + // Re-amortize loan on the same day as disbursement with EQUAL interest split should throw exception + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.EQUAL_AMORTIZATION_INTEREST_SPLIT.name())); + + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.reamortize.no.overdue.amount")); + }); + } + + private Long createInterestBearingProgressiveLoanProduct(int numberOfRepayments, int repaymentEvery) { + PostLoanProductsRequest product = create4IProgressive()// + .numberOfRepayments(numberOfRepayments)// + .repaymentEvery(repaymentEvery)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L)// + .interestRatePerPeriod(7.0)// 7% annual interest + .multiDisburseLoan(true)// + .maxTrancheCount(10)// + .outstandingLoanBalance(10000.0)// + .disallowExpectedDisbursements(true)// + .allowApprovedDisbursedAmountsOverApplied(true)// + .overAppliedCalculationType("percentage")// + .overAppliedNumber(50); + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper + .retrieveLoanProductById(loanProductResponse.getResourceId()); + assertNotNull(getLoanProductsProductIdResponse); + return loanProductResponse.getResourceId(); + } + private Long createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(int numberOfInstallments, int repaymentEvery) { return createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(numberOfInstallments, repaymentEvery, DOWN_PAYMENT_PERCENTAGE);