diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 84369c0d7..ed50efdde 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -1264,13 +1264,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.repaymentScheduleInstallments.clear(); for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { - if (scheduledLoanInstallment.isRepaymentPeriod()) { + if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) { final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(this, scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent(), - scheduledLoanInstallment.getLoanCompoundingDetails()); + scheduledLoanInstallment.getLoanCompoundingDetails(), scheduledLoanInstallment.rescheduleInterestPortion(), + scheduledLoanInstallment.isDownPaymentPeriod()); addLoanRepaymentScheduleInstallment(installment); } } @@ -2477,7 +2478,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public ChangedTransactionDetail disburse(final AppUser currentUser, final JsonCommand command, final Map actualChanges, - final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail) { + final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail, boolean downPaymentEnabled) { final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE); @@ -2495,7 +2496,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { validateDisbursementDateIsOnHoliday(holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(this.repaymentScheduleDetail().isInterestRecalculationEnabled(), - isDisbursementMissed(), scheduleGeneratorDTO); + isDisbursementMissed(), scheduleGeneratorDTO, downPaymentEnabled); updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); @@ -2527,14 +2528,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private void regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(boolean interestRecalculationEnabledParam, - boolean disbursementMissedParam, ScheduleGeneratorDTO scheduleGeneratorDTO) { + boolean disbursementMissedParam, ScheduleGeneratorDTO scheduleGeneratorDTO, final boolean downPaymentEnabled) { LocalDate firstInstallmentDueDate = fetchRepaymentScheduleInstallment(1).getDueDate(); - if (interestRecalculationEnabledParam - && (firstInstallmentDueDate.isBefore(DateUtils.getBusinessLocalDate()) || disbursementMissedParam)) { + if ((interestRecalculationEnabledParam + && (firstInstallmentDueDate.isBefore(DateUtils.getBusinessLocalDate()) || disbursementMissedParam))) { regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); + } else if (downPaymentEnabled) { + LoanScheduleDTO loanSchedule = getRecalculatedSchedule(scheduleGeneratorDTO); + updateLoanSchedule(loanSchedule.getInstallments()); } - } private List getDisbursedLoanDisbursementDetails() { @@ -2965,7 +2968,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { externalId = ExternalId.generate(); } Money downPaymentMoney = Money.of(getCurrency(), - MathUtil.percentageOf(disbursedAmount, disbursedAmountPercentageForDownPayment, 9)); + MathUtil.percentageOf(disbursedAmount, disbursedAmountPercentageForDownPayment, 19)); LoanTransaction downPaymentTransaction = LoanTransaction.downPayment(getOffice(), downPaymentMoney, null, disbursedOn, externalId); /* @@ -5553,7 +5556,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public void regenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) { - LocalDate lastTransactionDate = getLastUserTransactionDate(); final LoanScheduleDTO loanSchedule = getRecalculatedSchedule(generatorDTO); if (loanSchedule == null) { @@ -5788,15 +5790,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO generatorDTO) { - - if (!this.repaymentScheduleDetail().isInterestRecalculationEnabled() || isNpa || isChargedOff()) { + if (!this.repaymentScheduleDetail().isEnableDownPayment() + && (!this.repaymentScheduleDetail().isInterestRecalculationEnabled() || isNpa || isChargedOff())) { return null; } final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); final LoanScheduleGenerator loanScheduleGenerator = generatorDTO.getLoanScheduleFactory().create(interestMethod); final RoundingMode roundingMode = MoneyHelper.getRoundingMode(); - final MathContext mc = new MathContext(8, roundingMode); + final MathContext mc = new MathContext(19, roundingMode); final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(generatorDTO); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index b061f7695..98f8bbba2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -131,6 +131,9 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa @Column(name = "credits_amount", scale = 6, precision = 19, nullable = true) private BigDecimal credits; + @Column(name = "is_down_payment", nullable = false) + private boolean isDownPayment; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "loanRepaymentScheduleInstallment") private Set loanCompoundingDetails = new HashSet<>(); @@ -154,6 +157,15 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges, final BigDecimal penaltyCharges, final boolean recalculatedInterestComponent, final Set compoundingDetails, final BigDecimal rescheduleInterestPortion) { + this(loan, installmentNumber, fromDate, dueDate, principal, interest, feeCharges, penaltyCharges, recalculatedInterestComponent, + compoundingDetails, rescheduleInterestPortion, false); + } + + public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate fromDate, + final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges, + final BigDecimal penaltyCharges, final boolean recalculatedInterestComponent, + final Set compoundingDetails, final BigDecimal rescheduleInterestPortion, + final boolean isDownPayment) { this.loan = loan; this.installmentNumber = installmentNumber; this.fromDate = fromDate; @@ -169,6 +181,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa } this.loanCompoundingDetails = compoundingDetails; this.rescheduleInterestPortion = rescheduleInterestPortion; + this.isDownPayment = isDownPayment; } public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate fromDate, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java index fc74cbfa7..cbd789b02 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java @@ -91,6 +91,11 @@ public final class LoanSchedulePeriodData { totalInstallmentAmountForPeriod); } + public static LoanSchedulePeriodData downPaymentOnlyPeriod(final Integer periodNumber, final LocalDate periodDate, + final BigDecimal principalDue, final BigDecimal principalOutstanding) { + return new LoanSchedulePeriodData(periodNumber, periodDate, periodDate, principalDue, principalOutstanding); + } + public static LoanSchedulePeriodData repaymentPeriodWithPayments(@SuppressWarnings("unused") final Long loanId, final Integer periodNumber, final LocalDate fromDate, final LocalDate dueDate, final LocalDate obligationsMetOnDate, final boolean complete, final BigDecimal principalOriginalDue, final BigDecimal principalPaid, @@ -265,6 +270,61 @@ public final class LoanSchedulePeriodData { this.totalCredits = BigDecimal.ZERO; } + // TODO refactor the class to builder pattern + private LoanSchedulePeriodData(Integer periodNumber, LocalDate fromDate, LocalDate dueDate, BigDecimal principalDue, + BigDecimal principalOutstanding) { + this.period = periodNumber; + this.fromDate = fromDate; + this.dueDate = dueDate; + this.obligationsMetOnDate = null; + this.complete = null; + this.daysInPeriod = 1; + this.principalDisbursed = null; + this.principalOriginalDue = principalDue; + this.principalDue = principalOriginalDue; + this.principalPaid = null; + this.principalWrittenOff = null; + this.principalOutstanding = principalOriginalDue; + this.principalLoanBalanceOutstanding = principalOutstanding; + + this.interestOriginalDue = null; + this.interestDue = null; + this.interestPaid = null; + this.interestWaived = null; + this.interestWrittenOff = null; + this.interestOutstanding = null; + + this.feeChargesDue = null; + this.feeChargesPaid = null; + this.feeChargesWaived = null; + this.feeChargesWrittenOff = null; + this.feeChargesOutstanding = null; + + this.penaltyChargesDue = null; + this.penaltyChargesPaid = null; + this.penaltyChargesWaived = null; + this.penaltyChargesWrittenOff = null; + this.penaltyChargesOutstanding = null; + + this.totalOriginalDueForPeriod = principalDue; + this.totalDueForPeriod = principalDue; + this.totalPaidForPeriod = BigDecimal.ZERO; + this.totalPaidInAdvanceForPeriod = null; + this.totalPaidLateForPeriod = null; + this.totalWaivedForPeriod = null; + this.totalWrittenOffForPeriod = null; + this.totalOutstandingForPeriod = totalDueForPeriod; + this.totalActualCostOfLoanForPeriod = null; + this.totalInstallmentAmountForPeriod = totalDueForPeriod; + + if (dueDate.isBefore(DateUtils.getBusinessLocalDate())) { + this.totalOverdue = this.totalOutstandingForPeriod; + } else { + this.totalOverdue = null; + } + this.totalCredits = BigDecimal.ZERO; + } + /* * Used for creating loan schedule periods with full information on expected principal, interest & charges along * with what portion of each is paid. diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index b40103736..6d7ffe7d9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -1737,4 +1737,12 @@ public final class LoanApplicationTerms { public void setNewScheduledDueDateStart(LocalDate newScheduledDueDateStart) { this.newScheduledDueDateStart = newScheduledDueDateStart; } + + public boolean isDownPaymentEnabled() { + return isDownPaymentEnabled; + } + + public BigDecimal getDisbursedAmountPercentageForDownPayment() { + return disbursedAmountPercentageForDownPayment; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java index c7bc680a8..13da2efd6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java @@ -31,6 +31,8 @@ public interface LoanScheduleModelPeriod { boolean isRepaymentPeriod(); + boolean isDownPaymentPeriod(); + Integer periodNumber(); LocalDate periodFromDate(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleModelDownPaymentPeriod.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleModelDownPaymentPeriod.java new file mode 100644 index 000000000..d969c2e2f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleModelDownPaymentPeriod.java @@ -0,0 +1,148 @@ +/** + * 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.loanschedule.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Set; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; + +public final class LoanScheduleModelDownPaymentPeriod implements LoanScheduleModelPeriod { + + private final int periodNumber; + private final LocalDate periodDate; + private Money principalDue; + private final Money outstandingLoanBalance; + + public static LoanScheduleModelDownPaymentPeriod downPayment(final int periodNumber, final LocalDate periodDate, + final Money principalDue, final Money outstandingLoanBalance) { + + return new LoanScheduleModelDownPaymentPeriod(periodNumber, periodDate, principalDue, outstandingLoanBalance); + } + + private LoanScheduleModelDownPaymentPeriod(int periodNumber, LocalDate periodDate, Money principalDue, Money outstandingLoanBalance) { + this.periodNumber = periodNumber; + this.periodDate = periodDate; + this.principalDue = principalDue; + this.outstandingLoanBalance = outstandingLoanBalance; + } + + @Override + public LoanSchedulePeriodData toData() { + return LoanSchedulePeriodData.downPaymentOnlyPeriod(this.periodNumber, this.periodDate, this.principalDue.getAmount(), + this.outstandingLoanBalance.getAmount()); + } + + @Override + public boolean isRepaymentPeriod() { + return false; + } + + @Override + public boolean isDownPaymentPeriod() { + return true; + } + + @Override + public Integer periodNumber() { + return this.periodNumber; + } + + @Override + public LocalDate periodFromDate() { + return this.periodDate; + } + + @Override + public LocalDate periodDueDate() { + return this.periodDate; + } + + @Override + public BigDecimal principalDue() { + BigDecimal value = BigDecimal.ZERO; + if (this.principalDue != null) { + value = this.principalDue.getAmount(); + } + + return value; + } + + @Override + public BigDecimal interestDue() { + return BigDecimal.ZERO; + } + + @Override + public BigDecimal feeChargesDue() { + return BigDecimal.ZERO; + } + + @Override + public BigDecimal penaltyChargesDue() { + return BigDecimal.ZERO; + } + + @Override + public void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge) { + + } + + @Override + public boolean isRecalculatedInterestComponent() { + return false; + } + + @Override + public void addPrincipalAmount(Money principalDue) { + + } + + @Override + public void addInterestAmount(Money interestDue) { + + } + + @Override + public Set getLoanCompoundingDetails() { + return null; + } + + @Override + public void setEMIFixedSpecificToInstallmentTrue() { + + } + + @Override + public boolean isEMIFixedSpecificToInstallment() { + return false; + } + + @Override + public BigDecimal rescheduleInterestPortion() { + return null; + } + + @Override + public void setRescheduleInterestPortion(BigDecimal rescheduleInterestPortion) { + + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java index 0f28b88a7..282915b45 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java @@ -218,8 +218,8 @@ public final class LoanScheduleParams { final Collection recalculationDetails = null; final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = null; final LocalDate scheduleTillDate = null; - final boolean applyInterestRecalculation = false; final Money unCompoundedAmount = Money.zero(currency); + final boolean applyInterestRecalculation = false; return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index f6193dfe2..d7063ad2c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -29,9 +29,11 @@ import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -50,6 +52,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleIns import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException; import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException; @@ -88,7 +91,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); final int numberOfRepayments = loanApplicationTerms.fetchNumberOfRepaymentsAfterExceptions(); - LoanScheduleParams scheduleParams = null; + LoanScheduleParams scheduleParams; if (loanScheduleParams == null) { scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, Money.of(currency, chargesDueAtTimeOfDisbursement), loanApplicationTerms.getExpectedDisbursementDate(), getPrincipalToBeScheduled(loanApplicationTerms)); @@ -107,7 +110,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener Collection periods = new ArrayList<>(); if (!scheduleParams.isPartialUpdate()) { periods = createNewLoanScheduleListWithDisbursementDetails(numberOfRepayments, loanApplicationTerms, - chargesDueAtTimeOfDisbursement); + chargesDueAtTimeOfDisbursement, scheduleParams); } // Determine the total interest owed over the full loan for FLAT @@ -128,16 +131,17 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener loanApplicationTerms.getLoanCalendar(), loanApplicationTerms.getHolidayDetailDTO(), loanApplicationTerms); if (!scheduleParams.isPartialUpdate()) { + Long notDownPaymentPeriodNumber = periods.stream().filter(period -> !period.isDownPaymentPeriod()).count(); // Set Fixed Principal Amount - updateAmortization(mc, loanApplicationTerms, scheduleParams.getPeriodNumber(), scheduleParams.getOutstandingBalance()); + updateAmortization(mc, loanApplicationTerms, notDownPaymentPeriodNumber.intValue(), scheduleParams.getOutstandingBalance()); if (loanApplicationTerms.isMultiDisburseLoan()) { /* fetches the first tranche amount and also updates other tranche details to map */ BigDecimal disburseAmt = getDisbursementAmount(loanApplicationTerms, scheduleParams.getPeriodStartDate(), periods, - chargesDueAtTimeOfDisbursement, scheduleParams.getDisburseDetailMap(), scheduleParams.applyInterestRecalculation()); + chargesDueAtTimeOfDisbursement, scheduleParams.getDisburseDetailMap(), scheduleParams.applyInterestRecalculation(), + scheduleParams); scheduleParams.setPrincipalToBeScheduled(Money.of(currency, disburseAmt)); loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().zero().plus(disburseAmt)); - scheduleParams.setOutstandingBalance(Money.of(currency, disburseAmt)); scheduleParams.setOutstandingBalanceAsPerRest(Money.of(currency, disburseAmt)); } } @@ -178,6 +182,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener isFirstRepayment = false; } } + while (!scheduleParams.getOutstandingBalance().isZero() || !scheduleParams.getDisburseDetailMap().isEmpty()) { LocalDate previousRepaymentDate = scheduleParams.getActualRepaymentDate(); scheduleParams.setActualRepaymentDate(this.scheduledDateGenerator @@ -976,6 +981,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod .disbursement(disburseDetail.getKey(), disburseDetail.getValue(), chargesDueAtTimeOfDisbursement); periods.add(disbursementPeriod); + + if (loanApplicationTerms.isDownPaymentEnabled()) { + createDownPaymentPeriod(loanApplicationTerms, scheduleParams, periods, disburseDetail.getKey(), + disburseDetail.getValue().getAmount()); + } + // updates actual outstanding balance with new // disbursement detail scheduleParams.addOutstandingBalance(disburseDetail.getValue()); @@ -1461,7 +1472,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private Collection getApplicableTransactionsForPeriod(final boolean applyInterestRecalculation, LocalDate repaymentDate, final Collection transactions) { Collection applicableTransactions = new ArrayList<>(); - if (applyInterestRecalculation) { + if (applyInterestRecalculation && !Objects.isNull(transactions)) { for (RecalculationDetail detail : transactions) { if (!detail.getTransactionDate().isAfter(repaymentDate)) { applicableTransactions.add(detail); @@ -1912,26 +1923,38 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private BigDecimal getDisbursementAmount(final LoanApplicationTerms loanApplicationTerms, LocalDate disbursementDate, final Collection periods, final BigDecimal chargesDueAtTimeOfDisbursement, - final Map disurseDetail, final boolean excludePastUndisbursed) { - + final Map disburseDetails, final boolean excludePastUndisbursed, LoanScheduleParams scheduleParams) { // this method relates to multi-disbursement loans + MonetaryCurrency currency = loanApplicationTerms.getPrincipal().getCurrency(); + if (Objects.isNull(scheduleParams)) { + scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, Money.of(currency, chargesDueAtTimeOfDisbursement), + loanApplicationTerms.getExpectedDisbursementDate(), getPrincipalToBeScheduled(loanApplicationTerms)); + } BigDecimal principal = BigDecimal.ZERO; + scheduleParams.setOutstandingBalance(Money.zero(currency)); if (loanApplicationTerms.getDisbursementDatas().size() == 0) { // non tranche loans have no disbursement data entries in submitted and approved status // the appropriate approved amount or applied for amount is used to show a proposed schedule if (loanApplicationTerms.getApprovedPrincipal().getAmount().compareTo(BigDecimal.ZERO) > 0) { principal = loanApplicationTerms.getApprovedPrincipal().getAmount(); + scheduleParams.addOutstandingBalance(Money.of(currency, principal)); } else { principal = loanApplicationTerms.getPrincipal().getAmount(); + scheduleParams.addOutstandingBalance(Money.of(currency, principal)); } } else { - MonetaryCurrency currency = loanApplicationTerms.getPrincipal().getCurrency(); for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) { if (disbursementData.disbursementDate().equals(disbursementDate)) { final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( disbursementData.disbursementDate(), Money.of(currency, disbursementData.getPrincipal()), chargesDueAtTimeOfDisbursement); periods.add(disbursementPeriod); + scheduleParams.addOutstandingBalance(Money.of(currency, disbursementData.getPrincipal())); + if (loanApplicationTerms.isDownPaymentEnabled()) { + createDownPaymentPeriod(loanApplicationTerms, scheduleParams, periods, disbursementData.disbursementDate(), + disbursementData.getPrincipal()); + } + principal = principal.add(disbursementData.getPrincipal()); } else if (!excludePastUndisbursed || disbursementData.isDisbursed() || !disbursementData.disbursementDate().isBefore(DateUtils.getBusinessLocalDate())) { @@ -1939,13 +1962,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener * JW: sums up amounts by disbursal date in case of side-effect issues. Original assumed that there * were no duplicate disbursal dates and 'put' each amount into the map keyed by date */ - Money prevsum = disurseDetail.get(disbursementData.disbursementDate()); + Money prevsum = disburseDetails.get(disbursementData.disbursementDate()); BigDecimal sumToNow = BigDecimal.ZERO; if (prevsum != null) { sumToNow = prevsum.getAmount(); } sumToNow = sumToNow.add(disbursementData.getPrincipal()); - disurseDetail.put(disbursementData.disbursementDate(), Money.of(currency, sumToNow)); + disburseDetails.put(disbursementData.disbursementDate(), Money.of(currency, sumToNow)); } } } @@ -1953,9 +1976,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } private Collection createNewLoanScheduleListWithDisbursementDetails(final int numberOfRepayments, - final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement) { - - Collection periods = null; + final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement, + LoanScheduleParams scheduleParams) { + Collection periods; if (loanApplicationTerms.isMultiDisburseLoan()) { int multiDisbursalTrancheEntries = loanApplicationTerms.getDisbursementDatas().size(); if (multiDisbursalTrancheEntries == 0) { @@ -1968,11 +1991,35 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod .disbursement(loanApplicationTerms, chargesDueAtTimeOfDisbursement); periods.add(disbursementPeriod); + if (loanApplicationTerms.isDownPaymentEnabled()) { + createDownPaymentPeriod(loanApplicationTerms, scheduleParams, periods, loanApplicationTerms.getExpectedDisbursementDate(), + loanApplicationTerms.getPrincipal().getAmount()); + } } return periods; } + private void createDownPaymentPeriod(LoanApplicationTerms loanApplicationTerms, LoanScheduleParams scheduleParams, + Collection periods, LocalDate date, BigDecimal periodBaseAmount) { + BigDecimal downPaymentAmount = MathUtil.percentageOf(periodBaseAmount, + loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19); + Money downPayment = Money.of(loanApplicationTerms.getCurrency(), downPaymentAmount); + LoanScheduleModelDownPaymentPeriod installment = LoanScheduleModelDownPaymentPeriod + .downPayment(scheduleParams.getInstalmentNumber(), date, downPayment, scheduleParams.getOutstandingBalance()); + + addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment); + + scheduleParams.incrementPeriodNumber(); + scheduleParams.incrementInstalmentNumber(); + + periods.add(installment); + + scheduleParams.reduceOutstandingBalance(downPayment); + scheduleParams.addTotalCumulativePrincipal(downPayment); + scheduleParams.addTotalRepaymentExpected(downPayment); + } + private Set seperateTotalCompoundingPercentageCharges(final Set loanCharges) { Set interestCharges = new HashSet<>(); for (final LoanCharge loanCharge : loanCharges) { @@ -2090,11 +2137,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, Loan loan, final HolidayDetailDTO holidayDetailDTO, - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate rescheduleFrom, + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, LocalDate rescheduleFrom, final LocalDate scheduleTillDate) { // Loan transactions to process and find the variation on payments Collection recalculationDetails = new ArrayList<>(); List transactions = loan.getLoanTransactions(); + MonetaryCurrency currency = loanApplicationTerms.getCurrency(); + final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loan.getActiveCharges()); for (LoanTransaction loanTransaction : transactions) { if (loanTransaction.isPaymentTransaction()) { recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(), @@ -2110,6 +2159,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // this block is to retain the schedule installments prior to the // provided date and creates late and early payment details for further // calculations + if (Objects.isNull(rescheduleFrom) && loanApplicationTerms.isDownPaymentEnabled()) { + rescheduleFrom = loanApplicationTerms.getExpectedDisbursementDate(); + } if (rescheduleFrom != null) { Money principalToBeScheduled = getPrincipalToBeScheduled(loanApplicationTerms); // actual outstanding balance for interest calculation @@ -2119,10 +2171,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // this is required to update total fee amounts in the // LoanScheduleModel - final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loan.getActiveCharges()); + if (loanApplicationTerms.isDownPaymentEnabled()) { + loanScheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, + Money.of(currency, chargesDueAtTimeOfDisbursement), loanApplicationTerms.getExpectedDisbursementDate(), + getPrincipalToBeScheduled(loanApplicationTerms)); + } periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms.fetchNumberOfRepaymentsAfterExceptions(), - loanApplicationTerms, chargesDueAtTimeOfDisbursement); - MonetaryCurrency currency = outstandingBalance.getCurrency(); + loanApplicationTerms, chargesDueAtTimeOfDisbursement, loanScheduleParams); // early payments will be added here and as per the selected // strategy @@ -2160,7 +2215,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener Money totalCumulativeInterest = principalToBeScheduled.zero(); Money totalFeeChargesCharged = principalToBeScheduled.zero().plus(chargesDueAtTimeOfDisbursement); Money totalPenaltyChargesCharged = principalToBeScheduled.zero(); - Money totalRepaymentExpected = principalToBeScheduled.zero(); + Money totalRepaymentExpected; // Actual period Number as per the schedule int periodNumber = 1; @@ -2180,7 +2235,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // tranche // details to map BigDecimal disburseAmt = getDisbursementAmount(loanApplicationTerms, loanApplicationTerms.getExpectedDisbursementDate(), - periods, chargesDueAtTimeOfDisbursement, disburseDetailMap, true); + periods, chargesDueAtTimeOfDisbursement, disburseDetailMap, true, loanScheduleParams); outstandingBalance = outstandingBalance.zero().plus(disburseAmt); outstandingBalanceAsPerRest = outstandingBalance; principalToBeScheduled = principalToBeScheduled.zero().plus(disburseAmt); @@ -2273,6 +2328,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod .disbursement(disburseDetail.getKey(), disburseDetail.getValue(), chargesDueAtTimeOfDisbursement); periods.add(disbursementPeriod); + if (loanApplicationTerms.isDownPaymentEnabled()) { + loanScheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, + Money.of(currency, chargesDueAtTimeOfDisbursement), loanApplicationTerms.getExpectedDisbursementDate(), + getPrincipalToBeScheduled(loanApplicationTerms)); + createDownPaymentPeriod(loanApplicationTerms, loanScheduleParams, periods, disburseDetail.getKey(), + disbursementPeriod.principalDue()); + } // updates actual outstanding balance with new // disbursement detail outstandingBalance = outstandingBalance.plus(disburseDetail.getValue()); @@ -2388,9 +2450,16 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } // for complete schedule generation if (loanScheduleParams == null) { - loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForCompleteUpdate(recalculationDetails, - loanRepaymentScheduleTransactionProcessor, scheduleTillDate, applyInterestRecalculation); - periods.clear(); + if (loanApplicationTerms.isDownPaymentEnabled()) { + loanScheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, + Money.of(currency, chargesDueAtTimeOfDisbursement), loanApplicationTerms.getExpectedDisbursementDate(), + getPrincipalToBeScheduled(loanApplicationTerms)); + periods.clear(); + } else { + loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForCompleteUpdate(recalculationDetails, + loanRepaymentScheduleTransactionProcessor, scheduleTillDate, applyInterestRecalculation); + periods.clear(); + } } if (retainedInstallments.size() > 0 @@ -2402,7 +2471,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener loanScheduleParams); for (LoanScheduleModelPeriod loanScheduleModelPeriod : loanScheduleModel.getPeriods()) { - if (loanScheduleModelPeriod.isRepaymentPeriod()) { + if (loanScheduleModelPeriod.isRepaymentPeriod() || loanScheduleModelPeriod.isDownPaymentPeriod()) { // adding newly created repayment periods to installments addLoanRepaymentScheduleInstallment(retainedInstallments, loanScheduleModelPeriod); } @@ -2598,13 +2667,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private LoanRepaymentScheduleInstallment addLoanRepaymentScheduleInstallment(final List installments, final LoanScheduleModelPeriod scheduledLoanInstallment) { LoanRepaymentScheduleInstallment installment = null; - if (scheduledLoanInstallment.isRepaymentPeriod()) { + if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) { installment = new LoanRepaymentScheduleInstallment(null, scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent(), scheduledLoanInstallment.getLoanCompoundingDetails(), - scheduledLoanInstallment.rescheduleInterestPortion()); + scheduledLoanInstallment.rescheduleInterestPortion(), scheduledLoanInstallment.isDownPaymentPeriod()); installments.add(installment); } return installment; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java index 87e928b2b..8a5104e9c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java @@ -69,6 +69,11 @@ public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleMo return false; } + @Override + public boolean isDownPaymentPeriod() { + return false; + } + @Override public Integer periodNumber() { return null; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java index 42441457e..af13f3f3d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java @@ -80,6 +80,11 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel return true; } + @Override + public boolean isDownPaymentPeriod() { + return false; + } + @Override public Integer periodNumber() { return this.periodNumber; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index ada7fb2f8..7c3c739f8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -445,13 +445,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } regenerateScheduleOnDisbursement(command, loan, recalculateSchedule, scheduleGeneratorDTO, nextPossibleRepaymentDate, rescheduledRepaymentDate); - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + boolean downPaymentEnabled = loan.repaymentScheduleDetail().isEnableDownPayment(); + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || downPaymentEnabled) { createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); } if (isPaymentTypeApplicableForDisbursementCharge) { - changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail); + changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail, + downPaymentEnabled); } else { - changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); + changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null, downPaymentEnabled); } loan.adjustNetDisbursalAmount(amountToDisburse.getAmount()); loan.handleDownPayment(amountToDisburse.getAmount(), command, scheduleGeneratorDTO); @@ -730,13 +732,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); regenerateScheduleOnDisbursement(command, loan, recalculateSchedule, scheduleGeneratorDTO, nextPossibleRepaymentDate, rescheduledRepaymentDate); - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + boolean downPaymentEnabled = loan.repaymentScheduleDetail().isEnableDownPayment(); + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || downPaymentEnabled) { createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); } if (configurationDomainService.isPaymentTypeApplicableForDisbursementCharge()) { - changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail); + changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail, + downPaymentEnabled); } else { - changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); + changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null, downPaymentEnabled); } } if (!changes.isEmpty()) { @@ -2467,7 +2471,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private List retrieveRepaymentScheduleFromModel(LoanScheduleModel model) { final List installments = new ArrayList<>(); for (final LoanScheduleModelPeriod scheduledLoanInstallment : model.getPeriods()) { - if (scheduledLoanInstallment.isRepaymentPeriod()) { + if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) { final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(null, scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(), diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index b0dc99920..657505a99 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -142,4 +142,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0123_add_is_down_payment_to_repayment_schedule.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0123_add_is_down_payment_to_repayment_schedule.xml new file mode 100644 index 000000000..1d2d91695 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0123_add_is_down_payment_to_repayment_schedule.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java new file mode 100644 index 000000000..b99b998c1 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java @@ -0,0 +1,358 @@ +/** + * 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.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class LoanRepaymentScheduleWithDownPaymentTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private LoanTransactionHelper loanTransactionHelper; + private ClientHelper clientHelper; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); + } + + @Test + public void loanRepaymentScheduleWithSimpleDisbursementAndDownPayment() { + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = false; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, + "25", enableAutoRepaymentForDownPayment, false); + + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createApproveAndDisburseLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedDownPaymentAmount = 250.00; + LocalDate expectedDownPaymentDueDate = LocalDate.of(2022, 9, 3); + Double expectedRepaymentAmount = 750.00; + LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3); + + assertTrue(periods.stream() // + .anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalDueForPeriod()) // + && expectedDownPaymentDueDate.equals(period.getDueDate()))); + assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod()) + && expectedRepaymentDueDate.equals(period.getDueDate()))); + } + + @Test + public void loanRepaymentScheduleWithSimpleDisbursementAndAutoRepaymentDownPayment() { + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = true; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, + "25", enableAutoRepaymentForDownPayment, false); + + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createApproveAndDisburseLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedDownPaymentAmount = 250.00; + LocalDate expectedDownPaymentDueDate = LocalDate.of(2022, 9, 3); + Double expectedRepaymentAmount = 750.00; + LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3); + + assertTrue(periods.stream() // + .anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalPaidForPeriod()) // + && expectedDownPaymentDueDate.equals(period.getDueDate()))); + assertEquals(expectedRepaymentAmount, loanDetails.getSummary().getTotalOutstanding()); + assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod()) + && expectedRepaymentDueDate.equals(period.getDueDate()))); + } + + @Test + public void loanRepaymentScheduleWithMultiDisbursementProductOneDisbursementAndDownPayment() { + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = false; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, + "25", enableAutoRepaymentForDownPayment, true); + + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createApproveAndDisburseLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedDownPaymentAmount = 250.00; + LocalDate expectedDownPaymentDueDate = LocalDate.of(2022, 9, 3); + Double expectedRepaymentAmount = 750.00; + LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3); + + assertTrue(periods.stream() // + .anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalDueForPeriod()) // + && expectedDownPaymentDueDate.equals(period.getDueDate()))); + assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod()) + && expectedRepaymentDueDate.equals(period.getDueDate()))); + } + + @Test + public void loanRepaymentScheduleWithMultiDisbursementProductTwoDisbursementAndDownPayment() { + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = false; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, + "25", enableAutoRepaymentForDownPayment, true); + + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createApproveAndDisburseTwiceLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedFirstDownPaymentAmount = 175.00; + LocalDate expectedFirstDownPaymentDueDate = LocalDate.of(2022, 9, 3); + Double expectedSecondDownPaymentAmount = 75.00; + LocalDate expectedSecondDownPaymentDueDate = LocalDate.of(2022, 9, 4); + Double expectedRepaymentAmount = 750.00; + LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3); + + assertTrue(periods.stream() // + .anyMatch(period -> expectedFirstDownPaymentAmount.equals(period.getTotalDueForPeriod()) // + && expectedFirstDownPaymentDueDate.equals(period.getDueDate()))); + assertTrue(periods.stream() // + .anyMatch(period -> expectedSecondDownPaymentAmount.equals(period.getTotalDueForPeriod()) + && expectedSecondDownPaymentDueDate.equals(period.getDueDate()))); + assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod()) + && expectedRepaymentDueDate.equals(period.getDueDate()))); + } + + @Test + public void loanRepaymentScheduleWithMultiDisbursementProductTwoDisbursementAndAutoRepaymentDownPayment() { + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = true; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, + "25", enableAutoRepaymentForDownPayment, true); + + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createApproveAndDisburseTwiceLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedFirstDownPaymentAmount = 175.00; + LocalDate expectedFirstDownPaymentDueDate = LocalDate.of(2022, 9, 3); + Double expectedSecondDownPaymentAmount = 75.00; + LocalDate expectedSecondDownPaymentDueDate = LocalDate.of(2022, 9, 4); + Double expectedRepaymentAmount = 750.00; + LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3); + + assertTrue(periods.stream() // + .anyMatch(period -> expectedFirstDownPaymentAmount.equals(period.getTotalPaidForPeriod()) // + && expectedFirstDownPaymentDueDate.equals(period.getDueDate()))); + assertTrue(periods.stream() // + .anyMatch(period -> expectedSecondDownPaymentAmount.equals(period.getTotalPaidForPeriod()) + && expectedSecondDownPaymentDueDate.equals(period.getDueDate()))); + assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod()) + && expectedRepaymentDueDate.equals(period.getDueDate()))); + assertEquals(expectedRepaymentAmount, loanDetails.getSummary().getTotalOutstanding()); + } + + private Integer createLoanProductWithDownPaymentConfiguration(final LoanTransactionHelper loanTransactionHelper, + final Integer delinquencyBucketId, Boolean enableDownPayment, String disbursedAmountPercentageForDownPayment, + Boolean enableAutoRepaymentForDownPayment, boolean multiDisbursement) { + HashMap loanProductMap; + if (multiDisbursement) { + loanProductMap = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments() // + .withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true) + .withInterestTypeAsDecliningBalance() // + .withMultiDisburse() // + .withEnableDownPayment(enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment) // + .withDisallowExpectedDisbursements(true) // + .build(null, delinquencyBucketId); + } else { + loanProductMap = new LoanProductTestBuilder() // + .withEnableDownPayment(enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment) // + .build(null, delinquencyBucketId); + } + final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); + return loanProductId; + } + + private Integer createApproveAndDisburseLoanAccount(final Integer clientID, final Long loanProductID, final String externalId) { + + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("1").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance() + .withAmortizationTypeAsEqualPrincipalPayments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate("03 September 2022").withSubmittedOnDate("01 September 2022").withLoanType("individual") + .withExternalId(externalId).build(clientID.toString(), loanProductID.toString(), null); + + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan("02 September 2022", "1000", loanId, null); + loanTransactionHelper.disburseLoanWithTransactionAmount("03 September 2022", loanId, "1000"); + return loanId; + } + + private Integer createApproveAndDisburseTwiceLoanAccount(final Integer clientID, final Long loanProductID, final String externalId) { + + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("1").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance() + .withAmortizationTypeAsEqualPrincipalPayments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate("04 September 2022").withSubmittedOnDate("01 September 2022").withLoanType("individual") + .withExternalId(externalId).build(clientID.toString(), loanProductID.toString(), null); + + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan("02 September 2022", "1000", loanId, null); + loanTransactionHelper.disburseLoanWithTransactionAmount("03 September 2022", loanId, "700"); + loanTransactionHelper.disburseLoanWithTransactionAmount("04 September 2022", loanId, "300"); + return loanId; + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java index d01e31cd2..9d1f0fc39 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java @@ -211,6 +211,9 @@ public class LoanTransactionChargebackTest { final Long transactionId = loanIdTransactionsResponse.getResourceId(); reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("666.67")); + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + assertNotNull(getLoansLoanIdResponse); + final Long chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, amount.toString(), 0, responseSpec); reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("666.67"));