Skip to content

Commit

Permalink
FINERACT-2081: Fix Accrual Activity Reversal on Installment Due Date.
Browse files Browse the repository at this point in the history
* Accrual Activity is not recalculated during reopen transaction recalculation
* integration tests cover reverse replay verification and extra use cases
  • Loading branch information
somasorosdpc committed Jan 29, 2025
1 parent a9d76ef commit cd4f5de
Show file tree
Hide file tree
Showing 5 changed files with 518 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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:
* <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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -709,6 +712,142 @@ public void verifyLoanChargeAdjustmentPostBusinessEvent09() {
});
}

/**
* 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", () -> {
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(33.52, "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")); //
});
}


@Test
public void testInterestBearingProgressiveInterestRecalculationReopenDueReverseRepayment2() {
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(loanId, "Repayment", "17 January 2025", 450.0).getResourceId();
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "17 January 2025", 33.52);

GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getClosedObligationsMet);
verifyTransactions(loanId, //
transaction(450.0, "Disbursement", "17 August 2024"), //
transaction(450.0, "Repayment", "17 January 2025"), //
transaction(33.52, "Repayment", "17 January 2025"), //
transaction(33.52, "Accrual", "17 January 2025"), //
transaction(33.52, "Accrual Activity", "17 January 2025")); //

loanTransactionHelper.makeLoanRepayment(loanId, "MerchantIssuedRefund", "17 January 2025", 450.0).getResourceId();

loanDetails = loanTransactionHelper.getLoanDetails(loanId);
verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);

verifyTransactions(loanId, //
transaction(450.0, "Disbursement", "17 August 2024"), //
transaction(450.0, "Repayment", "17 January 2025"), //
transaction(450.0, "Merchant Issued Refund", "17 January 2025"), //
transaction(33.52, "Repayment", "17 January 2025"), //
transaction(33.52, "Accrual", "17 January 2025"), //
transaction(3.31, "Accrual Activity", "17 January 2025"), //
transaction(30.21, "Accrual Activity", "17 January 2025")); //


loanTransactionHelper.reverseLoanTransaction(loanId, repaymentId, "17 January 2025");


loanDetails = loanTransactionHelper.getLoanDetails(loanId);
if (loanDetails.getTransactions() != null) {
loanDetails.getTransactions()
.forEach(tr -> log.info("Transaction {} {} {} {}", tr.getType().getValue(), tr.getDate(), tr.getAmount(), tr.getReversedOnDate()));
}
verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getClosedObligationsMet);
verifyTransactions(loanId, //
transaction(450.0, "Disbursement", "17 August 2024"), //
reversedTransaction(450.0, "Repayment", "17 January 2025"), //
transaction(450.0, "Merchant Issued Refund", "17 January 2025"), //
transaction(33.52, "Repayment", "17 January 2025"), //
transaction(33.52, "Accrual", "17 January 2025"), //
transaction(3.31, "Accrual Activity", "17 January 2025"), //
transaction(3.31, "Accrual Activity", "17 January 2025"), //
transaction(26.9, "Accrual Activity", "17 January 2025")); //
});
}

@Test
public void verifyInterestRefundPostBusinessEventCreatedForMerchantIssuedRefundWithInterestRefund() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
Expand Down
Loading

0 comments on commit cd4f5de

Please sign in to comment.