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 bae506da01b..848f24a1e94 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 ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur * 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,13 @@ protected void calculateAccrualActivity(LoanTransaction loanTransaction, Monetar .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 a846f9bb356..3781a6830b7 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.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.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 @@ protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails 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 19eafb8652f..2582e15dc47 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.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.PostClientsResponse; import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest; import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse; @@ -51,6 +53,7 @@ import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; 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.event.external.service.validation.ExternalEventDTO; import org.apache.fineract.integrationtests.common.BusinessStepHelper; @@ -709,6 +712,82 @@ public void verifyLoanChargeAdjustmentPostBusinessEvent09() { }); } + /** + * 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", () -> { + 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 e5c9e25d8e0..d50294e3cf2 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.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 static void setup() { "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 void testInterestBearingProgressiveNoInterestRecalculationAutoDownPayment 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 void testInterestBearingProgressiveNoInterestRecalculationAutoDownPayment 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 void testInterestBearingProgressiveNoInterestRecalculationAutoDownPayment 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 void testInterestBearingProgressiveNoInterestRecalculationAutoDownPayment 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 67ca63554b7..1574808e25f 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 @@ @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; @@ -883,6 +884,11 @@ public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "repayment")); } + public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, final String command, final String date, final Double amount) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATE_FORMAT).transactionDate(date).locale("en").transactionAmount(amount), command)); + } + public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, final PostLoansLoanIdTransactionsRequest request, final String user, final String pass) { return ok(newFineract(user, pass).loanTransactions.executeLoanTransaction(loanId, request, "repayment")); @@ -1204,6 +1210,12 @@ public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final Long loa 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), "");