From 78cd5b106abb940893da26d54519fa2c54069390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Wed, 29 Jan 2025 10:44:13 +0100 Subject: [PATCH] FINERACT-2081: Fix Accrual Activity Reversal on Installment Due Date. * Accrual Activity is not recalculated during reopen transaction recalculation * integration tests cover reverse replay verification and extra use cases --- ...RepaymentScheduleTransactionProcessor.java | 13 +- .../BaseLoanIntegrationTest.java | 28 ++ .../ExternalBusinessEventTest.java | 78 ++++ ...TransactionAccrualActivityPostingTest.java | 343 +++++++++++++++++- .../common/loans/LoanTransactionHelper.java | 13 +- 5 files changed, 450 insertions(+), 25 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index bae506da0..c6c0da186 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -185,7 +185,10 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen * Check if the transaction amounts have changed. If so, reverse the original transaction and update * changedTransactionDetail accordingly **/ - if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { + if (newLoanTransaction.isReversed()) { + loanTransaction.reverse(); + changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), loanTransaction); + } else if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { loanTransaction.updateLoanTransactionToRepaymentScheduleMappings( newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings()); } else { @@ -225,8 +228,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen .filter(installment -> LoanRepaymentScheduleProcessingWrapper.isInPeriod(loanTransaction.getTransactionDate(), installment, installment.getInstallmentNumber().equals(firstNormalInstallmentNumber))) .findFirst().orElseThrow(); - if (loanTransaction.getDateOf().isEqual(currentInstallment.getDueDate()) || installments.stream() - .filter(i -> !i.isAdditional() && !i.isDownPayment()).noneMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)) { + + if (currentInstallment.isNotFullyPaidOff() && (currentInstallment.getDueDate().isAfter(loanTransaction.getTransactionDate()) + || (currentInstallment.getDueDate().isEqual(loanTransaction.getTransactionDate()) + && loanTransaction.getTransactionDate().equals(DateUtils.getBusinessLocalDate())))) { + loanTransaction.reverse(); + } else { loanTransaction.resetDerivedComponents(); final Money principalPortion = Money.zero(currency); Money interestPortion = currentInstallment.getInterestCharged(currency); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 2ed80f7cf..b484f8903 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -50,6 +50,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.function.Function; import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.ToString; @@ -62,6 +63,7 @@ import org.apache.fineract.client.models.BusinessDateRequest; import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdStatus; import org.apache.fineract.client.models.GetLoansLoanIdTransactions; import org.apache.fineract.client.models.JournalEntryTransactionItem; import org.apache.fineract.client.models.PaymentAllocationOrder; @@ -238,6 +240,32 @@ public abstract class BaseLoanIntegrationTest { assertEquals(paidLate, period.getTotalPaidLateForPeriod()); } + /** + * Verifies the loan status by applying the given extractor function to the status of the loan details. This method + * ensures that the loan details, loan status, and the result of the extractor are not null and asserts that the + * result of the extractor function is true. + * + * @param loanDetails + * the loan details object containing the loan status + * @param extractor + * a function that extracts a boolean value from the loan status for verification + * @throws AssertionError + * if any of the following conditions are not met: + * + */ + protected void verifyLoanStatus(GetLoansLoanIdResponse loanDetails, Function extractor) { + Assertions.assertNotNull(loanDetails); + Assertions.assertNotNull(loanDetails.getStatus()); + Boolean actualValue = extractor.apply(loanDetails.getStatus()); + Assertions.assertNotNull(actualValue); + Assertions.assertTrue(actualValue); + } + private String getNonByPassUserAuthKey(RequestSpecification requestSpec, ResponseSpecification responseSpec) { // creates the user UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java index 321e46256..3b2e7dbea 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java @@ -42,6 +42,8 @@ import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdStatus; import org.apache.fineract.client.models.GlobalConfigurationPropertyData; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest; @@ -53,6 +55,7 @@ import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansRequest; +import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO; @@ -715,6 +718,81 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest { }); } + /** + * Using Interest bearing Progressive Loan, Accrual Activity Posting, InterestRecalculation, 25% yearly interest 6 + * repayment 450 USD principal. + *
  • apply, approve and disburse backdated on 17 August 2024
  • + *
  • repay 600 on 17 January 2025
  • + *
  • verify Accrual and Accrual Activity transaction creation
  • + *
  • verify that the loan become overpaid
  • + *
  • reverse repayment on same day
  • + *
  • verify there is no reverse replayed transaction during reversing the repayment
  • + *
  • verify transaction reversals
  • + */ + @Test + public void testInterestBearingProgressiveInterestRecalculationReopenDueReverseRepayment() { + runAt("17 January 2025", () -> { + externalEventHelper.enableBusinessEvent("LoanAdjustTransactionBusinessEvent"); + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive() // + .description("Interest bearing Progressive Loan USD, Accrual Activity Posting, NO InterestRecalculation") // + .enableAccrualActivityPosting(true) // + .daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .isInterestRecalculationEnabled(false));// + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "17 August 2024", 450.0, 25.0, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(450.0, "17 August 2024")); + disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024"); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024") // + ); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 January 2025", 600.0f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(600.0, "Repayment", "17 January 2025"), // + transaction(33.52, "Accrual", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024"), // + transaction(4.99, "Accrual Activity", "17 January 2025")); // + deleteAllExternalEvents(); + loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "17 January 2025"); + + List allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec); + // Verify that there were no reverse-replay event + List list = allExternalEvents.stream() // + .filter(x -> "LoanAdjustTransactionBusinessEvent".equals(x.getType()) // + && x.getPayLoad().get("newTransactionDetail") != null // + && x.getPayLoad().get("transactionToAdjust") != null) // + .toList(); // + Assertions.assertEquals(0, list.size()); + + // verify that there were 2 transaction reversal event + list = allExternalEvents.stream() // + .filter(x -> "LoanAdjustTransactionBusinessEvent".equals(x.getType()) // + && x.getPayLoad().get("newTransactionDetail") == null // + && x.getPayLoad().get("transactionToAdjust") != null) // + .toList(); // + Assertions.assertEquals(2, list.size()); + + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive); + verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(33.52, "Accrual", "17 January 2025"), // + reversedTransaction(600.0, "Repayment", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024")); // + }); + } + @Test public void verifyInterestRefundPostBusinessEventCreatedForMerchantIssuedRefundWithInterestRefund() { AtomicReference loanIdRef = new AtomicReference<>(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java index e5c9e25d8..d50294e3c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java @@ -43,6 +43,7 @@ import org.apache.fineract.client.models.ChargeToGLAccountMapper; import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings; import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdStatus; import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostChargesRequest; import org.apache.fineract.client.models.PostChargesResponse; @@ -108,6 +109,323 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati "EXTERNAL_ASSET_OWNER_TRANSFER", "ACCRUAL_ACTIVITY_POSTING"); } + /** + * Using Interest bearing Progressive Loan, Accrual Activity Posting, NO InterestRecalculation, 25% yearly interest + * 6 repayment 450 USD principal. + *
  • apply, approve and disburse backdated on 17 August 2024
  • + *
  • repay 600 on 17 January 2025
  • + *
  • verify Accrual and Accrual Activity transaction creation
  • + *
  • verify that the loan become overpaid
  • + *
  • reverse repayment on same day
  • + *
  • verify transaction reversals
  • + */ + @Test + public void testInterestBearingProgressiveNoInterestRecalculationReopenDueReverseRepayment1() { + runAt("17 January 2025", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive() // + .description("Interest bearing Progressive Loan USD, Accrual Activity Posting, NO InterestRecalculation") // + .enableAccrualActivityPosting(true) // + .daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .isInterestRecalculationEnabled(false));// + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "17 August 2024", 450.0, 25.0, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(450.0, "17 August 2024")); + disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024"); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024") // + ); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 January 2025", 600.0f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(600.0, "Repayment", "17 January 2025"), // + transaction(33.52, "Accrual", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024"), // + transaction(4.99, "Accrual Activity", "17 January 2025")); // + loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "17 January 2025"); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive); + verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(33.52, "Accrual", "17 January 2025"), // + reversedTransaction(600.0, "Repayment", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024")); // + }); + } + + /** + * Using Interest bearing Progressive Loan, Accrual Activity Posting, InterestRecalculation, 25% yearly interest 6 + * repayment 450 USD principal. + *
  • apply, approve and disburse backdated on 17 August 2024
  • + *
  • repay 600 on 17 January 2025
  • + *
  • verify Accrual and Accrual Activity transaction creation
  • + *
  • verify that the loan become overpaid
  • + *
  • reverse repayment on same day
  • + *
  • verify transaction reversals
  • + */ + @Test + public void testInterestBearingProgressiveInterestRecalculationReopenDueReverseRepayment() { + runAt("17 January 2025", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive() // + .description("Interest bearing Progressive Loan USD, Accrual Activity Posting, NO InterestRecalculation") // + .enableAccrualActivityPosting(true) // + .daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .isInterestRecalculationEnabled(false));// + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "17 August 2024", 450.0, 25.0, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(450.0, "17 August 2024")); + disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024"); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024") // + ); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 January 2025", 600.0f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(600.0, "Repayment", "17 January 2025"), // + transaction(33.52, "Accrual", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024"), // + transaction(4.99, "Accrual Activity", "17 January 2025")); // + loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "17 January 2025"); + + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive); + verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(33.52, "Accrual", "17 January 2025"), // + reversedTransaction(600.0, "Repayment", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024")); // + }); + } + + /** + * Using Interest bearing Progressive Loan, Accrual Activity Posting, NO InterestRecalculation, 25% yearly interest + * 6 repayment 450 USD principal. + *
  • apply, approve and disburse backdated on 17 August 2024
  • + *
  • repay 600 on 17 January 2025
  • + *
  • verify Accrual and Accrual Activity transaction creation
  • + *
  • verify that the loan become overpaid
  • + *
  • reverse repayment on same day
  • + *
  • verify transaction reversals
  • + */ + @Test + public void testInterestBearingProgressiveNoInterestRecalculationReopenDueReverseRepayment1b() { + runAt("18 January 2025", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive() // + .description("Interest bearing Progressive Loan USD, Accrual Activity Posting, NO InterestRecalculation") // + .enableAccrualActivityPosting(true) // + .daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .isInterestRecalculationEnabled(false));// + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "17 August 2024", 450.0, 25.0, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(450.0, "17 August 2024")); + disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024"); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024") // + ); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 January 2025", 600.0f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(600.0, "Repayment", "17 January 2025"), // + transaction(33.52, "Accrual", "18 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024"), // + transaction(4.99, "Accrual Activity", "17 January 2025")); // + loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "17 January 2025"); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive); + verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(33.52, "Accrual", "18 January 2025"), // + reversedTransaction(600.0, "Repayment", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024"), // + transaction(3.31, "Accrual Activity", "17 January 2025")); // + }); + } + + @Test + public void testAccrualActivityPostingAndReversalsInterestBearingProgressiveInterestRecalculationMerchantIssuedRefund() { + runAt("17 January 2025", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive() // + .description("Interest bearing Progressive Loan USD, Accrual Activity Posting, InterestRecalculation") // + .enableAccrualActivityPosting(true) // + .currencyCode("USD") // + .daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .isInterestRecalculationEnabled(true));// + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "17 August 2024", 450.0, 25.0, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(450.0, "17 August 2024")); + disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024"); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024") // + ); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 January 2025", 497.04f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getClosedObligationsMet); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(497.04, "Repayment", "17 January 2025"), // + transaction(47.04, "Accrual", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(9.22, "Accrual Activity", "17 October 2024"), // + transaction(9.53, "Accrual Activity", "17 November 2024"), // + transaction(9.22, "Accrual Activity", "17 December 2024"), // + transaction(9.54, "Accrual Activity", "17 January 2025")); // + loanTransactionHelper.makeLoanRepayment("MerchantIssuedRefund", "17 August 2024", 450.0f, loanId.intValue()).getResourceId(); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid); + verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(450.0, "Merchant Issued Refund", "17 August 2024"), // + transaction(497.04, "Repayment", "17 January 2025"), // + transaction(47.04, "Accrual", "17 January 2025"), // + transaction(47.04, "Accrual Adjustment", "17 January 2025")); // + }); + } + + /** + * Using Interest bearing Progressive Loan, Accrual Activity Posting, NO InterestRecalculation, 25% yearly interest + * 6 repayment 450 USD principal. + *
  • apply, approve and disburse backdated on 17 August 2024
  • + *
  • repay 600 on 17 January 2025
  • + *
  • verify Accrual and Accrual Activity transaction creation
  • + *
  • verify that the loan become overpaid
  • + *
  • reverse repayment on same day verify transaction reversals
  • + */ + @Test + public void testInterestBearingProgressiveNoInterestRecalculationReopenDueReverseRepayment2b() { + runAt("17 January 2025", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive() // + .description("Interest bearing Progressive Loan USD, Accrual Activity Posting, NO InterestRecalculation") // + .enableAccrualActivityPosting(true) // + .currencyCode("USD") // + .daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .isInterestRecalculationEnabled(false));// + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "17 August 2024", 450.0, 25.0, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(450.0, "17 August 2024")); + disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024"); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024") // + ); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 January 2025", 483.52f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getClosedObligationsMet); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(483.52, "Repayment", "17 January 2025"), // + transaction(33.52, "Accrual", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024"), // + transaction(4.99, "Accrual Activity", "17 January 2025")); // + addCharge(loanId, false, 15.0, "15 January 2025"); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive); + verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(33.52, "Accrual", "17 January 2025"), // + transaction(483.52, "Repayment", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024")); // + }); + } + + /** + * Using Interest bearing Progressive Loan, Accrual Activity Posting, NO InterestRecalculation, 25% yearly interest + * 6 repayment 450 USD principal. + *
  • apply, approve and disburse backdated on 17 August 2024
  • + *
  • repay 600 on 17 January 2025
  • + *
  • verify Accrual and Accrual Activity transaction creation
  • + *
  • verify that the loan become overpaid
  • + *
  • reverse repayment on same day verify transaction reversals
  • + */ + @Test + public void testInterestBearingProgressiveNoInterestRecalculationReopenDueReverseRepayment2c() { + runAt("18 January 2025", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive() // + .description("Interest bearing Progressive Loan USD, Accrual Activity Posting, NO InterestRecalculation") // + .enableAccrualActivityPosting(true) // + .currencyCode("USD") // + .daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .isInterestRecalculationEnabled(false));// + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "17 August 2024", 450.0, 25.0, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(450.0, "17 August 2024")); + disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024"); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024") // + ); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 January 2025", 483.52f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getClosedObligationsMet); + verifyTransactions(loanId, // + transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(483.52, "Repayment", "17 January 2025"), // + transaction(33.52, "Accrual", "18 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024"), // + transaction(4.99, "Accrual Activity", "17 January 2025")); // + addCharge(loanId, false, 15.0, "15 January 2025"); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive); + verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 August 2024"), // + transaction(33.52, "Accrual", "18 January 2025"), // + transaction(483.52, "Repayment", "17 January 2025"), // + transaction(9.53, "Accrual Activity", "17 September 2024"), // + transaction(7.77, "Accrual Activity", "17 October 2024"), // + transaction(6.48, "Accrual Activity", "17 November 2024"), // + transaction(4.75, "Accrual Activity", "17 December 2024"), // + transaction(18.31, "Accrual Activity", "17 January 2025")); // + + }); + } + // Create Loan with Interest and enabled Accrual Activity Posting // Approve and disburse loan // charge penalty with due date as 1st installment @@ -955,10 +1273,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati Long repaymentId = loanTransactionHelper.makeLoanRepayment("02 January 2024", 370.0f, loanId.intValue()).getResourceId(); Assertions.assertNotNull(repaymentId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertNotNull(loanDetails); - Assertions.assertNotNull(loanDetails.getStatus()); - Assertions.assertNotNull(loanDetails.getStatus().getOverpaid()); - Assertions.assertTrue(loanDetails.getStatus().getOverpaid()); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid); verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), transaction(100.0, "Down Payment", "01 January 2024"), transaction(8.76, "Accrual", "02 January 2024"), @@ -991,10 +1306,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati Long repaymentId = loanTransactionHelper.makeLoanRepayment("01 January 2024", 370.0f, loanId.intValue()).getResourceId(); Assertions.assertNotNull(repaymentId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertNotNull(loanDetails); - Assertions.assertNotNull(loanDetails.getStatus()); - Assertions.assertNotNull(loanDetails.getStatus().getOverpaid()); - Assertions.assertTrue(loanDetails.getStatus().getOverpaid()); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid); verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), transaction(100.0, "Down Payment", "01 January 2024"), transaction(8.76, "Accrual", "01 January 2024"), @@ -1002,10 +1314,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "01 January 2024"); loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertNotNull(loanDetails); - Assertions.assertNotNull(loanDetails.getStatus()); - Assertions.assertNotNull(loanDetails.getStatus().getActive()); - Assertions.assertTrue(loanDetails.getStatus().getActive()); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive); verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), transaction(100.0, "Down Payment", "01 January 2024"), transaction(8.76, "Accrual", "01 January 2024"), reversedTransaction(370.0, "Repayment", "01 January 2024")); @@ -1038,20 +1347,14 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati Long repaymentId = loanTransactionHelper.makeLoanRepayment("01 January 2024", 370.0f, loanId.intValue()).getResourceId(); Assertions.assertNotNull(repaymentId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertNotNull(loanDetails); - Assertions.assertNotNull(loanDetails.getStatus()); - Assertions.assertNotNull(loanDetails.getStatus().getOverpaid()); - Assertions.assertTrue(loanDetails.getStatus().getOverpaid()); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid); verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), transaction(100.0, "Down Payment", "01 January 2024"), transaction(38.76, "Accrual", "01 January 2024"), transaction(38.76, "Accrual Activity", "01 January 2024"), transaction(370.0, "Repayment", "01 January 2024")); loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "01 January 2024"); loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertNotNull(loanDetails); - Assertions.assertNotNull(loanDetails.getStatus()); - Assertions.assertNotNull(loanDetails.getStatus().getActive()); - Assertions.assertTrue(loanDetails.getStatus().getActive()); + verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive); verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), transaction(100.0, "Down Payment", "01 January 2024"), transaction(38.76, "Accrual", "01 January 2024"), reversedTransaction(370.0, "Repayment", "01 January 2024")); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 81a51b314..c030ed20e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -100,6 +100,7 @@ import org.apache.poi.ss.usermodel.Workbook; @SuppressWarnings({ "rawtypes", "unchecked" }) public class LoanTransactionHelper extends IntegrationTest { + public static final String DATE_FORMAT = "d MMMM yyyy"; public static final String DATE_TIME_FORMAT = "dd MMMM yyyy HH:mm"; private static final String LOAN_PRODUCTS_URL = "/fineract-provider/api/v1/loanproducts"; private static final String CREATE_LOAN_PRODUCT_URL = "/fineract-provider/api/v1/loanproducts?" + Utils.TENANT_IDENTIFIER; @@ -871,9 +872,10 @@ public class LoanTransactionHelper extends IntegrationTest { } public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, final String command, final String date, - final Double amountToBePaid) { + final Double amount) { + log.info("Make {} with amount {} in {} for Loan {}", command, amount, date, loanId); return ok(fineract().loanTransactions.executeLoanTransaction(loanId, new PostLoansLoanIdTransactionsRequest() - .transactionAmount(amountToBePaid).transactionDate(date).dateFormat("dd MMMM yyyy").locale("en"), command)); + .transactionAmount(amount).transactionDate(date).dateFormat("dd MMMM yyyy").locale("en"), command)); } // TODO: Rewrite to use fineract-client instead! @@ -1210,6 +1212,13 @@ public class LoanTransactionHelper extends IntegrationTest { return ok(fineract().loanTransactions.adjustLoanTransaction(loanId, transactionId, request, "undo")); } + public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final Long loanId, final Long transactionId, String date) { + return ok(fineract().loanTransactions.adjustLoanTransaction(loanId, transactionId, + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATE_FORMAT).transactionDate(date).transactionAmount(0.0) + .locale("en"), + "undo")); + } + public HashMap makeRepaymentWithPDC(final String date, final Float amountToBePaid, final Integer loanID, final Long paymentType) { return (HashMap) performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID), getRepaymentWithPDCBodyAsJSON(date, amountToBePaid, paymentType), "");