From 3e64cc1536512e3668b6ec5fab4a7690e6f462f6 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Mon, 28 Oct 2024 17:32:30 +0100 Subject: [PATCH] FINERACT-2107: Fix transaction order --- .../ProgressiveLoanInterestScheduleModel.java | 4 +-- .../calc/ProgressiveEMICalculator.java | 24 +++++-------- .../domain/LoanAccountDomainServiceJpa.java | 15 +++++--- .../ExternalBusinessEventTest.java | 2 +- .../LoanInterestRefundTest.java | 36 +++++++++++++++++-- 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java index 7f51b9db4..9f0782603 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java @@ -179,11 +179,11 @@ public class ProgressiveLoanInterestScheduleModel { previousInterestPeriod.addDisbursementAmount(disbursedAmount); previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount); final InterestPeriod interestPeriod = new InterestPeriod(repaymentPeriod, previousInterestPeriod.getDueDate(), originalDueDate, - BigDecimal.ZERO, getZero(mc), getZero(mc), getZero(mc), mc); + BigDecimal.ZERO, getZero(), getZero(), getZero(), mc); repaymentPeriod.getInterestPeriods().add(interestPeriod); } - private Money getZero(MathContext mc) { + private Money getZero() { return Money.zero(loanProductRelatedDetail.getCurrency(), mc); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index dab618d09..1898e0dac 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -145,8 +145,10 @@ public final class ProgressiveEMICalculator implements EMICalculator { public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate, final LocalDate targetDate) { MathContext mc = scheduleModel.mc(); - RepaymentPeriod repaymentPeriod = scheduleModel.deepCopy(mc).repaymentPeriods().stream() + ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); + RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream() .filter(rp -> rp.getDueDate().equals(repaymentPeriodDueDate)).findFirst().orElseThrow(); + LocalDate adjustedTargetDate = targetDate; InterestPeriod interestPeriod; if (!targetDate.isAfter(repaymentPeriod.getFromDate())) { @@ -162,22 +164,12 @@ public final class ProgressiveEMICalculator implements EMICalculator { interestPeriod.setDueDate(adjustedTargetDate); int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod); repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear(); - calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); + scheduleModelCopy.repaymentPeriods().forEach(rp -> rp.getInterestPeriods().removeIf(ip -> ip.getDueDate().isAfter(targetDate))); + calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy); + calculateOutstandingBalance(scheduleModelCopy); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy); - // TODO: gather all the unrecognized interest from previous periods based on target date - Money payableInterest = targetDate.isBefore(repaymentPeriod.getFromDate()) - ? Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency(), mc) - : repaymentPeriod.getDueInterest(); - Money outstandingLoanBalance = interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount()); - - Money calculatedEmi = outstandingLoanBalance.plus(payableInterest, mc); - if (calculatedEmi.isLessThan(repaymentPeriod.getEmi())) { - // Review this logic - repaymentPeriod.setEmi(outstandingLoanBalance.plus(payableInterest).plus(repaymentPeriod.getPaidInterest(), mc) - .plus(repaymentPeriod.getPaidPrincipal(), mc)); - } - Money payablePrincipal = repaymentPeriod.getEmi().minus(payableInterest, mc); - return new PayableDetails(repaymentPeriod.getEmi(), payablePrincipal, payableInterest, + return new PayableDetails(repaymentPeriod.getEmi(), repaymentPeriod.getDuePrincipal(), repaymentPeriod.getDueInterest(), interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount(), mc)); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index daacb6b03..ce5332e85 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -860,7 +860,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { transactionDate, txnExternalId); final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(refundTransaction); - loan.getLoanTransactions().add(refundTransaction); boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream() .map(LoanSupportedInterestRefundTypes::getTransactionType) @@ -869,9 +868,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { if (shouldCreateInterestRefundTransaction) { interestRefundTransaction = createInterestRefundLoanTransaction(loan, transactionDate, transactionAmount); - if (interestRefundTransaction != null) { - loan.addLoanTransaction(interestRefundTransaction); - } } final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = transactionProcessorFactory @@ -887,13 +883,24 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { || !currentInstallment.getTotalOutstanding(loan.getCurrency()).isEqualTo(refundTransaction.getAmount(loan.getCurrency())); // if (!reprocess) { + loan.getLoanTransactions().add(refundTransaction); loanRepaymentScheduleTransactionProcessor.processLatestTransaction(refundTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + if (interestRefundTransaction != null) { + loan.addLoanTransaction(interestRefundTransaction); + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(interestRefundTransaction, + new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + } } else { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); } + loan.getLoanTransactions().add(refundTransaction); + if (interestRefundTransaction != null) { + loan.addLoanTransaction(interestRefundTransaction); + } final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(), 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 6aee674af..7d8b137e4 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 @@ -183,7 +183,7 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest { Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); verifyBusinessEvents(new LoanTransactionBusinessEvent("LoanTransactionInterestRefundPostBusinessEvent", "22 January 2021", 5.75, - 994.25, 5.75, 0.0, 0.0, 0.0)); + 0.0, 5.75, 0.0, 0.0, 0.0)); }); enableLoanInterestRefundPstBusinessEvent(false); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java index 858f334ef..2cf9cc0d7 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.integrationtests; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.REPLAYED; + import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; import io.restassured.http.ContentType; @@ -28,6 +30,7 @@ import java.util.ArrayList; import java.util.concurrent.atomic.AtomicReference; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; @@ -142,12 +145,39 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); logTransactions(loanId); - verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), - transaction(1000.0, "Payout Refund", "22 January 2021"), transaction(5.75, "Accrual", "22 January 2021"), - transaction(5.75, "Interest Refund", "22 January 2021")); + verifyTransactions(loanId, // + transaction(1000.0, "Disbursement", "01 January 2021"), // + transaction(1000.0, "Payout Refund", "22 January 2021"), // + transaction(5.75, "Interest Refund", "22 January 2021"), // + transaction(5.75, "Accrual", "22 January 2021")); // + + checkTransactionWasNotReverseReplayed(postLoansLoanIdTransactionsResponse.getLoanId(), + postLoansLoanIdTransactionsResponse.getResourceId()); + checkTransactionWasNotReverseReplayed(postLoansLoanIdTransactionsResponse.getLoanId(), + postLoansLoanIdTransactionsResponse.getSubResourceId()); + + verifyTRJournalEntries(postLoansLoanIdTransactionsResponse.getResourceId(), journalEntry(1000, fundSource, "DEBIT"), // + journalEntry(5.75, interestReceivableAccount, "CREDIT"), // + journalEntry(994.25, loansReceivableAccount, "CREDIT")); + + verifyTRJournalEntries(postLoansLoanIdTransactionsResponse.getSubResourceId(), + journalEntry(5.75, interestIncomeAccount, "DEBIT"), // + journalEntry(5.75, loansReceivableAccount, "CREDIT")); // }); } + private void checkTransactionWasNotReverseReplayed(Long loanId, Long transactionId) { + GetLoansLoanIdTransactionsTransactionIdResponse loanTransactionDetails = loanTransactionHelper.getLoanTransactionDetails(loanId, + transactionId); + if (loanTransactionDetails.getTransactionRelations() != null) { + loanTransactionDetails.getTransactionRelations().forEach(transactionRelation -> { + if (REPLAYED.name().equals(transactionRelation.getRelationType())) { + Assertions.fail("Transaction was replayed!"); + } + }); + } + } + @Test public void verifyInterestRefundCreatedForMerchantIssuedRefund() { AtomicReference loanIdRef = new AtomicReference<>();