FINERACT-2354: handle reamortization error code 500 issue on same disbursement day

This commit is contained in:
Attila Budai
2026-01-15 10:17:43 +01:00
committed by Adam Saghy
parent 53ff74425c
commit 442290ef92
6 changed files with 219 additions and 54 deletions
@@ -105,6 +105,31 @@ public class LoanReAmortizationStepDef extends AbstractStepDef {
testContext().set(TestContextKey.LOAN_REAMORTIZATION_UNDO_RESPONSE, response); 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") @When("Admin creates a Loan re-amortization transaction on current business date is forbidden as loan was charged-off")
public void reAmortizationChargedOffLoanFailure() { public void reAmortizationChargedOffLoanFailure() {
reAmortizationFailure(ErrorMessageHelper.reAmortizeChargedOffLoanFailure()); reAmortizationFailure(ErrorMessageHelper.reAmortizeChargedOffLoanFailure());
@@ -170,6 +195,20 @@ public class LoanReAmortizationStepDef extends AbstractStepDef {
testContext().set(TestContextKey.LOAN_REAMORTIZATION_PREVIEW_RESPONSE, response); 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<String> data = table.asLists().get(1);
final String reAmortizationInterestHandling = data.getFirst();
final Map<String, Object> 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:") @Then("Loan Re-Amortization Repayment schedule preview has the following data in Total row:")
public void loanRepaymentDScheduleAmountCheck(final DataTable table) { public void loanRepaymentDScheduleAmountCheck(final DataTable table) {
final List<List<String>> data = table.asLists(); final List<List<String>> data = table.asLists();
@@ -5101,9 +5101,8 @@ Feature: LoanReAmortization
When Loan Pay-off is made on "02 April 2024" 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 Then Loan is closed with zero outstanding balance and it's all installments have obligations met
@Skip
@TestRailId:C4510 @AdvancedPaymentAllocation @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 sets the business date to "01 January 2024"
When Admin creates a client with random data 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 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: Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 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 | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
# --- re-amortization transaction --- 500 error!! # --- 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 And Admin creates a Loan re-amortization transaction on current business date but fails with 403 error
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 |
# --- close the loan --- # # --- close the loan --- #
When Loan Pay-off is made on "01 January 2024" 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 Then Loan is closed with zero outstanding balance and it's all installments have obligations met
@Skip
@TestRailId:C4511 @AdvancedPaymentAllocation @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 sets the business date to "01 January 2024"
When Admin creates a client with random data 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 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: Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 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 | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
# --- re-amortization transaction --- 500 error!! # --- 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" And Admin creates a Loan re-amortization transaction on current business date with reAmortizationInterestHandling "EQUAL_AMORTIZATION_INTEREST_SPLIT" but fails with 403 error
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 |
# --- close the loan --- # # --- close the loan --- #
When Loan Pay-off is made on "01 January 2024" 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 Then Loan is closed with zero outstanding balance and it's all installments have obligations met
@@ -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 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 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 | | 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
# --- Re-amortization repayment schedule preview --- # --- 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: And Admin creates a Loan re-amortization preview by Loan external ID with the following data, but fails with 403 error code:
| reAmortizationInterestHandling | | reAmortizationInterestHandling |
| DEFAULT | | 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" 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 Then Loan is closed with zero outstanding balance and it's all installments have obligations met
@@ -131,6 +131,15 @@ public class LoanReAmortizationValidator {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reamortize.not.allowed.on.contract.terminated", throw new GeneralPlatformDomainRuleException("error.msg.loan.reamortize.not.allowed.on.contract.terminated",
"Loan re-amortization is not allowed on contract terminated loan.", loan.getId()); "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) { public LoanTransaction findAndValidateReAmortizeTransactionForUndo(Loan loan) {
@@ -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.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; 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.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; 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"); .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 @Test
public void testValidateUndoReAmortize_ShouldThrowException_WhenLoanDoesntHaveReAmortization() { public void testValidateUndoReAmortize_ShouldThrowException_WhenLoanDoesntHaveReAmortization() {
// given // given
@@ -238,6 +254,10 @@ class LoanReAmortizationValidatorTest {
} }
private Loan loan() { private Loan loan() {
return loan(true);
}
private Loan loan(boolean withOverdueInstallments) {
Loan loan = mock(Loan.class); Loan loan = mock(Loan.class);
given(loan.getStatus()).willReturn(LoanStatus.ACTIVE); given(loan.getStatus()).willReturn(LoanStatus.ACTIVE);
given(loan.getMaturityDate()).willReturn(actualDate.plusDays(30)); given(loan.getMaturityDate()).willReturn(actualDate.plusDays(30));
@@ -248,6 +268,24 @@ class LoanReAmortizationValidatorTest {
given(loanProductRelatedDetail.getLoanScheduleType()).willReturn(LoanScheduleType.PROGRESSIVE); given(loanProductRelatedDetail.getLoanScheduleType()).willReturn(LoanScheduleType.PROGRESSIVE);
given(loan.isInterestBearing()).willReturn(false); given(loan.isInterestBearing()).willReturn(false);
given(loan.getLoanTransactions()).willReturn(List.of()); 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; return loan;
} }
@@ -18,7 +18,10 @@
*/ */
package org.apache.fineract.integrationtests.loan.reamortization; 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicLong; 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.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse; 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.BaseLoanIntegrationTest;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; 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) { private Long createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(int numberOfInstallments, int repaymentEvery) {
return createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(numberOfInstallments, repaymentEvery, return createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(numberOfInstallments, repaymentEvery,
DOWN_PAYMENT_PERCENTAGE); DOWN_PAYMENT_PERCENTAGE);