From 0b9687d2aab6fce1097a8eeb86319879b2f3471c Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Mon, 27 Jan 2025 20:04:14 +0100 Subject: [PATCH] FINERACT-2081: Fix end of month and partial interest calculation for 360/30 --- .../domain/LoanApplicationTerms.java | 8 +- .../LoanProductRelatedDetailMinimumData.java | 34 +- ...MinimumRepaymentScheduleRelatedDetail.java | 5 - .../domain/LoanProductRelatedDetail.java | 2 - ...edPaymentScheduleTransactionProcessor.java | 33 +- .../ProgressiveLoanInterestScheduleModel.java | 23 +- .../ProgressiveLoanScheduleGenerator.java | 8 +- .../loanproduct/calc/EMICalculator.java | 6 +- .../calc/ProgressiveEMICalculator.java | 152 +++++- .../calc/ProgressiveEMICalculatorTest.java | 500 +++++++++++++++--- 10 files changed, 615 insertions(+), 156 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index aff82208350..c8769c39f2a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -1549,10 +1549,10 @@ public LoanProductRelatedDetail toLoanProductRelatedDetail() { public LoanProductMinimumRepaymentScheduleRelatedDetail toLoanProductRelatedDetailMinimumData() { final CurrencyData currency = new CurrencyData(this.currency.getCode(), this.currency.getDecimalPlaces(), this.currency.getInMultiplesOf()); - return new LoanProductRelatedDetailMinimumData(currency, principal, inArrearsTolerance, interestRatePerPeriod, - annualNominalInterestRate, interestChargingGrace, interestPaymentGrace, principalGrace, - recurringMoratoriumOnPrincipalPeriods, interestMethod, interestCalculationPeriodMethod, daysInYearType, daysInMonthType, - amortizationMethod, repaymentPeriodFrequencyType, repaymentEvery, numberOfRepayments); + return new LoanProductRelatedDetailMinimumData(currency, interestRatePerPeriod, annualNominalInterestRate, interestChargingGrace, + interestPaymentGrace, principalGrace, recurringMoratoriumOnPrincipalPeriods, interestMethod, + interestCalculationPeriodMethod, daysInYearType, daysInMonthType, amortizationMethod, repaymentPeriodFrequencyType, + repaymentEvery, numberOfRepayments); } public Integer getLoanTermFrequency() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java index 175d7d60543..5e2b80115a0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java @@ -20,7 +20,6 @@ import java.math.BigDecimal; import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -32,39 +31,28 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRepaymentScheduleRelatedDetail { private final CurrencyData currency; - - private final Money principal; - private final Money inArrearsTolerance; - private final BigDecimal interestRatePerPeriod; private final BigDecimal annualNominalInterestRate; - private final Integer interestChargingGrace; private final Integer interestPaymentGrace; private final Integer principalGrace; private final Integer recurringMoratoriumOnPrincipalPeriods; - private final InterestMethod interestMethod; private final InterestCalculationPeriodMethod interestCalculationPeriodMethod; - private final DaysInYearType daysInYearType; private final DaysInMonthType daysInMonthType; - private final AmortizationMethod amortizationMethod; - private final PeriodFrequencyType repaymentPeriodFrequencyType; private final Integer repaymentEvery; private final Integer numberOfRepayments; - public LoanProductRelatedDetailMinimumData(CurrencyData currency, Money principal, Money inArrearsTolerance, - BigDecimal interestRatePerPeriod, BigDecimal annualNominalInterestRate, Integer interestChargingGrace, - Integer interestPaymentGrace, Integer principalGrace, Integer recurringMoratoriumOnPrincipalPeriods, - InterestMethod interestMethod, InterestCalculationPeriodMethod interestCalculationPeriodMethod, DaysInYearType daysInYearType, - DaysInMonthType daysInMonthType, AmortizationMethod amortizationMethod, PeriodFrequencyType repaymentPeriodFrequencyType, - Integer repaymentEvery, Integer numberOfRepayments) { + public LoanProductRelatedDetailMinimumData(CurrencyData currency, BigDecimal interestRatePerPeriod, + BigDecimal annualNominalInterestRate, Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace, + Integer recurringMoratoriumOnPrincipalPeriods, InterestMethod interestMethod, + InterestCalculationPeriodMethod interestCalculationPeriodMethod, DaysInYearType daysInYearType, DaysInMonthType daysInMonthType, + AmortizationMethod amortizationMethod, PeriodFrequencyType repaymentPeriodFrequencyType, Integer repaymentEvery, + Integer numberOfRepayments) { this.currency = currency; - this.principal = principal; - this.inArrearsTolerance = inArrearsTolerance; this.interestRatePerPeriod = interestRatePerPeriod; this.annualNominalInterestRate = annualNominalInterestRate; this.interestChargingGrace = defaultToNullIfZero(interestChargingGrace); @@ -94,11 +82,6 @@ public CurrencyData getCurrencyData() { return currency; } - @Override - public Money getPrincipal() { - return principal; - } - @Override public Integer getGraceOnInterestCharged() { return interestChargingGrace; @@ -119,11 +102,6 @@ public Integer getRecurringMoratoriumOnPrincipalPeriods() { return recurringMoratoriumOnPrincipalPeriods; } - @Override - public Money getInArrearsTolerance() { - return inArrearsTolerance; - } - @Override public BigDecimal getNominalInterestRatePerPeriod() { return interestRatePerPeriod; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java index 2e3f8408983..0509405a5d0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java @@ -20,7 +20,6 @@ import java.math.BigDecimal; import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; /** @@ -30,8 +29,6 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail { CurrencyData getCurrencyData(); - Money getPrincipal(); - Integer getGraceOnInterestCharged(); Integer getGraceOnInterestPayment(); @@ -40,8 +37,6 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail { Integer getRecurringMoratoriumOnPrincipalPeriods(); - Money getInArrearsTolerance(); - BigDecimal getNominalInterestRatePerPeriod(); PeriodFrequencyType getInterestPeriodFrequencyType(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index 5dc7ecafd9b..52c14d046d7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -263,12 +263,10 @@ public CurrencyData getCurrencyData() { return currency.toData(); } - @Override public Money getPrincipal() { return Money.of(getCurrencyData(), this.principal); } - @Override public Money getInArrearsTolerance() { return Money.of(getCurrencyData(), this.inArrearsTolerance); } 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 ee5dfc072aa..49b86a365de 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 @@ -65,7 +65,6 @@ import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; -import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; @@ -76,6 +75,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; @@ -183,10 +183,8 @@ public Pair repr MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency)); final Loan loan = loanTransactions.get(0).getLoan(); - LoanTermVariationsDataWrapper loanTermVariations = Optional - .ofNullable(loan.getActiveLoanTermVariations()).map(loanTermVariationsSet -> loanTermVariationsSet.stream() - .map(LoanTermVariations::toData).collect(Collectors.toCollection(ArrayList::new))) - .map(LoanTermVariationsDataWrapper::new).orElse(null); + List loanTermVariations = loan.getActiveLoanTermVariations().stream().map(LoanTermVariations::toData) + .collect(Collectors.toCollection(ArrayList::new)); final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf(); final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail(); ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments, @@ -235,8 +233,9 @@ public Pair repr public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, MonetaryCurrency currency, List installments, Set charges) { LocalDate currentDate = DateUtils.getBusinessLocalDate(); - return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges) - .getLeft(); + Pair result = reprocessProgressiveLoanTransactions(disbursementDate, + currentDate, loanTransactions, currency, installments, charges); + return result.getLeft(); } @NotNull @@ -823,16 +822,18 @@ private void processSingleCharge(LoanCharge loanCharge, MonetaryCurrency currenc } @NotNull - private List createSortedChangeList(final LoanTermVariationsDataWrapper loanTermVariations, + private List createSortedChangeList(final List loanTermVariations, final List loanTransactions, final Set charges) { - final List changeOperations = new ArrayList<>(); - if (loanTermVariations != null) { - if (!loanTermVariations.getInterestPauseVariations().isEmpty()) { - changeOperations.addAll(loanTermVariations.getInterestPauseVariations().stream().map(ChangeOperation::new).toList()); - } - if (!loanTermVariations.getInterestRateFromInstallment().isEmpty()) { - changeOperations.addAll(loanTermVariations.getInterestRateFromInstallment().stream().map(ChangeOperation::new).toList()); - } + List changeOperations = new ArrayList<>(); + Map> loanTermVariationsMap = loanTermVariations.stream() + .collect(Collectors.groupingBy(ltvd -> LoanTermVariationType.fromInt(ltvd.getTermType().getId().intValue()))); + if (loanTermVariationsMap.get(LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT) != null) { + changeOperations.addAll(loanTermVariationsMap.get(LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT).stream() + .map(ChangeOperation::new).toList()); + } + if (loanTermVariationsMap.get(LoanTermVariationType.INTEREST_PAUSE) != null) { + changeOperations + .addAll(loanTermVariationsMap.get(LoanTermVariationType.INTEREST_PAUSE).stream().map(ChangeOperation::new).toList()); } if (charges != null) { changeOperations.addAll(charges.stream().map(ChangeOperation::new).toList()); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java index 513cc6ff6c5..528d8572556 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java @@ -27,18 +27,22 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.TreeSet; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.stream.Collectors; import lombok.Data; import lombok.experimental.Accessors; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; @Data @@ -48,18 +52,18 @@ public class ProgressiveLoanInterestScheduleModel { private final List repaymentPeriods; private final TreeSet interestRates; private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail; - private final LoanTermVariationsDataWrapper loanTermVariations; + private final Map> loanTermVariations; private final Integer installmentAmountInMultiplesOf; private final MathContext mc; private final Money zero; public ProgressiveLoanInterestScheduleModel(final List repaymentPeriods, final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, - final LoanTermVariationsDataWrapper loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { + final List loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { this.repaymentPeriods = repaymentPeriods; this.interestRates = new TreeSet<>(Collections.reverseOrder()); this.loanProductRelatedDetail = loanProductRelatedDetail; - this.loanTermVariations = loanTermVariations; + this.loanTermVariations = buildLoanTermVariationMap(loanTermVariations); this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf; this.mc = mc; this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc); @@ -67,7 +71,8 @@ public ProgressiveLoanInterestScheduleModel(final List repaymen private ProgressiveLoanInterestScheduleModel(final List repaymentPeriods, final TreeSet interestRates, final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, - final LoanTermVariationsDataWrapper loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { + final Map> loanTermVariations, final Integer installmentAmountInMultiplesOf, + final MathContext mc) { this.mc = mc; this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods, (previousPeriod, repaymentPeriod) -> new RepaymentPeriod(previousPeriod, repaymentPeriod, mc)); @@ -343,4 +348,12 @@ private LocalDate calculateNewDueDate(final InterestPeriod previousInterestPerio : date.isAfter(previousInterestPeriod.getDueDate()) ? previousInterestPeriod.getDueDate() : date; } + private Map> buildLoanTermVariationMap( + final List loanTermVariationsData) { + if (loanTermVariationsData == null) { + return new HashMap<>(); + } + return loanTermVariationsData.stream() + .collect(Collectors.groupingBy(ltvd -> LoanTermVariationType.fromInt(ltvd.getTermType().getId().intValue()))); + } } 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 18126360b4b..b71e340d7ec 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 @@ -37,6 +37,7 @@ import org.apache.fineract.organisation.monetary.domain.Money; 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; @@ -88,9 +89,12 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer // generate list of proposed schedule due dates final List expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc, periodStartDate, loanApplicationTerms, holidayDetailDTO); + List loanTermVariations = loanApplicationTerms.getLoanTermVariations() != null + ? loanApplicationTerms.getLoanTermVariations().getExceptionData() + : null; final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetailMinimumData(), - loanApplicationTerms.getLoanTermVariations(), loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc); + expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetailMinimumData(), loanTermVariations, + loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc); final List periods = new ArrayList<>(expectedRepaymentPeriods.size()); prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms); 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 f9b206a69e6..900950ca60b 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 @@ -25,7 +25,7 @@ import java.util.List; import java.util.Optional; import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails; @@ -39,13 +39,13 @@ public interface EMICalculator { @NotNull ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List periods, @NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, - LoanTermVariationsDataWrapper loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc); + List loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc); @NotNull ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel( @NotNull List installments, @NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, - LoanTermVariationsDataWrapper loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc); + List loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc); Optional findRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate dueDate); 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 fa349cbf085..5d09c43682c 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 @@ -23,6 +23,8 @@ import java.math.MathContext; import java.time.LocalDate; import java.time.Year; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -35,8 +37,9 @@ import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.EmiAdjustment; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails; @@ -58,7 +61,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { @NotNull public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List periods, @NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, - LoanTermVariationsDataWrapper loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { + List loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { return generateInterestScheduleModel(periods, LoanScheduleModelRepaymentPeriod::periodFromDate, LoanScheduleModelRepaymentPeriod::periodDueDate, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); @@ -69,7 +72,7 @@ public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel( public ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel( @NotNull List installments, @NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, - LoanTermVariationsDataWrapper loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { + List loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { installments = installments.stream().filter(installment -> !installment.isDownPayment() && !installment.isAdditional()).toList(); return generateInterestScheduleModel(installments, LoanRepaymentScheduleInstallment::getFromDate, LoanRepaymentScheduleInstallment::getDueDate, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, @@ -79,7 +82,7 @@ public ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleM @NotNull private ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(@NotNull List periods, Function from, Function to, @NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, - LoanTermVariationsDataWrapper loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { + List loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) { final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc); final AtomicReference prev = new AtomicReference<>(); List repaymentPeriods = periods.stream().map(e -> { @@ -314,8 +317,8 @@ private void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaym } calculateOutstandingBalance(scheduleModel); calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); - if (onlyOnActualModelShouldApply - && (scheduleModel.loanTermVariations() == null || scheduleModel.loanTermVariations().getDueDateVariation().isEmpty())) { + if (onlyOnActualModelShouldApply && (scheduleModel.loanTermVariations() == null + || scheduleModel.loanTermVariations().get(LoanTermVariationType.DUE_DATE) == null)) { checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods); } } @@ -420,18 +423,124 @@ private void calculateRateFactorForRepaymentPeriod(final RepaymentPeriod repayme final ProgressiveLoanInterestScheduleModel scheduleModel) { repaymentPeriod.getInterestPeriods().forEach(interestPeriod -> { interestPeriod.setRateFactor(calculateRateFactorPerPeriod(scheduleModel, repaymentPeriod, interestPeriod.getFromDate(), - interestPeriod.getDueDate(), false)); - interestPeriod.setRateFactorTillPeriodDueDate(calculateRateFactorPerPeriod(scheduleModel, repaymentPeriod, - interestPeriod.getFromDate(), repaymentPeriod.getDueDate(), true)); + interestPeriod.getDueDate())); + interestPeriod.setRateFactorTillPeriodDueDate(calculateRateFactorPerPeriodForInterest(scheduleModel, repaymentPeriod, + interestPeriod.getFromDate(), repaymentPeriod.getDueDate())); }); } + BigDecimal calculateRateFactorPerPeriodForInterest(final ProgressiveLoanInterestScheduleModel scheduleModel, + final RepaymentPeriod repaymentPeriod, final LocalDate interestPeriodFromDate, final LocalDate interestPeriodDueDate) { + final MathContext mc = scheduleModel.mc(); + final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail(); + final BigDecimal interestRate = calcNominalInterestRatePercentage(scheduleModel.getInterestRate(interestPeriodFromDate), + scheduleModel.mc()); + final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType()); + final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType()); + final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType(); + + final BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(interestPeriodFromDate)); + final BigDecimal actualDaysInPeriod = BigDecimal + .valueOf(DateUtils.getDifferenceInDays(interestPeriodFromDate, interestPeriodDueDate)); + final BigDecimal calculatedDaysInPeriod = BigDecimal + .valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate())); + final int numberOfYearsDifferenceInPeriod = interestPeriodDueDate.getYear() - interestPeriodFromDate.getYear(); + final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0; + + // TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data + // TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30) + // TODO refactor this logic to represent in interest period + if (partialPeriodCalculationNeeded) { + final BigDecimal cumulatedPeriodFractions = calculatePeriodFractions(interestPeriodFromDate, interestPeriodDueDate, mc); + return rateFactorByRepaymentPartialPeriod(interestRate, BigDecimal.ONE, cumulatedPeriodFractions, BigDecimal.ONE, + BigDecimal.ONE, mc); + } + + if (daysInMonthType.equals(DaysInMonthType.ACTUAL)) { + return rateFactorByRepaymentPeriod(interestRate, actualDaysInPeriod, BigDecimal.ONE, daysInYear, BigDecimal.ONE, BigDecimal.ONE, + mc); + } else if (daysInMonthType.isDaysInMonth_30()) { + BigDecimal periodRatio = switch (repaymentFrequency) { + case YEARS -> calculatePeriodRatio(scheduleModel, repaymentPeriod, ChronoUnit.YEARS, mc); + case MONTHS -> calculatePeriodRatio(scheduleModel, repaymentPeriod, ChronoUnit.MONTHS, mc); + case WEEKS -> calculatePeriodRatio(scheduleModel, repaymentPeriod, ChronoUnit.WEEKS, mc); + case DAYS -> calculatePeriodRatio(scheduleModel, repaymentPeriod, ChronoUnit.DAYS, mc); + default -> throw new UnsupportedOperationException("Unsupported repayment frequency: " + repaymentFrequency); + }; + + return calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, periodRatio, + BigDecimal.valueOf(30), daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc); + } + throw new UnsupportedOperationException( + "Unsupported combination: Days in year: " + daysInYearType + ", days in month: " + daysInMonthType); + } + + private static BigDecimal calculatePeriodRatio(ProgressiveLoanInterestScheduleModel scheduleModel, RepaymentPeriod repaymentPeriod, + ChronoUnit chronoUnit, MathContext mc) { + + LocalDate seedDate = calculateSeedDate(scheduleModel, repaymentPeriod); + int numberOfPeriodBetweenSeedDateAndActualRepaymentPeriod = switch (chronoUnit) { + case DAYS, WEEKS, YEARS -> DateUtils.getExactDifference(seedDate, repaymentPeriod.getFromDate(), chronoUnit); + case MONTHS -> { + int seedDateDay = seedDate.getDayOfMonth(); + int targetDateDay = repaymentPeriod.getFromDate().getDayOfMonth(); + int targetDateLastDay = ((LocalDate) TemporalAdjusters.lastDayOfMonth().adjustInto(repaymentPeriod.getFromDate())) + .getDayOfMonth(); + // In case target date is the last day of the month and the seed date day is later than the target date + // day, we need to move it by 1 days + if (targetDateLastDay == targetDateDay && seedDateDay > targetDateDay) { + yield DateUtils.getExactDifference(seedDate, repaymentPeriod.getFromDate().plusDays(1), chronoUnit); + } else { + yield DateUtils.getExactDifference(seedDate, repaymentPeriod.getFromDate(), chronoUnit); + } + } + default -> throw new UnsupportedOperationException("Unsupported chrono unit: " + chronoUnit); + }; + + int multiplicator = numberOfPeriodBetweenSeedDateAndActualRepaymentPeriod + 1; + LocalDate fromDate = repaymentPeriod.getFromDate(); + while (fromDate.isBefore(repaymentPeriod.getDueDate())) { + fromDate = seedDate.plus(multiplicator, chronoUnit); + if (!fromDate.isAfter(repaymentPeriod.getDueDate())) { + multiplicator++; + } else { + LocalDate fullPeriodDate = fromDate; + multiplicator = multiplicator - numberOfPeriodBetweenSeedDateAndActualRepaymentPeriod - 1; + fromDate = seedDate.plus(multiplicator, chronoUnit); + final long differenceInDays = DateUtils.getDifferenceInDays(fromDate, repaymentPeriod.getDueDate()); + final long fullPeriodDifferenceInDays = DateUtils.getDifferenceInDays(fromDate, fullPeriodDate); + return BigDecimal.valueOf(differenceInDays).divide(BigDecimal.valueOf(fullPeriodDifferenceInDays), mc) + .add(BigDecimal.valueOf(multiplicator)); + } + } + multiplicator = multiplicator - numberOfPeriodBetweenSeedDateAndActualRepaymentPeriod - 1; + return BigDecimal.valueOf(multiplicator); + } + + private static LocalDate calculateSeedDate(ProgressiveLoanInterestScheduleModel scheduleModel, RepaymentPeriod repaymentPeriod) { + LocalDate seedDate = scheduleModel.getStartDate(); + LocalDate calculatedDate; + int multiplicator = 1; + ChronoUnit chronoUnit = switch (scheduleModel.loanProductRelatedDetail().getRepaymentPeriodFrequencyType()) { + case YEARS -> ChronoUnit.YEARS; + case MONTHS -> ChronoUnit.MONTHS; + case WEEKS -> ChronoUnit.WEEKS; + case DAYS -> ChronoUnit.DAYS; + default -> throw new UnsupportedOperationException( + "Unsupported repayment frequency: " + scheduleModel.loanProductRelatedDetail().getRepaymentPeriodFrequencyType()); + }; + do { + calculatedDate = seedDate.plus(multiplicator, chronoUnit); + multiplicator++; + } while (calculatedDate.isBefore(repaymentPeriod.getDueDate())); + return calculatedDate.equals(repaymentPeriod.getDueDate()) ? seedDate : repaymentPeriod.getFromDate(); + } + /** * Calculate Rate Factor for an exact Period */ private BigDecimal calculateRateFactorPerPeriod(final ProgressiveLoanInterestScheduleModel scheduleModel, - final RepaymentPeriod repaymentPeriod, final LocalDate interestPeriodFromDate, final LocalDate interestPeriodDueDate, - final boolean isTillDate) { + final RepaymentPeriod repaymentPeriod, final LocalDate interestPeriodFromDate, final LocalDate interestPeriodDueDate) { final MathContext mc = scheduleModel.mc(); final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail(); final BigDecimal interestRate = calcNominalInterestRatePercentage(scheduleModel.getInterestRate(interestPeriodFromDate), @@ -441,7 +550,6 @@ private BigDecimal calculateRateFactorPerPeriod(final ProgressiveLoanInterestSch final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType(); final BigDecimal repaymentEvery = BigDecimal.valueOf(loanProductRelatedDetail.getRepayEvery()); - final BigDecimal daysInMonth = BigDecimal.valueOf(daysInMonthType.getNumberOfDays(interestPeriodFromDate)); final BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(interestPeriodFromDate)); final BigDecimal actualDaysInPeriod = BigDecimal .valueOf(DateUtils.getDifferenceInDays(interestPeriodFromDate, interestPeriodDueDate)); @@ -449,26 +557,24 @@ private BigDecimal calculateRateFactorPerPeriod(final ProgressiveLoanInterestSch .valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate())); final int numberOfYearsDifferenceInPeriod = interestPeriodDueDate.getYear() - interestPeriodFromDate.getYear(); final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0; - - long addedDays = !isTillDate || scheduleModel.loanTermVariations() == null ? 0L - : scheduleModel.loanTermVariations().getDueDateVariation().stream() - .filter(x -> !repaymentPeriod.getFromDate().isAfter(x.getTermVariationApplicableFrom()) - && !repaymentPeriod.getDueDate().isBefore(x.getTermVariationApplicableFrom()) - && !repaymentPeriod.getDueDate().isAfter(x.getDateValue())) - .map(x -> DateUtils.getDifferenceInDays(x.getTermVariationApplicableFrom(), x.getDateValue())) - .reduce(0L, Long::sum); + final BigDecimal daysInMonth = daysInMonthType.isDaysInMonth_30() ? BigDecimal.valueOf(30) : calculatedDaysInPeriod; // TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data // TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30) // TODO refactor this logic to represent in interest period if (partialPeriodCalculationNeeded) { final BigDecimal cumulatedPeriodFractions = calculatePeriodFractions(interestPeriodFromDate, interestPeriodDueDate, mc); - return rateFactorByRepaymentPartialPeriod(interestRate, repaymentEvery, cumulatedPeriodFractions, BigDecimal.ONE, + return rateFactorByRepaymentPartialPeriod(interestRate, BigDecimal.ONE, cumulatedPeriodFractions, BigDecimal.ONE, BigDecimal.ONE, mc); } - return calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery, daysInMonth, - daysInYear, actualDaysInPeriod, calculatedDaysInPeriod.subtract(BigDecimal.valueOf(addedDays), mc), mc); + return switch (daysInMonthType) { + case ACTUAL -> rateFactorByRepaymentPeriod(interestRate, actualDaysInPeriod, BigDecimal.ONE, daysInYear, BigDecimal.ONE, + BigDecimal.ONE, mc); + case DAYS_30 -> calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery, + daysInMonth, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc); + default -> throw new UnsupportedOperationException("Unsupported combination: Days in month: " + daysInMonthType); + }; } /** 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 c3bb6bcaf86..011fe65851d 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 @@ -33,7 +33,6 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; -import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod; @@ -46,7 +45,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; @@ -64,7 +62,6 @@ class ProgressiveEMICalculatorTest { private static MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN); private static LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = Mockito .mock(LoanProductMinimumRepaymentScheduleRelatedDetail.class); - private static LoanTermVariationsDataWrapper loanTermVariations = Mockito.mock(LoanTermVariationsDataWrapper.class); private static final CurrencyData currency = new CurrencyData("USD", "USD", 2, 1, "$", "USD"); @@ -87,11 +84,6 @@ public static void init() { moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN)); } - @BeforeEach - public void setup() { - Mockito.when(loanTermVariations.getDueDateVariation()).thenReturn(new ArrayList<>()); - } - @AfterAll public static void tearDown() { threadLocalContextUtil.close(); @@ -178,7 +170,7 @@ public void test_generateInterestScheduleModel() { Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); Assertions.assertTrue(interestScheduleModel != null); Assertions.assertTrue(interestScheduleModel.loanProductRelatedDetail() != null); @@ -217,7 +209,7 @@ public void test_emi_calculator_performance() { Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -261,7 +253,7 @@ public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() { Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -297,7 +289,7 @@ public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -333,7 +325,7 @@ public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_ Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -381,7 +373,7 @@ public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_d threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -423,7 +415,7 @@ public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt1 threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -470,7 +462,7 @@ public void test_reschedule_interest_on0120_adjsLst_dsbAmt100_dayInYears360_days threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -516,7 +508,7 @@ public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_d threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -562,7 +554,7 @@ public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInM threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -669,7 +661,7 @@ public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -740,7 +732,7 @@ public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -817,7 +809,7 @@ public void test_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_re Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); Money disbursedAmount = toMoney(100); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -866,7 +858,7 @@ public void test_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -915,7 +907,7 @@ public void test_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_re Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount); @@ -945,10 +937,10 @@ public void test_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1M final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)), repayment(2, LocalDate.of(2024, 1, 12), LocalDate.of(2024, 2, 12)), - repayment(3, LocalDate.of(2024, 2, 12), LocalDate.of(2024, 3, 1)), - repayment(4, LocalDate.of(2024, 3, 12), LocalDate.of(2024, 4, 1)), - repayment(5, LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 1)), - repayment(6, LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 1))); + repayment(3, LocalDate.of(2024, 2, 12), LocalDate.of(2024, 3, 12)), + repayment(4, LocalDate.of(2024, 3, 12), LocalDate.of(2024, 4, 12)), + repayment(5, LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 12)), + repayment(6, LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 12))); final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; @@ -961,7 +953,7 @@ public void test_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1M Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount); @@ -997,7 +989,7 @@ public void test_multidisbursement_total_repay1st_dayInYears360_daysInMonth30_re Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), toMoney(300.0)); Assertions.assertEquals(6.15, toDouble(interestSchedule.getTotalDueInterest())); @@ -1039,7 +1031,7 @@ public void test_disbursedAmt1000_NoInterest_repayEvery1Month() { Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(1000.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1072,7 +1064,7 @@ public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1105,7 +1097,7 @@ public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1132,13 +1124,13 @@ public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEver Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); - Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1153,7 +1145,7 @@ public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEver } @Test - public void soma_test_disbursedAmt1000_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() { + public void test_disbursedAmt1000_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() { final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)), @@ -1180,7 +1172,7 @@ public void soma_test_disbursedAmt1000_dayInYears360_daysInMonthDoesntMatter_rep Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(1000.0); LocalDate disbursementDate = LocalDate.of(2024, 1, 1); @@ -1228,7 +1220,7 @@ public void test_dailyInterest_disbursedAmt1000_dayInYears360_daysInMonth30_repa Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(1000.0); emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1290,7 +1282,7 @@ public void test_dailyInterest_disbursedAmt2000_dayInYears360_daysInMonth30_repa Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount1st = toMoney(1000.0); final Money disbursedAmount2nd = toMoney(1000.0); @@ -1363,7 +1355,7 @@ public void test_singleInterestPauseAmt100_dayInYears360_daysInMonth30_repayEver Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1401,7 +1393,7 @@ public void test_interestPauseBetweenTwoPeriodsAmt100_dayInYears360_daysInMonth3 Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1437,6 +1429,7 @@ public void test_interestPauseFirstDayOfMonthAmt100_dayInYears360_daysInMonth30_ Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); @@ -1475,6 +1468,7 @@ public void test_interestPauseLastDayOfMonthAmt100_dayInYears360_daysInMonth30_r Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); @@ -1513,6 +1507,7 @@ public void test_interestPauseWholeMonthAmt100_dayInYears360_daysInMonth30_repay Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); @@ -1551,6 +1546,7 @@ public void test_interestPauseTwoWholeMonthsAmt100_dayInYears360_daysInMonth30_r Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); @@ -1589,6 +1585,7 @@ public void test_interestPauseAndExistedMultipleInterestPeriodsAmt100_dayInYears Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); @@ -1630,6 +1627,7 @@ public void test_interestPauseBetweenRepaymentPeriodsAndExistedMultipleInterestP Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); @@ -1674,6 +1672,7 @@ public void test_interestPauseOnFirstDayOfMonthAndExistedMultipleInterestPeriods Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); @@ -1714,6 +1713,7 @@ public void test_interestPauseBorderedByPeriod_Amt100_dayInYears360_daysInMonth3 Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); @@ -1751,24 +1751,24 @@ public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEve Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanTermVariations.getDueDateVariation()).thenReturn(new ArrayList<>(List.of(// + List loanTermVariationsData = List.of(// new LoanTermVariationsData(1L, LoanTermVariationType.DUE_DATE.getValue(), // LocalDate.of(2024, 2, 1), null, // - LocalDate.of(2024, 3, 1), false)))); + LocalDate.of(2024, 3, 1), false)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariationsData, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 1.13, 15.88, 84.12); - checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 1.13, 15.88, 84.12); - checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.60); - checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.98); - checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.30, 16.71, 34.27); - checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 17.46); - checkPeriod(interestSchedule, 5, 0, 17.56, 0.005833333333, 0.10, 17.46, 0.0); + checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 1.17, 15.84, 84.16); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 1.17, 15.84, 84.16); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.64); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 51.02); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.30, 16.71, 34.31); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 17.50); + checkPeriod(interestSchedule, 5, 0, 17.60, 0.005833333333, 0.10, 17.50, 0.0); } @Test @@ -1790,27 +1790,27 @@ public void test_two_reschedules_disbursedAmt100_dayInYears360_daysInMonth30_rep Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanTermVariations.getDueDateVariation()).thenReturn(new ArrayList<>(List.of(// + List loanTermVariationsData = List.of(// new LoanTermVariationsData(1L, LoanTermVariationType.DUE_DATE.getValue(), // LocalDate.of(2024, 2, 1), null, // LocalDate.of(2024, 3, 1), false), // new LoanTermVariationsData(2L, LoanTermVariationType.DUE_DATE.getValue(), // LocalDate.of(2024, 5, 1), null, // - LocalDate.of(2024, 6, 1), false)))); + LocalDate.of(2024, 6, 1), false)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariationsData, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 1.13, 15.88, 84.12); - checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 1.13, 15.88, 84.12); - checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.60); - checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.80, 16.21, 51.39); - checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.30, 16.71, 34.68); - checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 17.87); - checkPeriod(interestSchedule, 5, 0, 17.97, 0.005833333333, 0.10, 17.87, 0.0); + checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 1.17, 15.84, 84.16); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 1.17, 15.84, 84.16); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.64); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.79, 16.22, 51.42); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.30, 16.71, 34.71); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 17.90); + checkPeriod(interestSchedule, 5, 0, 18.00, 0.005833333333, 0.10, 17.90, 0.0); } @Test @@ -1832,24 +1832,388 @@ public void test_reschedule_partial_period_disbursedAmt100_dayInYears360_daysInM Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanTermVariations.getDueDateVariation()).thenReturn(new ArrayList<>(List.of(// + List loanTermVariationsData = List.of(// new LoanTermVariationsData(1L, LoanTermVariationType.DUE_DATE.getValue(), // LocalDate.of(2024, 3, 1), null, // - LocalDate.of(2024, 4, 15), false)))); + LocalDate.of(2024, 4, 15), false)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( - expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariationsData, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); 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.58, 16.43, 83.57); - checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 1.24, 15.77, 67.80); - checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.40, 16.61, 51.19); - checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.30, 16.71, 34.48); - checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 17.67); - checkPeriod(interestSchedule, 5, 0, 17.77, 0.005833333333, 0.10, 17.67, 0.0); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 1.20, 15.81, 67.76); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.40, 16.61, 51.15); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.30, 16.71, 34.44); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 17.63); + checkPeriod(interestSchedule, 5, 0, 17.73, 0.005833333333, 0.10, 17.63, 0.0); + } + + @Test + public void test_actual_actual_repayment_schedule_across_multiple_years() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2023, 11, 13), LocalDate.of(2023, 12, 13)), + repayment(2, LocalDate.of(2023, 12, 13), LocalDate.of(2024, 1, 13)), + repayment(3, LocalDate.of(2024, 1, 13), LocalDate.of(2024, 2, 13)), + repayment(4, LocalDate.of(2024, 2, 13), LocalDate.of(2024, 3, 13)), + repayment(5, LocalDate.of(2024, 3, 13), LocalDate.of(2024, 4, 13)), + repayment(6, LocalDate.of(2024, 4, 13), LocalDate.of(2024, 5, 13))); + + final BigDecimal interestRate = BigDecimal.valueOf(9.99); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(5000.0); + LocalDate disbursementDate = LocalDate.of(2023, 11, 13); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + checkPeriod(interestSchedule, 0, 857.71, 41.05, 816.66, 4183.34, false); + checkPeriod(interestSchedule, 1, 857.71, 35.45, 822.26, 3361.08, false); + checkPeriod(interestSchedule, 2, 857.71, 28.44, 829.27, 2531.81, false); + checkPeriod(interestSchedule, 3, 857.71, 20.04, 837.67, 1694.14, false); + checkPeriod(interestSchedule, 4, 857.71, 14.33, 843.38, 850.76, false); + checkPeriod(interestSchedule, 5, 857.73, 6.97, 850.76, 0, false); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2023, 12, 13), LocalDate.of(2023, 12, 10), + Money.of(currency, BigDecimal.valueOf(820.76))); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2023, 12, 13), LocalDate.of(2023, 12, 10), + Money.of(currency, BigDecimal.valueOf(36.95))); + + checkPeriod(interestSchedule, 0, 857.71, 36.95, 820.76, 4179.24, true); + checkPeriod(interestSchedule, 1, 857.71, 38.85, 818.86, 3360.38, false); + checkPeriod(interestSchedule, 2, 857.71, 28.43, 829.28, 2531.1, false); + checkPeriod(interestSchedule, 3, 857.71, 20.04, 837.67, 1693.43, false); + checkPeriod(interestSchedule, 4, 857.71, 14.33, 843.38, 850.05, false); + checkPeriod(interestSchedule, 5, 857.01, 6.96, 850.05, 0, false); + } + + @Test + public void test_actual_actual_repayment_schedule_across_multiple_years_overdue() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2023, 11, 13), LocalDate.of(2023, 12, 13)), + repayment(2, LocalDate.of(2023, 12, 13), LocalDate.of(2024, 1, 13)), + repayment(3, LocalDate.of(2024, 1, 13), LocalDate.of(2024, 2, 13)), + repayment(4, LocalDate.of(2024, 2, 13), LocalDate.of(2024, 3, 13)), + repayment(5, LocalDate.of(2024, 3, 13), LocalDate.of(2024, 4, 13)), + repayment(6, LocalDate.of(2024, 4, 13), LocalDate.of(2024, 5, 13))); + + final BigDecimal interestRate = BigDecimal.valueOf(9.99); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(5000.0); + LocalDate disbursementDate = LocalDate.of(2023, 11, 13); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + for (int i = 0; i < 6; i++) { + LocalDate dueDate = expectedRepaymentPeriods.get(i).getDueDate(); + PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, dueDate, dueDate); + Money duePrincipal = dueAmounts.getDuePrincipal(); + emiCalculator.addBalanceCorrection(interestSchedule, dueDate, duePrincipal); + } + + checkPeriod(interestSchedule, 0, 857.71, 41.05, 816.66, 5000.0, false); + checkPeriod(interestSchedule, 1, 857.71, 42.37, 815.34, 5000.0, false); + checkPeriod(interestSchedule, 2, 857.71, 42.31, 815.4, 5000.0, false); + checkPeriod(interestSchedule, 3, 857.71, 39.58, 818.13, 5000.0, false); + checkPeriod(interestSchedule, 4, 857.71, 42.31, 815.4, 5000.0, false); + checkPeriod(interestSchedule, 5, 960.01, 40.94, 919.07, 5000.0, false); + } + + @Test + public void test_repayment_actual_actual_repayment_schedule_across_multiple_years_overdue() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2023, 11, 13), LocalDate.of(2023, 12, 13)), + repayment(2, LocalDate.of(2023, 12, 13), LocalDate.of(2024, 1, 13)), + repayment(3, LocalDate.of(2024, 1, 13), LocalDate.of(2024, 2, 13)), + repayment(4, LocalDate.of(2024, 2, 13), LocalDate.of(2024, 3, 13)), + repayment(5, LocalDate.of(2024, 3, 13), LocalDate.of(2024, 4, 13)), + repayment(6, LocalDate.of(2024, 4, 13), LocalDate.of(2024, 5, 13))); + + final BigDecimal interestRate = BigDecimal.valueOf(9.99); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(5000.0); + LocalDate disbursementDate = LocalDate.of(2023, 11, 13); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + emiCalculator.payInterest(interestSchedule, LocalDate.of(2023, 12, 13), LocalDate.of(2023, 12, 10), + Money.of(currency, BigDecimal.valueOf(36.95))); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2023, 12, 13), LocalDate.of(2023, 12, 10), + Money.of(currency, BigDecimal.valueOf(820.76))); + + for (int i = 1; i < 6; i++) { + LocalDate dueDate = expectedRepaymentPeriods.get(i).getDueDate(); + PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, dueDate, dueDate); + Money duePrincipal = dueAmounts.getDuePrincipal(); + emiCalculator.addBalanceCorrection(interestSchedule, dueDate, duePrincipal); + } + + checkPeriod(interestSchedule, 0, 857.71, 36.95, 820.76, 4179.24, true); + checkPeriod(interestSchedule, 1, 857.71, 38.85, 818.86, 4179.24, false); + checkPeriod(interestSchedule, 2, 857.71, 35.36, 822.35, 4179.24, false); + checkPeriod(interestSchedule, 3, 857.71, 33.08, 824.63, 4179.24, false); + checkPeriod(interestSchedule, 4, 857.71, 35.36, 822.35, 4179.24, false); + checkPeriod(interestSchedule, 5, 925.27, 34.22, 891.05, 4179.24, false); + } + + @Test + public void test_360_30_repayment_schedule_disbursement_month_end() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2023, 10, 31), LocalDate.of(2023, 11, 30)), + repayment(2, LocalDate.of(2023, 11, 30), LocalDate.of(2023, 12, 31)), + repayment(3, LocalDate.of(2023, 12, 31), LocalDate.of(2024, 1, 31)), + repayment(4, LocalDate.of(2024, 1, 31), LocalDate.of(2024, 2, 29)), + repayment(5, LocalDate.of(2024, 2, 29), LocalDate.of(2024, 3, 31)), + repayment(6, LocalDate.of(2024, 3, 31), LocalDate.of(2024, 4, 30))); + + final BigDecimal interestRate = BigDecimal.valueOf(9.99); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(2450.0); + LocalDate disbursementDate = LocalDate.of(2023, 10, 31); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + checkPeriod(interestSchedule, 0, 420.31, 20.40, 399.91, 2050.09, false); + checkPeriod(interestSchedule, 1, 420.31, 17.07, 403.24, 1646.85, false); + checkPeriod(interestSchedule, 2, 420.31, 13.71, 406.60, 1240.25, false); + checkPeriod(interestSchedule, 3, 420.31, 10.33, 409.98, 830.27, false); + checkPeriod(interestSchedule, 4, 420.31, 6.91, 413.40, 416.87, false); + checkPeriod(interestSchedule, 5, 420.34, 3.47, 416.87, 0.00, false); + } + + @Test + public void test_360_30_repayment_schedule_disbursement_near_month_end() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2023, 10, 30), LocalDate.of(2023, 11, 30)), + repayment(2, LocalDate.of(2023, 11, 30), LocalDate.of(2023, 12, 30)), + repayment(3, LocalDate.of(2023, 12, 30), LocalDate.of(2024, 1, 30)), + repayment(4, LocalDate.of(2024, 1, 30), LocalDate.of(2024, 2, 29)), + repayment(5, LocalDate.of(2024, 2, 29), LocalDate.of(2024, 3, 30)), + repayment(6, LocalDate.of(2024, 3, 30), LocalDate.of(2024, 4, 30))); + + final BigDecimal interestRate = BigDecimal.valueOf(45.0); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(10000.0); + LocalDate disbursementDate = LocalDate.of(2023, 10, 30); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + checkPeriod(interestSchedule, 0, 1892.12, 375.00, 1517.12, 8482.88, false); + checkPeriod(interestSchedule, 1, 1892.12, 318.11, 1574.01, 6908.87, false); + checkPeriod(interestSchedule, 2, 1892.12, 259.08, 1633.04, 5275.83, false); + checkPeriod(interestSchedule, 3, 1892.12, 197.84, 1694.28, 3581.55, false); + checkPeriod(interestSchedule, 4, 1892.12, 134.31, 1757.81, 1823.74, false); + checkPeriod(interestSchedule, 5, 1892.13, 68.39, 1823.74, 0.00, false); + } + + @Test + public void test_360_30_repayment_schedule_disbursement_repay_every_2_months() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2023, 10, 29), LocalDate.of(2023, 12, 29)), + repayment(2, LocalDate.of(2023, 12, 29), LocalDate.of(2024, 2, 29)), + repayment(3, LocalDate.of(2024, 2, 29), LocalDate.of(2024, 4, 29)), + repayment(4, LocalDate.of(2024, 4, 29), LocalDate.of(2024, 6, 29)), + repayment(5, LocalDate.of(2024, 6, 29), LocalDate.of(2024, 8, 29)), + repayment(6, LocalDate.of(2024, 8, 29), LocalDate.of(2024, 10, 29))); + + final BigDecimal interestRate = BigDecimal.valueOf(19.99); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(3333.0); + LocalDate disbursementDate = LocalDate.of(2023, 10, 29); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + checkPeriod(interestSchedule, 0, 622.04, 111.04, 511.00, 2822.00, false); + checkPeriod(interestSchedule, 1, 622.04, 94.02, 528.02, 2293.98, false); + checkPeriod(interestSchedule, 2, 622.04, 76.43, 545.61, 1748.37, false); + checkPeriod(interestSchedule, 3, 622.04, 58.25, 563.79, 1184.58, false); + checkPeriod(interestSchedule, 4, 622.04, 39.47, 582.57, 602.01, false); + checkPeriod(interestSchedule, 5, 622.07, 20.06, 602.01, 0.00, false); + } + + @Test + public void test_actual_actual_repayment_schedule_disbursement_month_end() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2023, 10, 31), LocalDate.of(2023, 11, 30)), + repayment(2, LocalDate.of(2023, 11, 30), LocalDate.of(2023, 12, 31)), + repayment(3, LocalDate.of(2023, 12, 31), LocalDate.of(2024, 1, 31)), + repayment(4, LocalDate.of(2024, 1, 31), LocalDate.of(2024, 2, 29)), + repayment(5, LocalDate.of(2024, 2, 29), LocalDate.of(2024, 3, 31)), + repayment(6, LocalDate.of(2024, 3, 31), LocalDate.of(2024, 4, 30))); + + final BigDecimal interestRate = BigDecimal.valueOf(45.0); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(245000.0); + LocalDate disbursementDate = LocalDate.of(2023, 10, 31); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + checkPeriod(interestSchedule, 0, 46343.27, 9061.64, 37281.63, 207718.37, false); + checkPeriod(interestSchedule, 1, 46343.27, 7938.83, 38404.44, 169313.93, false); + checkPeriod(interestSchedule, 2, 46343.27, 6453.36, 39889.91, 129424.02, false); + checkPeriod(interestSchedule, 3, 46343.27, 4614.71, 41728.56, 87695.46, false); + checkPeriod(interestSchedule, 4, 46343.27, 3342.49, 43000.78, 44694.68, false); + checkPeriod(interestSchedule, 5, 46343.25, 1648.57, 44694.68, 0.00, false); + } + + @Test + public void test_actual_actual_repayment_schedule_disbursement_near_month_end() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2021, 10, 30), LocalDate.of(2021, 11, 30)), + repayment(2, LocalDate.of(2021, 11, 30), LocalDate.of(2021, 12, 30)), + repayment(3, LocalDate.of(2021, 12, 30), LocalDate.of(2022, 1, 30)), + repayment(4, LocalDate.of(2022, 1, 30), LocalDate.of(2022, 2, 28)), + repayment(5, LocalDate.of(2022, 2, 28), LocalDate.of(2022, 3, 30)), + repayment(6, LocalDate.of(2022, 3, 30), LocalDate.of(2022, 4, 30))); + + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(1500.0); + LocalDate disbursementDate = LocalDate.of(2021, 10, 30); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + checkPeriod(interestSchedule, 0, 256.95, 12.08, 244.87, 1255.13, false); + checkPeriod(interestSchedule, 1, 256.95, 9.78, 247.17, 1007.96, false); + checkPeriod(interestSchedule, 2, 256.95, 8.12, 248.83, 759.13, false); + checkPeriod(interestSchedule, 3, 256.95, 5.72, 251.23, 507.90, false); + checkPeriod(interestSchedule, 4, 256.95, 3.96, 252.99, 254.91, false); + checkPeriod(interestSchedule, 5, 256.96, 2.05, 254.91, 0.00, false); + } + + @Test + public void test_actual_actual_repayment_schedule_disbursement_near_month_end_repay_every_2_months() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2022, 10, 29), LocalDate.of(2022, 12, 29)), + repayment(2, LocalDate.of(2022, 12, 29), LocalDate.of(2023, 2, 28)), + repayment(3, LocalDate.of(2023, 2, 28), LocalDate.of(2023, 4, 29)), + repayment(4, LocalDate.of(2023, 4, 29), LocalDate.of(2023, 6, 29)), + repayment(5, LocalDate.of(2023, 6, 29), LocalDate.of(2023, 8, 29)), + repayment(6, LocalDate.of(2023, 8, 29), LocalDate.of(2023, 10, 29))); + + final BigDecimal interestRate = BigDecimal.valueOf(7); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(5000); + LocalDate disbursementDate = LocalDate.of(2022, 10, 29); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + checkPeriod(interestSchedule, 0, 867.68, 58.49, 809.19, 4190.81, false); + checkPeriod(interestSchedule, 1, 867.68, 49.03, 818.65, 3372.16, false); + checkPeriod(interestSchedule, 2, 867.68, 38.80, 828.88, 2543.28, false); + checkPeriod(interestSchedule, 3, 867.68, 29.75, 837.93, 1705.35, false); + checkPeriod(interestSchedule, 4, 867.68, 19.95, 847.73, 857.62, false); + checkPeriod(interestSchedule, 5, 867.65, 10.03, 857.62, 0.00, false); } private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {