This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 78cd5b106 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
78cd5b106 is described below
commit 78cd5b106abb940893da26d54519fa2c54069390
Author: Soma Sörös <[email protected]>
AuthorDate: Wed Jan 29 10:44:13 2025 +0100
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
---
...tLoanRepaymentScheduleTransactionProcessor.java | 13 +-
.../integrationtests/BaseLoanIntegrationTest.java | 28 ++
.../ExternalBusinessEventTest.java | 78 +++++
.../LoanTransactionAccrualActivityPostingTest.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:
+ * <ul>
+ * <li>The loan details object is not null</li>
+ * <li>The loan status in the loan details is not null</li>
+ * <li>The value extracted by the extractor function is not
null</li>
+ * <li>The value extracted by the extractor function is
true</li>
+ * </ul>
+ */
+ protected void verifyLoanStatus(GetLoansLoanIdResponse loanDetails,
Function<GetLoansLoanIdStatus, Boolean> 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.
+ * <li>apply, approve and disburse backdated on 17 August 2024</li>
+ * <li>repay 600 on 17 January 2025</li>
+ * <li>verify Accrual and Accrual Activity transaction creation</li>
+ * <li>verify that the loan become overpaid</li>
+ * <li>reverse repayment on same day</li>
+ * <li>verify there is no reverse replayed transaction during reversing
the repayment</li>
+ * <li>verify transaction reversals</li>
+ */
+ @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<ExternalEventDTO> allExternalEvents =
ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+ // Verify that there were no reverse-replay event
+ List<ExternalEventDTO> 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<Long> 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.
+ * <li>apply, approve and disburse backdated on 17 August 2024</li>
+ * <li>repay 600 on 17 January 2025</li>
+ * <li>verify Accrual and Accrual Activity transaction creation</li>
+ * <li>verify that the loan become overpaid</li>
+ * <li>reverse repayment on same day</li>
+ * <li>verify transaction reversals</li>
+ */
+ @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.
+ * <li>apply, approve and disburse backdated on 17 August 2024</li>
+ * <li>repay 600 on 17 January 2025</li>
+ * <li>verify Accrual and Accrual Activity transaction creation</li>
+ * <li>verify that the loan become overpaid</li>
+ * <li>reverse repayment on same day</li>
+ * <li>verify transaction reversals</li>
+ */
+ @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.
+ * <li>apply, approve and disburse backdated on 17 August 2024</li>
+ * <li>repay 600 on 17 January 2025</li>
+ * <li>verify Accrual and Accrual Activity transaction creation</li>
+ * <li>verify that the loan become overpaid</li>
+ * <li>reverse repayment on same day</li>
+ * <li>verify transaction reversals</li>
+ */
+ @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.
+ * <li>apply, approve and disburse backdated on 17 August 2024</li>
+ * <li>repay 600 on 17 January 2025</li>
+ * <li>verify Accrual and Accrual Activity transaction creation</li>
+ * <li>verify that the loan become overpaid</li>
+ * <li>reverse repayment on same day verify transaction reversals</li>
+ */
+ @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.
+ * <li>apply, approve and disburse backdated on 17 August 2024</li>
+ * <li>repay 600 on 17 January 2025</li>
+ * <li>verify Accrual and Accrual Activity transaction creation</li>
+ * <li>verify that the loan become overpaid</li>
+ * <li>reverse repayment on same day verify transaction reversals</li>
+ */
+ @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), "");