diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java index 808886a88..8ce760217 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java @@ -335,6 +335,10 @@ public final class MathUtil { return nullToDefault(value, Money.zero(currency)); } + public static Money nullToZero(Money value, @NotNull MonetaryCurrency currency, @NotNull MathContext mc) { + return nullToDefault(value, Money.zero(currency, mc)); + } + public static Money nullToDefault(Money value, Money def) { return value == null ? def : value; } 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 45c82e2c1..092a520cb 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 @@ -125,8 +125,8 @@ public final class ProgressiveEMICalculator implements EMICalculator { } private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) { - scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate())).forEach( - rp -> rp.setTotalDisbursedAmount(MathUtil.nullToZero(rp.getTotalDisbursedAmount()).add(operation.getAmount().getAmount()))); + scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate())) + .forEach(rp -> rp.setTotalDisbursedAmount(rp.getTotalDisbursedAmount().add(operation.getAmount()))); scheduleModel .changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(), @@ -162,8 +162,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { private void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) { scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate())) - .forEach(rp -> rp.setTotalCapitalizedIncomeAmount( - MathUtil.nullToZero(rp.getTotalCapitalizedIncomeAmount()).add(operation.getAmount().getAmount()))); + .forEach(rp -> rp.setTotalCapitalizedIncomeAmount(rp.getTotalCapitalizedIncomeAmount().plus(operation.getAmount()))); scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), scheduleModel.zero(), scheduleModel.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> { @@ -225,7 +224,6 @@ public final class ProgressiveEMICalculator implements EMICalculator { return; } Optional repaymentPeriod = findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate); - boolean transactionDateIsBefore = transactionDate.isBefore(repaymentPeriod.get().getFromDate()); repaymentPeriod.ifPresent(rp -> rp.addPaidPrincipalAmount(principalAmount)); // If it is paid late, we need to calculate with the period due date LocalDate balanceCorrectionDate = DateUtils.isBefore(repaymentPeriodDueDate, transactionDate) ? repaymentPeriodDueDate @@ -235,6 +233,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { repaymentPeriod.ifPresent(rp -> { // If any period total paid > calculated EMI, then set EMI to total paid -> effectively it is marked as // fully paid + boolean transactionDateIsBefore = transactionDate.isBefore(repaymentPeriod.get().getFromDate()); if (transactionDateIsBefore && rp.getTotalPaidAmount().isGreaterThan(rp.getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest())) { rp.setEmi(rp.getTotalPaidAmount().minus(rp.getTotalCreditedAmount())); @@ -368,7 +367,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { return recalculatedScheduleModelTillDate.getLastRepaymentPeriod(); } else { // if target date is before 1st disbursement date, we use 1st repayment period - return recalculatedScheduleModelTillDate.repaymentPeriods().get(0); + return recalculatedScheduleModelTillDate.repaymentPeriods().getFirst(); } }); @@ -406,7 +405,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { @NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate targetDate) { MathContext mc = scheduleModel.mc(); ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); - boolean isBeforeFirstDisbursement = targetDate.isBefore(scheduleModelCopy.repaymentPeriods().get(0).getFromDate()); + boolean isBeforeFirstDisbursement = targetDate.isBefore(scheduleModelCopy.repaymentPeriods().getFirst().getFromDate()); boolean isAfterMaturityDate = !targetDate.isBefore(scheduleModelCopy.getLastRepaymentPeriod().getDueDate()); if (isBeforeFirstDisbursement) { scheduleModelCopy.repaymentPeriods().forEach(rp -> rp.getInterestPeriods().clear()); @@ -436,13 +435,13 @@ public final class ProgressiveEMICalculator implements EMICalculator { .subList(nextIdx, ip.getRepaymentPeriod().getInterestPeriods().size()).clear(); }); } else if (rp.getPrevious().isPresent() && rp.getPrevious().get().equals(repaymentPeriod) - && (rp.getInterestPeriods().get(0).getCreditedInterest().isGreaterThanZero() - || rp.getInterestPeriods().get(0).getCreditedPrincipal().isGreaterThanZero())) { + && (rp.getInterestPeriods().getFirst().getCreditedInterest().isGreaterThanZero() + || rp.getInterestPeriods().getFirst().getCreditedPrincipal().isGreaterThanZero())) { // NOTE: we need to check whether there is credited on the 1st interest period of the next // period // if so, we need to retain that interest period, but need to update due date to match with from // date -> 0 interest - rp.getInterestPeriods().get(0).setDueDate(rp.getInterestPeriods().get(0).getFromDate()); + rp.getInterestPeriods().getFirst().setDueDate(rp.getInterestPeriods().getFirst().getFromDate()); if (rp.getInterestPeriods().size() > 1) { rp.getInterestPeriods().subList(1, rp.getInterestPeriods().size()).clear(); } @@ -580,7 +579,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { if (newScheduleModel == null) { newScheduleModel = scheduleModel.deepCopy(mc); } - final LocalDate relatedPeriodsFirstDueDate = relatedRepaymentPeriods.get(0).getDueDate(); + final LocalDate relatedPeriodsFirstDueDate = relatedRepaymentPeriods.getFirst().getDueDate(); newScheduleModel.repaymentPeriods().forEach(period -> { if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate) && !adjustedEqualMonthlyInstallmentValue.isLessThan(period.getTotalPaidAmount())) { @@ -913,8 +912,8 @@ public final class ProgressiveEMICalculator implements EMICalculator { final MathContext mc = scheduleModel.mc(); final CurrencyData currency = scheduleModel.loanProductRelatedDetail().getCurrencyData(); - RepaymentPeriod first = repaymentPeriods.getFirst(); - RepaymentPeriod last = repaymentPeriods.getLast(); + RepaymentPeriod firstRepaymentPeriod = repaymentPeriods.getFirst(); + RepaymentPeriod lastRepaymentPeriod = repaymentPeriods.getLast(); Money sumOfInterest = Money.zero(currency); for (RepaymentPeriod rp : repaymentPeriods) { Money interest = rp.calculateCalculatedDueInterest(); @@ -923,12 +922,11 @@ public final class ProgressiveEMICalculator implements EMICalculator { } // already repaid principals should be subtracted from total disbursed amount to calculate correct EMI. - BigDecimal alreadyRepaidPrincipals = first.getPrevious() - .map(rp -> rp.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(null) - .subtract(rp.getOutstandingLoanBalance().getAmount())) - .orElse(BigDecimal.ZERO); - Money total = Money - .of(currency, first.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(first.getLastInterestPeriod())) + Money alreadyRepaidPrincipals = firstRepaymentPeriod.getPrevious() + .map(rp -> rp.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(null).minus(rp.getOutstandingLoanBalance())) + .orElse(null); + Money total = firstRepaymentPeriod + .calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(firstRepaymentPeriod.getLastInterestPeriod()) .plus(sumOfInterest).minus(alreadyRepaidPrincipals); Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc); @@ -936,7 +934,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { Money remainder = total.minus(periodEmiInMultiplesOf.multipliedBy(repaymentPeriods.size(), mc)); repaymentPeriods.forEach(rp -> { - Money emi = rp.equals(last) ? periodEmiInMultiplesOf.add(remainder) : periodEmiInMultiplesOf; + Money emi = rp.equals(lastRepaymentPeriod) ? periodEmiInMultiplesOf.add(remainder) : periodEmiInMultiplesOf; rp.setEmi(emi); rp.setOriginalEmi(emi); rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance); @@ -959,7 +957,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { final MathContext mc = scheduleModel.mc(); final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods, mc)); final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc)); - final RepaymentPeriod startPeriod = repaymentPeriods.get(0); + final RepaymentPeriod startPeriod = repaymentPeriods.getFirst(); final Money outstandingBalance = startPeriod.getInitialBalanceForEmiRecalculation(); @@ -985,7 +983,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { addDisbursement(scheduleModelCopy, operation.withZeroAmount()); addCapitalizedIncome(scheduleModelCopy, operation.withZeroAmount()); - final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate(); + final LocalDate firstDueDate = repaymentPeriods.getFirst().getDueDate(); scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> { actualRepaymentPeriod.setEmi(newRepaymentPeriod.getEmi()); actualRepaymentPeriod.setOriginalEmi(newRepaymentPeriod.getOriginalEmi()); @@ -1009,7 +1007,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { getUncountablePeriods(repaymentPeriods, penultimatePeriod.getEmi())); } } - return new EmiAdjustment(repaymentPeriods.get(0).getEmi(), repaymentPeriods.get(0).getEmi().copy(0.0), repaymentPeriods, 0); + return new EmiAdjustment(repaymentPeriods.getFirst().getEmi(), repaymentPeriods.getFirst().getEmi().copy(0.0), repaymentPeriods, 0); } private void calculateRateFactorForScheduleTillDateInclusive(ProgressiveLoanInterestScheduleModel scheduleModelCopy, diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java index 68989dece..740b2752b 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java @@ -32,13 +32,14 @@ import lombok.ToString; import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; @Getter @ToString(exclude = { "repaymentPeriod" }) @EqualsAndHashCode(exclude = { "repaymentPeriod" }) -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) public class InterestPeriod implements Comparable { @JsonExclude @@ -49,6 +50,7 @@ public class InterestPeriod implements Comparable { @Setter @NotNull private LocalDate dueDate; + @Setter private BigDecimal rateFactor; @Setter @@ -58,15 +60,14 @@ public class InterestPeriod implements Comparable { private Money creditedPrincipal; /** Stores credited interest. Related transaction: Chargeback */ private Money creditedInterest; - private Money disbursementAmount; private Money balanceCorrectionAmount; private Money outstandingLoanBalance; - private Money capitalizedIncomePrincipal; - @JsonExclude + @Getter private final MathContext mc; + private final boolean isPaused; public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod, MathContext mc) { @@ -90,59 +91,60 @@ public class InterestPeriod implements Comparable { public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate) { - final Money zero = repaymentPeriod.getEmi().zero(); + final Money zero = repaymentPeriod.getZero(); return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero, zero.getMc(), false); } public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate, boolean isPaused) { - final Money zero = repaymentPeriod.getEmi().zero(); + final Money zero = repaymentPeriod.getZero(); return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero, zero.getMc(), isPaused); } public static InterestPeriod withPausedAndEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate) { - final Money zero = repaymentPeriod.getEmi().zero(); + final Money zero = repaymentPeriod.getZero(); return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero, zero.getMc(), true); } @Override public int compareTo(@NotNull InterestPeriod o) { - return dueDate.compareTo(o.dueDate); + return getDueDate().compareTo(o.getDueDate()); } - public void addBalanceCorrectionAmount(final Money balanceCorrectionAmount) { - this.balanceCorrectionAmount = MathUtil.plus(this.balanceCorrectionAmount, balanceCorrectionAmount); + public void addBalanceCorrectionAmount(final Money additionalBalanceCorrectionAmount) { + this.balanceCorrectionAmount = MathUtil.plus(this.getBalanceCorrectionAmount(), additionalBalanceCorrectionAmount); } - public void addDisbursementAmount(final Money disbursementAmount) { - this.disbursementAmount = MathUtil.plus(this.disbursementAmount, disbursementAmount, mc); + public void addDisbursementAmount(final Money additionalDisbursementAmount) { + this.disbursementAmount = MathUtil.plus(this.getDisbursementAmount(), additionalDisbursementAmount, getMc()); } - public void addCreditedPrincipalAmount(final Money creditedPrincipal) { - this.creditedPrincipal = MathUtil.plus(this.creditedPrincipal, creditedPrincipal, mc); + public void addCreditedPrincipalAmount(final Money additionalCreditedPrincipal) { + this.creditedPrincipal = MathUtil.plus(this.getCreditedPrincipal(), additionalCreditedPrincipal, getMc()); } - public void addCreditedInterestAmount(final Money creditedInterest) { - this.creditedInterest = MathUtil.plus(this.creditedInterest, creditedInterest, mc); + public void addCreditedInterestAmount(final Money additionalCreditedInterest) { + this.creditedInterest = MathUtil.plus(this.getCreditedInterest(), additionalCreditedInterest, getMc()); } - public void addCapitalizedIncomePrincipalAmount(final Money capitalizedIncomePrincipal) { - this.capitalizedIncomePrincipal = MathUtil.plus(this.capitalizedIncomePrincipal, capitalizedIncomePrincipal, mc); + public void addCapitalizedIncomePrincipalAmount(final Money additionalCapitalizedIncomePrincipal) { + this.capitalizedIncomePrincipal = MathUtil.plus(this.getCapitalizedIncomePrincipal(), additionalCapitalizedIncomePrincipal, + getMc()); } public BigDecimal getCalculatedDueInterest() { - if (isPaused) { - return creditedInterest.getAmount(); + if (isPaused()) { + return getCreditedInterest().getAmount(); } long lengthTillPeriodDueDate = getLengthTillPeriodDueDate(); final BigDecimal interestDueTillRepaymentDueDate = getCalculatedDueInterest( getRepaymentPeriod().getLoanProductRelatedDetail().getInterestMethod(), lengthTillPeriodDueDate); // - return MathUtil.negativeToZero(MathUtil.add(mc, creditedInterest.getAmount(), interestDueTillRepaymentDueDate)); + return MathUtil.negativeToZero(MathUtil.add(getMc(), getCreditedInterest().getAmount(), interestDueTillRepaymentDueDate)); } public BigDecimal getCalculatedDueInterest(InterestMethod method, long lengthTillPeriodDueDate) { @@ -150,22 +152,22 @@ public class InterestPeriod implements Comparable { return BigDecimal.ZERO; } BigDecimal baseAmount = switch (method) { - case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(this)); + case FLAT -> getRepaymentPeriod().calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(this).getAmount(); case DECLINING_BALANCE -> getOutstandingLoanBalance().getAmount(); default -> throw new UnsupportedOperationException("Method not implemented: " + method); }; return baseAmount // - .multiply(getRateFactorTillPeriodDueDate(), mc) // - .divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) // - .multiply(BigDecimal.valueOf(getLength()), mc); + .multiply(getRateFactorTillPeriodDueDate(), getMc()) // + .divide(BigDecimal.valueOf(lengthTillPeriodDueDate), getMc()) // + .multiply(BigDecimal.valueOf(getLength()), getMc()); } public long getLength() { - return DateUtils.getDifferenceInDays(fromDate, dueDate); + return DateUtils.getDifferenceInDays(getFromDate(), getDueDate()); } public long getLengthTillPeriodDueDate() { - return DateUtils.getDifferenceInDays(fromDate, repaymentPeriod.getDueDate()); + return DateUtils.getDifferenceInDays(getFromDate(), getRepaymentPeriod().getDueDate()); } public void updateOutstandingLoanBalance() { @@ -174,19 +176,19 @@ public class InterestPeriod implements Comparable { if (previousRepaymentPeriod.isPresent()) { InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod(); this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance()// - .plus(previousInterestPeriod.getDisbursementAmount(), mc)// - .plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc)// - .plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)// - .minus(previousRepaymentPeriod.get().getDuePrincipal(), mc)// - .plus(previousRepaymentPeriod.get().getPaidPrincipal(), mc), mc);// + .plus(previousInterestPeriod.getDisbursementAmount(), getMc())// + .plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), getMc())// + .plus(previousInterestPeriod.getBalanceCorrectionAmount(), getMc())// + .minus(previousRepaymentPeriod.get().getDuePrincipal(), getMc())// + .plus(previousRepaymentPeriod.get().getPaidPrincipal(), getMc()), getMc());// } } else { int index = getRepaymentPeriod().getInterestPeriods().indexOf(this); InterestPeriod previousInterestPeriod = getRepaymentPeriod().getInterestPeriods().get(index - 1); this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance() // - .plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc) // - .plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc) // - .plus(previousInterestPeriod.getDisbursementAmount(), mc)); // + .plus(previousInterestPeriod.getBalanceCorrectionAmount(), getMc()) // + .plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), getMc()) // + .plus(previousInterestPeriod.getDisbursementAmount(), getMc())); // } } @@ -194,10 +196,47 @@ public class InterestPeriod implements Comparable { * Include principal like amounts (all disbursement amount + credited principal) */ public Money getCreditedAmounts() { - return getDisbursementAmount().plus(getCreditedPrincipal(), mc).plus(getCapitalizedIncomePrincipal(), mc); + return MathUtil.plus(mc, getDisbursementAmount(), getCreditedPrincipal(), getCapitalizedIncomePrincipal()); } public boolean isFirstInterestPeriod() { return this.equals(getRepaymentPeriod().getFirstInterestPeriod()); } + + private MonetaryCurrency getCurrency() { + return getRepaymentPeriod().getCurrency(); + } + + public Money getCreditedPrincipal() { + return MathUtil.nullToZero(creditedPrincipal, getCurrency(), getMc()); + } + + public Money getCreditedInterest() { + return MathUtil.nullToZero(creditedInterest, getCurrency(), getMc()); + } + + public Money getDisbursementAmount() { + return MathUtil.nullToZero(disbursementAmount, getCurrency(), getMc()); + } + + public Money getBalanceCorrectionAmount() { + return MathUtil.nullToZero(balanceCorrectionAmount, getCurrency(), getMc()); + } + + public Money getOutstandingLoanBalance() { + return MathUtil.nullToZero(outstandingLoanBalance, getCurrency(), getMc()); + } + + public Money getCapitalizedIncomePrincipal() { + return MathUtil.nullToZero(capitalizedIncomePrincipal, getCurrency(), getMc()); + } + + public BigDecimal getRateFactor() { + return MathUtil.nullToZero(rateFactor); + } + + public BigDecimal getRateFactorTillPeriodDueDate() { + return MathUtil.nullToZero(rateFactorTillPeriodDueDate); + } + } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java index 6e6f0e497..a3735eb8f 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java @@ -33,13 +33,14 @@ import lombok.Setter; import lombok.ToString; import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude; import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; import org.apache.fineract.portfolio.util.Memo; @ToString(exclude = { "previous" }) @EqualsAndHashCode(exclude = { "previous" }) -public final class RepaymentPeriod { +public class RepaymentPeriod { @JsonExclude private final RepaymentPeriod previous; @@ -52,20 +53,16 @@ public final class RepaymentPeriod { @Setter private List interestPeriods; @Setter - @Getter private Money emi; @Setter - @Getter private Money originalEmi; - @Getter private Money paidPrincipal; - @Getter private Money paidInterest; @Setter - @Getter private Money futureUnrecognizedInterest; @JsonExclude + @Getter private final MathContext mc; @JsonExclude @@ -81,18 +78,17 @@ public final class RepaymentPeriod { private boolean isInterestMoved = false; @Setter - @Getter - private BigDecimal totalDisbursedAmount; + private Money totalDisbursedAmount; @Setter - @Getter - private BigDecimal totalCapitalizedIncomeAmount; - + private Money totalCapitalizedIncomeAmount; @JsonExclude @Getter private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail; + @JsonExclude + private MonetaryCurrency currency; - private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List interestPeriods, + protected RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List interestPeriods, Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc, LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) { this.previous = previous; @@ -119,34 +115,34 @@ public final class RepaymentPeriod { final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero, zero, mc, loanProductRelatedDetail); // There is always at least 1 interest period, by default with same from-due date as repayment period - newRepaymentPeriod.interestPeriods.add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate)); + newRepaymentPeriod.getInterestPeriods().add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate)); return newRepaymentPeriod; } public static RepaymentPeriod copy(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) { - final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate, - new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, repaymentPeriod.paidPrincipal, - repaymentPeriod.paidInterest, repaymentPeriod.futureUnrecognizedInterest, mc, + final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(), + repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), + repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc, repaymentPeriod.getLoanProductRelatedDetail()); // There is always at least 1 interest period, by default with same from-due date as repayment period - for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) { - newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc)); + for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) { + newRepaymentPeriod.getInterestPeriods().add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc)); } return newRepaymentPeriod; } public static RepaymentPeriod copyWithoutPaidAmounts(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) { - final Money zero = repaymentPeriod.emi.zero(); - final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate, - new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, zero, zero, zero, mc, - repaymentPeriod.getLoanProductRelatedDetail()); + final Money zero = Money.zero(repaymentPeriod.getCurrency(), mc); + final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(), + repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), zero, zero, + zero, mc, repaymentPeriod.getLoanProductRelatedDetail()); // There is always at least 1 interest period, by default with same from-due date as repayment period - for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) { + for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) { var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod); if (!interestPeriodCopy.getBalanceCorrectionAmount().isZero()) { interestPeriodCopy.addBalanceCorrectionAmount(interestPeriodCopy.getBalanceCorrectionAmount().negated()); } - newRepaymentPeriod.interestPeriods.add(interestPeriodCopy); + newRepaymentPeriod.getInterestPeriods().add(interestPeriodCopy); } return newRepaymentPeriod; } @@ -186,17 +182,17 @@ public final class RepaymentPeriod { } public Money calculateCalculatedDueInterest() { - Money calculatedDueInterest = getZero(mc); - if (!isInterestMoved) { - calculatedDueInterest = Money.of(emi.getCurrencyData(), + Money calculatedDueInterest = getZero(); + if (!isInterestMoved()) { + calculatedDueInterest = Money.of(getEmi().getCurrencyData(), getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(BigDecimal.ZERO, BigDecimal::add), mc); } - calculatedDueInterest = calculatedDueInterest.add(getFutureUnrecognizedInterest(), mc); + calculatedDueInterest = calculatedDueInterest.add(getFutureUnrecognizedInterest(), getMc()); if (getPrevious().isPresent()) { - calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest(), mc); + calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest(), getMc()); } - return MathUtil.negativeToZero(calculatedDueInterest, mc); + return MathUtil.negativeToZero(calculatedDueInterest, getMc()); } /** @@ -223,7 +219,7 @@ public final class RepaymentPeriod { * @return */ public Money getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest() { - return getEmi().plus(getTotalCreditedAmount(), mc).plus(getFutureUnrecognizedInterest(), mc); // + return getEmi().plus(getTotalCreditedAmount(), mc).plus(getFutureUnrecognizedInterest(), getMc()); // } /** @@ -232,7 +228,8 @@ public final class RepaymentPeriod { * @return */ public Money getCalculatedDuePrincipal() { - return MathUtil.negativeToZero(getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest().minus(getCalculatedDueInterest(), mc), mc); + return MathUtil.negativeToZero(getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest().minus(getCalculatedDueInterest(), getMc()), + getMc()); } /** @@ -241,9 +238,9 @@ public final class RepaymentPeriod { * @return */ public Money getCreditedPrincipal() { - return MathUtil.negativeToZero(interestPeriods.stream() // + return MathUtil.negativeToZero(getInterestPeriods().stream() // .map(InterestPeriod::getCreditedPrincipal) // - .reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); // + .reduce(getZero(), (value, previous) -> value.plus(previous, getMc())), getMc()); // } /** @@ -252,9 +249,9 @@ public final class RepaymentPeriod { * @return */ public Money getCreditedInterest() { - return MathUtil.negativeToZero(interestPeriods.stream() // + return MathUtil.negativeToZero(getInterestPeriods().stream() // .map(InterestPeriod::getCreditedInterest) // - .reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); // + .reduce(getZero(), (value, previous) -> value.plus(previous, getMc())), getMc()); // } /** @@ -263,9 +260,9 @@ public final class RepaymentPeriod { * @return */ public Money getCapitalizedIncomePrincipal() { - return MathUtil.negativeToZero(interestPeriods.stream() // + return MathUtil.negativeToZero(getInterestPeriods().stream() // .map(InterestPeriod::getCapitalizedIncomePrincipal) // - .reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); // + .reduce(getZero(), (value, previous) -> value.plus(previous, getMc())), getMc()); // } /** @@ -275,8 +272,8 @@ public final class RepaymentPeriod { */ public Money getDuePrincipal() { // Due principal might be the maximum paid if there is pay-off or early repayment - return MathUtil.max( - MathUtil.negativeToZero(getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest().minus(getDueInterest(), mc), mc), + return MathUtil.max(MathUtil + .negativeToZero(getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest().minus(getDueInterest(), getMc()), getMc()), getPaidPrincipal(), false); } @@ -286,7 +283,7 @@ public final class RepaymentPeriod { * @return */ public Money getTotalCreditedAmount() { - return getCreditedPrincipal().plus(getCreditedInterest(), mc); + return getCreditedPrincipal().plus(getCreditedInterest(), getMc()); } /** @@ -295,7 +292,7 @@ public final class RepaymentPeriod { * @return */ public Money getTotalPaidAmount() { - return getPaidPrincipal().plus(getPaidInterest()); + return getPaidPrincipal().plus(getPaidInterest(), getMc()); } public boolean isFullyPaid() { @@ -309,35 +306,35 @@ public final class RepaymentPeriod { * @return */ public Money getUnrecognizedInterest() { - return getCalculatedDueInterest().minus(getDueInterest(), mc); + return getCalculatedDueInterest().minus(getDueInterest(), getMc()); } public Money getCreditedAmounts() { - return interestPeriods.stream().map(InterestPeriod::getCreditedAmounts).reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); + return interestPeriods.stream().map(InterestPeriod::getCreditedAmounts).reduce(getZero(), (m1, m2) -> m1.plus(m2, getMc())); } public Money getOutstandingLoanBalance() { if (outstandingBalanceCalculation == null) { outstandingBalanceCalculation = Memo.of(() -> { - InterestPeriod lastInterestPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1); + InterestPeriod lastInterestPeriod = getInterestPeriods().getLast(); Money calculatedOutStandingLoanBalance = lastInterestPeriod.getOutstandingLoanBalance() // - .plus(lastInterestPeriod.getBalanceCorrectionAmount(), mc) // - .plus(lastInterestPeriod.getCapitalizedIncomePrincipal(), mc) // - .plus(lastInterestPeriod.getDisbursementAmount(), mc) // - .minus(getDuePrincipal(), mc)// - .plus(getPaidPrincipal(), mc);// - return MathUtil.negativeToZero(calculatedOutStandingLoanBalance, mc); + .plus(lastInterestPeriod.getBalanceCorrectionAmount(), getMc()) // + .plus(lastInterestPeriod.getCapitalizedIncomePrincipal(), getMc()) // + .plus(lastInterestPeriod.getDisbursementAmount(), getMc()) // + .plus(getPaidPrincipal(), getMc()) // + .minus(getDuePrincipal(), getMc()); // + return MathUtil.negativeToZero(calculatedOutStandingLoanBalance, getMc()); }, () -> new Object[] { paidPrincipal, paidInterest, interestPeriods, totalDisbursedAmount }); } return outstandingBalanceCalculation.get(); } public void addPaidPrincipalAmount(Money paidPrincipal) { - this.paidPrincipal = MathUtil.plus(this.paidPrincipal, paidPrincipal, mc); + this.paidPrincipal = MathUtil.plus(this.getPaidPrincipal(), paidPrincipal, getMc()); } public void addPaidInterestAmount(Money paidInterest) { - this.paidInterest = MathUtil.plus(this.paidInterest, paidInterest, mc); + this.paidInterest = MathUtil.plus(this.getPaidInterest(), paidInterest, getMc()); } public Money getInitialBalanceForEmiRecalculation() { @@ -345,29 +342,28 @@ public final class RepaymentPeriod { if (getPrevious().isPresent()) { initialBalance = getPrevious().get().getOutstandingLoanBalance(); } else { - initialBalance = getZero(mc); + initialBalance = getZero(); } Money totalDisbursedAmount = getInterestPeriods().stream() // .map(InterestPeriod::getDisbursementAmount) // - .reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); // + .reduce(getZero(), (m1, m2) -> m1.plus(m2, getMc())); // Money totalCapitalizedIncomeAmount = getInterestPeriods().stream() // .map(InterestPeriod::getCapitalizedIncomePrincipal) // - .reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); // - return initialBalance.add(totalDisbursedAmount, mc).add(totalCapitalizedIncomeAmount, mc); + .reduce(getZero(), (m1, m2) -> m1.plus(m2, getMc())); // + return initialBalance.add(totalDisbursedAmount, getMc()).add(totalCapitalizedIncomeAmount, getMc()); } - private Money getZero(MathContext mc) { - // EMI is always initiated - return this.emi.zero(mc); + public Money getZero() { + return Money.zero(getCurrency(), getMc()); } public InterestPeriod getFirstInterestPeriod() { - return getInterestPeriods().get(0); + return getInterestPeriods().getFirst(); } public InterestPeriod getLastInterestPeriod() { List interestPeriods = getInterestPeriods(); - return interestPeriods.get(interestPeriods.size() - 1); + return interestPeriods.getLast(); } public Optional findInterestPeriod(@NotNull LocalDate transactionDate) { @@ -387,11 +383,11 @@ public final class RepaymentPeriod { * @return */ public Money getOutstandingInterest() { - return MathUtil.negativeToZero(getDueInterest().minus(getPaidInterest()), mc); + return MathUtil.negativeToZero(getDueInterest().minus(getPaidInterest()), getMc()); } public Money getOutstandingPrincipal() { - return MathUtil.negativeToZero(getDuePrincipal().minus(getPaidPrincipal()), mc); + return MathUtil.negativeToZero(getDuePrincipal().minus(getPaidPrincipal()), getMc()); } public void resetDerivedComponents() { @@ -404,21 +400,56 @@ public final class RepaymentPeriod { * can be null. if null it calculates total disbursement including last interest period. * @return disbursed and capitalized income amount till interest period. */ - public BigDecimal calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(InterestPeriod tillPeriod) { - BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount()).add(MathUtil.nullToZero(getTotalCapitalizedIncomeAmount())); - for (InterestPeriod interestPeriod : this.interestPeriods) { + public Money calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(InterestPeriod tillPeriod) { + Money res = MathUtil.plus(getMc(), getTotalDisbursedAmount(), getTotalCapitalizedIncomeAmount()); + for (InterestPeriod interestPeriod : this.getInterestPeriods()) { if (interestPeriod.equals(tillPeriod)) { break; } if (!interestPeriod.getDueDate().equals(getFromDate())) { if (interestPeriod.getDisbursementAmount() != null) { - res = res.add(interestPeriod.getDisbursementAmount().getAmount()); + res = res.plus(interestPeriod.getDisbursementAmount(), getMc()); } if (interestPeriod.getCapitalizedIncomePrincipal() != null) { - res = res.add(interestPeriod.getCapitalizedIncomePrincipal().getAmount()); + res = res.plus(interestPeriod.getCapitalizedIncomePrincipal(), getMc()); } } } return res; } + + public MonetaryCurrency getCurrency() { + if (currency == null) { + currency = MonetaryCurrency.fromCurrencyData(loanProductRelatedDetail.getCurrencyData()); + } + return currency; + } + + public Money getEmi() { + return MathUtil.nullToZero(emi, getCurrency(), getMc()); + } + + public Money getOriginalEmi() { + return MathUtil.nullToZero(originalEmi, getCurrency(), getMc()); + } + + public Money getPaidPrincipal() { + return MathUtil.nullToZero(paidPrincipal, getCurrency(), getMc()); + } + + public Money getPaidInterest() { + return MathUtil.nullToZero(paidInterest, getCurrency(), getMc()); + } + + public Money getFutureUnrecognizedInterest() { + return MathUtil.nullToZero(futureUnrecognizedInterest, getCurrency(), getMc()); + } + + public Money getTotalDisbursedAmount() { + return MathUtil.nullToZero(totalDisbursedAmount, getCurrency(), getMc()); + } + + public Money getTotalCapitalizedIncomeAmount() { + return MathUtil.nullToZero(totalCapitalizedIncomeAmount, getCurrency(), getMc()); + } } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriodTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriodTest.java new file mode 100644 index 000000000..68c67b4d7 --- /dev/null +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriodTest.java @@ -0,0 +1,138 @@ +/** + * 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.loanproduct.calc.data; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; +import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class InterestPeriodTest { + + private static final MathContext MC = new MathContext(12, RoundingMode.HALF_EVEN); + private static final CurrencyData USD = new CurrencyData("USD", "US Dollar", 2, 1, "$", "USD"); + private static final Money ZERO = Money.of(USD, BigDecimal.ZERO, MC); + + private static MockedStatic moneyHelper; + + @BeforeAll + static void init() { + moneyHelper = Mockito.mockStatic(MoneyHelper.class); + moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN); + moneyHelper.when(MoneyHelper::getMathContext).thenReturn(MC); + } + + @Test + void testGettersNeverReturnNull() { + // Create an empty InterestPeriod with all null Money fields + RepaymentPeriod repaymentPeriod = createMinimalRepaymentPeriod(); + InterestPeriod period = InterestPeriod.empty(repaymentPeriod, MC); + + // Test all Money getters + assertNotNull(period.getCreditedPrincipal()); + assertNotNull(period.getCreditedInterest()); + assertNotNull(period.getDisbursementAmount()); + assertNotNull(period.getBalanceCorrectionAmount()); + assertNotNull(period.getOutstandingLoanBalance()); + assertNotNull(period.getCapitalizedIncomePrincipal()); + + // Test BigDecimal getters + assertNotNull(period.getRateFactor()); + assertNotNull(period.getRateFactorTillPeriodDueDate()); + } + + @Test + void testMethodsDoNotThrowNPE() { + RepaymentPeriod repaymentPeriod = createMinimalRepaymentPeriod(); + InterestPeriod period = InterestPeriod.empty(repaymentPeriod, MC); + when(repaymentPeriod.getInterestPeriods()).thenReturn(List.of(period)); + when(repaymentPeriod.getFirstInterestPeriod()).thenReturn(period); + LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = mock( + LoanProductMinimumRepaymentScheduleRelatedDetail.class); + when(loanProductRelatedDetail.getCurrencyData()).thenReturn(USD); + when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE); + when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY); + when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true); + when(repaymentPeriod.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); + // Set some test data + period.setFromDate(LocalDate.now(ZoneId.of("UTC"))); + period.setDueDate(LocalDate.now(ZoneId.of("UTC")).plusDays(30)); + period.setRateFactor(BigDecimal.valueOf(0.01)); + period.setRateFactorTillPeriodDueDate(BigDecimal.valueOf(0.01)); + + // Test methods that perform calculations + assertDoesNotThrow(period::updateOutstandingLoanBalance); + assertDoesNotThrow(period::getCreditedAmounts); + assertDoesNotThrow(() -> period.getCalculatedDueInterest()); + assertDoesNotThrow(() -> period.getCalculatedDueInterest(InterestMethod.DECLINING_BALANCE, 30)); + assertDoesNotThrow(period::getLength); + assertDoesNotThrow(period::getLengthTillPeriodDueDate); + } + + @Test + void testWithNullFields() { + RepaymentPeriod repaymentPeriod = createMinimalRepaymentPeriod(); + InterestPeriod period = new InterestPeriod(repaymentPeriod, null, // fromDate + null, // dueDate + null, // rateFactor + null, // rateFactorTillPeriodDueDate + null, // creditedPrincipal + null, // creditedInterest + null, // disbursementAmount + null, // balanceCorrectionAmount + null, // outstandingLoanBalance + null, // capitalizedIncomePrincipal + MC, false // isPaused + ); + + // Test that getters don't throw and return non-null + assertDoesNotThrow(period::getCreditedPrincipal); + assertDoesNotThrow(period::getCreditedInterest); + assertDoesNotThrow(period::getDisbursementAmount); + assertDoesNotThrow(period::getBalanceCorrectionAmount); + assertDoesNotThrow(period::getOutstandingLoanBalance); + assertDoesNotThrow(period::getCapitalizedIncomePrincipal); + } + + private RepaymentPeriod createMinimalRepaymentPeriod() { + RepaymentPeriod repaymentPeriod = mock(RepaymentPeriod.class); + when(repaymentPeriod.getZero()).thenReturn(ZERO); + when(repaymentPeriod.getDueDate()).thenReturn(LocalDate.now(ZoneId.of("UTC")).plusMonths(1)); + when(repaymentPeriod.getCurrency()).thenReturn(ZERO.getCurrency()); + when(repaymentPeriod.getMc()).thenReturn(MC); + return repaymentPeriod; + } +} diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriodTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriodTest.java new file mode 100644 index 000000000..f7e05cc51 --- /dev/null +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriodTest.java @@ -0,0 +1,130 @@ +/** + * 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.loanproduct.calc.data; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.ZoneId; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; +import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class RepaymentPeriodTest { + + private static final MathContext MC = new MathContext(12, RoundingMode.HALF_EVEN); + private static final CurrencyData USD = new CurrencyData("USD", "US Dollar", 2, 1, "$", "USD"); + private static final Money ZERO = Money.of(USD, BigDecimal.ZERO, MC); + + private static MockedStatic moneyHelper; + private static LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail; + + @BeforeAll + static void init() { + moneyHelper = Mockito.mockStatic(MoneyHelper.class); + moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN); + moneyHelper.when(MoneyHelper::getMathContext).thenReturn(MC); + + loanProductRelatedDetail = mock(LoanProductMinimumRepaymentScheduleRelatedDetail.class); + when(loanProductRelatedDetail.getCurrencyData()).thenReturn(USD); + when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE); + when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY); + when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true); + } + + @Test + void testGettersNeverReturnNull() { + // Create a minimal RepaymentPeriod + LocalDate now = LocalDate.now(ZoneId.of("UTC")); + Money emi = ZERO; + RepaymentPeriod period = RepaymentPeriod.create(null, now, now.plusMonths(1), emi, MC, loanProductRelatedDetail); + + // Test all Money getters + assertNotNull(period.getEmi()); + assertNotNull(period.getOriginalEmi()); + assertNotNull(period.getPaidPrincipal()); + assertNotNull(period.getPaidInterest()); + assertNotNull(period.getFutureUnrecognizedInterest()); + + // Test BigDecimal getters + assertNotNull(period.getTotalDisbursedAmount()); + assertNotNull(period.getTotalCapitalizedIncomeAmount()); + } + + @Test + void testMethodsDoNotThrowNPE() { + LocalDate now = LocalDate.now(ZoneId.of("UTC")); + Money emi = ZERO; + RepaymentPeriod period = RepaymentPeriod.create(null, now, now.plusMonths(1), emi, MC, loanProductRelatedDetail); + + // Add an interest period + InterestPeriod interestPeriod = InterestPeriod.withEmptyAmounts(period, now, now.plusMonths(1)); + period.getInterestPeriods().add(interestPeriod); + + // Test methods that perform calculations + assertDoesNotThrow(period::getCalculatedDueInterest); + assertDoesNotThrow(period::getDueInterest); + assertDoesNotThrow(period::getCalculatedDuePrincipal); + assertDoesNotThrow(period::getDuePrincipal); + assertDoesNotThrow(period::getTotalCreditedAmount); + assertDoesNotThrow(period::getTotalPaidAmount); + assertDoesNotThrow(period::getUnrecognizedInterest); + assertDoesNotThrow(period::getCreditedAmounts); + assertDoesNotThrow(period::getOutstandingLoanBalance); + assertDoesNotThrow(period::getInitialBalanceForEmiRecalculation); + } + + @Test + void testEmptyRepaymentPeriod() { + // Create an empty repayment period with all null fields + RepaymentPeriod period = new RepaymentPeriod(null, // previous + null, // fromDate + null, // dueDate + null, // interestPeriods + null, // emi + null, // originalEmi + null, // paidPrincipal + null, // paidInterest + null, // futureUnrecognizedInterest + MC, // mc + loanProductRelatedDetail); + + // Test that getters don't throw and return non-null + assertDoesNotThrow(period::getEmi); + assertDoesNotThrow(period::getOriginalEmi); + assertDoesNotThrow(period::getPaidPrincipal); + assertDoesNotThrow(period::getPaidInterest); + assertDoesNotThrow(period::getFutureUnrecognizedInterest); + assertDoesNotThrow(period::getTotalDisbursedAmount); + assertDoesNotThrow(period::getTotalCapitalizedIncomeAmount); + } +}