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
This commit is contained in:
Soma Sörös
2025-01-30 12:45:38 +01:00
committed by Adam Saghy
parent 8b80f5a934
commit 78cd5b106a
5 changed files with 450 additions and 25 deletions
@@ -185,7 +185,10 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
* Check if the transaction amounts have changed. If so, reverse the original transaction and update * Check if the transaction amounts have changed. If so, reverse the original transaction and update
* changedTransactionDetail accordingly * 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( loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings()); newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
} else { } else {
@@ -225,8 +228,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
.filter(installment -> LoanRepaymentScheduleProcessingWrapper.isInPeriod(loanTransaction.getTransactionDate(), installment, .filter(installment -> LoanRepaymentScheduleProcessingWrapper.isInPeriod(loanTransaction.getTransactionDate(), installment,
installment.getInstallmentNumber().equals(firstNormalInstallmentNumber))) installment.getInstallmentNumber().equals(firstNormalInstallmentNumber)))
.findFirst().orElseThrow(); .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(); loanTransaction.resetDerivedComponents();
final Money principalPortion = Money.zero(currency); final Money principalPortion = Money.zero(currency);
Money interestPortion = currentInstallment.getInterestCharged(currency); Money interestPortion = currentInstallment.getInterestCharged(currency);
@@ -50,6 +50,7 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.ToString; 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.GetJournalEntriesTransactionIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdResponse; 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.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.JournalEntryTransactionItem; import org.apache.fineract.client.models.JournalEntryTransactionItem;
import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PaymentAllocationOrder;
@@ -238,6 +240,32 @@ public abstract class BaseLoanIntegrationTest {
assertEquals(paidLate, period.getTotalPaidLateForPeriod()); 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) { private String getNonByPassUserAuthKey(RequestSpecification requestSpec, ResponseSpecification responseSpec) {
// creates the user // creates the user
UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec); UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec);
@@ -42,6 +42,8 @@ import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; 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.GlobalConfigurationPropertyData;
import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest; 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.PostLoansLoanIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest; import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO; 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 @Test
public void verifyInterestRefundPostBusinessEventCreatedForMerchantIssuedRefundWithInterestRefund() { public void verifyInterestRefundPostBusinessEventCreatedForMerchantIssuedRefundWithInterestRefund() {
AtomicReference<Long> loanIdRef = new AtomicReference<>(); AtomicReference<Long> loanIdRef = new AtomicReference<>();
@@ -43,6 +43,7 @@ import org.apache.fineract.client.models.ChargeToGLAccountMapper;
import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings; import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings;
import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings; import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings;
import org.apache.fineract.client.models.GetLoansLoanIdResponse; 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.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostChargesRequest; import org.apache.fineract.client.models.PostChargesRequest;
import org.apache.fineract.client.models.PostChargesResponse; import org.apache.fineract.client.models.PostChargesResponse;
@@ -108,6 +109,323 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
"EXTERNAL_ASSET_OWNER_TRANSFER", "ACCRUAL_ACTIVITY_POSTING"); "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 // Create Loan with Interest and enabled Accrual Activity Posting
// Approve and disburse loan // Approve and disburse loan
// charge penalty with due date as 1st installment // 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(); Long repaymentId = loanTransactionHelper.makeLoanRepayment("02 January 2024", 370.0f, loanId.intValue()).getResourceId();
Assertions.assertNotNull(repaymentId); Assertions.assertNotNull(repaymentId);
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails); verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
Assertions.assertNotNull(loanDetails.getStatus());
Assertions.assertNotNull(loanDetails.getStatus().getOverpaid());
Assertions.assertTrue(loanDetails.getStatus().getOverpaid());
verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"),
transaction(100.0, "Down Payment", "01 January 2024"), transaction(8.76, "Accrual", "02 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(); Long repaymentId = loanTransactionHelper.makeLoanRepayment("01 January 2024", 370.0f, loanId.intValue()).getResourceId();
Assertions.assertNotNull(repaymentId); Assertions.assertNotNull(repaymentId);
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails); verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
Assertions.assertNotNull(loanDetails.getStatus());
Assertions.assertNotNull(loanDetails.getStatus().getOverpaid());
Assertions.assertTrue(loanDetails.getStatus().getOverpaid());
verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"),
transaction(100.0, "Down Payment", "01 January 2024"), transaction(8.76, "Accrual", "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"); loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "01 January 2024");
loanDetails = loanTransactionHelper.getLoanDetails(loanId); loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails); verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
Assertions.assertNotNull(loanDetails.getStatus());
Assertions.assertNotNull(loanDetails.getStatus().getActive());
Assertions.assertTrue(loanDetails.getStatus().getActive());
verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"),
transaction(100.0, "Down Payment", "01 January 2024"), transaction(8.76, "Accrual", "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")); 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(); Long repaymentId = loanTransactionHelper.makeLoanRepayment("01 January 2024", 370.0f, loanId.intValue()).getResourceId();
Assertions.assertNotNull(repaymentId); Assertions.assertNotNull(repaymentId);
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails); verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
Assertions.assertNotNull(loanDetails.getStatus());
Assertions.assertNotNull(loanDetails.getStatus().getOverpaid());
Assertions.assertTrue(loanDetails.getStatus().getOverpaid());
verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), 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(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")); transaction(38.76, "Accrual Activity", "01 January 2024"), transaction(370.0, "Repayment", "01 January 2024"));
loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "01 January 2024"); loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "01 January 2024");
loanDetails = loanTransactionHelper.getLoanDetails(loanId); loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails); verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
Assertions.assertNotNull(loanDetails.getStatus());
Assertions.assertNotNull(loanDetails.getStatus().getActive());
Assertions.assertTrue(loanDetails.getStatus().getActive());
verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), 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(100.0, "Down Payment", "01 January 2024"), transaction(38.76, "Accrual", "01 January 2024"),
reversedTransaction(370.0, "Repayment", "01 January 2024")); reversedTransaction(370.0, "Repayment", "01 January 2024"));
@@ -100,6 +100,7 @@ import org.apache.poi.ss.usermodel.Workbook;
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
public class LoanTransactionHelper extends IntegrationTest { 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"; 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 LOAN_PRODUCTS_URL = "/fineract-provider/api/v1/loanproducts";
private static final String CREATE_LOAN_PRODUCT_URL = "/fineract-provider/api/v1/loanproducts?" + Utils.TENANT_IDENTIFIER; 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, 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() 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! // 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")); 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) { public HashMap makeRepaymentWithPDC(final String date, final Float amountToBePaid, final Integer loanID, final Long paymentType) {
return (HashMap) performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID), return (HashMap) performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID),
getRepaymentWithPDCBodyAsJSON(date, amountToBePaid, paymentType), ""); getRepaymentWithPDCBodyAsJSON(date, amountToBePaid, paymentType), "");