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:
+ *
+ * - The loan details object is not null
+ * - The loan status in the loan details is not null
+ * - The value extracted by the extractor function is not null
+ * - The value extracted by the extractor function is true
+ *
+ */
+ 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), "");