From 885e7b0ab019498972e05b7f2d759563ad101668 Mon Sep 17 00:00:00 2001 From: Kristof Jozsa Date: Wed, 28 Aug 2024 13:55:10 +0200 Subject: [PATCH] FINERACT-1981: pay off schedule handling --- .../data/OutstandingAmountsDTO.java | 12 +- .../LoanRepaymentScheduleInstallment.java | 6 + ...edPaymentScheduleTransactionProcessor.java | 325 +++++++++++------- .../ProgressiveLoanScheduleGenerator.java | 86 ++--- .../loanproduct/calc/EMICalculator.java | 1 + .../calc/ProgressiveEMICalculator.java | 2 + .../ProgressiveLoanScheduleGeneratorTest.java | 2 +- 7 files changed, 257 insertions(+), 177 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java index aa1351880b3..265cc6a34fd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java @@ -46,20 +46,24 @@ public Money getTotalOutstanding() { .plus(penaltyCharges()); } - public void plusPrincipal(Money principal) { + public OutstandingAmountsDTO plusPrincipal(Money principal) { this.principal = this.principal.plus(principal); + return this; } - public void plusInterest(Money interest) { + public OutstandingAmountsDTO plusInterest(Money interest) { this.interest = this.interest.plus(interest); + return this; } - public void plusFeeCharges(Money feeCharges) { + public OutstandingAmountsDTO plusFeeCharges(Money feeCharges) { this.feeCharges = this.feeCharges.plus(feeCharges); + return this; } - public void plusPenaltyCharges(Money penaltyCharges) { + public OutstandingAmountsDTO plusPenaltyCharges(Money penaltyCharges) { this.penaltyCharges = this.penaltyCharges.plus(penaltyCharges); + return this; } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 34d8baf6b5c..85091c5ff4d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; + import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -488,6 +490,10 @@ public void resetChargesCharged() { this.penaltyCharges = null; } + public boolean isCurrentInstallment(LocalDate transactionDate) { + return getFromDate().isBefore(transactionDate) && !getDueDate().isBefore(transactionDate); + } + public interface PaymentFunction { Money accept(LocalDate transactionDate, Money transactionAmountRemaining); 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 a9b5773b94e..6e62f93f20e 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 @@ -18,36 +18,6 @@ */ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; -import static java.math.BigDecimal.ZERO; -import static java.util.stream.Collectors.mapping; -import static java.util.stream.Collectors.toList; -import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK; -import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE; -import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST; -import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; -import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL; -import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -55,6 +25,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -77,8 +48,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ProgressiveLoanScheduleGenerator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; @@ -89,6 +62,37 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.math.BigDecimal.ZERO; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL; +import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT; + @Slf4j @RequiredArgsConstructor public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { @@ -111,44 +115,45 @@ public String getName() { @Override protected Money handleTransactionThatIsALateRepaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment, - List installments, LoanTransaction loanTransaction, Money transactionAmountUnprocessed, - List transactionMappings, Set charges) { + List installments, LoanTransaction loanTransaction, Money transactionAmountUnprocessed, + List transactionMappings, Set charges) { throw new NotImplementedException(); } @Override protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(LoanRepaymentScheduleInstallment currentInstallment, - List installments, LoanTransaction loanTransaction, Money paymentInAdvance, - List transactionMappings, Set charges) { + List installments, LoanTransaction loanTransaction, Money paymentInAdvance, + List transactionMappings, Set charges) { throw new NotImplementedException(); } @Override protected Money handleTransactionThatIsOnTimePaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment, - LoanTransaction loanTransaction, Money transactionAmountUnprocessed, - List transactionMappings, Set charges) { + LoanTransaction loanTransaction, Money transactionAmountUnprocessed, + List transactionMappings, Set charges) { throw new NotImplementedException(); } @Override protected Money handleRefundTransactionPaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment, - LoanTransaction loanTransaction, Money transactionAmountUnprocessed, - List transactionMappings) { + LoanTransaction loanTransaction, Money transactionAmountUnprocessed, + List transactionMappings) { throw new NotImplementedException(); } @Override public Money handleRepaymentSchedule(List transactionsPostDisbursement, MonetaryCurrency currency, - List installments, Set loanCharges) { + List installments, Set loanCharges) { throw new NotImplementedException(); } - @Override - public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, - MonetaryCurrency currency, List installments, Set charges) { + // only for progressive loans + public Pair reprocessProgressiveLoanTransactions( + LocalDate disbursementDate, List loanTransactions, MonetaryCurrency currency, + List installments, Set charges) { final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); if (loanTransactions.isEmpty()) { - return changedTransactionDetail; + return Pair.of(changedTransactionDetail, null); } if (charges != null) { for (final LoanCharge loanCharge : charges) { @@ -185,10 +190,18 @@ public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursement chargeOrTransaction.getLoanCharge() .ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate)); } - List txs = chargeOrTransactions.stream().map(ChargeOrTransaction::getLoanTransaction).filter(Optional::isPresent) + List txs = chargeOrTransactions.stream() // + .map(ChargeOrTransaction::getLoanTransaction) // + .filter(Optional::isPresent) // .map(Optional::get).toList(); reprocessInstallments(disbursementDate, txs, installments, currency); - return changedTransactionDetail; + return Pair.of(changedTransactionDetail, scheduleModel); + } + + @Override + public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, + MonetaryCurrency currency, List installments, Set charges) { + return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft(); } @Override @@ -198,11 +211,9 @@ public void processLatestTransaction(LoanTransaction loanTransaction, Transactio case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments()); case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges()); case CHARGEBACK -> handleChargeback(loanTransaction, ctx); - case CREDIT_BALANCE_REFUND -> - handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder()); + case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder()); case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT, - WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER -> - handleRepayment(loanTransaction, ctx); + WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER -> handleRepayment(loanTransaction, ctx); case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx); case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx); case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed."); @@ -281,7 +292,7 @@ protected LoanTransaction findOriginalTransaction(LoanTransaction loanTransactio } return originalTransaction.get(); } else { // when there is no id, then it might be that the original transaction is changed, so we need to look - // it up from the Ctx. + // it up from the Ctx. Long originalChargebackTransactionId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction); Collection updatedTransactions = ctx.getChangedTransactionDetail().getNewTransactionMappings().values(); Optional updatedTransaction = updatedTransactions.stream().filter(tr -> tr.getLoanTransactionRelations() @@ -382,8 +393,8 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac } private Map adjustOriginalAllocationWithFormerChargebacks(LoanTransaction originalTransaction, - Map originalAllocation, LoanTransaction chargeBackTransaction, TransactionCtx ctx, - LoanCreditAllocationRule chargeBackAllocationRule) { + Map originalAllocation, LoanTransaction chargeBackTransaction, TransactionCtx ctx, + LoanCreditAllocationRule chargeBackAllocationRule) { // these are the list of existing transactions List allTransactions = new ArrayList<>(chargeBackTransaction.getLoan().getLoanTransactions()); @@ -433,7 +444,7 @@ private Comparator loanTransactionDateComparator() { } private void recognizeAmountsAfterChargeback(MonetaryCurrency currency, LocalDate localDate, - LoanRepaymentScheduleInstallment installment, Map chargebackAllocation) { + LoanRepaymentScheduleInstallment installment, Map chargebackAllocation) { Money principal = chargebackAllocation.get(PRINCIPAL); if (principal.isGreaterThanZero()) { installment.addToCreditedPrincipal(principal.getAmount()); @@ -473,7 +484,7 @@ private Map getOriginalAllocation(LoanTransaction origina } protected Map calculateChargebackAllocationMap(Map originalAllocation, - BigDecimal amountToDistribute, List allocationTypes, MonetaryCurrency currency) { + BigDecimal amountToDistribute, List allocationTypes, MonetaryCurrency currency) { BigDecimal remainingAmount = amountToDistribute; Map result = new HashMap<>(); Arrays.stream(AllocationType.values()).forEach(allocationType -> result.put(allocationType, Money.of(currency, BigDecimal.ZERO))); @@ -500,7 +511,7 @@ private Predicate hasMatchingToLoanTransaction(Long id, @Override protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set charges) { + List installments, Set charges) { Money zero = Money.zero(currency); List transactionMappings = new ArrayList<>(); Money transactionAmountUnprocessed = loanTransaction.getAmount(currency); @@ -554,8 +565,8 @@ protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency cu } private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set charges, ChangedTransactionDetail changedTransactionDetail, - MoneyHolder overpaymentHolder, ProgressiveLoanInterestScheduleModel scheduleModel) { + List installments, Set charges, ChangedTransactionDetail changedTransactionDetail, + MoneyHolder overpaymentHolder, ProgressiveLoanInterestScheduleModel scheduleModel) { TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail, scheduleModel); if (loanTransaction.getId() == null) { @@ -595,7 +606,7 @@ private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryC } private void checkAndUpdateReplayedChargebackRelationWithReplayedTransaction(LoanTransaction loanTransaction, - LoanTransaction newLoanTransaction, TransactionCtx ctx) { + LoanTransaction newLoanTransaction, TransactionCtx ctx) { // if chargeback is getting reverse-replayed // find replayed transaction with CHARGEBACK relation with reversed chargeback transaction // for replayed transaction, add relation to point to new Chargeback transaction @@ -607,13 +618,13 @@ private void checkAndUpdateReplayedChargebackRelationWithReplayedTransaction(Loa } private void processSingleCharge(LoanCharge loanCharge, MonetaryCurrency currency, List installments, - LocalDate disbursementDate) { + LocalDate disbursementDate) { loanChargeProcessor.reprocess(currency, disbursementDate, installments, loanCharge); } @NotNull private List createSortedChargesAndTransactionsList(List loanTransactions, - Set charges) { + Set charges) { List chargeOrTransactions = new ArrayList<>(); if (charges != null) { chargeOrTransactions.addAll(charges.stream().map(ChargeOrTransaction::new).toList()); @@ -683,7 +694,7 @@ private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursement disbursementTransaction.resetDerivedComponents(); final MathContext mc = MoneyHelper.getMathContext(); List candidateRepaymentInstallments = transactionCtx.getInstallments().stream().filter( - i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional()) + i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional()) .toList(); int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size(); LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail(); @@ -743,7 +754,7 @@ private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx Balances balances = new Balances(zero, zero, zero, zero); if (LoanScheduleProcessingType.HORIZONTAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { - transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(), + transactionAmountUnprocessed = processPeriodsHorizontally(transactionCtx, loanTransaction, transactionCtx.getCurrency(), transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(), defaultPaymentAllocationRule, transactionMappings, Set.of(), balances); } else if (LoanScheduleProcessingType.VERTICAL @@ -781,9 +792,9 @@ private LoanTransactionToRepaymentScheduleMapping getTransactionMapping( } private Money processPaymentAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment, - LoanTransaction loanTransaction, Money transactionAmountUnprocessed, - LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set chargesOfInstallment, - Balances balances, LoanRepaymentScheduleInstallment.PaymentAction action) { + LoanTransaction loanTransaction, Money transactionAmountUnprocessed, + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set chargesOfInstallment, + Balances balances, LoanRepaymentScheduleInstallment.PaymentAction action) { LocalDate transactionDate = loanTransaction.getTransactionDate(); Money zero = transactionAmountUnprocessed.zero(); @@ -818,7 +829,7 @@ private Money processPaymentAllocation(PaymentAllocationType paymentAllocationTy } private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, - Money principalPortion, Money interestPortion, Money feePortion, Money penaltyPortion) { + Money principalPortion, Money interestPortion, Money feePortion, Money penaltyPortion) { BigDecimal aggregatedPenalty = ObjectUtils .defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPenaltyChargesPortion(), BigDecimal.ZERO) .add(penaltyPortion.getAmount()); @@ -916,12 +927,13 @@ private void handleChargePayment(LoanTransaction loanTransaction, TransactionCtx } private Money refundTransactionHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, - List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, - List transactionMappings, Set charges, Balances balances) { + List installments, Money transactionAmountUnprocessed, + List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, + List transactionMappings, Set charges, Balances balances) { Money zero = Money.zero(currency); Money refundedPortion; - outerLoop: do { + outerLoop: + do { LoanRepaymentScheduleInstallment latestPastDueInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, installments, zero); LoanRepaymentScheduleInstallment dueInstallment = getDueInstallmentForRefund(loanTransaction, currency, installments, zero); @@ -982,6 +994,8 @@ private Money refundTransactionHorizontally(LoanTransaction loanTransaction, Mon break outerLoop; } } + default -> { + } } } } while (installments.stream().anyMatch(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) @@ -990,10 +1004,10 @@ private Money refundTransactionHorizontally(LoanTransaction loanTransaction, Mon } private Money refundTransactionVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money zero, - List transactionMappings, Money transactionAmountUnprocessed, - FutureInstallmentAllocationRule futureInstallmentAllocationRule, Set charges, Balances balances, - PaymentAllocationType paymentAllocationType) { + List installments, Money zero, + List transactionMappings, Money transactionAmountUnprocessed, + FutureInstallmentAllocationRule futureInstallmentAllocationRule, Set charges, Balances balances, + PaymentAllocationType paymentAllocationType) { LoanRepaymentScheduleInstallment currentInstallment = null; Money refundedPortion = zero; int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); @@ -1061,7 +1075,7 @@ private Money refundTransactionVertically(LoanTransaction loanTransaction, Monet @Nullable private static LoanRepaymentScheduleInstallment getDueInstallmentForRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money zero) { + List installments, Money zero) { return installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) .filter(installment -> loanTransaction.isOn(installment.getDueDate())) .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); @@ -1069,7 +1083,7 @@ private static LoanRepaymentScheduleInstallment getDueInstallmentForRefund(LoanT @Nullable private static LoanRepaymentScheduleInstallment getLatestPastDueInstallmentForRefund(LoanTransaction loanTransaction, - MonetaryCurrency currency, List installments, Money zero) { + MonetaryCurrency currency, List installments, Money zero) { return installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) .filter(e -> loanTransaction.isAfter(e.getDueDate())) .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); @@ -1077,8 +1091,8 @@ private static LoanRepaymentScheduleInstallment getLatestPastDueInstallmentForRe @NotNull private static List getFutureInstallmentsForRefund(LoanTransaction loanTransaction, - MonetaryCurrency currency, List installments, - FutureInstallmentAllocationRule futureInstallmentAllocationRule, Money zero) { + MonetaryCurrency currency, List installments, + FutureInstallmentAllocationRule futureInstallmentAllocationRule, Money zero) { List inAdvanceInstallments = new ArrayList<>(); if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { inAdvanceInstallments = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) @@ -1109,7 +1123,7 @@ private void processTransaction(LoanTransaction loanTransaction, TransactionCtx if (LoanScheduleProcessingType.HORIZONTAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { - transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(), + transactionAmountUnprocessed = processPeriodsHorizontally(transactionCtx, loanTransaction, transactionCtx.getCurrency(), transactionCtx.getInstallments(), transactionAmountUnprocessed, paymentAllocationRule, transactionMappings, transactionCtx.getCharges(), balances); } else if (LoanScheduleProcessingType.VERTICAL @@ -1125,26 +1139,26 @@ private void processTransaction(LoanTransaction loanTransaction, TransactionCtx handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder()); } - private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, - LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, - Set charges, Balances balances) { + private Money processPeriodsHorizontally(TransactionCtx transactionCtx, LoanTransaction loanTransaction, MonetaryCurrency currency, + List installments, Money transactionAmountUnprocessed, + LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, + Set charges, Balances balances) { LinkedHashMap> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream() .collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList()))); for (Map.Entry> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { - transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments, + transactionAmountUnprocessed = processAllocationsHorizontally(transactionCtx, loanTransaction, currency, installments, transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, charges, balances); } return transactionAmountUnprocessed; } - private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, - List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, - List transactionMappings, Set charges, Balances balances) { + private Money processAllocationsHorizontally(TransactionCtx transactionCtx, LoanTransaction loanTransaction, MonetaryCurrency currency, + List installments, Money transactionAmountUnprocessed, + List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, + List transactionMappings, Set charges, Balances balances) { Money paidPortion; boolean exit = false; do { @@ -1157,19 +1171,18 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo // For having similar logic we are populating installment list even when the future installment // allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element. - List inAdvanceInstallments = new ArrayList<>(); - if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { - inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + List inAdvanceInstallments = switch (futureInstallmentAllocationRule) { + case REAMORTIZATION -> installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) .filter(e -> loanTransaction.isBefore(e.getDueDate())).toList(); - } else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) { - inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(e -> loanTransaction.isBefore(e.getDueDate())) - .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); - } else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) { - inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(e -> loanTransaction.isBefore(e.getDueDate())) - .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); - } + case NEXT_INSTALLMENT -> // first future unpaid installment + installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + .filter(e -> loanTransaction.isBefore(e.getDueDate())) + .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); + case LAST_INSTALLMENT -> // last future unpaid installment + installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + .filter(e -> loanTransaction.isBefore(e.getDueDate())) + .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); + }; int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); @@ -1195,6 +1208,33 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo firstNormalInstallmentNumber); LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, dueInstallment, currency); + + // recalculate interest, adjust principal for current installment +// if (transactionCtx instanceof ProgressiveTransactionCtx ctx) { // for progressive installment +// ProgressiveLoanInterestScheduleModel model = ctx.getModel(); +// LocalDate transactionDate = switch (loanTransaction.getLoan().getLoanProduct().preCloseInterestCalculationStrategy()) { +// case TILL_PRE_CLOSURE_DATE -> loanTransaction.getTransactionDate(); +// case TILL_REST_FREQUENCY_DATE -> dueInstallment.getDueDate(); +// case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); +// }; +// ProgressiveLoanInterestRepaymentModel payableDetails = emiCalculator.getPayableDetails(model, transactionDate).orElseThrow(); +// switch (paymentAllocationType) { +// case DUE_INTEREST -> { +// BigDecimal moneyToPay = payableDetails.getInterestDue().getAmount(); +// log.info("{} - paying DUE interest: {}", transactionDate, moneyToPay); +//// emiCalculator.addBalanceCorrection(model, transactionDate, Money.of(currency, moneyToPay)); +// dueInstallment.updateInterestCharged(moneyToPay); +// } +// case DUE_PRINCIPAL -> { +// BigDecimal moneyToPay = payableDetails.getPrincipalDue().getAmount(); +// log.info("{} - paying DUE principal: {}", transactionDate, moneyToPay); +//// emiCalculator.addBalanceCorrection(model, transactionDate, Money.of(currency, moneyToPay)); +// dueInstallment.updatePrincipal(moneyToPay); +// } +// default -> {} // only recalculate interest and principal values +// } +// } + paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction, transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); @@ -1205,24 +1245,49 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo } case IN_ADVANCE -> { int numberOfInstallments = inAdvanceInstallments.size(); - if (numberOfInstallments > 0) { - // This will be the same amount as transactionAmountUnprocessed in case of the future - // installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT - Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode()); - // Adjustment might be needed due to the divide operation and the rounding mode - Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); - for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { - Set inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment, + if (numberOfInstallments > 0 && !transactionAmountUnprocessed.isZero()) { + for (LoanRepaymentScheduleInstallment installment : inAdvanceInstallments) { + Set inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, installment, firstNormalInstallmentNumber); - // Adjust the portion for the last installment - if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { - evenPortion = evenPortion.add(balanceAdjustment); - } LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( - transactionMappings, loanTransaction, inAdvanceInstallment, currency); - paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, - evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, - LoanRepaymentScheduleInstallment.PaymentAction.PAY); + transactionMappings, loanTransaction, installment, currency); + + // recalculate interest, adjust principal for current installment + if (transactionCtx instanceof ProgressiveTransactionCtx ctx) { // for progressive installment + ProgressiveLoanInterestScheduleModel model = ctx.getModel(); + LocalDate transactionDate = loanTransaction.getTransactionDate(); + LocalDate payDate = installment.getFromDate().isAfter(transactionDate) ? installment.getFromDate() : transactionDate; + + ProgressiveLoanInterestRepaymentModel payableDetails = emiCalculator.getPayableDetails(model, installment.getDueDate(), payDate).orElseThrow(); + switch (paymentAllocationType) { + case IN_ADVANCE_INTEREST -> { + BigDecimal moneyToPay = payableDetails.getInterestDue().getAmount(); + log.info("{} - IN_ADVANCE interest: {}", installment.getDueDate(), moneyToPay); + installment.updateInterestCharged(moneyToPay); + } + case IN_ADVANCE_PRINCIPAL -> { + BigDecimal moneyToPay = payableDetails.getPrincipalDue().getAmount(); + log.info("{} - IN_ADVANCE principal: {}", installment.getDueDate(), moneyToPay); + installment.updatePrincipal(moneyToPay); + } + } + + paidPortion = processPaymentAllocation(paymentAllocationType, installment, loanTransaction, + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, + LoanRepaymentScheduleInstallment.PaymentAction.PAY); + + // sum installment's principal due MINUS sum installment's principal paid ==> outstanding loan principal + + // model + -> tartozas + if (paymentAllocationType == PaymentAllocationType.IN_ADVANCE_PRINCIPAL) { + emiCalculator.addBalanceCorrection(model, installment.getDueDate(), payableDetails.getPrincipalDue().minus(paidPortion)); + } + } else { + paidPortion = processPaymentAllocation(paymentAllocationType, installment, loanTransaction, + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, + LoanRepaymentScheduleInstallment.PaymentAction.PAY); + } + transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); } } else { @@ -1239,20 +1304,34 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo return transactionAmountUnprocessed; } + @Deprecated + private Money calculateInterestDelta(Money originalInterestOfCurrentInstallment, LoanRepaymentScheduleInstallment installment, + LocalDate transactionDate, MonetaryCurrency currency) { + if (originalInterestOfCurrentInstallment == null) { + // interest was not recalculated yet + Money payableInterest = ProgressiveLoanScheduleGenerator.calculatePayableInterest(installment, transactionDate); + originalInterestOfCurrentInstallment = installment.getInterestCharged(currency); + return originalInterestOfCurrentInstallment.minus(payableInterest); + } else { + // interest was already recalculated + return originalInterestOfCurrentInstallment.minus(installment.getInterestCharged(currency)); + } + } + @NotNull private static Set getLoanChargesOfInstallment(Set charges, LoanRepaymentScheduleInstallment currentInstallment, - int firstNormalInstallmentNumber) { + int firstNormalInstallmentNumber) { return charges.stream().filter(loanCharge -> currentInstallment.getInstallmentNumber().equals(firstNormalInstallmentNumber) - ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(currentInstallment.getFromDate(), + ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(currentInstallment.getFromDate(), currentInstallment.getDueDate()) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(currentInstallment.getFromDate(), currentInstallment.getDueDate())) + : loanCharge.isDueForCollectionFromAndUpToAndIncluding(currentInstallment.getFromDate(), currentInstallment.getDueDate())) .collect(Collectors.toSet()); } private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, - LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, - Set charges, Balances balances) { + List installments, Money transactionAmountUnprocessed, + LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, + Set charges, Balances balances) { int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) { FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule(); @@ -1347,7 +1426,7 @@ private Money processPeriodsVertically(LoanTransaction loanTransaction, Monetary } private Predicate getFilterPredicate(PaymentAllocationType paymentAllocationType, - MonetaryCurrency currency) { + MonetaryCurrency currency) { return switch (paymentAllocationType.getAllocationType()) { case PENALTY -> (p) -> p.getPenaltyChargesOutstanding(currency).isGreaterThanZero(); case FEE -> (p) -> p.getFeeChargesOutstanding(currency).isGreaterThanZero(); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index 5fd7d21c547..8d4495af670 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; import static java.time.temporal.ChronoUnit.DAYS; +import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE; import java.math.BigDecimal; import java.math.MathContext; @@ -29,8 +30,8 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.MathUtil; @@ -46,6 +47,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; @@ -247,58 +249,44 @@ public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicatio public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { - return switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { - case TILL_PRE_CLOSURE_DATE -> { - log.debug("calculating prepayment amount till pre closure date (Strategy A)"); - OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); - AtomicBoolean firstAfterPayoff = new AtomicBoolean(true); - loan.getRepaymentScheduleInstallments().forEach(installment -> { - boolean isInstallmentAfterPayoff = installment.getDueDate().isAfter(onDate); - - outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); - if (isInstallmentAfterPayoff) { - if (firstAfterPayoff.getAndSet(false)) { - outstandingAmounts.plusInterest(calculatePayableInterest(loan, installment, onDate)); - } else { - log.debug("Installment {} - {} is after payoff, not counting interest", installment.getFromDate(), - installment.getDueDate()); - } - } else { - log.debug("adding interest for {} - {}: {}", installment.getFromDate(), installment.getDueDate(), - installment.getInterestOutstanding(currency)); - outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); - } - outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency)); - outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)); - }); - yield outstandingAmounts; - } - - case TILL_REST_FREQUENCY_DATE -> { - log.debug("calculating prepayment amount till rest frequency date (Strategy B)"); - OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); - loan.getRepaymentScheduleInstallments().forEach(installment -> { - boolean isPayoffBeforeInstallment = installment.getFromDate().isBefore(onDate); - - outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); - if (isPayoffBeforeInstallment) { - outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); - } else { - log.debug("Payoff after installment {}, not counting interest", installment.getDueDate()); - } - outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency)); - outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)); - }); - - yield outstandingAmounts; - } - case NONE -> throw new UnsupportedOperationException("Pre-closure interest calculation strategy not supported"); + List installments = loan.getRepaymentScheduleInstallments(); + + LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { + case TILL_PRE_CLOSURE_DATE -> onDate; + case TILL_REST_FREQUENCY_DATE -> // find due date of current installment + installments.stream().filter(it -> it.getFromDate().isBefore(onDate) && it.getDueDate().isAfter(onDate)).findFirst() + .orElseThrow(() -> new IllegalStateException("No installment found for transaction date: " + onDate)).getDueDate(); + case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); }; + + if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { + throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); + } + ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), + loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight(); + + LoanRepaymentScheduleInstallment actualInstallment = installments.stream() + .filter(it -> transactionDate.isAfter(it.getFromDate()) && !transactionDate.isAfter(it.getDueDate())) + .findFirst() + .orElse(installments.get(0)); + + ProgressiveLoanInterestRepaymentModel result = emiCalculator.getPayableDetails(model, actualInstallment.getDueDate(), transactionDate).orElseThrow(); + + OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) // + .principal(result.getOutstandingBalance()) // + .interest(result.getInterestDue()); + + installments.forEach(installment -> amounts // + .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) + .plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency))); + + return amounts; } - private Money calculatePayableInterest(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate onDate) { + @Deprecated + public static Money calculatePayableInterest(LoanRepaymentScheduleInstallment installment, LocalDate onDate) { RoundingMode roundingMode = MoneyHelper.getRoundingMode(); - MonetaryCurrency currency = loan.getCurrency(); + MonetaryCurrency currency = installment.getLoan().getCurrency(); Money originalInterest = installment.getInterestCharged(currency); log.debug("calculating interest for {} from {} to {}", originalInterest, installment.getFromDate(), installment.getDueDate()); 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 afcca8db75b..b9145fe2320 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 @@ -28,6 +28,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PrincipalInterest; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; public interface EMICalculator { 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 80107f76ed4..07487f4840c 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 @@ -33,6 +33,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -41,6 +42,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PrincipalInterest; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.mapper.ProgressiveLoanInterestRepaymentModelMapper; import org.jetbrains.annotations.NotNull; diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java index 214bff051c9..945d370e6c6 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java @@ -121,7 +121,7 @@ public void calculatePrepaymentAmount_TILL_PRE_CLOSURE_DATE() { OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms, MathContext.DECIMAL32, loan, holidays, processor); - assertEquals(BigDecimal.valueOf(83.84), amounts.getTotalOutstanding().getAmount()); + assertEquals(BigDecimal.valueOf(83.81), amounts.getTotalOutstanding().getAmount()); } @Test