From 98055682d3a5b57858f5b9c6da3c8cc49fd44f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Mon, 19 May 2025 21:16:28 +0200 Subject: [PATCH] FINERACT-2281: Interest Activities Not Posting For Loan With Repayment Reversal --- .../fineract/client/util/FineractClient.java | 3 + .../resources/features/EMICalculation.feature | 4 +- ...edPaymentScheduleTransactionProcessor.java | 112 +++++- .../loanproduct/calc/EMICalculator.java | 11 +- .../calc/ProgressiveEMICalculator.java | 70 ++-- .../loanproduct/calc/data/InterestPeriod.java | 30 +- .../ProgressiveLoanInterestScheduleModel.java | 11 +- .../calc/data/RepaymentPeriod.java | 46 +-- .../calc/ProgressiveEMICalculatorTest.java | 42 +-- ...rogressiveLoanCreditBalanceRefundTest.java | 324 ++++++++++++++++++ .../inlinecob/InlineLoanCOBHelper.java | 9 + 11 files changed, 549 insertions(+), 113 deletions(-) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanCreditBalanceRefundTest.java diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java index 624c73b44..6465a0a41 100644 --- a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java @@ -79,6 +79,7 @@ import org.apache.fineract.client.services.GroupsApi; import org.apache.fineract.client.services.HolidaysApi; import org.apache.fineract.client.services.HooksApi; import org.apache.fineract.client.services.ImagesApi; +import org.apache.fineract.client.services.InlineJobApi; import org.apache.fineract.client.services.InterestRateChartApi; import org.apache.fineract.client.services.InterestRateSlabAKAInterestBandsApi; import org.apache.fineract.client.services.JournalEntriesApi; @@ -300,6 +301,7 @@ public final class FineractClient { public final ExternalAssetOwnersApi externalAssetOwners; public final ExternalAssetOwnerLoanProductAttributesApi externalAssetOwnerLoanProductAttributes; public final LoanAccountLockApi loanAccountLockApi; + public final InlineJobApi inlineJobApi; private FineractClient(OkHttpClient okHttpClient, Retrofit retrofit) { this.okHttpClient = okHttpClient; @@ -424,6 +426,7 @@ public final class FineractClient { workingDays = retrofit.create(WorkingDaysApi.class); loanInterestPauseApi = retrofit.create(LoanInterestPauseApi.class); progressiveLoanApi = retrofit.create(ProgressiveLoanApi.class); + inlineJobApi = retrofit.create(InlineJobApi.class); } public static Builder builder() { diff --git a/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature b/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature index f863a64dd..0a8affcb7 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature @@ -6037,10 +6037,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Repayment schedule has 1 periods, with the following data for periods: | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | | | 01 January 2021 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 01 February 2021 | | 0.0 | 1085.63 | 5.7 | 0.0 | 0.0 | 1091.33 | 1005.7 | 1005.7 | 0.0 | 85.63 | + | 1 | 31 | 01 February 2021 | | 0.0 | 1085.63 | 5.9 | 0.0 | 0.0 | 1091.53 | 1005.7 | 1005.7 | 0.0 | 85.83 | Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 1085.63 | 5.7 | 0.0 | 0.0 | 1091.33 | 1005.7 | 1005.7 | 0.0 | 85.63 | + | 1085.63 | 5.9 | 0.0 | 0.0 | 1091.53 | 1005.7 | 1005.7 | 0.0 | 85.83 | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2021 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index fcfa66de1..d4cbfcd79 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -438,8 +438,108 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep processCreditTransaction(loanTransaction, ctx); } - protected void handleCreditBalanceRefund(LoanTransaction transaction, TransactionCtx ctx) { - super.handleCreditBalanceRefund(transaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder()); + protected void handleCreditBalanceRefund(LoanTransaction loanTransaction, TransactionCtx ctx) { + MonetaryCurrency currency = ctx.getCurrency(); + List installments = ctx.getInstallments(); + MoneyHolder overpaymentHolder = ctx.getOverpaymentHolder(); + + if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx + && loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) { + var model = progressiveTransactionCtx.getModel(); + + // Copy and paste Logic from super.handleCreditBalanceRefund + loanTransaction.resetDerivedComponents(); + List transactionMappings = new ArrayList<>(); + final Comparator byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate); + List installmentToBeProcessed = installments.stream().filter(i -> !i.isDownPayment()) + .sorted(byDate).toList(); + final Money zeroMoney = Money.zero(currency); + Money transactionAmount = loanTransaction.getAmount(currency); + Money principalPortion = MathUtil.negativeToZero(loanTransaction.getAmount(currency).minus(overpaymentHolder.getMoneyObject())); + Money repaidAmount = MathUtil.negativeToZero(transactionAmount.minus(principalPortion)); + loanTransaction.setOverPayments(repaidAmount); + overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(repaidAmount)); + loanTransaction.updateComponents(principalPortion, zeroMoney, zeroMoney, zeroMoney); + + if (principalPortion.isGreaterThanZero()) { + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + boolean loanTransactionMapped = false; + LocalDate pastDueDate = null; + for (final LoanRepaymentScheduleInstallment currentInstallment : installmentToBeProcessed) { + pastDueDate = currentInstallment.getDueDate(); + if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) { + + emiCalculator.creditPrincipal(model, transactionDate, transactionAmount); + updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx); + + if (repaidAmount.isGreaterThanZero()) { + emiCalculator.payPrincipal(model, currentInstallment.getDueDate(), transactionDate, repaidAmount); + updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx); + currentInstallment.payPrincipalComponent(transactionDate, repaidAmount); + transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, + currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney)); + } + loanTransactionMapped = true; + break; + + // If already exists an additional installment just update the due date and + // principal from the Loan chargeback / CBR transaction + } else if (currentInstallment.isAdditional()) { + if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) { + currentInstallment.updateDueDate(transactionDate); + } + + currentInstallment.updateCredits(transactionDate, transactionAmount); + if (repaidAmount.isGreaterThanZero()) { + currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); + transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, + currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney)); + } + loanTransactionMapped = true; + break; + } + } + + // New installment will be added (N+1 scenario) + if (!loanTransactionMapped) { + if (transactionDate.equals(pastDueDate)) { + // Transaction is on Maturity date, no additional installment is needed + LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed + .get(installmentToBeProcessed.size() - 1); + + emiCalculator.creditPrincipal(model, transactionDate, transactionAmount); + updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx); + + if (repaidAmount.isGreaterThanZero()) { + emiCalculator.payPrincipal(model, currentInstallment.getDueDate(), transactionDate, repaidAmount); + updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx); + currentInstallment.payPrincipalComponent(transactionDate, repaidAmount); + transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, + currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney)); + } + } else { + // transaction is after maturity date, create an additional installment + Loan loan = loanTransaction.getLoan(); + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, (installments.size() + 1), + pastDueDate, transactionDate, transactionAmount.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), + zeroMoney.getAmount(), false, null); + installment.markAsAdditional(); + installment.addToCreditedPrincipal(transactionAmount.getAmount()); + loan.addLoanRepaymentScheduleInstallment(installment); + + if (repaidAmount.isGreaterThanZero()) { + installment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); + transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment, + repaidAmount, zeroMoney, zeroMoney, zeroMoney)); + } + } + } + + loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); + } + } else { + super.handleCreditBalanceRefund(loanTransaction, currency, installments, overpaymentHolder); + } } private boolean hasNoCustomCreditAllocationRule(LoanTransaction loanTransaction) { @@ -556,11 +656,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep // handle charge back before or on last installments due date if (chargebackAllocation.get(PRINCIPAL).isGreaterThanZero()) { - emiCalculator.chargebackPrincipal(model, loanTransaction.getTransactionDate(), chargebackAllocation.get(PRINCIPAL)); + emiCalculator.creditPrincipal(model, loanTransaction.getTransactionDate(), chargebackAllocation.get(PRINCIPAL)); } if (chargebackAllocation.get(INTEREST).isGreaterThanZero()) { - emiCalculator.chargebackInterest(model, loanTransaction.getTransactionDate(), chargebackAllocation.get(INTEREST)); + emiCalculator.creditInterest(model, loanTransaction.getTransactionDate(), chargebackAllocation.get(INTEREST)); } // update repayment periods until maturity date, for principal and interest portions @@ -2171,8 +2271,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep if (installment != null) { installment.updatePrincipal(rm.getDuePrincipal().getAmount()); installment.updateInterestCharged(rm.getDueInterest().getAmount()); - installment.setCreditedInterest(rm.getChargebackInterest().getAmount()); - installment.setCreditedPrincipal(rm.getChargebackPrincipal().getAmount()); + installment.setCreditedInterest(rm.getCreditedInterest().getAmount()); + installment.setCreditedPrincipal(rm.getCreditedPrincipal().getAmount()); installment.updateObligationsMet(ctx.getCurrency(), loanTransaction.getTransactionDate()); } }); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java index 24487c499..44aa869d2 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java @@ -93,17 +93,16 @@ public interface EMICalculator { Money principalAmount); /** - * This method used for charge back principal portion. This method increases the outstanding balance. This method - * creates a calculated "virtual" EMI for the applied period. + * This method used for credit principal portion. This method increases the outstanding balance. This method creates + * a calculated "virtual" EMI for the applied period. */ - void chargebackPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, - Money chargebackPrincipalAmount); + void creditPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount); /** - * This method used for charge back interest portion. This method adds extra interest due. This method creates a + * This method used for credit interest portion. This method adds extra interest due. This method creates a * calculated "virtual" EMI for the applied period. */ - void chargebackInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money chargebackInterestAmount); + void creditInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedInterestAmount); /** * This method gives back the maximum of the due principal and maximum of the due interest for a requested day. 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 35c1b5c47..0a5d5d991 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 @@ -193,31 +193,30 @@ public final class ProgressiveEMICalculator implements EMICalculator { repaymentPeriod.ifPresent(rp -> { // If any period total paid > calculated EMI, then set EMI to total paid -> effectively it is marked as // fully paid - if (transactionDateIsBefore && rp.getTotalPaidAmount().isGreaterThan(rp.getEmiPlusChargeback())) { - rp.setEmi(rp.getTotalPaidAmount().minus(rp.getTotalChargebackAmount())); + if (transactionDateIsBefore && rp.getTotalPaidAmount().isGreaterThan(rp.getEmiPlusCreditedAmounts())) { + rp.setEmi(rp.getTotalPaidAmount().minus(rp.getTotalCreditedAmount())); } else if (transactionDateIsBefore - && rp.getTotalPaidAmount().isEqualTo(rp.getOriginalEmi().add(rp.getTotalChargebackAmount()))) { - rp.setEmi(rp.getTotalPaidAmount().minus(rp.getTotalChargebackAmount())); + && rp.getTotalPaidAmount().isEqualTo(rp.getOriginalEmi().add(rp.getTotalCreditedAmount()))) { + rp.setEmi(rp.getTotalPaidAmount().minus(rp.getTotalCreditedAmount())); } calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); }); } } - private void addChargebackAmountsToInterestPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, - Money chargebackPrincipalAmount, Money chargeBackInterestAmount) { - scheduleModel.repaymentPeriods().stream().filter(checkRepaymentPeriodIsInChargebackRange(scheduleModel, transactionDate)) - .findFirst() + private void addCreditedAmountsToInterestPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, + Money creditedPrincipalAmount, Money creditedInterestAmount) { + scheduleModel.repaymentPeriods().stream().filter(checkRepaymentPeriodIsInCreditRange(scheduleModel, transactionDate)).findFirst() .flatMap(repaymentPeriod -> repaymentPeriod.getInterestPeriods().stream() .filter(interestPeriod -> interestPeriod.getFromDate().equals(transactionDate)).reduce((v1, v2) -> v2)) .ifPresent(interestPeriod -> { - interestPeriod.addChargebackPrincipalAmount(chargebackPrincipalAmount); - interestPeriod.addChargebackInterestAmount(chargeBackInterestAmount); + interestPeriod.addCreditedPrincipalAmount(creditedPrincipalAmount); + interestPeriod.addCreditedInterestAmount(creditedInterestAmount); }); } @Nonnull - private static Predicate checkRepaymentPeriodIsInChargebackRange(ProgressiveLoanInterestScheduleModel scheduleModel, + private static Predicate checkRepaymentPeriodIsInCreditRange(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate) { return repaymentPeriod -> scheduleModel.isLastRepaymentPeriod(repaymentPeriod) ? !transactionDate.isBefore(repaymentPeriod.getFromDate()) && !transactionDate.isAfter(repaymentPeriod.getDueDate()) @@ -225,23 +224,22 @@ public final class ProgressiveEMICalculator implements EMICalculator { } @Override - public void chargebackPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, - Money chargebackPrincipalAmount) { - addChargeback(scheduleModel, transactionDate, chargebackPrincipalAmount, scheduleModel.zero()); + public void creditPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, + Money creditedPrincipalAmount) { + addCredit(scheduleModel, transactionDate, creditedPrincipalAmount, scheduleModel.zero()); } @Override - public void chargebackInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, - Money chargebackInterestAmount) { - addChargeback(scheduleModel, transactionDate, scheduleModel.zero(), chargebackInterestAmount); + public void creditInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, + Money creditedInterestAmount) { + addCredit(scheduleModel, transactionDate, scheduleModel.zero(), creditedInterestAmount); } - private void addChargeback(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, - Money chargebackPrincipalAmount, Money chargebackInterestAmount) { - scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), chargebackPrincipalAmount) + private void addCredit(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount, + Money creditedInterestAmount) { + scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount) .ifPresent(repaymentPeriod -> { - addChargebackAmountsToInterestPeriod(scheduleModel, transactionDate, chargebackPrincipalAmount, - chargebackInterestAmount); + addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount); calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); calculateOutstandingBalance(scheduleModel); calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); @@ -288,12 +286,12 @@ public final class ProgressiveEMICalculator implements EMICalculator { @Override @NotNull public Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, - @NotNull LocalDate targetDate, boolean includeChargebackInterest) { + @NotNull LocalDate targetDate, boolean includeCreditedInterest) { ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel, targetDate); RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow(); - return includeChargebackInterest ? repaymentPeriod.getCalculatedDueInterest() - : repaymentPeriod.getCalculatedDueInterest().minus(repaymentPeriod.getChargebackInterest(), + return includeCreditedInterest ? repaymentPeriod.getCalculatedDueInterest() + : repaymentPeriod.getCalculatedDueInterest().minus(repaymentPeriod.getCreditedInterest(), recalculatedScheduleModelTillDate.mc()); } @@ -379,18 +377,18 @@ public final class ProgressiveEMICalculator implements EMICalculator { .size() > nextIdx; if (thereIsInterestPeriodFromDateOnTargetDate) { // NOTE: If there is a next interest period with fromDate on the target date - // then the related chargeback amounts comes from the next interest period too. + // then the related credited amount comes from the next interest period too. InterestPeriod nextInterestPeriod = ip.getRepaymentPeriod().getInterestPeriods().get(nextIdx); - ip.addChargebackPrincipalAmount(nextInterestPeriod.getChargebackPrincipal()); - ip.addChargebackInterestAmount(nextInterestPeriod.getChargebackInterest()); + ip.addCreditedPrincipalAmount(nextInterestPeriod.getCreditedPrincipal()); + ip.addCreditedInterestAmount(nextInterestPeriod.getCreditedInterest()); } ip.getRepaymentPeriod().getInterestPeriods() .subList(nextIdx, ip.getRepaymentPeriod().getInterestPeriods().size()).clear(); }); } else if (rp.getPrevious().isPresent() && rp.getPrevious().get().equals(repaymentPeriod) - && (rp.getInterestPeriods().get(0).getChargebackInterest().isGreaterThanZero() - || rp.getInterestPeriods().get(0).getChargebackPrincipal().isGreaterThanZero())) { - // NOTE: we need to check whether there is chargeback on the 1st interest period of the next + && (rp.getInterestPeriods().get(0).getCreditedInterest().isGreaterThanZero() + || rp.getInterestPeriods().get(0).getCreditedPrincipal().isGreaterThanZero())) { + // NOTE: we need to check whether there is credited on the 1st interest period of the next // period // if so, we need to retain that interest period, but need to update due date to match with from // date -> 0 interest @@ -438,12 +436,12 @@ public final class ProgressiveEMICalculator implements EMICalculator { MathContext mc = scheduleModel.mc(); Money totalDueInterest = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getDueInterest).reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 1.46 - Money totalEMI = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getEmiPlusChargeback).reduce(scheduleModel.zero(), - (m1, m2) -> m1.plus(m2, mc)); // 101.48 + Money totalEMI = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getEmiPlusCreditedAmounts) + .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 101.48 Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream() .flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount)) .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)) // 100 - .plus(scheduleModel.getTotalChargebackPrincipal(), mc); // + .plus(scheduleModel.getTotalCreditedPrincipal(), mc); // Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc); Optional findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()) @@ -451,8 +449,8 @@ public final class ProgressiveEMICalculator implements EMICalculator { findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> { repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff, mc)); if (repaymentPeriod.getEmi() - .isLessThan(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalChargebackAmount(), mc))) { - repaymentPeriod.setEmi(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalChargebackAmount(), mc)); + .isLessThan(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalCreditedAmount(), mc))) { + repaymentPeriod.setEmi(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalCreditedAmount(), mc)); calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); } }); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java index 486312569..436a9d133 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java @@ -53,8 +53,10 @@ public class InterestPeriod implements Comparable { @Setter private BigDecimal rateFactorTillPeriodDueDate; - private Money chargebackPrincipal; - private Money chargebackInterest; + /** Stores credited principals. Related transactions: Chargeback or Credit Balance Refound */ + private Money creditedPrincipal; + /** Stores credited principals. Related transaction: Chargeback */ + private Money creditedInterest; private Money disbursementAmount; private Money balanceCorrectionAmount; @@ -66,8 +68,8 @@ public class InterestPeriod implements Comparable { public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod, MathContext mc) { return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), - interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getChargebackPrincipal(), - interestPeriod.getChargebackInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(), + interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(), + interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(), interestPeriod.getOutstandingLoanBalance(), mc, interestPeriod.isPaused()); } @@ -77,8 +79,8 @@ public class InterestPeriod implements Comparable { public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) { return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), - interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getChargebackPrincipal(), - interestPeriod.getChargebackInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(), + interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(), + interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(), interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), interestPeriod.isPaused()); } @@ -116,17 +118,17 @@ public class InterestPeriod implements Comparable { this.disbursementAmount = MathUtil.plus(this.disbursementAmount, disbursementAmount, mc); } - public void addChargebackPrincipalAmount(final Money chargebackPrincipal) { - this.chargebackPrincipal = MathUtil.plus(this.chargebackPrincipal, chargebackPrincipal, mc); + public void addCreditedPrincipalAmount(final Money creditedPrincipal) { + this.creditedPrincipal = MathUtil.plus(this.creditedPrincipal, creditedPrincipal, mc); } - public void addChargebackInterestAmount(final Money chargebackInterest) { - this.chargebackInterest = MathUtil.plus(this.chargebackInterest, chargebackInterest, mc); + public void addCreditedInterestAmount(final Money creditedInterest) { + this.creditedInterest = MathUtil.plus(this.creditedInterest, creditedInterest, mc); } public BigDecimal getCalculatedDueInterest() { if (isPaused) { - return chargebackInterest.getAmount(); + return creditedInterest.getAmount(); } long lengthTillPeriodDueDate = getLengthTillPeriodDueDate(); @@ -136,7 +138,7 @@ public class InterestPeriod implements Comparable { .multiply(getRateFactorTillPeriodDueDate(), mc) // .divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) // .multiply(BigDecimal.valueOf(getLength()), mc); // - return MathUtil.negativeToZero(MathUtil.add(chargebackInterest.getAmount(), interestDueTillRepaymentDueDate, mc)); + return MathUtil.negativeToZero(MathUtil.add(creditedInterest.getAmount(), interestDueTillRepaymentDueDate, mc)); } public long getLength() { @@ -168,10 +170,10 @@ public class InterestPeriod implements Comparable { } /** - * Include principal like amounts (all disbursement amount + chargeback principal) + * Include principal like amounts (all disbursement amount + credited principal) */ public Money getCreditedAmounts() { - return getDisbursementAmount().plus(getChargebackPrincipal(), mc); + return getDisbursementAmount().plus(getCreditedPrincipal(), mc); } public boolean isFirstInterestPeriod() { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java index e8f655be0..beb3a126d 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java @@ -304,8 +304,7 @@ public class ProgressiveLoanInterestScheduleModel { } /** - * Gives back the total due interest amount in the whole repayment schedule. Also includes chargeback interest - * amount. + * Gives back the total due interest amount in the whole repayment schedule. Also includes credited interest amount. * * @return */ @@ -315,7 +314,7 @@ public class ProgressiveLoanInterestScheduleModel { /** * Gives back the total due principal amount in the whole repayment schedule based on disbursements. Do not contain - * chargeback principal amount. + * credited principal amount. * * @return */ @@ -342,12 +341,12 @@ public class ProgressiveLoanInterestScheduleModel { } /** - * Gives back the total chargeback principal amount in the whole repayment schedule. + * Gives back the total credited principal amount in the whole repayment schedule. * * @return */ - public Money getTotalChargebackPrincipal() { - return MathUtil.negativeToZero(repaymentPeriods().stream().map(RepaymentPeriod::getChargebackPrincipal).reduce(zero, Money::plus), + public Money getTotalCreditedPrincipal() { + return MathUtil.negativeToZero(repaymentPeriods().stream().map(RepaymentPeriod::getCreditedPrincipal).reduce(zero, Money::plus), mc); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java index e3faf4791..2564dc1b9 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java @@ -146,7 +146,7 @@ public final class RepaymentPeriod { } /** - * Gives back calculated due interest + chargeback interest + * Gives back calculated due interest + credited interest * * @return */ @@ -169,28 +169,29 @@ public final class RepaymentPeriod { } /** - * Gives back due interest + chargeback interest OR paid interest + * Gives back due interest + credited interest OR paid interest * * @return */ public Money getDueInterest() { if (dueInterestCalculation == null) { // Due interest might be the maximum paid if there is pay-off or early repayment - dueInterestCalculation = Memo.of( - () -> MathUtil.max(getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest() - : MathUtil.min(getCalculatedDueInterest(), getEmiPlusChargeback(), false), getPaidInterest(), false), - () -> new Object[] { paidPrincipal, paidInterest, interestPeriods }); + dueInterestCalculation = Memo + .of(() -> MathUtil.max( + getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest() + : MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmounts(), false), + getPaidInterest(), false), () -> new Object[] { paidPrincipal, paidInterest, interestPeriods }); } return dueInterestCalculation.get(); } /** - * Gives back an EMI amount which includes chargeback amounts as well + * Gives back an EMI amount which includes credited amounts as well * * @return */ - public Money getEmiPlusChargeback() { - return getEmi().plus(getTotalChargebackAmount(), mc); // + public Money getEmiPlusCreditedAmounts() { + return getEmi().plus(getTotalCreditedAmount(), mc); // } /** @@ -199,48 +200,49 @@ public final class RepaymentPeriod { * @return */ public Money getCalculatedDuePrincipal() { - return MathUtil.negativeToZero(getEmiPlusChargeback().minus(getCalculatedDueInterest(), mc), mc); + return MathUtil.negativeToZero(getEmiPlusCreditedAmounts().minus(getCalculatedDueInterest(), mc), mc); } /** - * Sum of chargeback principals + * Sum of credited principals * * @return */ - public Money getChargebackPrincipal() { + public Money getCreditedPrincipal() { return MathUtil.negativeToZero(interestPeriods.stream() // - .map(InterestPeriod::getChargebackPrincipal) // + .map(InterestPeriod::getCreditedPrincipal) // .reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); // } /** - * Sum of chargeback interests + * Sum of credited interests * * @return */ - public Money getChargebackInterest() { + public Money getCreditedInterest() { return MathUtil.negativeToZero(interestPeriods.stream() // - .map(InterestPeriod::getChargebackInterest) // + .map(InterestPeriod::getCreditedInterest) // .reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); // } /** - * Gives back due principal + chargeback principal or paid principal + * Gives back due principal + credited principal or paid principal * * @return */ public Money getDuePrincipal() { // Due principal might be the maximum paid if there is pay-off or early repayment - return MathUtil.max(MathUtil.negativeToZero(getEmiPlusChargeback().minus(getDueInterest(), mc), mc), getPaidPrincipal(), false); + return MathUtil.max(MathUtil.negativeToZero(getEmiPlusCreditedAmounts().minus(getDueInterest(), mc), mc), getPaidPrincipal(), + false); } /** - * Gives back sum of all chargeback principal + chargeback interest + * Gives back sum of all credited principal + credited interest * * @return */ - public Money getTotalChargebackAmount() { - return getChargebackPrincipal().plus(getChargebackInterest(), mc); + public Money getTotalCreditedAmount() { + return getCreditedPrincipal().plus(getCreditedInterest(), mc); } /** @@ -253,7 +255,7 @@ public final class RepaymentPeriod { } public boolean isFullyPaid() { - return getEmiPlusChargeback().isEqualTo(getTotalPaidAmount()); + return getEmiPlusCreditedAmounts().isEqualTo(getTotalPaidAmount()); } /** diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 5bb211460..38cca69e3 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -1305,7 +1305,7 @@ class ProgressiveEMICalculatorTest { final LocalDate startDay = LocalDate.of(2024, 1, 1); emiCalculator.payInterest(interestModel, dueDate, startDay.plusDays(3), toMoney(0.56)); - emiCalculator.chargebackInterest(interestModel, startDay.plusDays(3), toMoney(0.0)); + emiCalculator.creditInterest(interestModel, startDay.plusDays(3), toMoney(0.0)); emiCalculator.addBalanceCorrection(interestModel, startDay.plusDays(3), toMoney(0.0)); checkDailyInterest(interestModel, dueDate, startDay, 1, 0.19, 0.19); @@ -2403,7 +2403,7 @@ class ProgressiveEMICalculatorTest { emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(0.49)); // full chargeback on duedate - emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(17.01)); + emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(17.01)); checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.5833333333, 0.58, 16.43, 83.57); @@ -2456,7 +2456,7 @@ class ProgressiveEMICalculatorTest { emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(0.49)); // partial chargeback - emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(15.0)); + emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(15.0)); checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.5833333333, 0.58, 16.43, 83.57); @@ -2467,7 +2467,7 @@ class ProgressiveEMICalculatorTest { checkPeriod(interestSchedule, 5, 0, 17.09, 0.005833333333, 0.0991083333276, 0.1, 16.99, 0.0); // full chargeback - emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(17.01)); + emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(17.01)); checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.5833333333, 0.58, 16.43, 83.57); @@ -2520,7 +2520,7 @@ class ProgressiveEMICalculatorTest { emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(0.49)); // full chargeback on duedate - emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 15), toMoney(17.01)); + emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 3, 15), toMoney(17.01)); checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.5833333333, 0.58, 16.43, 83.57); @@ -2579,8 +2579,8 @@ class ProgressiveEMICalculatorTest { // chargeback txnDate = LocalDate.of(2024, 3, 1); - emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52)); - emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49)); + emiCalculator.creditPrincipal(interestSchedule, txnDate, toMoney(16.52)); + emiCalculator.creditInterest(interestSchedule, txnDate, toMoney(0.49)); checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.5833333333, 0.58, 16.43, 83.57); @@ -2637,8 +2637,8 @@ class ProgressiveEMICalculatorTest { // chargeback txnDate = LocalDate.of(2024, 7, 1); - emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52)); - emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49)); + emiCalculator.creditPrincipal(interestSchedule, txnDate, toMoney(16.52)); + emiCalculator.creditInterest(interestSchedule, txnDate, toMoney(0.49)); checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.0985833333276, 0.59, 33.42, 0.0); } @@ -2689,8 +2689,8 @@ class ProgressiveEMICalculatorTest { // chargeback txnDate = LocalDate.of(2024, 3, 1); - emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(14.51)); - emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49)); + emiCalculator.creditPrincipal(interestSchedule, txnDate, toMoney(14.51)); + emiCalculator.creditInterest(interestSchedule, txnDate, toMoney(0.49)); checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.5833333333, 0.58, 16.43, 83.57); @@ -2747,7 +2747,7 @@ class ProgressiveEMICalculatorTest { // chargeback txnDate = LocalDate.of(2024, 3, 1); - emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(15.0)); + emiCalculator.creditPrincipal(interestSchedule, txnDate, toMoney(15.0)); checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.5833333333, 0.58, 16.43, 83.57); @@ -2804,10 +2804,10 @@ class ProgressiveEMICalculatorTest { // chargeback 1st txnDate = LocalDate.of(2024, 3, 1); - emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(15.0)); + emiCalculator.creditPrincipal(interestSchedule, txnDate, toMoney(15.0)); // chargeback 2nd - emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52)); - emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49)); + emiCalculator.creditPrincipal(interestSchedule, txnDate, toMoney(16.52)); + emiCalculator.creditInterest(interestSchedule, txnDate, toMoney(0.49)); checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.5833333333, 0.58, 16.43, 83.57); @@ -3098,8 +3098,8 @@ class ProgressiveEMICalculatorTest { // chargeback txnDate = LocalDate.of(2024, 3, 15); - emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52)); - emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49)); + emiCalculator.creditPrincipal(interestSchedule, txnDate, toMoney(16.52)); + emiCalculator.creditInterest(interestSchedule, txnDate, toMoney(0.49)); PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 4, 1), txnDate); Assertions.assertEquals(33.09, toDouble(interestSchedule.repaymentPeriods().get(2).getDuePrincipal())); @@ -3172,10 +3172,10 @@ class ProgressiveEMICalculatorTest { // chargeback txnDate = LocalDate.of(2024, 3, 15); - emiCalculator.chargebackPrincipal(interestScheduleExpected, txnDate, toMoney(16.52)); - emiCalculator.chargebackInterest(interestScheduleExpected, txnDate, toMoney(0.49)); - emiCalculator.chargebackPrincipal(interestScheduleActual, txnDate, toMoney(16.52)); - emiCalculator.chargebackInterest(interestScheduleActual, txnDate, toMoney(0.49)); + emiCalculator.creditPrincipal(interestScheduleExpected, txnDate, toMoney(16.52)); + emiCalculator.creditInterest(interestScheduleExpected, txnDate, toMoney(0.49)); + emiCalculator.creditPrincipal(interestScheduleActual, txnDate, toMoney(16.52)); + emiCalculator.creditInterest(interestScheduleActual, txnDate, toMoney(0.49)); verifyAllPeriods(interestScheduleExpected, interestScheduleActual); interestScheduleActual = copyJson(interestScheduleExpected); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanCreditBalanceRefundTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanCreditBalanceRefundTest.java new file mode 100644 index 000000000..6e4ea3bb5 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanCreditBalanceRefundTest.java @@ -0,0 +1,324 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import java.math.BigDecimal; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.junit.jupiter.api.Test; + +public class ProgressiveLoanCreditBalanceRefundTest extends BaseLoanIntegrationTest { + + Long clientId = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + Long loanProductId = loanProductHelper.createLoanProduct(create4IProgressive()).getResourceId(); + + @Test + public void testAccrualCreationAfterCBRThenReverseRepayment() { + AtomicReference loanIdRef = new AtomicReference<>(); + AtomicReference reverseRepaymentIdRef = new AtomicReference<>(); + runAt("13 February 2021", () -> { + loanIdRef.set(applyAndApproveProgressiveLoan(clientId, loanProductId, "13 February 2021", 300.0, 37.56, 12, null)); + + disburseLoan(loanIdRef.get(), BigDecimal.valueOf(300.0), "13 February 2021"); + + verifyRepaymentSchedule(loanIdRef.get(), // + installment(300.0, null, "13 February 2021"), // + installment(20.98, 9.39, 30.37, false, "13 March 2021"), // + installment(21.64, 8.73, 30.37, false, "13 April 2021"), // + installment(22.31, 8.06, 30.37, false, "13 May 2021"), // + installment(23.01, 7.36, 30.37, false, "13 June 2021"), // + installment(23.73, 6.64, 30.37, false, "13 July 2021"), // + installment(24.48, 5.89, 30.37, false, "13 August 2021"), // + installment(25.24, 5.13, 30.37, false, "13 September 2021"), // + installment(26.03, 4.34, 30.37, false, "13 October 2021"), // + installment(26.85, 3.52, 30.37, false, "13 November 2021"), // + installment(27.69, 2.68, 30.37, false, "13 December 2021"), // + installment(28.55, 1.82, 30.37, false, "13 January 2022"), // + installment(29.49, 0.92, 30.41, false, "13 February 2022") // + ); + + loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "13 February 2021", 60.0); + Long repaymentId = loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "13 February 2021", 40.0) + .getResourceId(); + reverseRepaymentIdRef.set(repaymentId); + loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "MerchantIssuedRefund", "13 February 2021", 300.0); + + verifyRepaymentSchedule(loanIdRef.get(), // + installment(300.0, null, "13 February 2021"), // + installment(30.37, 0.0, 0.0, true, "13 March 2021"), // + installment(30.37, 0.0, 0.0, true, "13 April 2021"), // + installment(30.37, 0.0, 0.0, true, "13 May 2021"), // + installment(30.37, 0.0, 0.0, true, "13 June 2021"), // + installment(30.37, 0.0, 0.0, true, "13 July 2021"), // + installment(30.37, 0.0, 0.0, true, "13 August 2021"), // + installment(30.37, 0.0, 0.0, true, "13 September 2021"), // + installment(30.37, 0.0, 0.0, true, "13 October 2021"), // + installment(30.37, 0.0, 0.0, true, "13 November 2021"), // + installment(26.67, 0.0, 0.0, true, "13 December 2021"), // + installment(0.0, 0.0, 0.0, true, "13 January 2022"), // + installment(0.0, 0.0, 0.0, true, "13 February 2022") // + ); + verifyTransactions(loanIdRef.get(), // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(60.0, "Repayment", "13 February 2021", 240.0, 60.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(40.0, "Repayment", "13 February 2021", 200.0, 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 100.0, false) // + ); + }); + + runAt("19 February 2021", () -> { + final Long loanId = loanIdRef.get(); + inlineLoanCOBHelper.executeInlineCOB(loanId); + loanTransactionHelper.makeLoanRepayment(loanId, "CreditBalanceRefund", "19 February 2021", 100.0); + // 0 overpaid + verifyRepaymentSchedule(loanId, // + installment(300.0, null, "13 February 2021"), // + installment(30.37, 0.0, 0.0, true, "13 March 2021"), // + installment(30.37, 0.0, 0.0, true, "13 April 2021"), // + installment(30.37, 0.0, 0.0, true, "13 May 2021"), // + installment(30.37, 0.0, 0.0, true, "13 June 2021"), // + installment(30.37, 0.0, 0.0, true, "13 July 2021"), // + installment(30.37, 0.0, 0.0, true, "13 August 2021"), // + installment(30.37, 0.0, 0.0, true, "13 September 2021"), // + installment(30.37, 0.0, 0.0, true, "13 October 2021"), // + installment(30.37, 0.0, 0.0, true, "13 November 2021"), // + installment(26.67, 0.0, 0.0, true, "13 December 2021"), // + installment(0.0, 0.0, 0.0, true, "13 January 2022"), // + installment(0.0, 0.0, 0.0, true, "13 February 2022") // + ); + verifyTransactions(loanId, // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(60.0, "Repayment", "13 February 2021", 240.0, 60.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(40.0, "Repayment", "13 February 2021", 200.0, 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 100.0, false), // + transaction(100.0, "Credit Balance Refund", "19 February 2021", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 100.0, false) // + ); + }); + runAt("23 February 2021", () -> { + final Long loanId = loanIdRef.get(); + final Long reverseRepaymentId = reverseRepaymentIdRef.get(); + inlineLoanCOBHelper.executeInlineCOB(loanId); + loanTransactionHelper.reverseLoanTransaction(loanId, reverseRepaymentId, "23 February 2021"); + // 40 outstanding + verifyRepaymentSchedule(loanId, // + installment(300.0, null, "13 February 2021"), // + installment(130.37, 0.98, 40.98, false, "13 March 2021"), // + installment(30.37, 0.0, 0.0, true, "13 April 2021"), // + installment(30.37, 0.0, 0.0, true, "13 May 2021"), // + installment(30.37, 0.0, 0.0, true, "13 June 2021"), // + installment(30.37, 0.0, 0.0, true, "13 July 2021"), // + installment(30.37, 0.0, 0.0, true, "13 August 2021"), // + installment(30.37, 0.0, 0.0, true, "13 September 2021"), // + installment(30.37, 0.0, 0.0, true, "13 October 2021"), // + installment(30.37, 0.0, 0.0, true, "13 November 2021"), // + installment(26.67, 0.0, 0.0, true, "13 December 2021"), // + installment(0.0, 0.0, 0.0, true, "13 January 2022"), // + installment(0.0, 0.0, 0.0, true, "13 February 2022") // + ); + + verifyTransactions(loanId, // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(60.0, "Repayment", "13 February 2021", 240.0, 60.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(40.0, "Repayment", "13 February 2021", 200.0, 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 240.0, 0.0, 0.0, 0.0, 0.0, 60.0, false), // + transaction(100.0, "Credit Balance Refund", "19 February 2021", 40.0, 40.0, 0.0, 0.0, 0.0, 0.0, 60.0, false) // + ); + }); + runAt("24 February 2021", () -> { + final Long loanId = loanIdRef.get(); + inlineLoanCOBHelper.executeInlineCOB(loanId); + verifyTransactions(loanId, // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(60.0, "Repayment", "13 February 2021", 240.0, 60.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(40.0, "Repayment", "13 February 2021", 200.0, 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 240.0, 0.0, 0.0, 0.0, 0.0, 60.0, false), // + transaction(100.0, "Credit Balance Refund", "19 February 2021", 40.0, 40.0, 0.0, 0.0, 0.0, 0.0, 60.0, false), // + transaction(0.18, "Accrual", "23 February 2021", 0.0, 0.0, 0.18, 0.0, 0.0, 0.0, 0.0, false) // + ); + }); + runAt("28 February 2021", () -> { + final Long loanId = loanIdRef.get(); + inlineLoanCOBHelper.executeInlineCOB(loanId); + verifyTransactions(loanId, // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(60.0, "Repayment", "13 February 2021", 240.0, 60.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(40.0, "Repayment", "13 February 2021", 200.0, 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 240.0, 0.0, 0.0, 0.0, 0.0, 60.0, false), // + transaction(100.0, "Credit Balance Refund", "19 February 2021", 40.0, 40.0, 0.0, 0.0, 0.0, 0.0, 60.0, false), // + transaction(0.18, "Accrual", "23 February 2021", 0.0, 0.0, 0.18, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.04, "Accrual", "24 February 2021", 0.0, 0.0, 0.04, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.05, "Accrual", "25 February 2021", 0.0, 0.0, 0.05, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.04, "Accrual", "26 February 2021", 0.0, 0.0, 0.04, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.05, "Accrual", "27 February 2021", 0.0, 0.0, 0.05, 0.0, 0.0, 0.0, 0.0, false) // + ); + }); + } + + @Test + public void testAccrualCreationAfterCBRThenReverseRepaymentThenRepayment() { + AtomicReference loanIdRef = new AtomicReference<>(); + AtomicReference reverseRepaymentIdRef = new AtomicReference<>(); + runAt("13 February 2021", () -> { + Long loanId = applyAndApproveProgressiveLoan(clientId, loanProductId, "13 February 2021", 300.0, 37.56, 12, null); + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(300.0), "13 February 2021"); + + verifyRepaymentSchedule(loanId, // + installment(300.0, null, "13 February 2021"), // + installment(20.98, 9.39, 30.37, false, "13 March 2021"), // + installment(21.64, 8.73, 30.37, false, "13 April 2021"), // + installment(22.31, 8.06, 30.37, false, "13 May 2021"), // + installment(23.01, 7.36, 30.37, false, "13 June 2021"), // + installment(23.73, 6.64, 30.37, false, "13 July 2021"), // + installment(24.48, 5.89, 30.37, false, "13 August 2021"), // + installment(25.24, 5.13, 30.37, false, "13 September 2021"), // + installment(26.03, 4.34, 30.37, false, "13 October 2021"), // + installment(26.85, 3.52, 30.37, false, "13 November 2021"), // + installment(27.69, 2.68, 30.37, false, "13 December 2021"), // + installment(28.55, 1.82, 30.37, false, "13 January 2022"), // + installment(29.49, 0.92, 30.41, false, "13 February 2022") // + ); + Long resourceId = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "13 February 2021", 100.0).getResourceId(); + reverseRepaymentIdRef.set(resourceId); + + loanTransactionHelper.makeLoanRepayment(loanId, "MerchantIssuedRefund", "13 February 2021", 300.0); + + verifyRepaymentSchedule(loanId, // + installment(300.0, null, "13 February 2021"), // + installment(30.37, 0.0, 0.0, true, "13 March 2021"), // + installment(30.37, 0.0, 0.0, true, "13 April 2021"), // + installment(30.37, 0.0, 0.0, true, "13 May 2021"), // + installment(30.37, 0.0, 0.0, true, "13 June 2021"), // + installment(30.37, 0.0, 0.0, true, "13 July 2021"), // + installment(30.37, 0.0, 0.0, true, "13 August 2021"), // + installment(30.37, 0.0, 0.0, true, "13 September 2021"), // + installment(30.37, 0.0, 0.0, true, "13 October 2021"), // + installment(30.37, 0.0, 0.0, true, "13 November 2021"), // + installment(26.67, 0.0, 0.0, true, "13 December 2021"), // + installment(0.0, 0.0, 0.0, true, "13 January 2022"), // + installment(0.0, 0.0, 0.0, true, "13 February 2022") // + ); + verifyTransactions(loanId, // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(100.0, "Repayment", "13 February 2021", 200.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 100.0, false) // + ); + + }); + runAt("19 February 2021", () -> { + final Long loanId = loanIdRef.get(); + inlineLoanCOBHelper.executeInlineCOB(loanId); + loanTransactionHelper.makeLoanRepayment(loanId, "CreditBalanceRefund", "19 February 2021", 100.0); + verifyRepaymentSchedule(loanId, // + installment(300.0, null, "13 February 2021"), // + installment(30.37, 0.0, 0.0, true, "13 March 2021"), // + installment(30.37, 0.0, 0.0, true, "13 April 2021"), // + installment(30.37, 0.0, 0.0, true, "13 May 2021"), // + installment(30.37, 0.0, 0.0, true, "13 June 2021"), // + installment(30.37, 0.0, 0.0, true, "13 July 2021"), // + installment(30.37, 0.0, 0.0, true, "13 August 2021"), // + installment(30.37, 0.0, 0.0, true, "13 September 2021"), // + installment(30.37, 0.0, 0.0, true, "13 October 2021"), // + installment(30.37, 0.0, 0.0, true, "13 November 2021"), // + installment(26.67, 0.0, 0.0, true, "13 December 2021"), // + installment(0.0, 0.0, 0.0, true, "13 January 2022"), // + installment(0.0, 0.0, 0.0, true, "13 February 2022") // + ); + verifyTransactions(loanId, // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(100.0, "Repayment", "13 February 2021", 200.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 100.0, false), // + transaction(100.0, "Credit Balance Refund", "19 February 2021", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 100.0, false) // + ); + }); + runAt("23 February 2021", () -> { + final Long loanId = loanIdRef.get(); + inlineLoanCOBHelper.executeInlineCOB(loanId); + + final Long reverseRepaymentId = reverseRepaymentIdRef.get(); + loanTransactionHelper.reverseLoanTransaction(loanId, reverseRepaymentId, "23 February 2021"); + verifyRepaymentSchedule(loanId, // + installment(300.0, null, "13 February 2021"), // + installment(130.37, 2.46, 102.46, false, "13 March 2021"), // + installment(30.37, 0.0, 0.0, true, "13 April 2021"), // + installment(30.37, 0.0, 0.0, true, "13 May 2021"), // + installment(30.37, 0.0, 0.0, true, "13 June 2021"), // + installment(30.37, 0.0, 0.0, true, "13 July 2021"), // + installment(30.37, 0.0, 0.0, true, "13 August 2021"), // + installment(30.37, 0.0, 0.0, true, "13 September 2021"), // + installment(30.37, 0.0, 0.0, true, "13 October 2021"), // + installment(30.37, 0.0, 0.0, true, "13 November 2021"), // + installment(26.67, 0.0, 0.0, true, "13 December 2021"), // + installment(0.0, 0.0, 0.0, true, "13 January 2022"), // + installment(0.0, 0.0, 0.0, true, "13 February 2022") // + ); + verifyTransactions(loanId, // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(100.0, "Repayment", "13 February 2021", 200.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(100.0, "Credit Balance Refund", "19 February 2021", 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, false) // + ); + }); + runAt("24 February 2021", () -> { + final Long loanId = loanIdRef.get(); + inlineLoanCOBHelper.executeInlineCOB(loanId); + verifyTransactions(loanId, // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(100.0, "Repayment", "13 February 2021", 200.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(100.0, "Credit Balance Refund", "19 February 2021", 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.45, "Accrual", "23 February 2021", 0.0, 0.0, 0.45, 0.0, 0.0, 0.0, 0.0, false) // + ); + }); + runAt("28 February 2021", () -> { + final Long loanId = loanIdRef.get(); + inlineLoanCOBHelper.executeInlineCOB(loanId); + loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "28 February 2021", 101.01); + verifyRepaymentSchedule(loanId, // + installment(300.0, null, "13 February 2021"), // + installment(130.37, 1.01, 0.0, true, "13 March 2021"), // + installment(30.37, 0.0, 0.0, true, "13 April 2021"), // + installment(30.37, 0.0, 0.0, true, "13 May 2021"), // + installment(30.37, 0.0, 0.0, true, "13 June 2021"), // + installment(30.37, 0.0, 0.0, true, "13 July 2021"), // + installment(30.37, 0.0, 0.0, true, "13 August 2021"), // + installment(30.37, 0.0, 0.0, true, "13 September 2021"), // + installment(30.37, 0.0, 0.0, true, "13 October 2021"), // + installment(30.37, 0.0, 0.0, true, "13 November 2021"), // + installment(26.67, 0.0, 0.0, true, "13 December 2021"), // + installment(0.0, 0.0, 0.0, true, "13 January 2022"), // + installment(0.0, 0.0, 0.0, true, "13 February 2022") // + ); + verifyTransactions(loanId, // + transaction(300.0, "Disbursement", "13 February 2021", 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(100.0, "Repayment", "13 February 2021", 200.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), // + transaction(300.0, "Merchant Issued Refund", "13 February 2021", 0.0, 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(100.0, "Credit Balance Refund", "19 February 2021", 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.45, "Accrual", "23 February 2021", 0.0, 0.0, 0.45, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.11, "Accrual", "24 February 2021", 0.0, 0.0, 0.11, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.11, "Accrual", "25 February 2021", 0.0, 0.0, 0.11, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.11, "Accrual", "26 February 2021", 0.0, 0.0, 0.11, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.11, "Accrual", "27 February 2021", 0.0, 0.0, 0.11, 0.0, 0.0, 0.0, 0.0, false), // + transaction(101.01, "Repayment", "28 February 2021", 0.0, 100.0, 1.01, 0.0, 0.0, 0.0, 0.0, false), // + transaction(0.12, "Accrual", "28 February 2021", 0.0, 0.0, 0.12, 0.0, 0.0, 0.0, 0.0, false) // + ); + }); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBHelper.java index 6d24df6d0..3513114b0 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBHelper.java @@ -24,6 +24,10 @@ import io.restassured.specification.ResponseSpecification; import java.util.HashMap; import java.util.List; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.InlineJobRequest; +import org.apache.fineract.client.models.InlineJobResponse; +import org.apache.fineract.client.util.Calls; +import org.apache.fineract.integrationtests.common.FineractClientHelper; import org.apache.fineract.integrationtests.common.Utils; @Slf4j @@ -52,6 +56,11 @@ public class InlineLoanCOBHelper { return Utils.performServerPost(requestSpec, responseSpec, EXECUTE_INLINE_COB_API, buildInlineCOBRequest(loanIds)); } + public InlineJobResponse executeInlineCOB(Long loanId) { + return Calls.ok(FineractClientHelper.getFineractClient().inlineJobApi.executeInlineJob("LOAN_COB", + new InlineJobRequest().addLoanIdsItem(loanId))); + } + // TODO: Rewrite to use fineract-client instead! // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest)