[FINERACT-1958] Repayment schedule for down payment

This commit is contained in:
taskain7
2023-08-18 14:51:54 +02:00
parent b616a96fc9
commit baa447196e
15 changed files with 758 additions and 47 deletions
@@ -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<String, Object> 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<LoanDisbursementDetails> 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);
@@ -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<LoanInterestRecalcualtionAdditionalDetails> 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<LoanInterestRecalcualtionAdditionalDetails> 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<LoanInterestRecalcualtionAdditionalDetails> 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,
@@ -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.
@@ -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;
}
}
@@ -31,6 +31,8 @@ public interface LoanScheduleModelPeriod {
boolean isRepaymentPeriod();
boolean isDownPaymentPeriod();
Integer periodNumber();
LocalDate periodFromDate();
@@ -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<LoanInterestRecalcualtionAdditionalDetails> 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) {
}
}
@@ -218,8 +218,8 @@ public final class LoanScheduleParams {
final Collection<RecalculationDetail> 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,
@@ -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<LoanScheduleModelPeriod> 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<RecalculationDetail> getApplicableTransactionsForPeriod(final boolean applyInterestRecalculation,
LocalDate repaymentDate, final Collection<RecalculationDetail> transactions) {
Collection<RecalculationDetail> 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<LoanScheduleModelPeriod> periods, final BigDecimal chargesDueAtTimeOfDisbursement,
final Map<LocalDate, Money> disurseDetail, final boolean excludePastUndisbursed) {
final Map<LocalDate, Money> 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<LoanScheduleModelPeriod> createNewLoanScheduleListWithDisbursementDetails(final int numberOfRepayments,
final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement) {
Collection<LoanScheduleModelPeriod> periods = null;
final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement,
LoanScheduleParams scheduleParams) {
Collection<LoanScheduleModelPeriod> 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<LoanScheduleModelPeriod> 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<LoanCharge> seperateTotalCompoundingPercentageCharges(final Set<LoanCharge> loanCharges) {
Set<LoanCharge> 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<RecalculationDetail> recalculationDetails = new ArrayList<>();
List<LoanTransaction> 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<LoanRepaymentScheduleInstallment> 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;
@@ -69,6 +69,11 @@ public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleMo
return false;
}
@Override
public boolean isDownPaymentPeriod() {
return false;
}
@Override
public Integer periodNumber() {
return null;
@@ -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;
@@ -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<LoanRepaymentScheduleInstallment> retrieveRepaymentScheduleFromModel(LoanScheduleModel model) {
final List<LoanRepaymentScheduleInstallment> 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(),
@@ -142,4 +142,5 @@
<include file="parts/0120_transaction_summary_with_asset_owner_report_typo_fix.xml" relativeToChangelogFile="true" />
<include file="parts/0121_transaction_summary_with_asset_owner_report_typo_fix_2.xml" relativeToChangelogFile="true" />
<include file="parts/0122_add_batch_job_execution_params_index.xml" relativeToChangelogFile="true" />
<include file="parts/0123_add_is_down_payment_to_repayment_schedule.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_loan_repayment_schedule">
<column name="is_down_payment" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -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<GetLoansLoanIdRepaymentPeriod> 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<GetLoansLoanIdRepaymentPeriod> 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<GetLoansLoanIdRepaymentPeriod> 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<GetLoansLoanIdRepaymentPeriod> 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<GetLoansLoanIdRepaymentPeriod> 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<String, Object> 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;
}
}
@@ -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"));