From 582cfdefb32511dc6094ef099abdf8875765337e Mon Sep 17 00:00:00 2001 From: Janos Meszaros Date: Tue, 29 Oct 2024 10:44:52 +0100 Subject: [PATCH] FINERACT-2114: Interest rate modification - Advanced payment allocation processor support --- .../core/service/DateUtils.java | 5 + .../domain/LoanTermVariationType.java | 8 +- ...RepaymentScheduleTransactionProcessor.java | 2 +- ...edPaymentScheduleTransactionProcessor.java | 67 +++- ...rTransaction.java => ChangeOperation.java} | 54 +++- .../loanschedule/data/InterestRate.java | 6 +- .../ProgressiveLoanInterestScheduleModel.java | 30 +- .../loanschedule/data/RepaymentPeriod.java | 6 +- .../ProgressiveLoanScheduleGenerator.java | 66 ++-- .../calc/ProgressiveEMICalculator.java | 39 ++- .../impl/ChangeOperationTest.java | 159 +++++++++ .../impl/ChargeOrTransactionTest.java | 127 -------- .../calc/ProgressiveEMICalculatorTest.java | 302 ++++++++++-------- .../service/reaging/LoanReAgingValidator.java | 4 +- .../LoanReAmortizationValidator.java | 4 +- ...ntAllocationLoanRepaymentScheduleTest.java | 119 +++++++ 16 files changed, 634 insertions(+), 364 deletions(-) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/{ChargeOrTransaction.java => ChangeOperation.java} (73%) create mode 100644 fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperationTest.java delete mode 100644 fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index be86204ab2e..f151a37abc2 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -79,6 +79,11 @@ public static OffsetDateTime getOffsetDateTimeOfTenant(ChronoUnit truncate) { return truncate == null ? now : now.truncatedTo(truncate); } + @NotNull + public static OffsetDateTime getOffsetDateTimeOfTenantFromLocalDate(@NotNull final LocalDate date) { + return OffsetDateTime.of(date.atStartOfDay(), getOffsetDateTimeOfTenant().getOffset()); + } + public static LocalDateTime getLocalDateTimeOfSystem() { return getLocalDateTimeOfSystem(null); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java index a8c6b0feecf..7a462c00458 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java @@ -26,9 +26,11 @@ public enum LoanTermVariationType { PRINCIPAL_AMOUNT(3, "loanTermType.principalAmount"), // DUE_DATE(4, "loanTermType.dueDate"), // INSERT_INSTALLMENT(5, "loanTermType.insertInstallment"), // - DELETE_INSTALLMENT(6, "loanTermType.deleteInstallment"), GRACE_ON_INTEREST(7, "loanTermType.graceOnInterest"), GRACE_ON_PRINCIPAL(8, - "loanTermType.graceOnPrincipal"), EXTEND_REPAYMENT_PERIOD(9, - "loanTermType.extendRepaymentPeriod"), INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment"); + DELETE_INSTALLMENT(6, "loanTermType.deleteInstallment"), // + GRACE_ON_INTEREST(7, "loanTermType.graceOnInterest"), // + GRACE_ON_PRINCIPAL(8, "loanTermType.graceOnPrincipal"), // + EXTEND_REPAYMENT_PERIOD(9, "loanTermType.extendRepaymentPeriod"), // + INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment"); // private final Integer value; private final String code; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 73d909b05bc..da674327749 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -478,7 +478,7 @@ private boolean isObligationsMetOnDisbursementDate(LocalDate disbursementDate, && disbursementDate.equals(loanRepaymentScheduleInstallment.getObligationsMetOnDate()); } - private boolean isNotObligationsMet(LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment) { + protected boolean isNotObligationsMet(LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment) { return !loanRepaymentScheduleInstallment.isObligationsMet() && loanRepaymentScheduleInstallment.getObligationsMetOnDate() == null; } 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 2852da4bace..6fa0875e3ad 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 @@ -63,6 +63,8 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.data.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; @@ -71,6 +73,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; @@ -169,8 +172,6 @@ public Pair repr currentInstallment.updateObligationsMet(currency, disbursementDate); } - List chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges); - MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency)); final Loan loan = loanTransactions.get(0).getLoan(); final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf(); @@ -180,17 +181,26 @@ public Pair repr ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail, scheduleModel); + LoanTermVariationsDataWrapper loanTermVariations = Optional + .ofNullable(loan.getActiveLoanTermVariations()).map(loanTermVariationsSet -> loanTermVariationsSet.stream() + .map(LoanTermVariations::toData).collect(Collectors.toCollection(ArrayList::new))) + .map(LoanTermVariationsDataWrapper::new).orElse(null); + List changeOperations = createSortedChangeList(loanTermVariations, loanTransactions, charges); + List overpaidTransactions = new ArrayList<>(); - for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) { - if (chargeOrTransaction.isTransaction()) { - LoanTransaction transaction = chargeOrTransaction.getLoanTransaction().get(); + for (final ChangeOperation changeOperation : changeOperations) { + if (changeOperation.isInterestRateChange()) { + final LoanTermVariationsData interestRateChange = changeOperation.getInterestRateChange().get(); + processInterestRateChange(installments, interestRateChange, scheduleModel); + } else if (changeOperation.isTransaction()) { + LoanTransaction transaction = changeOperation.getLoanTransaction().get(); processSingleTransaction(transaction, ctx); transaction = getProcessedTransaction(changedTransactionDetail, transaction); if (transaction.isOverPaid() && transaction.isRepaymentLikeType()) { // TODO CREDIT, DEBIT overpaidTransactions.add(transaction); } } else { - LoanCharge loanCharge = chargeOrTransaction.getLoanCharge().get(); + LoanCharge loanCharge = changeOperation.getLoanCharge().get(); processSingleCharge(loanCharge, currency, installments, disbursementDate); if (!loanCharge.isFullyPaid() && !overpaidTransactions.isEmpty()) { overpaidTransactions = processOverpaidTransactions(overpaidTransactions, currency, installments, charges, @@ -205,13 +215,37 @@ public Pair repr createNewTransaction(oldTransaction, newTransaction, ctx); } recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx); - List txs = chargeOrTransactions.stream() // - .filter(ChargeOrTransaction::isTransaction) // + List txs = changeOperations.stream() // + .filter(ChangeOperation::isTransaction) // .map(e -> e.getLoanTransaction().get()).toList(); reprocessInstallments(disbursementDate, txs, installments, currency); return Pair.of(changedTransactionDetail, scheduleModel); } + private void processInterestRateChange(final List installments, + final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) { + final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); + final BigDecimal newInterestRate = interestRateChange.getDecimalValue(); + emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate); + processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments); + } + + private void processInterestRateChangeOnInstallments(final ProgressiveLoanInterestScheduleModel scheduleModel, + final LocalDate interestRateChangeSubmittedOnDate, final List installments) { + installments.stream() // + .filter(installment -> isNotObligationsMet(installment) + && !interestRateChangeSubmittedOnDate.isAfter(installment.getDueDate())) + .forEach(installment -> updateInstallmentIfInterestPeriodPresent(scheduleModel, installment)); // + } + + private void updateInstallmentIfInterestPeriodPresent(final ProgressiveLoanInterestScheduleModel scheduleModel, + final LoanRepaymentScheduleInstallment installment) { + emiCalculator.findRepaymentPeriod(scheduleModel, installment.getDueDate()).ifPresent(interestRepaymentPeriod -> { + installment.updateInterestCharged(interestRepaymentPeriod.getDueInterest().getAmount()); + installment.updatePrincipal(interestRepaymentPeriod.getDuePrincipal().getAmount()); + }); + } + @Override public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, MonetaryCurrency currency, List installments, Set charges) { @@ -736,17 +770,20 @@ private void processSingleCharge(LoanCharge loanCharge, MonetaryCurrency currenc } @NotNull - private List createSortedChargesAndTransactionsList(List loanTransactions, - Set charges) { - List chargeOrTransactions = new ArrayList<>(); + private List createSortedChangeList(final LoanTermVariationsDataWrapper loanTermVariations, + final List loanTransactions, final Set charges) { + List changeOperations = new ArrayList<>(); + if (loanTermVariations != null && !loanTermVariations.getInterestRateFromInstallment().isEmpty()) { + changeOperations.addAll(loanTermVariations.getInterestRateFromInstallment().stream().map(ChangeOperation::new).toList()); + } if (charges != null) { - chargeOrTransactions.addAll(charges.stream().map(ChargeOrTransaction::new).toList()); + changeOperations.addAll(charges.stream().map(ChangeOperation::new).toList()); } if (loanTransactions != null) { - chargeOrTransactions.addAll(loanTransactions.stream().map(ChargeOrTransaction::new).toList()); + changeOperations.addAll(loanTransactions.stream().map(ChangeOperation::new).toList()); } - Collections.sort(chargeOrTransactions); - return chargeOrTransactions; + Collections.sort(changeOperations); + return changeOperations; } private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperation.java similarity index 73% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperation.java index 504719b6712..16469f8a0d6 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperation.java @@ -24,26 +24,40 @@ import java.util.Optional; import lombok.Getter; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.jetbrains.annotations.NotNull; @Getter -public class ChargeOrTransaction implements Comparable { +public class ChangeOperation implements Comparable { + private final Optional interestRateChange; private final Optional loanCharge; private final Optional loanTransaction; - public ChargeOrTransaction(LoanCharge loanCharge) { + public ChangeOperation(LoanCharge loanCharge) { + this.interestRateChange = Optional.empty(); this.loanCharge = Optional.of(loanCharge); this.loanTransaction = Optional.empty(); } - public ChargeOrTransaction(LoanTransaction loanTransaction) { + public ChangeOperation(LoanTransaction loanTransaction) { + this.interestRateChange = Optional.empty(); this.loanTransaction = Optional.of(loanTransaction); this.loanCharge = Optional.empty(); } + public ChangeOperation(LoanTermVariationsData interestRateChange) { + this.interestRateChange = Optional.of(interestRateChange); + this.loanTransaction = Optional.empty(); + this.loanCharge = Optional.empty(); + } + + public boolean isInterestRateChange() { + return interestRateChange.isPresent(); + } + public boolean isTransaction() { return loanTransaction.isPresent(); } @@ -52,8 +66,18 @@ public boolean isCharge() { return loanCharge.isPresent(); } + private boolean isAccrualActivity() { + return isTransaction() && loanTransaction.get().isAccrualActivity(); + } + + private boolean isBackdatedCharge() { + return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate()); + } + private LocalDate getEffectiveDate() { - if (loanCharge.isPresent()) { + if (interestRateChange.isPresent()) { + return getSubmittedOnDate(); + } else if (loanCharge.isPresent()) { if (isBackdatedCharge()) { return loanCharge.get().getDueDate(); } else { @@ -66,16 +90,10 @@ private LocalDate getEffectiveDate() { } } - private boolean isAccrualActivity() { - return isTransaction() && loanTransaction.get().isAccrualActivity(); - } - - private boolean isBackdatedCharge() { - return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate()); - } - private LocalDate getSubmittedOnDate() { - if (loanCharge.isPresent()) { + if (interestRateChange.isPresent()) { + return interestRateChange.get().getTermVariationApplicableFrom(); + } else if (loanCharge.isPresent()) { return loanCharge.get().getSubmittedOnDate(); } else if (loanTransaction.isPresent()) { return loanTransaction.get().getSubmittedOnDate(); @@ -85,7 +103,9 @@ private LocalDate getSubmittedOnDate() { } private OffsetDateTime getCreatedDateTime() { - if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) { + if (interestRateChange.isPresent()) { + return DateUtils.getOffsetDateTimeOfTenantFromLocalDate(getSubmittedOnDate()); + } else if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) { return loanCharge.get().getCreatedDate().get(); } else if (loanTransaction.isPresent()) { return loanTransaction.get().getCreatedDateTime(); @@ -96,16 +116,16 @@ private OffsetDateTime getCreatedDateTime() { @Override @SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "TODO: fix this! See: https://stackoverflow.com/questions/2609037/findbugs-how-to-solve-eq-compareto-use-object-equals") - public int compareTo(@NotNull ChargeOrTransaction o) { + public int compareTo(@NotNull ChangeOperation o) { int datePortion = DateUtils.compare(this.getEffectiveDate(), o.getEffectiveDate()); if (datePortion == 0) { - boolean isAccrual = isAccrualActivity(); + final boolean isAccrual = isAccrualActivity(); if (isAccrual != o.isAccrualActivity()) { return isAccrual ? 1 : -1; } int submittedDate = DateUtils.compare(getSubmittedOnDate(), o.getSubmittedOnDate()); if (submittedDate == 0) { - return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime()); + return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime(), null); } return submittedDate; } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java index bf5fe3474ce..50e78716aac 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java @@ -22,9 +22,9 @@ import java.time.LocalDate; import org.jetbrains.annotations.NotNull; -public record InterestRate(LocalDate effectiveFrom, // - LocalDate validFrom, // - BigDecimal interestRate // +public record InterestRate(// + LocalDate effectiveFrom, // + BigDecimal interestRate// ) implements Comparable { @Override 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 9f078260339..70b0ca0a83e 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 @@ -23,8 +23,10 @@ import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.TreeSet; import java.util.function.Consumer; import lombok.Data; import lombok.experimental.Accessors; @@ -36,7 +38,7 @@ public class ProgressiveLoanInterestScheduleModel { private final List repaymentPeriods; - private final List interestRates; + private final TreeSet interestRates; private final LoanProductRelatedDetail loanProductRelatedDetail; private final Integer installmentAmountInMultiplesOf; private MathContext mc; @@ -44,17 +46,17 @@ public class ProgressiveLoanInterestScheduleModel { public ProgressiveLoanInterestScheduleModel(List repaymentPeriods, LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) { this.repaymentPeriods = repaymentPeriods; - this.interestRates = new ArrayList<>(); + this.interestRates = new TreeSet<>(Collections.reverseOrder()); this.loanProductRelatedDetail = loanProductRelatedDetail; this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf; this.mc = mc; } - private ProgressiveLoanInterestScheduleModel(List repaymentPeriods, final List interestRates, + private ProgressiveLoanInterestScheduleModel(List repaymentPeriods, final TreeSet interestRates, LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) { this.mc = mc; this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods); - this.interestRates = new ArrayList<>(interestRates); + this.interestRates = new TreeSet<>(interestRates); this.loanProductRelatedDetail = loanProductRelatedDetail; this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf; } @@ -80,8 +82,15 @@ public BigDecimal getInterestRate(final LocalDate effectiveDate) { } private BigDecimal findInterestRate(final LocalDate effectiveDate) { - return interestRates.stream().filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)).map(InterestRate::interestRate).findFirst() - .orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); + return interestRates.stream() // + .filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)) // + .map(InterestRate::interestRate) // + .findFirst() // + .orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); // + } + + public void addInterestRate(final LocalDate newInterestEffectiveDate, final BigDecimal newInterestRate) { + interestRates.add(new InterestRate(newInterestEffectiveDate, newInterestRate)); } public Optional findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) { @@ -128,11 +137,12 @@ Optional findRepaymentPeriodForBalanceChange(final LocalDate ba } return repaymentPeriods.stream()// .filter(repaymentPeriod -> { - if (repaymentPeriod.getPrevious().isPresent()) { - return balanceChangeDate.isAfter(repaymentPeriod.getFromDate()) + final boolean isFirstPeriod = repaymentPeriod.getPrevious().isEmpty(); + if (isFirstPeriod) { + return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate()) && !balanceChangeDate.isAfter(repaymentPeriod.getDueDate()); } else { - return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate()) + return balanceChangeDate.isAfter(repaymentPeriod.getFromDate()) && !balanceChangeDate.isAfter(repaymentPeriod.getDueDate()); } })// @@ -183,7 +193,7 @@ void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate repaymentPeriod.getInterestPeriods().add(interestPeriod); } - private Money getZero() { + public Money getZero() { return Money.zero(loanProductRelatedDetail.getCurrency(), mc); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java index 3a21b51b384..2a393522704 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java @@ -127,8 +127,12 @@ public Money getCalculatedDuePrincipal() { return getEmi().minus(getCalculatedDueInterest(), mc); } + public Money getTotalPaidAmount() { + return getPaidPrincipal().plus(getPaidInterest()); + } + public boolean isFullyPaid() { - return getEmi().isEqualTo(getPaidPrincipal().plus(getPaidInterest())); + return getEmi().isEqualTo(getTotalPaidAmount()); } public Money getDueInterest() { 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 bb4b69e3d25..411e0c71b04 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 @@ -35,7 +35,6 @@ 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; @@ -61,27 +60,19 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { private final ScheduledDateGenerator scheduledDateGenerator; private final EMICalculator emiCalculator; + public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) { + LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc); + return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null)); + } + @Override public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final Set loanCharges, final HolidayDetailDTO holidayDetailDTO) { final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency(); - // generate list of proposed schedule due dates - LocalDate loanEndDate = scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO); - if (loanApplicationTerms.getLoanTermVariations() != null) { - LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations() - .fetchLoanTermDueDateVariationsData(loanEndDate); - if (lastDueDateVariation != null) { - loanEndDate = lastDueDateVariation.getDateValue(); - } - } - // determine the total charges due at time of disbursement final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges); - // setup variables for tracking important facts required for loan - // schedule generation. - final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType()) ? loanApplicationTerms.getExpectedDisbursementDate() @@ -94,6 +85,7 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer // set and handled separately after all installments generated final Set nonCompoundingCharges = separateTotalCompoundingPercentageCharges(loanCharges); + // generate list of proposed schedule due dates final List expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc, periodStartDate, loanApplicationTerms, holidayDetailDTO); final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel( @@ -102,29 +94,17 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer final List periods = new ArrayList<>(expectedRepaymentPeriods.size()); prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms); - - final ArrayList disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas()); - disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate)); + final List disbursementDataList = getSortedDisbursementList(loanApplicationTerms); for (LoanScheduleModelRepaymentPeriod repaymentPeriod : expectedRepaymentPeriods) { scheduleParams.setPeriodStartDate(repaymentPeriod.getFromDate()); scheduleParams.setActualRepaymentDate(repaymentPeriod.getDueDate()); - + // in same repayment period the logic firstly applies interest rate changes and just after the disbursements + applyInterestRateChangesOnPeriod(loanApplicationTerms, repaymentPeriod, interestScheduleModel); processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods, chargesDueAtTimeOfDisbursement, false, mc); repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber()); - if (loanApplicationTerms.getLoanTermVariations() != null) { - for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) { - final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); - final BigDecimal newInterestRate = interestRateChange.getDecimalValue(); - if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate()) - && !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) { - emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate); - } - } - } - emiCalculator.findRepaymentPeriod(interestScheduleModel, repaymentPeriod.getDueDate()).ifPresent(interestRepaymentPeriod -> { final Money principalDue = interestRepaymentPeriod.getDuePrincipal(); final Money interestDue = interestRepaymentPeriod.getDueInterest(); @@ -166,10 +146,24 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding); } - public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) { + private List getSortedDisbursementList(LoanApplicationTerms loanApplicationTerms) { + final List disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas()); + disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate)); + return disbursementDataList; + } - LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc); - return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null)); + private void applyInterestRateChangesOnPeriod(final LoanApplicationTerms loanApplicationTerms, + final LoanScheduleModelRepaymentPeriod repaymentPeriod, final ProgressiveLoanInterestScheduleModel interestScheduleModel) { + if (loanApplicationTerms.getLoanTermVariations() != null) { + for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) { + final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); + final BigDecimal newInterestRate = interestRateChange.getDecimalValue(); + if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate()) + && !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) { + emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate); + } + } + } } private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) { @@ -181,10 +175,10 @@ private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTer } } - private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, - final ArrayList disbursementDataList, final LoanScheduleParams scheduleParams, - final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List periods, - final BigDecimal chargesDueAtTimeOfDisbursement, final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) { + private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final List disbursementDataList, + final LoanScheduleParams scheduleParams, final ProgressiveLoanInterestScheduleModel interestScheduleModel, + final List periods, final BigDecimal chargesDueAtTimeOfDisbursement, + final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) { for (DisbursementData disbursementData : disbursementDataList) { final LocalDate disbursementDate = disbursementData.disbursementDate(); 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 1898e0dac52..97b1d3bcc1b 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 @@ -81,16 +81,14 @@ public Optional findRepaymentPeriod(final ProgressiveLoanIntere @Override public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate, final Money disbursedAmount) { - scheduleModel - .changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount, - Money.zero(disbursedAmount.getCurrency(), scheduleModel.mc())) + scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount, scheduleModel.getZero()) .ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors( getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, disbursementDueDate), scheduleModel)); } private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestScheduleModel scheduleModel, - final RepaymentPeriod changedRepaymentPeriod, final LocalDate disbursementDueDate) { - final boolean isRelatedToNextRepaymentPeriod = changedRepaymentPeriod.getDueDate().isEqual(disbursementDueDate); + final RepaymentPeriod changedRepaymentPeriod, final LocalDate operationDueDate) { + final boolean isRelatedToNextRepaymentPeriod = changedRepaymentPeriod.getDueDate().isEqual(operationDueDate); if (isRelatedToNextRepaymentPeriod) { final Optional nextRepaymentPeriod = scheduleModel.repaymentPeriods().stream() .filter(repaymentPeriod -> changedRepaymentPeriod.equals(repaymentPeriod.getPrevious().orElse(null))).findFirst(); @@ -106,14 +104,20 @@ private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestSche @Override public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate, final BigDecimal newInterestRate) { - // TODO: impl + final LocalDate interestRateChangeEffectiveDate = newInterestSubmittedOnDate.minusDays(1); + scheduleModel.addInterestRate(interestRateChangeEffectiveDate, newInterestRate); + scheduleModel + .changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.getZero(), + scheduleModel.getZero()) + .ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors( + getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel)); } @Override public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate, Money balanceCorrectionAmount) { - final Money zeroAmount = Money.zero(balanceCorrectionAmount.getCurrency()); - scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, zeroAmount, balanceCorrectionAmount) + scheduleModel + .changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.getZero(), balanceCorrectionAmount) .ifPresent(repaymentPeriod -> { calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); calculateOutstandingBalance(scheduleModel); @@ -186,10 +190,8 @@ void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPerio final ProgressiveLoanInterestScheduleModel scheduleModel) { final List relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate); calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel); - // TODO: optimalize calculateOutstandingBalance(scheduleModel); calculateEMIOnPeriods(relatedRepaymentPeriods, scheduleModel); - // TODO: optimalize calculateOutstandingBalance(scheduleModel); calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods); @@ -229,7 +231,9 @@ private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final Progressiv .isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods), mc)); if (shouldBeAdjusted) { - Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc); + long uncountablePeriods = relatedRepaymentPeriods.stream().filter(rp -> originalEmi.isLessThan(rp.getTotalPaidAmount())) + .count(); + Money adjustment = emiDifference.dividedBy(Math.max(1, numberOfRelatedPeriods - uncountablePeriods), mc); Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel, originalEmi.plus(adjustment, mc)); if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) { @@ -238,7 +242,8 @@ private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final Progressiv final LocalDate relatedPeriodsFirstDueDate = relatedRepaymentPeriods.get(0).getDueDate(); final ProgressiveLoanInterestScheduleModel newScheduleModel = scheduleModel.deepCopy(mc); newScheduleModel.repaymentPeriods().forEach(period -> { - if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate)) { + if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate) + && !adjustedEqualMonthlyInstallmentValue.isLessThan(period.getTotalPaidAmount())) { period.setEmi(adjustedEqualMonthlyInstallmentValue); } }); @@ -393,7 +398,11 @@ void calculateEMIOnPeriods(final List repaymentPeriods, final P calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult, mc), mc); final Money finalEqualMonthlyInstallment = applyInstallmentAmountInMultiplesOf(scheduleModel, equalMonthlyInstallment); - repaymentPeriods.forEach(period -> period.setEmi(finalEqualMonthlyInstallment)); + repaymentPeriods.forEach(period -> { + if (!finalEqualMonthlyInstallment.isLessThan(period.getTotalPaidAmount())) { + period.setEmi(finalEqualMonthlyInstallment); + } + }); } Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleModel scheduleModel, @@ -571,7 +580,7 @@ BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, final BigD return interestRate// .multiply(interestFractionPerPeriod, mc)// .multiply(actualDaysInPeriod, mc)// - .divide(calculatedDaysInPeriod, mc);// + .divide(calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());// } /** @@ -585,7 +594,7 @@ BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, fin return interestRate// .multiply(interestFractionPerPeriod, mc)// .multiply(actualDaysInPeriod, mc)// - .divide(calculatedDaysInPeriod, mc);// + .divide(calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());// } /** diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperationTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperationTest.java new file mode 100644 index 00000000000..4269573884f --- /dev/null +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperationTest.java @@ -0,0 +1,159 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; + +import com.google.common.collect.Collections2; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class ChangeOperationTest { + + // DateUtils.getOffsetDateTimeOfTenantFromLocalDate + private static final MockedStatic threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class); + + @BeforeAll + public static void init() { + threadLocalContextUtil.when(ThreadLocalContextUtil::getTenant) + .thenReturn(new FineractPlatformTenant(null, null, null, "Europe/Budapest", null)); + } + + @AfterAll + public static void tearDown() { + threadLocalContextUtil.close(); + } + + @Test + public void testCompareToEqual() { + ChangeOperation interestChange = createInterestRateChange("2023-10-17"); + ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(interestChange.compareTo(transaction) < 0); + Assertions.assertTrue(interestChange.compareTo(charge) < 0); + Assertions.assertTrue(charge.compareTo(transaction) == 0); + Assertions.assertTrue(transaction.compareTo(charge) == 0); + } + + @Test + public void testCompareToEqualBackdatedCharge() { + ChangeOperation interestChange = createInterestRateChange("2023-10-17"); + ChangeOperation charge = createCharge("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(interestChange.compareTo(transaction) > 0); + Assertions.assertTrue(interestChange.compareTo(charge) > 0); + Assertions.assertTrue(charge.compareTo(transaction) == 0); + Assertions.assertTrue(transaction.compareTo(charge) == 0); + } + + @Test + public void testCompareToCreatedDateTime() { + ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00"); + ChangeOperation transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) > 0); + Assertions.assertTrue(transaction.compareTo(charge) < 0); + } + + @Test + public void testCompareToSubmittedOnDate() { + ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation transaction = createTransaction("2023-10-17", "2023-10-16", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) > 0); + Assertions.assertTrue(transaction.compareTo(charge) < 0); + } + + @Test + public void testComparatorEffectiveDate() { + ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) > 0); + Assertions.assertTrue(transaction.compareTo(charge) < 0); + } + + @Test + public void testComparatorOnDifferentSubmittedDay() { + ChangeOperation cot1 = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:16:30+01:00"); + ChangeOperation cot3 = createCharge("2023-10-17", "2023-10-18", "2023-10-18T10:14:30+01:00"); + Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); + List expected = List.of(cot1, cot3, cot2); + for (List permutation : permutations) { + Assertions.assertEquals(expected, permutation.stream().sorted().toList()); + } + } + + @Test + public void testComparatorOnSameDayBackdatedCharge() { + ChangeOperation cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00"); + ChangeOperation cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:15:33+01:00"); + ChangeOperation cot3 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:32+01:00"); + Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); + List expected = List.of(cot1, cot3, cot2); + for (List permutation : permutations) { + Assertions.assertEquals(expected, permutation.stream().sorted().toList()); + } + } + + @Test + public void testComparatorOnSameDay() { + ChangeOperation cot1 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:31+01:00"); + ChangeOperation cot2 = createTransaction("2023-10-19", "2023-10-19", "2023-10-19T10:15:33+01:00"); + ChangeOperation cot3 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:32+01:00"); + Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); + List expected = List.of(cot1, cot3, cot2); + for (List permutation : permutations) { + Assertions.assertEquals(expected, permutation.stream().sorted().toList()); + } + } + + private ChangeOperation createInterestRateChange(String submittedDate) { + LoanTermVariationsData interestRateChange = Mockito.mock(LoanTermVariationsData.class); + Mockito.when(interestRateChange.getTermVariationApplicableFrom()).thenReturn(LocalDate.parse(submittedDate)); + return new ChangeOperation(interestRateChange); + } + + private ChangeOperation createCharge(String effectiveDate, String submittedDate, String creationDateTime) { + LoanCharge charge = Mockito.mock(LoanCharge.class); + Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate)); + Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); + Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime))); + return new ChangeOperation(charge); + } + + private ChangeOperation createTransaction(String transactionDate, String submittedDate, String creationDateTime) { + LoanTransaction transaction = Mockito.mock(LoanTransaction.class); + Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); + Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate)); + Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime)); + return new ChangeOperation(transaction); + } + +} diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java deleted file mode 100644 index 20ac2238b4e..00000000000 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; - -import com.google.common.collect.Collections2; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -public class ChargeOrTransactionTest { - - @Test - public void testCompareToEqual() { - ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) == 0); - Assertions.assertTrue(transaction.compareTo(charge) == 0); - } - - @Test - public void testCompareToEqualBackdatedCharge() { - ChargeOrTransaction charge = createCharge("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) == 0); - Assertions.assertTrue(transaction.compareTo(charge) == 0); - } - - @Test - public void testCompareToCreatedDateTime() { - ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) > 0); - Assertions.assertTrue(transaction.compareTo(charge) < 0); - } - - @Test - public void testCompareToSubmittedOnDate() { - ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-16", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) > 0); - Assertions.assertTrue(transaction.compareTo(charge) < 0); - } - - @Test - public void testComparatorEffectiveDate() { - ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) > 0); - Assertions.assertTrue(transaction.compareTo(charge) < 0); - } - - @Test - public void testComparatorOnDifferentSubmittedDay() { - ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:16:30+01:00"); - ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-18", "2023-10-18T10:14:30+01:00"); - Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); - List expected = List.of(cot1, cot3, cot2); - for (List permutation : permutations) { - Assertions.assertEquals(expected, permutation.stream().sorted().toList()); - } - } - - @Test - public void testComparatorOnSameDayBackdatedCharge() { - ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00"); - ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:15:33+01:00"); - ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:32+01:00"); - Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); - List expected = List.of(cot1, cot3, cot2); - for (List permutation : permutations) { - Assertions.assertEquals(expected, permutation.stream().sorted().toList()); - } - } - - @Test - public void testComparatorOnSameDay() { - ChargeOrTransaction cot1 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:31+01:00"); - ChargeOrTransaction cot2 = createTransaction("2023-10-19", "2023-10-19", "2023-10-19T10:15:33+01:00"); - ChargeOrTransaction cot3 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:32+01:00"); - Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); - List expected = List.of(cot1, cot3, cot2); - for (List permutation : permutations) { - Assertions.assertEquals(expected, permutation.stream().sorted().toList()); - } - } - - private ChargeOrTransaction createCharge(String effectiveDate, String submittedDate, String creationDateTime) { - LoanCharge charge = Mockito.mock(LoanCharge.class); - Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate)); - Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); - Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime))); - return new ChargeOrTransaction(charge); - } - - private ChargeOrTransaction createTransaction(String transactionDate, String submittedDate, String creationDateTime) { - LoanTransaction transaction = Mockito.mock(LoanTransaction.class); - Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); - Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate)); - Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime)); - return new ChargeOrTransaction(transaction); - } - -} 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 c129a5e4f9e..aca3641bc6d 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 @@ -44,7 +44,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; @@ -95,13 +94,12 @@ private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, fi final BigDecimal daysInPeriod = BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate())); final BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(period.getFromDate())); final BigDecimal daysInMonth = BigDecimal.valueOf(daysInMonthType.getNumberOfDays(period.getFromDate())); - final BigDecimal rateFactor = emiCalculator.rateFactorByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear, - daysInPeriod, daysInPeriod, mc); - return rateFactor.setScale(12, MoneyHelper.getRoundingMode()); + return emiCalculator.rateFactorByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear, daysInPeriod, + daysInPeriod, mc); } @Test - public void testRateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthActual() { + public void test_rateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthActual() { // Given final DaysInYearType daysInYearType = DaysInYearType.DAYS_365; final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL; @@ -116,7 +114,7 @@ public void testRateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthAc } @Test - public void testRateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMonthActual() { + public void test_rateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMonthActual() { // Given final DaysInYearType daysInYearType = DaysInYearType.ACTUAL; final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL; @@ -132,7 +130,7 @@ public void testRateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMont } @Test - public void testFnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual() { + public void test_fnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual() { // Given final DaysInYearType daysInYearType = DaysInYearType.DAYS_365; final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL; @@ -159,7 +157,7 @@ public void testFnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual( } @Test - public void testEMICalculator_generateInterestScheduleModel() { + public void test_generateInterestScheduleModel() { final List expectedRepaymentPeriods = new ArrayList<>(); final Integer installmentAmountInMultiplesOf = null; @@ -184,7 +182,7 @@ public void testEMICalculator_generateInterestScheduleModel() { @Test @Timeout(1) // seconds - public void testEMICalculation_performance() { + public void test_emi_calculator_performance() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -223,19 +221,19 @@ public void testEMICalculation_performance() { List repaymentPeriods = interestSchedule.repaymentPeriods(); for (int i = 0; i < repaymentPeriods.size(); i++) { final RepaymentPeriod repaymentPeriod = repaymentPeriods.get(i); - Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDuePrincipal().getAmount())); - Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDueInterest().getAmount())); + Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDuePrincipal())); + Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDueInterest())); if (i == repaymentPeriods.size() - 1) { - Assertions.assertEquals(0.0, toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount())); + Assertions.assertEquals(0.0, toDouble(repaymentPeriod.getOutstandingLoanBalance())); } else { - Assertions.assertEquals(8.65, toDouble(repaymentPeriod.getEmi().getAmount())); - Assertions.assertTrue(0 < toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount())); + Assertions.assertEquals(8.65, toDouble(repaymentPeriod.getEmi())); + Assertions.assertTrue(0 < toDouble(repaymentPeriod.getOutstandingLoanBalance())); } } } @Test - public void testEMICalculation_CheckEmiButNewEmiNotBetterThanOriginal() { + public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -272,7 +270,7 @@ public void testEMICalculation_CheckEmiButNewEmiNotBetterThanOriginal() { } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { + public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -283,7 +281,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -296,7 +294,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -309,7 +307,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay } @Test - public void testEMICalculation_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() { + public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -356,7 +354,7 @@ public void testEMICalculation_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_ } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule() { + public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -367,7 +365,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 15), LocalDate.of(2024, 6, 15))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 15), LocalDate.of(2024, 7, 15))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -380,7 +378,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -392,9 +390,8 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0); } - @Disabled("till interest rate change got implemented") @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0201_4per() { + public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -405,7 +402,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -420,10 +417,10 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - final BigDecimal interestRateNewValue = new BigDecimal("4"); + final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0); final LocalDate interestChangeDate = LocalDate.of(2024, 2, 2); emiCalculator.changeInterestRate(interestSchedule, interestChangeDate, interestRateNewValue); @@ -436,9 +433,55 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay checkPeriod(interestSchedule, 5, 0, 16.89, 0.003333333333, 0.06, 16.83, 0.0); } - @Disabled("till interest rate change got implemented") @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0215_4per() { + public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { + + final List expectedRepaymentPeriods = new ArrayList<>(); + + expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); + expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1))); + expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1))); + expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1))); + expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); + expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + final BigDecimal interestRate = BigDecimal.valueOf(7.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.getCurrency()).thenReturn(monetaryCurrency); + + threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); + + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, + loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(100.0); + emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); + emiCalculator.payInterest(interestModel, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(0.58)); + emiCalculator.payPrincipal(interestModel, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(16.43)); + + emiCalculator.payPrincipal(interestModel, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 1), toMoney(16.90)); + + final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0); + final LocalDate interestChangeDate = LocalDate.of(2024, 2, 2); + emiCalculator.changeInterestRate(interestModel, interestChangeDate, interestRateNewValue); + + checkPeriod(interestModel, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 66.67); + checkPeriod(interestModel, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 66.67); + checkPeriod(interestModel, 1, 0, 17.01, 0.003333333333, 0.22, 0.0, 17.01, 66.56); + checkPeriod(interestModel, 2, 0, 16.85, 0.003333333333, 0.22, 0.44, 16.41, 50.15); + checkPeriod(interestModel, 3, 0, 16.85, 0.003333333333, 0.17, 16.68, 33.47); + checkPeriod(interestModel, 4, 0, 16.85, 0.003333333333, 0.11, 16.74, 16.73); + checkPeriod(interestModel, 5, 0, 16.79, 0.003333333333, 0.06, 16.73, 0.0); + } + + @Test + public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -449,7 +492,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -464,14 +507,15 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - final BigDecimal interestRateNewValue = new BigDecimal("4"); + final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0); final LocalDate interestChangeDate = LocalDate.of(2024, 2, 15); emiCalculator.changeInterestRate(interestSchedule, interestChangeDate, interestRateNewValue); - checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + 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, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 1, 0, 16.90, 0.002614942529, 0.22, 0.37, 16.53, 67.04); checkPeriod(interestSchedule, 1, 1, 16.90, 0.001839080460, 0.15, 0.37, 16.53, 67.04); checkPeriod(interestSchedule, 2, 0, 16.90, 0.003333333333, 0.22, 16.68, 50.36); @@ -484,7 +528,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay * This test case tests a period early and late repayment with balance correction */ @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_add_balance_correction_on0215() { + public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -495,7 +539,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -510,37 +554,36 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); // schedule 1st period 1st day PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 1)); - Assertions.assertEquals(100, toDouble(payableDetails.getOutstandingBalance().getAmount())); - Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(100, toDouble(payableDetails.getOutstandingBalance())); + Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest())); // schedule 2nd period last day payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); - Assertions.assertEquals(83.57, toDouble(payableDetails.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(83.57, toDouble(payableDetails.getOutstandingBalance())); + Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest())); // pay off a period with balance correction final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1); final LocalDate op1stCorrectionDate = LocalDate.of(2024, 2, 15); - final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(16.77)); + final Money op1stCorrectionAmount = toMoney(16.77); // get remaining balance and dues for a date final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate); - Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest().getAmount())); + Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance())); + Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal())); + Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest())); emiCalculator.payPrincipal(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, op1stCorrectionAmount); - emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, - Money.of(monetaryCurrency, BigDecimal.valueOf(0.24))); + emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, toMoney(0.24)); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 16.77, 66.80); @@ -553,18 +596,17 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay // totally pay off another period with balance correction final LocalDate op2ndCorrectionPeriodDueDate = LocalDate.of(2024, 4, 1); final LocalDate op2ndCorrectionDate = LocalDate.of(2024, 3, 1); - final Money op2ndCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(16.42)); + final Money op2ndCorrectionAmount = toMoney(16.42); // get remaining balance and dues for a date final PayableDetails repaymentDetails2st = emiCalculator.getPayableDetails(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate); - Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest().getAmount())); + Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance())); + Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal())); + Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest())); emiCalculator.payPrincipal(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, op2ndCorrectionAmount); - emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, - Money.of(monetaryCurrency, BigDecimal.valueOf(0.49))); + emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, toMoney(0.49)); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 16.77, 50.38); @@ -578,17 +620,17 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay LocalDate periodDueDate = LocalDate.of(2024, 7, 1); LocalDate payDate = LocalDate.of(2024, 7, 1); final PayableDetails repaymentDetails3rd = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate); - Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest().getAmount())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getOutstandingBalance())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal())); + Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest())); // check numbers after the last period due date periodDueDate = LocalDate.of(2024, 7, 1); payDate = LocalDate.of(2024, 7, 15); final PayableDetails repaymentDetails4th = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate); - Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest().getAmount())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getOutstandingBalance())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal())); + Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest())); // balance update on the last period, check the right interest interval split emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 10), Money.of(monetaryCurrency, BigDecimal.ZERO)); @@ -601,7 +643,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_payoff_on0215() { + public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -612,7 +654,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -627,51 +669,43 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); // partially pay off a period with balance correction final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1); final LocalDate op1stCorrectionDate = LocalDate.of(2024, 2, 15); - final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(15.0)); + final Money op1stCorrectionAmount = toMoney(15.0); // get remaining balance and dues for a date final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate); - Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest().getAmount())); + Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance())); + Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal())); + Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest())); PayableDetails details = null; // check getPayableDetails forcast details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); - Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.49, toDouble(details.getPayableInterest().getAmount())); + Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance())); + Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal())); + Assertions.assertEquals(0.49, toDouble(details.getPayableInterest())); // apply balance change and check again emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), op1stCorrectionDate, op1stCorrectionAmount); details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); - Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.49, toDouble(details.getPayableInterest().getAmount())); - - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(1.43))); - emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(0.58))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(16.77))); - emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(0.24))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(17.01))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(17.01))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(17.01))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(15.77))); + Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance())); + Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal())); + Assertions.assertEquals(0.49, toDouble(details.getPayableInterest())); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(1.43)); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(0.58)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), toMoney(16.77)); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), toMoney(0.24)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 2, 15), toMoney(17.01)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 2, 15), toMoney(17.01)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 15), toMoney(17.01)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 15), toMoney(15.77)); // check periods in model @@ -685,7 +719,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_payoff_on0115() { + public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -696,7 +730,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -717,13 +751,13 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay // get remaining balance and dues on due date PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1)); - Assertions.assertEquals(16.43, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(16.43, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest())); // check numbers on payoff date payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15)); - Assertions.assertEquals(16.75, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(16.75, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest())); emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(16.75)); emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(0.26)); @@ -738,8 +772,8 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1)); - Assertions.assertEquals(15.21, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(15.21, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest())); // check periods in model checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.26, 16.75, 15.21); @@ -765,7 +799,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay } @Test - public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month() { + public void test_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -776,7 +810,7 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -789,7 +823,7 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + Money disbursedAmount = toMoney(100); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -800,7 +834,7 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0); checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0); - disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(200)); + disbursedAmount = toMoney(200.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 51.33, 0.0, 0.0, 2.02, 49.31, 250.69); @@ -814,7 +848,7 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da } @Test - public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth30_repayEvery1Month() { + public void test_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -825,7 +859,7 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -838,7 +872,7 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -849,7 +883,7 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0); checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0); - disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 2, 15), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -863,7 +897,7 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 } @Test - public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month_backdated_disbursement() { + public void test_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month_backdated_disbursement() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -874,7 +908,7 @@ public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_da expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -887,14 +921,14 @@ public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_da final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount); - disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(50)); + disbursedAmount = toMoney(50.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); // add disbursement on same date - disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(25)); + disbursedAmount = toMoney(25.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 29.94, 0.001019591398, 0.00, 1.15, 28.79, 146.21); @@ -908,7 +942,7 @@ public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_da } @Test - public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month() { + public void test_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month() { final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)), @@ -918,7 +952,7 @@ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActua repayment(5, LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 1)), repayment(6, LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -931,7 +965,7 @@ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActua final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.80, 16.33, 83.67); @@ -944,7 +978,7 @@ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActua } @Test - public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { + public void test_disbursedAmt1000_NoInterest_repayEvery1Month() { final List expectedRepaymentPeriods = List.of( repayment(2, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)), @@ -952,7 +986,7 @@ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { repayment(4, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)), repayment(5, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1))); - final BigDecimal interestRate = new BigDecimal("0"); + final BigDecimal interestRate = BigDecimal.ZERO; final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -965,7 +999,7 @@ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(1000)); + final Money disbursedAmount = toMoney(1000.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 250.0, 0.0, 0.0, 250.0, 750.0); @@ -975,7 +1009,7 @@ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { } @Test - public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week() { + public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week() { final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 8)), @@ -985,7 +1019,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r repayment(5, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 5)), repayment(6, LocalDate.of(2024, 2, 5), LocalDate.of(2024, 2, 12))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -998,7 +1032,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 16.77, 0.0, 0.0, 0.18, 16.59, 83.41); @@ -1011,14 +1045,14 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r } @Test - public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week() { + public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week() { final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 15)), repayment(2, LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 29)), repayment(3, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 12))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -1031,7 +1065,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 33.57, 0.0, 0.0, 0.36, 33.21, 66.79); @@ -1041,7 +1075,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() { + public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() { final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)), @@ -1051,7 +1085,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMa repayment(5, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 16)), repayment(6, LocalDate.of(2024, 3, 16), LocalDate.of(2024, 3, 31))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -1064,7 +1098,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMa final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 16.90, 0.0, 0.0, 0.40, 16.50, 83.50); @@ -1105,16 +1139,20 @@ private static void checkPeriod(final ProgressiveLoanInterestScheduleModel inter final RepaymentPeriod repaymentPeriod = interestScheduleModel.repaymentPeriods().get(repaymentIdx); final InterestPeriod interestPeriod = repaymentPeriod.getInterestPeriods().get(interestIdx); - Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi().getAmount())); + Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi())); Assertions.assertEquals(rateFactor, toDouble(applyMathContext(interestPeriod.getRateFactor()))); - Assertions.assertEquals(interestDue, toDouble(interestPeriod.getCalculatedDueInterest().getAmount())); - Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getDueInterest().getAmount())); - Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getDuePrincipal().getAmount())); - Assertions.assertEquals(remaingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount())); + Assertions.assertEquals(interestDue, toDouble(interestPeriod.getCalculatedDueInterest())); + Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getDueInterest())); + Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getDuePrincipal())); + Assertions.assertEquals(remaingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance())); + } + + private static double toDouble(final Money value) { + return value == null ? 0.0 : toDouble(value.getAmount()); } private static double toDouble(final BigDecimal value) { - return value == null ? 0 : value.doubleValue(); + return value == null ? 0.0 : value.doubleValue(); } private static BigDecimal applyMathContext(final BigDecimal value) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java index 99785147cf9..196a386d788 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java @@ -35,7 +35,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; -import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChargeOrTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChangeOperation; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.springframework.stereotype.Component; @@ -145,6 +145,6 @@ private void throwExceptionIfValidationErrorsExist(List dataV } private boolean transactionHappenedAfterOther(LoanTransaction transaction, LoanTransaction otherTransaction) { - return new ChargeOrTransaction(transaction).compareTo(new ChargeOrTransaction(otherTransaction)) > 0; + return new ChangeOperation(transaction).compareTo(new ChangeOperation(otherTransaction)) > 0; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java index 28087f74a41..58ad2fb7c00 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java @@ -34,7 +34,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; -import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChargeOrTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChangeOperation; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.springframework.stereotype.Component; @@ -130,6 +130,6 @@ private void throwExceptionIfValidationErrorsExist(List dataV } private boolean transactionHappenedAfterOther(LoanTransaction transaction, LoanTransaction otherTransaction) { - return new ChargeOrTransaction(transaction).compareTo(new ChargeOrTransaction(otherTransaction)) > 0; + return new ChangeOperation(transaction).compareTo(new ChangeOperation(otherTransaction)) > 0; } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 5091ade6966..c01171f4750 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -83,6 +83,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -5213,6 +5214,124 @@ public void uc147c() { }); } + // UC14-: Advanced payment allocation with Interest, Interest Recalculation Enabled + // Interest Calculation Period Type: daily + // Use original EMI and apply disbursement and change Interest Rate + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Payment. Alloc. and with 7% Interest, 360/30, 1 repayment per month + // 2. Submit Loan and approve + // 3. Disburse + // 4. Validate Repayment Schedule + // 5. Reschedule Interest Rate to 4% + // 6. Validate Repayment Schedule + @Test + public void uc147d() { + final String operationDate = "1 January 2024"; + runAt(operationDate, () -> { + BigDecimal interestRatePerPeriod = BigDecimal.valueOf(7.0); + + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestRateFrequencyType(YEARS)// + .daysInMonthType(DaysInMonthType.DAYS_30)// + .daysInYearType(DaysInYearType.DAYS_360)// + .numberOfRepayments(6)// + .repaymentEvery(1)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())// + .repaymentStartDateType(LoanProduct.RepaymentStartDateTypeEnum.SUBMITTED_ON_DATE.ordinal())// + .enableDownPayment(false)// + .allowPartialPeriodInterestCalcualtion(null)// + .enableAutoRepaymentForDownPayment(null)// + .isInterestRecalculationEnabled(true)// + .interestRecalculationCompoundingMethod(0)// + .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)// + .multiDisburseLoan(true)// + .disallowExpectedDisbursements(true)// + .allowApprovedDisbursedAmountsOverApplied(true)// + .overAppliedCalculationType("flat")// + .overAppliedNumber(10000)// + .maxTrancheCount(10)// + .outstandingLoanBalance(10000.0)// + .installmentAmountInMultiplesOf(null)// + .rescheduleStrategyMethod(LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.getValue())// + .recalculationRestFrequencyType(1);// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 6); + + applicationRequest = applicationRequest.numberOfRepayments(6)// + .loanTermFrequency(6)// + .loanTermFrequencyType(2)// + .interestRatePerPeriod(interestRatePerPeriod)// + .interestCalculationPeriodType(DAYS)// + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)// + .repaymentEvery(1)// + .repaymentFrequencyType(2)// + .maxOutstandingLoanBalance(BigDecimal.valueOf(10000.0))// + ;// + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()// + .approvedLoanAmount(BigDecimal.valueOf(100))// + .approvedOnDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));// + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()// + .transactionAmount(BigDecimal.valueOf(100.0))// + .actualDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));// + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 102.05, 0.0, 100.0, 0.0, null); + + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 16.43, 0.0, 16.43, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.58, 0.0, + 0.58, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 16.52, 0.0, 16.52, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.49, 0.0, + 0.49, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 16.62, 0.0, 16.62, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.39, 0.0, + 0.39, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 16.72, 0.0, 16.72, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.29, 0.0, + 0.29, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 16.81, 0.0, 16.81, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.20, 0.0, + 0.20, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 16.90, 0.0, 16.90, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.10, 0.0, + 0.10, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(loanDetails.getNumberOfRepayments(), 6); + + updateBusinessDate("1 February 2024"); + loanTransactionHelper.makeRepayment("1 February 2024", 17.01f, loanResponse.getLoanId().intValue()); + + updateBusinessDate("14 February 2024"); + PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper// + .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()// + .loanId(loanDetails.getId())// + .rescheduleReasonId(1L)// + .rescheduleFromDate("15 February 2024").dateFormat(DATETIME_PATTERN).locale("en")// + .submittedOnDate("14 February 2024")// + .newInterestRate(BigDecimal.valueOf(4.0)));// + + loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(), // + new PostUpdateRescheduleLoansRequest()// + .approvedOnDate("14 February 2024").locale("en").dateFormat(DATETIME_PATTERN));// + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 84.5, 17.01, 83.57, 16.43, null); + + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 16.43, 16.43, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.58, 0.58, + 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 16.53, 0.0, 16.53, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.37, 0.0, + 0.37, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 16.68, 0.0, 16.68, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.22, 0.0, + 0.22, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 16.73, 0.0, 16.73, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.17, 0.0, + 0.17, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 16.79, 0.0, 16.79, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.11, 0.0, + 0.11, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 16.84, 0.0, 16.84, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.06, 0.0, + 0.06, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + }); + } + // uc148a: Advanced payment allocation, with Interest Recalculation in Loan Product and Adjust last, unpaid period // ADVANCED_PAYMENT_ALLOCATION_STRATEGY // 1. Create a Loan product with Adv. Pment. Alloc. with Interest Recalculation enabled and Adjust last, unpaid