From f8710587b06ae022f61b4b91e386a97c3a71305c Mon Sep 17 00:00:00 2001 From: Kristof Jozsa Date: Wed, 14 Aug 2024 11:14:48 +0200 Subject: [PATCH] FINERACT-1981: pay-off transaction for progressive loans --- .../data/OutstandingAmountsDTO.java | 49 +++++++++ .../portfolio/loanaccount/domain/Loan.java | 25 ++--- .../LoanRepaymentScheduleInstallment.java | 12 +-- ...stractCumulativeLoanScheduleGenerator.java | 19 ++-- .../domain/LoanScheduleGenerator.java | 7 +- .../ProgressiveLoanScheduleGenerator.java | 92 +++++++++++------ .../ProgressiveLoanScheduleGeneratorTest.java | 99 +++++++++++++++++++ .../service/LoanScheduleAssembler.java | 7 +- .../service/LoanReadPlatformServiceImpl.java | 17 ++-- 9 files changed, 258 insertions(+), 69 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java create mode 100644 fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java 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 new file mode 100644 index 00000000000..954f8674160 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java @@ -0,0 +1,49 @@ +package org.apache.fineract.portfolio.loanaccount.data; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; + +@Data +@Accessors(chain = true, fluent = true) +public class OutstandingAmountsDTO { + + private Money principal; + private Money interest; + private Money feeCharges; + private Money penaltyCharges; + + public OutstandingAmountsDTO(MonetaryCurrency currency) { + this.principal = Money.zero(currency); + this.interest = Money.zero(currency); + this.feeCharges = Money.zero(currency); + this.penaltyCharges = Money.zero(currency); + } + + public Money getTotalOutstanding() { + return principal() // + .plus(interest()) // + .plus(feeCharges()) // + .plus(penaltyCharges()); + } + + public void plusPrincipal(Money principal) { + this.principal = this.principal.plus(principal); + } + + public void plusInterest(Money interest) { + this.interest = this.interest.plus(interest); + } + + public void plusFeeCharges(Money feeCharges) { + this.feeCharges = this.feeCharges.plus(feeCharges); + } + + public void plusPenaltyCharges(Money penaltyCharges) { + this.penaltyCharges = this.penaltyCharges.plus(penaltyCharges); + } + +} + + diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 17c7281e651..4788d867a17 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -109,6 +109,7 @@ import org.apache.fineract.portfolio.loanaccount.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; @@ -4366,8 +4367,8 @@ private LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO gener loanRepaymentScheduleTransactionProcessor, generatorDTO.getRecalculateFrom()); } - public LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate) { - LoanRepaymentScheduleInstallment installment; + public OutstandingAmountsDTO fetchPrepaymentDetail(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate) { + OutstandingAmountsDTO outstandingAmounts; if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) { final MathContext mc = MoneyHelper.getMathContext(); @@ -4379,12 +4380,12 @@ public LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final ScheduleGene .create(loanApplicationTerms.getLoanScheduleType(), interestMethod); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); - installment = loanScheduleGenerator.calculatePrepaymentAmount(getCurrency(), onDate, loanApplicationTerms, mc, this, + outstandingAmounts = loanScheduleGenerator.calculatePrepaymentAmount(getCurrency(), onDate, loanApplicationTerms, mc, this, scheduleGeneratorDTO.getHolidayDetailDTO(), loanRepaymentScheduleTransactionProcessor); } else { - installment = this.getTotalOutstandingOnLoan(); + outstandingAmounts = this.getTotalOutstandingOnLoan(); } - return installment; + return outstandingAmounts; } public LoanApplicationTerms constructLoanApplicationTerms(final ScheduleGeneratorDTO scheduleGeneratorDTO) { @@ -4458,11 +4459,11 @@ public BigDecimal constructLoanTermVariations(FloatingRateDTO floatingRateDTO, B return annualNominalInterestRate; } - private LoanRepaymentScheduleInstallment getTotalOutstandingOnLoan() { - Money feeCharges = Money.zero(loanCurrency()); - Money penaltyCharges = Money.zero(loanCurrency()); + private OutstandingAmountsDTO getTotalOutstandingOnLoan() { Money totalPrincipal = Money.zero(loanCurrency()); Money totalInterest = Money.zero(loanCurrency()); + Money feeCharges = Money.zero(loanCurrency()); + Money penaltyCharges = Money.zero(loanCurrency()); final Set compoundingDetails = null; List repaymentSchedule = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedule) { @@ -4471,9 +4472,11 @@ private LoanRepaymentScheduleInstallment getTotalOutstandingOnLoan() { feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(loanCurrency())); penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(loanCurrency())); } - LocalDate businessDate = DateUtils.getBusinessLocalDate(); - return new LoanRepaymentScheduleInstallment(null, 0, businessDate, businessDate, totalPrincipal.getAmount(), - totalInterest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount(), false, compoundingDetails); + return new OutstandingAmountsDTO(totalPrincipal.getCurrency()) + .principal(totalPrincipal) + .interest(totalInterest) + .feeCharges(feeCharges) + .penaltyCharges(penaltyCharges); } public LocalDate fetchInterestRecalculateFromDate() { 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 7c072bf1b16..34d8baf6b5c 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 @@ -583,20 +583,20 @@ public Money payInterestComponent(final LocalDate transactionDate, final Money t return interestPortionOfTransaction; } - public Money payPrincipalComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { + public Money payPrincipalComponent(final LocalDate transactionDate, final Money transactionAmount) { - final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); + final MonetaryCurrency currency = transactionAmount.getCurrency(); Money principalPortionOfTransaction = Money.zero(currency); - if (transactionAmountRemaining.isZero()) { + if (transactionAmount.isZero()) { return principalPortionOfTransaction; } final Money principalDue = getPrincipalOutstanding(currency); - if (transactionAmountRemaining.isGreaterThanOrEqualTo(principalDue)) { + if (transactionAmount.isGreaterThanOrEqualTo(principalDue)) { this.principalCompleted = getPrincipalCompleted(currency).plus(principalDue).getAmount(); principalPortionOfTransaction = principalPortionOfTransaction.plus(principalDue); } else { - this.principalCompleted = getPrincipalCompleted(currency).plus(transactionAmountRemaining).getAmount(); - principalPortionOfTransaction = principalPortionOfTransaction.plus(transactionAmountRemaining); + this.principalCompleted = getPrincipalCompleted(currency).plus(transactionAmount).getAmount(); + principalPortionOfTransaction = principalPortionOfTransaction.plus(transactionAmount); } this.principalCompleted = defaultToNullIfZero(this.principalCompleted); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index c4d4b884797..43037ac796a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java @@ -45,6 +45,7 @@ import org.apache.fineract.portfolio.loanaccount.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; @@ -2775,9 +2776,9 @@ private LocalDate getNextCompoundScheduleDate(LocalDate startDate, LoanApplicati * Method returns the amount payable to close the loan account as of today. */ @Override - public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final MonetaryCurrency currency, final LocalDate onDate, - final LoanApplicationTerms loanApplicationTerms, final MathContext mc, Loan loan, final HolidayDetailDTO holidayDetailDTO, - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { + public OutstandingAmountsDTO calculatePrepaymentAmount(final MonetaryCurrency currency, final LocalDate onDate, + final LoanApplicationTerms loanApplicationTerms, final MathContext mc, Loan loan, final HolidayDetailDTO holidayDetailDTO, + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { LocalDate calculateTill = onDate; if (loanApplicationTerms.getPreClosureInterestCalculationStrategy().calculateTillRestFrequencyEnabled()) { @@ -2790,10 +2791,10 @@ public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final Monetary loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(), loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges()); - Money feeCharges = Money.zero(currency); - Money penaltyCharges = Money.zero(currency); Money totalPrincipal = Money.zero(currency); Money totalInterest = Money.zero(currency); + Money feeCharges = Money.zero(currency); + Money penaltyCharges = Money.zero(currency); for (final LoanRepaymentScheduleInstallment currentInstallment : loanScheduleDTO.getInstallments()) { if (currentInstallment.isNotFullyPaidOff()) { totalPrincipal = totalPrincipal.plus(currentInstallment.getPrincipalOutstanding(currency)); @@ -2802,8 +2803,10 @@ public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final Monetary penaltyCharges = penaltyCharges.plus(currentInstallment.getPenaltyChargesOutstanding(currency)); } } - final Set compoundingDetails = null; - return new LoanRepaymentScheduleInstallment(null, 0, onDate, onDate, totalPrincipal.getAmount(), totalInterest.getAmount(), - feeCharges.getAmount(), penaltyCharges.getAmount(), false, compoundingDetails); + return new OutstandingAmountsDTO(currency) // + .principal(totalPrincipal) // + .interest(totalInterest) // + .feeCharges(feeCharges) // + .penaltyCharges(penaltyCharges); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java index def9a53b460..b4ffd2ac1ae 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java @@ -23,6 +23,7 @@ import java.util.Set; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; +import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; @@ -38,8 +39,8 @@ LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, LocalDate rescheduleFrom); - LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, - LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, - LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor); + OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, + LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, + LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor); } 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 a53e0b4b79e..83d301de6c6 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 @@ -18,15 +18,8 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; -import java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -34,9 +27,9 @@ import org.apache.fineract.portfolio.loanaccount.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; 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.loanschedule.data.LoanScheduleDTO; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod; @@ -48,6 +41,16 @@ import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; import org.springframework.stereotype.Component; +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Slf4j @Component @RequiredArgsConstructor public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { @@ -57,7 +60,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { @Override public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, - final Set loanCharges, final HolidayDetailDTO holidayDetailDTO) { + final Set loanCharges, final HolidayDetailDTO holidayDetailDTO) { final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency(); // generate list of proposed schedule due dates @@ -154,8 +157,8 @@ private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTer } private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final LoanScheduleParams scheduleParams, - final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List periods, - final BigDecimal chargesDueAtTimeOfDisbursement) { + final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List periods, + final BigDecimal chargesDueAtTimeOfDisbursement) { for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) { final LocalDate disbursementDate = disbursementData.disbursementDate(); @@ -215,17 +218,44 @@ private void processDisbursements(final LoanApplicationTerms loanApplicationTerm @Override public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan, - HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, - LocalDate rescheduleFrom) { + HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, + LocalDate rescheduleFrom) { LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO); return LoanScheduleDTO.from(null, model); } @Override - public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, - LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, - LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { - return null; + 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 -> { + // Strategy A + log.info("calculating prepayment amount till pre closure date (Strategy A)"); + throw new UnsupportedOperationException("TODO"); +// yield new OutstandingAmountsDTO(); + } + case TILL_REST_FREQUENCY_DATE -> { + // Strategy B + log.info("calculating prepayment amount till rest frequency date (Strategy B)"); + OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); + loan.getRepaymentScheduleInstallments().forEach(installment -> { + boolean isPayoffAfterInstallment = installment.getFromDate().isAfter(onDate); + + outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); + if (!isPayoffAfterInstallment) { + outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); + } else { + log.info("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"); + }; } // Private, internal methods @@ -240,7 +270,7 @@ private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set loanCharges, - final LoanScheduleParams scheduleParams, final MonetaryCurrency currency, final MathContext mc) { + final LoanScheduleParams scheduleParams, final MonetaryCurrency currency, final MathContext mc) { final PrincipalInterest principalInterest = new PrincipalInterest(repaymentPeriod.getPrincipalDue(), repaymentPeriod.getInterestDue(), null); @@ -257,9 +287,9 @@ private void applyChargesForCurrentPeriod(final LoanScheduleModelRepaymentPeriod } private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, - final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, - final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, - final MathContext mc) { + final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, + final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, + final MathContext mc) { Money cumulative = Money.zero(monetaryCurrency); for (final LoanCharge loanCharge : loanCharges) { if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) { @@ -271,8 +301,8 @@ private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final L } private Money getCumulativeAmountOfCharge(LocalDate periodStart, LocalDate periodEnd, PrincipalInterest principalInterestForThisPeriod, - Money principalDisbursed, Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, - boolean isFirstPeriod, LoanCharge loanCharge, Money cumulative, MathContext mc) { + Money principalDisbursed, Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, + boolean isFirstPeriod, LoanCharge loanCharge, Money cumulative, MathContext mc) { boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { @@ -289,10 +319,10 @@ private Money getCumulativeAmountOfCharge(LocalDate periodStart, LocalDate perio } private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency monetaryCurrency, - final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, - final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, - final MathContext mc) { + final Set loanCharges, final MonetaryCurrency monetaryCurrency, + final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, + final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, + final MathContext mc) { Money cumulative = Money.zero(monetaryCurrency); for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isPenaltyCharge()) { @@ -304,7 +334,7 @@ private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, fin } private Money calculateInstallmentCharge(final PrincipalInterest principalInterestForThisPeriod, Money cumulative, - final LoanCharge loanCharge, final MathContext mc) { + final LoanCharge loanCharge, final MathContext mc) { if (loanCharge.getChargeCalculation().isPercentageBased()) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { @@ -324,7 +354,7 @@ private Money calculateInstallmentCharge(final PrincipalInterest principalIntere } private Money calculateSpecificDueDateChargeWithPercentage(final Money principalDisbursed, - final Money totalInterestChargedForFullLoanTerm, Money cumulative, final LoanCharge loanCharge, final MathContext mc) { + final Money totalInterestChargedForFullLoanTerm, Money cumulative, final LoanCharge loanCharge, final MathContext mc) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { amount = amount.add(principalDisbursed.getAmount()).add(totalInterestChargedForFullLoanTerm.getAmount()); @@ -339,7 +369,7 @@ private Money calculateSpecificDueDateChargeWithPercentage(final Money principal } private void updatePeriodsWithCharges(final MonetaryCurrency currency, LoanScheduleParams scheduleParams, - final Collection periods, final Set nonCompoundingCharges, MathContext mc) { + final Collection periods, final Set nonCompoundingCharges, MathContext mc) { for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) { if (loanScheduleModelPeriod.isRepaymentPeriod()) { PrincipalInterest principalInterest = new PrincipalInterest(Money.of(currency, loanScheduleModelPeriod.principalDue()), 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 new file mode 100644 index 00000000000..dbb122d8518 --- /dev/null +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java @@ -0,0 +1,99 @@ +package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; + +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; +import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.math.BigDecimal.ZERO; +import static java.math.BigDecimal.valueOf; +import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_REST_FREQUENCY_DATE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ProgressiveLoanScheduleGeneratorTest { + static class TestRow { + LocalDate fromDate; + LocalDate dueDate; + BigDecimal balance; + BigDecimal principal; + BigDecimal interest; + BigDecimal fee; + BigDecimal penalty; + boolean paid; + + public TestRow(LocalDate fromDate, LocalDate dueDate, BigDecimal balance, BigDecimal principal, BigDecimal interest, BigDecimal fee, BigDecimal penalty, boolean paid) { + this.fromDate = fromDate; + this.dueDate = dueDate; + this.balance = balance; + this.principal = principal; + this.interest = interest; + this.fee = fee; + this.penalty = penalty; + this.paid = paid; + } + } + + List rows = List.of( + new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), valueOf(83.57), valueOf(16.43), valueOf(0.58), ZERO, ZERO, true), + new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), valueOf(67.05), valueOf(16.52), valueOf(0.49), ZERO, ZERO, false), + new TestRow(LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1), valueOf(50.43), valueOf(16.62), valueOf(0.39), ZERO, ZERO, false), + new TestRow(LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1), valueOf(33.71), valueOf(16.72), valueOf(0.29), ZERO, ZERO, false), + new TestRow(LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1), valueOf(16.90), valueOf(16.81), valueOf(0.20), ZERO, ZERO, false), + new TestRow(LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1), valueOf(00.90), valueOf(16.90), valueOf(0.10), ZERO, ZERO, false) + ); + + LocalDate onDate = LocalDate.of(2024, 2, 15); + + private ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(null, null); + private MonetaryCurrency usd = new MonetaryCurrency("USD", 2, null); + private HolidayDetailDTO holidays = new HolidayDetailDTO(false, null, null); + LoanRepaymentScheduleTransactionProcessor processor = mock(LoanRepaymentScheduleTransactionProcessor.class); + + static { + ReflectionTestUtils.setField(MoneyHelper.class, "staticConfigurationDomainService", mock(ConfigurationDomainService.class)); + } + + @Test + public void calculatePrepaymentAmount_TILL_REST_FREQUENCY_DATE() { + LoanApplicationTerms terms = mock(LoanApplicationTerms.class); + when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_REST_FREQUENCY_DATE); + + Loan loan = mock(Loan.class); + List installments = createInstallments(rows, loan, usd); + when(loan.getRepaymentScheduleInstallments()).thenReturn(installments); + + // start calculation + OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, onDate, terms, MathContext.DECIMAL32, loan, holidays, processor); + System.out.println(amounts); + System.out.println("total outstanding: " + amounts.getTotalOutstanding()); + assertEquals(valueOf(84.06), amounts.getTotalOutstanding().getAmount()); + } + + private List createInstallments(List rows, Loan loan, MonetaryCurrency usd) { + AtomicInteger count = new AtomicInteger(1); + return rows.stream().map(row -> { + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, count.incrementAndGet(), row.fromDate, row.fromDate, row.principal, row.interest, row.fee, row.penalty, true, null, null, row.paid); + if (row.paid) { + installment.payPrincipalComponent(row.fromDate, Money.of(usd, row.principal)); + installment.payInterestComponent(row.fromDate, Money.of(usd, row.interest)); + installment.updateObligationMet(true); + } + return installment; + }).toList(); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 6e8b22d9c3b..b5ee51bc668 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -91,6 +91,7 @@ import org.apache.fineract.portfolio.loanaccount.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; @@ -732,9 +733,9 @@ public LoanScheduleModel assembleForInterestRecalculation(final LoanApplicationT loanRepaymentScheduleTransactionProcessor, rescheduleFrom).getLoanScheduleModel(); } - public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, - LoanApplicationTerms loanApplicationTerms, Loan loan, final Long officeId, - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { + public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, + LoanApplicationTerms loanApplicationTerms, Loan loan, final Long officeId, + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), loanApplicationTerms.getInterestMethod()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index b30c2e0a453..43ed78e6e8d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -104,6 +104,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionRelationData; +import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; @@ -472,19 +473,21 @@ public LoanTransactionData retrieveLoanPrePaymentTemplate(final LoanTransactionT final LocalDate earliestUnpaidInstallmentDate = DateUtils.getBusinessLocalDate(); final LocalDate recalculateFrom = null; final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchPrepaymentDetail(scheduleGeneratorDTO, onDate); + final OutstandingAmountsDTO outstandingAmounts = loan.fetchPrepaymentDetail(scheduleGeneratorDTO, onDate); final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(repaymentTransactionType); final Collection paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); - final BigDecimal outstandingLoanBalance = loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount(); + final BigDecimal outstandingLoanBalance = outstandingAmounts.principal().getAmount(); final BigDecimal unrecognizedIncomePortion = null; + BigDecimal adjustedChargeAmount = adjustPrepayInstallmentCharge(loan, onDate); + BigDecimal totalAdjusted = outstandingAmounts.getTotalOutstanding().getAmount().subtract(adjustedChargeAmount); return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate, - loanRepaymentScheduleInstallment.getTotalOutstanding(currency).getAmount().subtract(adjustedChargeAmount), - loan.getNetDisbursalAmount(), loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount(), - loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(), - loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount().subtract(adjustedChargeAmount), - loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(), null, unrecognizedIncomePortion, + totalAdjusted, + loan.getNetDisbursalAmount(), outstandingAmounts.principal().getAmount(), + outstandingAmounts.interest().getAmount(), + outstandingAmounts.feeCharges().getAmount().subtract(adjustedChargeAmount), + outstandingAmounts.penaltyCharges().getAmount(), null, unrecognizedIncomePortion, paymentOptions, ExternalId.empty(), null, null, outstandingLoanBalance, false, loanId, loan.getExternalId()); }