FINERACT-1958-Loan-Product-repayment-start-date-configuration

This commit is contained in:
Ruchi Dhamankar
2023-08-22 12:28:36 +02:00
committed by Adam Saghy
parent b1cde8a809
commit 3c6fc1e47b
20 changed files with 794 additions and 49 deletions
@@ -139,6 +139,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.rate.domain.Rate;
@@ -5853,6 +5854,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
RecalculationFrequencyType compoundingFrequencyType = null;
LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
CalendarHistoryDataWrapper calendarHistoryDataWrapper;
RepaymentStartDateType repaymentStartDateType = this.getLoanProduct().getRepaymentStartDateType();
boolean allowCompoundingOnEod = false;
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = scheduleGeneratorDTO.getCalendarInstanceForInterestRecalculation();
@@ -5886,7 +5888,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(),
holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(),
scheduleGeneratorDTO.isInterestToBeRecoveredFirstWhenGreaterThanEMI(), this.fixedPrincipalPercentagePerInstallment,
scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans());
scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans(), repaymentStartDateType, getSubmittedOnDate());
}
public BigDecimal constructLoanTermVariations(FloatingRateDTO floatingRateDTO, BigDecimal annualNominalInterestRate,
@@ -6153,6 +6155,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
InterestRecalculationCompoundingMethod compoundingMethod = null;
RecalculationFrequencyType compoundingFrequencyType = null;
LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
RepaymentStartDateType repaymentStartDateType = loanProduct.getRepaymentStartDateType();
boolean allowCompoundingOnEod = false;
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
recalculationFrequencyType = this.loanInterestRecalculationDetails.getRestFrequencyType();
@@ -6173,7 +6176,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(),
rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false,
false, this.fixedPrincipalPercentagePerInstallment, false);
false, this.fixedPrincipalPercentagePerInstallment, false, repaymentStartDateType, getSubmittedOnDate());
}
/**
@@ -51,6 +51,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCa
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
@Slf4j
public final class LoanApplicationTerms {
@@ -211,6 +212,9 @@ public final class LoanApplicationTerms {
private BigDecimal disbursedAmountPercentageForDownPayment;
private boolean isAutoRepaymentForDownPaymentEnabled;
private RepaymentStartDateType repaymentStartDateType;
private LocalDate submittedOnDate;
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
final PeriodFrequencyType repaymentPeriodFrequencyType, Integer nthDay, DayOfWeekType weekDayType,
@@ -235,7 +239,8 @@ public final class LoanApplicationTerms {
final boolean isEqualAmortization, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI,
final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans,
final Boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
final Boolean isAutoRepaymentForDownPaymentEnabled) {
final Boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType,
final LocalDate submittedOnDate) {
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
@@ -253,7 +258,7 @@ public final class LoanApplicationTerms {
isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false,
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment,
isAutoRepaymentForDownPaymentEnabled);
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate);
}
@@ -273,7 +278,8 @@ public final class LoanApplicationTerms {
final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Integer numberOfDays,
final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod,
final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI,
final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans) {
final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans,
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate) {
final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments();
final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery();
@@ -321,7 +327,7 @@ public final class LoanApplicationTerms {
allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday,
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment,
isAutoRepaymentForDownPaymentEnabled);
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate);
}
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
@@ -348,7 +354,8 @@ public final class LoanApplicationTerms {
final boolean allowCompoundingOnEod, final boolean isEqualAmortization, final boolean isFirstRepaymentDateAllowedOnHoliday,
final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, final BigDecimal fixedPrincipalPercentagePerInstallment,
final boolean isPrincipalCompoundingDisabledForOverdueLoans, final boolean isDownPaymentEnabled,
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled) {
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled,
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate) {
this.currency = currency;
this.loanTermFrequency = loanTermFrequency;
@@ -428,6 +435,8 @@ public final class LoanApplicationTerms {
this.isDownPaymentEnabled = isDownPaymentEnabled;
this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment;
this.isAutoRepaymentForDownPaymentEnabled = isAutoRepaymentForDownPaymentEnabled;
this.repaymentStartDateType = repaymentStartDateType;
this.submittedOnDate = submittedOnDate;
}
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
@@ -1745,4 +1754,12 @@ public final class LoanApplicationTerms {
public BigDecimal getDisbursedAmountPercentageForDownPayment() {
return disbursedAmountPercentageForDownPayment;
}
public RepaymentStartDateType getRepaymentStartDateType() {
return repaymentStartDateType;
}
public LocalDate getSubmittedOnDate() {
return submittedOnDate;
}
}
@@ -151,5 +151,6 @@ public interface LoanProductConstants {
String ENABLE_DOWN_PAYMENT = "enableDownPayment";
String DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT = "disbursedAmountPercentageForDownPayment";
String ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT = "enableAutoRepaymentForDownPayment";
String REPAYMENT_START_DATE_TYPE = "repaymentStartDateType";
}
@@ -213,6 +213,9 @@ public class LoanProduct extends AbstractPersistableCustom {
@Column(name = "overdue_days_for_repayment_event")
private Integer overDueDaysForRepaymentEvent;
@Column(name = "repayment_start_date_type_enum", nullable = false)
private RepaymentStartDateType repaymentStartDateType;
public static LoanProduct assembleFromJson(final Fund fund, final String loanTransactionProcessingStrategy,
final List<Charge> productCharges, final JsonCommand command, final AprCalculator aprCalculator, FloatingRate floatingRate,
final List<Rate> productRates, List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules) {
@@ -394,6 +397,9 @@ public class LoanProduct extends AbstractPersistableCustom {
final boolean enableAutoRepaymentForDownPayment = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT);
final RepaymentStartDateType repaymentStartDateType = RepaymentStartDateType
.fromInt(command.integerValueOfParameterNamed(LoanProductConstants.REPAYMENT_START_DATE_TYPE));
return new LoanProduct(fund, loanTransactionProcessingStrategy, loanProductPaymentAllocationRules, name, shortName, description,
currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod,
interestFrequencyType, annualInterestRate, interestMethod, interestCalculationPeriodMethod,
@@ -411,7 +417,7 @@ public class LoanProduct extends AbstractPersistableCustom {
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, productRates, fixedPrincipalPercentagePerInstallment,
disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber,
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment,
enableAutoRepaymentForDownPayment);
enableAutoRepaymentForDownPayment, repaymentStartDateType);
}
@@ -625,7 +631,7 @@ public class LoanProduct extends AbstractPersistableCustom {
final boolean allowApprovedDisbursedAmountsOverApplied, final String overAppliedCalculationType,
final Integer overAppliedNumber, final Integer dueDaysForRepaymentEvent, final Integer overDueDaysForRepaymentEvent,
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
final boolean enableAutoRepaymentForDownPayment) {
final boolean enableAutoRepaymentForDownPayment, final RepaymentStartDateType repaymentStartDateType) {
this.fund = fund;
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
@@ -719,6 +725,7 @@ public class LoanProduct extends AbstractPersistableCustom {
this.dueDaysForRepaymentEvent = dueDaysForRepaymentEvent;
this.overDueDaysForRepaymentEvent = overDueDaysForRepaymentEvent;
this.repaymentStartDateType = repaymentStartDateType;
validateLoanProductPreSave();
}
@@ -1281,6 +1288,12 @@ public class LoanProduct extends AbstractPersistableCustom {
this.loanProductRelatedDetail.updateEnableAutoRepaymentForDownPayment(newValue);
}
if (command.isChangeInIntegerParameterNamed(LoanProductConstants.REPAYMENT_START_DATE_TYPE,
this.repaymentStartDateType.getValue())) {
final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.REPAYMENT_START_DATE_TYPE);
actualChanges.put(LoanProductConstants.REPAYMENT_START_DATE_TYPE, newValue);
this.repaymentStartDateType = RepaymentStartDateType.fromInt(newValue);
}
return actualChanges;
}
@@ -1676,4 +1689,8 @@ public class LoanProduct extends AbstractPersistableCustom {
return this.overDueDaysForRepaymentEvent;
}
public RepaymentStartDateType getRepaymentStartDateType() {
return this.repaymentStartDateType == null ? RepaymentStartDateType.INVALID : this.repaymentStartDateType;
}
}
@@ -0,0 +1,62 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanproduct.domain;
public enum RepaymentStartDateType {
INVALID(0, "repaymentStartDateType.invalid"), //
DISBURSEMENT_DATE(1, "repaymentStartDateType.disbursementDate"), //
SUBMITTED_ON_DATE(2, "repaymentStartDateType.submittedOnDate");
private final Integer value;
private final String code;
RepaymentStartDateType(Integer value, String code) {
this.value = value;
this.code = code;
}
public static RepaymentStartDateType fromInt(final Integer repaymentStartDateType) {
if (repaymentStartDateType == null) {
return RepaymentStartDateType.DISBURSEMENT_DATE;
}
return switch (repaymentStartDateType) {
case 1 -> RepaymentStartDateType.DISBURSEMENT_DATE;
case 2 -> RepaymentStartDateType.SUBMITTED_ON_DATE;
default -> RepaymentStartDateType.INVALID;
};
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
public boolean isDisbursementDate() {
return RepaymentStartDateType.DISBURSEMENT_DATE.equals(this);
}
public boolean isSubmittedOnDate() {
return RepaymentStartDateType.SUBMITTED_ON_DATE.equals(this);
}
}
@@ -38,6 +38,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductParamType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
public final class LoanEnumerations {
@@ -57,6 +58,7 @@ public final class LoanEnumerations {
public static final String LOAN_TYPE = "loanType";
public static final String INTEREST_RECALCULATION_COMPOUNDING_TYPE = "interestRecalculationCompoundingType";
public static final String RESCHEDULE_STRATEGY_TYPE = "rescheduleStrategyType";
public static final String REPAYMENT_START_DATE_TYPE = "repaymentStartDateType";
public static EnumOptionData loanEnumeration(final String typeName, final int id) {
return switch (typeName) {
@@ -71,10 +73,26 @@ public final class LoanEnumerations {
case LOAN_TYPE -> AccountEnumerations.loanType(id);
case INTEREST_RECALCULATION_COMPOUNDING_TYPE -> interestRecalculationCompoundingType(id);
case RESCHEDULE_STRATEGY_TYPE -> rescheduleStrategyType(id);
case REPAYMENT_START_DATE_TYPE -> repaymentStartDateType(id);
default -> null;
};
}
public static EnumOptionData repaymentStartDateType(int id) {
return repaymentStartDateType(RepaymentStartDateType.fromInt(id));
}
public static EnumOptionData repaymentStartDateType(final RepaymentStartDateType type) {
return switch (type) {
case DISBURSEMENT_DATE -> new EnumOptionData(RepaymentStartDateType.DISBURSEMENT_DATE.getValue().longValue(),
RepaymentStartDateType.DISBURSEMENT_DATE.getCode(), "Disbursement Date");
case SUBMITTED_ON_DATE -> new EnumOptionData(RepaymentStartDateType.SUBMITTED_ON_DATE.getValue().longValue(),
RepaymentStartDateType.SUBMITTED_ON_DATE.getCode(), "Submitted On Date");
default -> new EnumOptionData(RepaymentStartDateType.INVALID.getValue().longValue(), RepaymentStartDateType.INVALID.getCode(),
"Invalid");
};
}
public static EnumOptionData loanTermFrequencyType(final int id) {
return loanTermFrequencyType(PeriodFrequencyType.fromInt(id));
}
@@ -27,4 +27,5 @@
<include relativeToChangelogFile="true" file="parts/1002_add_payment_allocation_rule.xml"/>
<include relativeToChangelogFile="true" file="parts/1003_add_loan_product_auto_repayment_down_payment_configuration.xml"/>
<include relativeToChangelogFile="true" file="parts/1004_add_external_event_configuration_for_down_payment_transaction_event.xml"/>
<include relativeToChangelogFile="true" file="parts/1005_add_loan_product_repayment_start_date_configuration.xml"/>
</databaseChangeLog>
@@ -0,0 +1,32 @@
<?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.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_product_loan">
<column defaultValueNumeric="1" name="repayment_start_date_type_enum" type="SMALLINT">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -57,6 +57,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.ScheduleDateException;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGenerator {
@@ -91,14 +92,16 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
final MonetaryCurrency currency = loanApplicationTerms.getCurrency();
final int numberOfRepayments = loanApplicationTerms.fetchNumberOfRepaymentsAfterExceptions();
LoanScheduleParams scheduleParams;
LoanScheduleParams scheduleParams = null;
LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType())
? loanApplicationTerms.getExpectedDisbursementDate()
: loanApplicationTerms.getSubmittedOnDate();
if (loanScheduleParams == null) {
scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, Money.of(currency, chargesDueAtTimeOfDisbursement),
loanApplicationTerms.getExpectedDisbursementDate(), getPrincipalToBeScheduled(loanApplicationTerms));
periodStartDate, getPrincipalToBeScheduled(loanApplicationTerms));
} else if (!loanScheduleParams.isPartialUpdate()) {
scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, Money.of(currency, chargesDueAtTimeOfDisbursement),
loanApplicationTerms.getExpectedDisbursementDate(), getPrincipalToBeScheduled(loanApplicationTerms),
loanScheduleParams);
periodStartDate, getPrincipalToBeScheduled(loanApplicationTerms), loanScheduleParams);
} else {
scheduleParams = loanScheduleParams;
}
@@ -124,8 +127,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
boolean isFirstRepayment = true;
LocalDate firstRepaymentdate = this.scheduledDateGenerator
.generateNextRepaymentDate(loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms, isFirstRepayment);
LocalDate lastRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType())
? loanApplicationTerms.getExpectedDisbursementDate()
: loanApplicationTerms.getSubmittedOnDate();
LocalDate firstRepaymentdate = this.scheduledDateGenerator.generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms,
isFirstRepayment);
final LocalDate idealDisbursementDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate(
loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), firstRepaymentdate,
loanApplicationTerms.getLoanCalendar(), loanApplicationTerms.getHolidayDetailDTO(), loanApplicationTerms);
@@ -2207,7 +2213,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
if (loanApplicationTerms.isInterestRecalculationEnabled()) {
lastRestDate = getNextRestScheduleDate(currentDate.minusDays(1), loanApplicationTerms, holidayDetailDTO);
}
LocalDate actualRepaymentDate = loanApplicationTerms.getExpectedDisbursementDate();
LocalDate actualRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE
.equals(loanApplicationTerms.getRepaymentStartDateType()) ? loanApplicationTerms.getExpectedDisbursementDate()
: loanApplicationTerms.getSubmittedOnDate();
boolean isFirstRepayment = true;
// cumulative fields
@@ -2222,7 +2230,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// Actual period Number plus interest only repayments
int instalmentNumber = 1;
LocalDate lastInstallmentDate = actualRepaymentDate;
LocalDate periodStartDate = loanApplicationTerms.getExpectedDisbursementDate();
LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType())
? loanApplicationTerms.getExpectedDisbursementDate()
: loanApplicationTerms.getSubmittedOnDate();
// Set fixed Amortization Amounts(either EMI or Principal )
updateAmortization(mc, loanApplicationTerms, periodNumber, outstandingBalance);
@@ -32,6 +32,7 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarHistory;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,7 +45,11 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
LocalDate lastRepaymentDate = loanApplicationTerms.getExpectedDisbursementDate();
RepaymentStartDateType repaymentStartDateType = loanApplicationTerms.getRepaymentStartDateType();
LocalDate lastRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? loanApplicationTerms.getExpectedDisbursementDate()
: loanApplicationTerms.getSubmittedOnDate();
boolean isFirstRepayment = true;
for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) {
lastRepaymentDate = generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, isFirstRepayment);
@@ -125,8 +130,6 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
* @param adjustedDateDetailsDTO
* @param loanApplicationTerms
* @param holidayDetailDTO
* @param nextRepaymentPeriodDueDate
* @param rescheduleType
* @param isFirstRepayment
* @return
*/
@@ -112,6 +112,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductVariableInstallmentConfig;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.springframework.beans.factory.annotation.Autowired;
@@ -234,7 +235,11 @@ public class LoanScheduleAssembler {
final Money principalMoney = Money.of(currency, principal);
final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element);
final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element);
LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element);
final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed("submittedOnDate", element);
final RepaymentStartDateType repaymentStartDateType = loanProduct.getRepaymentStartDateType();
LocalDate calculatedRepaymentsStartingFromDate = repaymentsStartingFromDate;
final Boolean synchDisbursement = this.fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element);
@@ -261,7 +266,8 @@ public class LoanScheduleAssembler {
&& !nthDay.equals(NthDayType.INVALID.getValue())) {
LocalDate calendarStartDate = repaymentsStartingFromDate;
if (calendarStartDate == null) {
calendarStartDate = expectedDisbursementDate;
calendarStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType) ? expectedDisbursementDate
: submittedOnDate;
}
calendar = createLoanCalendar(calendarStartDate, repaymentEvery, CalendarFrequencyType.MONTHLY, dayOfWeek, nthDay);
}
@@ -272,7 +278,8 @@ public class LoanScheduleAssembler {
*/
if (calculatedRepaymentsStartingFromDate == null) {
calculatedRepaymentsStartingFromDate = deriveFirstRepaymentDate(loanType, repaymentEvery, expectedDisbursementDate,
repaymentPeriodFrequencyType, loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment(), calendar);
repaymentPeriodFrequencyType, loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment(), calendar, submittedOnDate,
repaymentStartDateType);
}
/*
@@ -305,8 +312,10 @@ public class LoanScheduleAssembler {
}
}
validateMinimumDaysBetweenDisbursalAndFirstRepayment(expectedDisbursementDate, calculatedRepaymentsStartingFromDate,
loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment());
if (RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)) {
validateMinimumDaysBetweenDisbursalAndFirstRepayment(expectedDisbursementDate, calculatedRepaymentsStartingFromDate,
loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment());
}
// grace details
final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element);
@@ -471,7 +480,7 @@ public class LoanScheduleAssembler {
loanTermVariations, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipMeetingOnFirstDay, detailDTO,
allowCompoundingOnEod, isEqualAmortization, isInterestToBeRecoveredFirstWhenGreaterThanEMI,
fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled,
disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled);
disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate);
}
private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery,
@@ -1070,19 +1079,20 @@ public class LoanScheduleAssembler {
private LocalDate deriveFirstRepaymentDate(final AccountType loanType, final Integer repaymentEvery,
final LocalDate expectedDisbursementDate, final PeriodFrequencyType repaymentPeriodFrequencyType,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar) {
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar, final LocalDate submittedOnDate,
final RepaymentStartDateType repaymentStartDateType) {
LocalDate derivedFirstRepayment = null;
final LocalDate dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment = expectedDisbursementDate
.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
final LocalDate dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment = RepaymentStartDateType.DISBURSEMENT_DATE.equals(
repaymentStartDateType) ? expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment) : submittedOnDate;
if (calendar != null) {
final LocalDate refernceDateForCalculatingFirstRepaymentDate = expectedDisbursementDate;
derivedFirstRepayment = deriveFirstRepaymentDateForLoans(repaymentEvery, expectedDisbursementDate,
refernceDateForCalculatingFirstRepaymentDate, repaymentPeriodFrequencyType,
minimumDaysBetweenDisbursalAndFirstRepayment, calendar);
minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate, repaymentStartDateType);
} /*** Individual or group account, or JLG not linked to a meeting ***/
else {
@@ -1092,14 +1102,26 @@ public class LoanScheduleAssembler {
// (disbursement date + minimum between disbursal and first
// repayment )
if (repaymentPeriodFrequencyType.isDaily()) {
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusDays(repaymentEvery);
dateBasedOnRepaymentFrequency = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusDays(repaymentEvery)
: submittedOnDate.plusDays(repaymentEvery);
} else if (repaymentPeriodFrequencyType.isWeekly()) {
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusWeeks(repaymentEvery);
dateBasedOnRepaymentFrequency = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusWeeks(repaymentEvery)
: submittedOnDate.plusWeeks(repaymentEvery);
} else if (repaymentPeriodFrequencyType.isMonthly()) {
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusMonths(repaymentEvery);
dateBasedOnRepaymentFrequency = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusMonths(repaymentEvery)
: submittedOnDate.plusMonths(repaymentEvery);
} /** yearly loan **/
else {
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusYears(repaymentEvery);
dateBasedOnRepaymentFrequency = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusYears(repaymentEvery)
: submittedOnDate.plusYears(repaymentEvery);
}
derivedFirstRepayment = dateBasedOnRepaymentFrequency.isAfter(dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment)
? dateBasedOnRepaymentFrequency
@@ -1111,16 +1133,20 @@ public class LoanScheduleAssembler {
private LocalDate deriveFirstRepaymentDateForLoans(final Integer repaymentEvery, final LocalDate expectedDisbursementDate,
final LocalDate refernceDateForCalculatingFirstRepaymentDate, final PeriodFrequencyType repaymentPeriodFrequencyType,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar) {
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar, final LocalDate submittedOnDate,
final RepaymentStartDateType repaymentStartDateType) {
boolean isMeetingSkipOnFirstDayOfMonth = configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled();
int numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
final LocalDate derivedFirstRepayment = CalendarUtils.getFirstRepaymentMeetingDate(calendar,
refernceDateForCalculatingFirstRepaymentDate, repaymentEvery, frequency, isMeetingSkipOnFirstDayOfMonth, numberOfDays);
final LocalDate minimumFirstRepaymentDate = expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
final LocalDate minimumFirstRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment)
: submittedOnDate;
return minimumFirstRepaymentDate.isBefore(derivedFirstRepayment) ? derivedFirstRepayment
: deriveFirstRepaymentDateForLoans(repaymentEvery, expectedDisbursementDate, derivedFirstRepayment,
repaymentPeriodFrequencyType, minimumDaysBetweenDisbursalAndFirstRepayment, calendar);
repaymentPeriodFrequencyType, minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate,
repaymentStartDateType);
}
private void validateMinimumDaysBetweenDisbursalAndFirstRepayment(final LocalDate disbursalDate, final LocalDate firstRepaymentDate,
@@ -112,7 +112,8 @@ public class LoanProductsApiResource {
LoanProductConstants.CAN_USE_FOR_TOPUP, LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, LoanProductConstants.RATES_PARAM_NAME,
LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT,
LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.ENABLE_DOWN_PAYMENT,
LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT));
LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT,
LoanProductConstants.REPAYMENT_START_DATE_TYPE));
private static final Set<String> PRODUCT_MIX_DATA_PARAMETERS = new HashSet<>(
Arrays.asList("productId", "productName", "restrictedProducts", "allowedProducts", "productOptions"));
@@ -147,7 +148,7 @@ public class LoanProductsApiResource {
@Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n"
+ "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n"
+ "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n"
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment\n\n"
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n"
+ "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
+ "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
+ "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n"
@@ -405,6 +406,7 @@ public class LoanProductsApiResource {
final List<EnumOptionData> preCloseInterestCalculationStrategyOptions = dropdownReadPlatformService
.retrievePreCloseInterestCalculationStrategyOptions();
final List<FloatingRateData> floatingRateOptions = this.floatingRateReadPlatformService.retrieveLookupActive();
final List<EnumOptionData> repaymentStartDateTypeOptions = dropdownReadPlatformService.retrieveRepaymentStartDateTypeOptions();
return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions,
interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions,
@@ -412,7 +414,7 @@ public class LoanProductsApiResource {
loanCycleValueConditionTypeOptions, daysInMonthTypeOptions, daysInYearTypeOptions,
interestRecalculationCompoundingTypeOptions, rescheduleStrategyTypeOptions, interestRecalculationFrequencyTypeOptions,
preCloseInterestCalculationStrategyOptions, floatingRateOptions, interestRecalculationNthDayTypeOptions,
interestRecalculationDayOfWeekTypeOptions, isRatesEnabled, delinquencyBucketOptions);
interestRecalculationDayOfWeekTypeOptions, isRatesEnabled, delinquencyBucketOptions, repaymentStartDateTypeOptions);
}
}
@@ -162,6 +162,8 @@ final class LoanProductsApiResourceSwagger {
public BigDecimal disbursedAmountPercentageForDownPayment;
@Schema(example = "false")
public Boolean enableAutoRepaymentForDownPayment;
@Schema(example = "1")
public Integer repaymentStartDateType;
// Interest Recalculation
@Schema(example = "false")
@@ -419,6 +421,18 @@ final class LoanProductsApiResourceSwagger {
public String description;
}
static final class GetLoanProductsRepaymentStartDateType {
private GetLoanProductsRepaymentStartDateType() {}
@Schema(example = "1")
public Long id;
@Schema(example = "repaymentStartDateType.disbursementDate")
public String code;
@Schema(example = "Disbursement Date")
public String description;
}
static final class GetLoanProductsInterestRecalculationData {
private GetLoanProductsInterestRecalculationData() {}
@@ -562,6 +576,7 @@ final class LoanProductsApiResourceSwagger {
public GetLoanProductsResponse.GetLoanProductsAccountingRule accountingRule;
@Schema(example = "0")
public Integer principalThresholdForLastInstalment;
public GetLoanProductsResponse.GetLoanProductsRepaymentStartDateType repaymentStartDateType;
}
@Schema(description = "GetLoanProductsTemplateResponse")
@@ -1015,6 +1030,7 @@ final class LoanProductsApiResourceSwagger {
public Set<GetLoanProductsResponse.GetLoanProductsInterestRecalculationData.GetLoanProductsInterestRecalculationCompoundingType> interestRecalculationCompoundingTypeOptions;
public Set<GetLoanProductsResponse.GetLoanProductsInterestRecalculationData.GetLoanProductsRescheduleStrategyType> rescheduleStrategyTypeOptions;
public Set<GetLoanProductsResponse.GetLoanProductsInterestRecalculationData.GetLoanProductsInterestRecalculationCompoundingFrequencyType> interestRecalculationFrequencyTypeOptions;
public Set<GetLoanProductsResponse.GetLoanProductsRepaymentStartDateType> repaymentStartDateTypeOptions;
}
@Schema(description = "GetLoanProductsProductIdResponse")
@@ -1034,6 +1050,18 @@ final class LoanProductsApiResourceSwagger {
public String description;
}
static final class GetLoanProductsRepaymentStartDateType {
private GetLoanProductsRepaymentStartDateType() {}
@Schema(example = "1")
public Long id;
@Schema(example = "repaymentStartDateType.disbursementDate")
public String code;
@Schema(example = "Disbursement Date")
public String description;
}
static final class GetLoanProductsPrincipalVariationsForBorrowerCycle {
private GetLoanProductsPrincipalVariationsForBorrowerCycle() {}
@@ -1218,6 +1246,7 @@ final class LoanProductsApiResourceSwagger {
public BigDecimal disbursedAmountPercentageForDownPayment;
@Schema(example = "false")
public Boolean enableAutoRepaymentForDownPayment;
public GetLoanProductsRepaymentStartDateType repaymentStartDateType;
}
@Schema(description = "PutLoanProductsProductIdRequest")
@@ -1346,6 +1375,8 @@ final class LoanProductsApiResourceSwagger {
public BigDecimal disbursedAmountPercentageForDownPayment;
@Schema(example = "false")
public Boolean enableAutoRepaymentForDownPayment;
@Schema(example = "1")
public Integer repaymentStartDateType;
// Interest Recalculation
@Schema(example = "false")
@@ -48,6 +48,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductConfigurableAttributes;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import org.apache.fineract.portfolio.rate.data.RateData;
@@ -123,6 +124,7 @@ public class LoanProductData implements Serializable {
private final Integer minimumDaysBetweenDisbursalAndFirstRepayment;
private final boolean canDefineInstallmentAmount;
private final Integer installmentAmountInMultiplesOf;
private final EnumOptionData repaymentStartDateType;
// charges
private final Collection<ChargeData> charges;
@@ -168,6 +170,7 @@ public class LoanProductData implements Serializable {
private final List<EnumOptionData> interestRecalculationFrequencyTypeOptions;
private final List<FloatingRateData> floatingRateOptions;
private final List<EnumOptionData> repaymentStartDateTypeOptions;
private final Boolean multiDisburseLoan;
private final Integer maxTrancheCount;
@@ -290,6 +293,7 @@ public class LoanProductData implements Serializable {
final BigDecimal disbursedAmountPercentageDownPayment = null;
final Collection<AdvancedPaymentData> paymentAllocation = null;
final boolean enableAutoRepaymentForDownPayment = false;
final EnumOptionData repaymentStartDateType = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -309,7 +313,7 @@ public class LoanProductData implements Serializable {
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation);
paymentAllocation, repaymentStartDateType);
}
@@ -403,6 +407,7 @@ public class LoanProductData implements Serializable {
final BigDecimal disbursedAmountPercentageDownPayment = null;
final boolean enableAutoRepaymentForDownPayment = false;
final Collection<AdvancedPaymentData> paymentAllocation = null;
final EnumOptionData repaymentStartDateType = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -422,7 +427,7 @@ public class LoanProductData implements Serializable {
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation);
paymentAllocation, repaymentStartDateType);
}
@@ -523,6 +528,7 @@ public class LoanProductData implements Serializable {
final BigDecimal disbursedAmountPercentageDownPayment = null;
final boolean enableAutoRepaymentForDownPayment = false;
final Collection<AdvancedPaymentData> paymentAllocation = null;
final EnumOptionData repaymentStartDateType = LoanEnumerations.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE);
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -542,7 +548,7 @@ public class LoanProductData implements Serializable {
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation);
paymentAllocation, repaymentStartDateType);
}
@@ -637,6 +643,7 @@ public class LoanProductData implements Serializable {
final BigDecimal disbursedAmountPercentageDownPayment = null;
final boolean enableAutoRepaymentForDownPayment = false;
final Collection<AdvancedPaymentData> paymentAllocation = null;
final EnumOptionData repaymentStartDateType = LoanEnumerations.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE);
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -656,7 +663,7 @@ public class LoanProductData implements Serializable {
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation);
paymentAllocation, repaymentStartDateType);
}
public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings,
@@ -704,7 +711,7 @@ public class LoanProductData implements Serializable {
final DelinquencyBucketData delinquencyBucket, final Integer dueDaysForRepaymentEvent,
final Integer overDueDaysForRepaymentEvent, final boolean enableDownPayment,
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment,
final Collection<AdvancedPaymentData> paymentAllocation) {
final Collection<AdvancedPaymentData> paymentAllocation, final EnumOptionData repaymentStartDateType) {
this.id = id;
this.name = name;
this.shortName = shortName;
@@ -826,6 +833,8 @@ public class LoanProductData implements Serializable {
this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment;
this.paymentAllocation = paymentAllocation;
this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment;
this.repaymentStartDateType = repaymentStartDateType;
this.repaymentStartDateTypeOptions = null;
}
public LoanProductData(final LoanProductData productData, final Collection<ChargeData> chargeOptions,
@@ -842,7 +851,7 @@ public class LoanProductData implements Serializable {
final List<EnumOptionData> preCloseInterestCalculationStrategyOptions, final List<FloatingRateData> floatingRateOptions,
final List<EnumOptionData> interestRecalculationNthDayTypeOptions,
final List<EnumOptionData> interestRecalculationDayOfWeekTypeOptions, final boolean isRatesEnabled,
final Collection<DelinquencyBucketData> delinquencyBucketOptions) {
final Collection<DelinquencyBucketData> delinquencyBucketOptions, final List<EnumOptionData> repaymentStartDateTypeOptions) {
this.id = productData.id;
this.name = productData.name;
this.shortName = productData.shortName;
@@ -981,6 +990,8 @@ public class LoanProductData implements Serializable {
this.disbursedAmountPercentageForDownPayment = productData.disbursedAmountPercentageForDownPayment;
this.enableAutoRepaymentForDownPayment = productData.enableAutoRepaymentForDownPayment;
this.paymentAllocation = productData.paymentAllocation;
this.repaymentStartDateType = productData.repaymentStartDateType;
this.repaymentStartDateTypeOptions = repaymentStartDateTypeOptions;
}
private Collection<ChargeData> nullIfEmpty(final Collection<ChargeData> charges) {
@@ -162,7 +162,7 @@ public final class LoanProductDataValidator {
LoanProductConstants.OVER_APPLIED_NUMBER, LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME,
LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT,
LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT,
LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT));
LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.REPAYMENT_START_DATE_TYPE));
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
@@ -755,6 +755,13 @@ public final class LoanProductDataValidator {
validateAutoRepaymentForDownPayment(enableDownPayment, baseDataValidator, element);
}
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.REPAYMENT_START_DATE_TYPE, element)) {
final Integer repaymentStartDateType = this.fromApiJsonHelper
.extractIntegerNamed(LoanProductConstants.REPAYMENT_START_DATE_TYPE, element, Locale.getDefault());
baseDataValidator.reset().parameter(LoanProductConstants.REPAYMENT_START_DATE_TYPE).value(repaymentStartDateType).notNull()
.isOneOfTheseValues(1, 2);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@@ -1679,6 +1686,15 @@ public final class LoanProductDataValidator {
validateDownPaymentPercentage(enableDownPayment, baseDataValidator, element);
validateAutoRepaymentForDownPayment(enableDownPayment, baseDataValidator, element);
}
Integer repaymentStartDateType = loanProduct.getRepaymentStartDateType().getValue();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.REPAYMENT_START_DATE_TYPE, element)) {
repaymentStartDateType = this.fromApiJsonHelper.extractIntegerNamed(LoanProductConstants.REPAYMENT_START_DATE_TYPE, element,
Locale.getDefault());
}
baseDataValidator.reset().parameter(LoanProductConstants.REPAYMENT_START_DATE_TYPE).value(repaymentStartDateType).notNull()
.isOneOfTheseValues(1, 2);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@@ -57,4 +57,5 @@ public interface LoanDropdownReadPlatformService {
List<EnumOptionData> retrievePreCloseInterestCalculationStrategyOptions();
List<EnumOptionData> retrieveRepaymentStartDateTypeOptions();
}
@@ -32,6 +32,7 @@ import static org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations
import static org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.repaymentFrequencyDayOfWeekType;
import static org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.repaymentFrequencyNthDayType;
import static org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.repaymentFrequencyType;
import static org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.repaymentStartDateType;
import static org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.rescheduleStrategyType;
import java.util.Arrays;
@@ -52,6 +53,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCa
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.springframework.stereotype.Service;
@Service
@@ -177,4 +179,10 @@ public class LoanDropdownReadPlatformServiceImpl implements LoanDropdownReadPlat
return Arrays.asList(preCloseInterestCalculationStrategy(LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE),
preCloseInterestCalculationStrategy(LoanPreClosureInterestCalculationStrategy.TILL_REST_FREQUENCY_DATE));
}
@Override
public List<EnumOptionData> retrieveRepaymentStartDateTypeOptions() {
return Arrays.asList(repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE),
repaymentStartDateType(RepaymentStartDateType.SUBMITTED_ON_DATE));
}
}
@@ -229,7 +229,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
+ "lp.disallow_expected_disbursements as disallowExpectedDisbursements, lp.allow_approved_disbursed_amounts_over_applied as allowApprovedDisbursedAmountsOverApplied, lp.over_applied_calculation_type as overAppliedCalculationType, over_applied_number as overAppliedNumber, "
+ "lp.days_in_month_enum as daysInMonth, lp.days_in_year_enum as daysInYear, lp.interest_recalculation_enabled as isInterestRecalculationEnabled, "
+ "lp.can_define_fixed_emi_amount as canDefineInstallmentAmount, lp.instalment_amount_in_multiples_of as installmentAmountInMultiplesOf, "
+ "lp.due_days_for_repayment_event as dueDaysForRepaymentEvent, lp.overdue_days_for_repayment_event as overDueDaysForRepaymentEvent, lp.enable_down_payment as enableDownPayment, lp.disbursed_amount_percentage_for_down_payment as disbursedAmountPercentageForDownPayment, lp.enable_auto_repayment_for_down_payment as enableAutoRepaymentForDownPayment, "
+ "lp.due_days_for_repayment_event as dueDaysForRepaymentEvent, lp.overdue_days_for_repayment_event as overDueDaysForRepaymentEvent, lp.enable_down_payment as enableDownPayment, lp.disbursed_amount_percentage_for_down_payment as disbursedAmountPercentageForDownPayment, lp.enable_auto_repayment_for_down_payment as enableAutoRepaymentForDownPayment, lp.repayment_start_date_type_enum as repaymentStartDateType, "
+ "lpr.pre_close_interest_calculation_strategy as preCloseInterestCalculationStrategy, "
+ "lpr.id as lprId, lpr.product_id as productId, lpr.compound_type_enum as compoundType, lpr.reschedule_strategy_enum as rescheduleStrategy, "
+ "lpr.rest_frequency_type_enum as restFrequencyEnum, lpr.rest_frequency_interval as restFrequencyInterval, "
@@ -364,6 +364,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final boolean enableDownPayment = rs.getBoolean("enableDownPayment");
final BigDecimal disbursedAmountPercentageForDownPayment = rs.getBigDecimal("disbursedAmountPercentageForDownPayment");
final boolean enableAutoRepaymentForDownPayment = rs.getBoolean("enableAutoRepaymentForDownPayment");
final Integer repaymentStartDateTypeId = JdbcSupport.getInteger(rs, "repaymentStartDateType");
final EnumOptionData repaymentStartDateType = LoanEnumerations.repaymentStartDateType(repaymentStartDateTypeId);
String status = "";
if (closeDate != null && closeDate.isBefore(DateUtils.getBusinessLocalDate())) {
@@ -521,7 +523,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, this.rates,
isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket,
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment,
enableAutoRepaymentForDownPayment, advancedPaymentData);
enableAutoRepaymentForDownPayment, advancedPaymentData, repaymentStartDateType);
}
}
@@ -0,0 +1,474 @@
/**
* 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 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.time.LocalDate;
import java.util.HashMap;
import java.util.UUID;
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
import org.apache.fineract.client.models.PutLoanProductsProductIdResponse;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
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 LoanProductRepaymentStartDateConfigurationTest {
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 loanProductWithRepaymentStartDateTypeConfigurationCreateAndModifyTest() {
// create product with repayment start date configuration, get , modify
// Delinquency Bucket
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec,
delinquencyBucketId);
final Integer repaymentStartDateType = 2;
// create loan product with repayment start date configuration
Integer loanProductId = createLoanProductWithRepaymentStartDateTypeConfiguration(loanTransactionHelper, delinquencyBucketId,
repaymentStartDateType);
GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId);
assertNotNull(getLoanProductsProductResponse);
assertEquals(repaymentStartDateType, getLoanProductsProductResponse.getRepaymentStartDateType().getId().intValue());
assertEquals("repaymentStartDateType.submittedOnDate", getLoanProductsProductResponse.getRepaymentStartDateType().getCode());
// modify loan product repayment start date configuration to disbursement date
PutLoanProductsProductIdResponse loanProductModifyResponse = updateLoanProduct(loanTransactionHelper,
getLoanProductsProductResponse.getId());
assertNotNull(loanProductModifyResponse);
getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId);
assertNotNull(getLoanProductsProductResponse);
assertEquals(1, getLoanProductsProductResponse.getRepaymentStartDateType().getId().intValue());
assertEquals("repaymentStartDateType.disbursementDate", getLoanProductsProductResponse.getRepaymentStartDateType().getCode());
}
@Test
public void loanProductWithNoRepaymentStartDateTypeConfigurationDefaultsToDisbursementDateTest() {
// create loan product with no configuration for repayment start date and verify that it is disbursement date by
// default
// Delinquency Bucket
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec,
delinquencyBucketId);
final Integer repaymentStartDateType = null;
// create loan product with repayment start date configuration
Integer loanProductId = createLoanProductWithRepaymentStartDateTypeConfiguration(loanTransactionHelper, delinquencyBucketId,
repaymentStartDateType);
GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId);
assertNotNull(getLoanProductsProductResponse);
assertEquals(1, getLoanProductsProductResponse.getRepaymentStartDateType().getId().intValue());
assertEquals("repaymentStartDateType.disbursementDate", getLoanProductsProductResponse.getRepaymentStartDateType().getCode());
}
@Test
public void loanAccountWithLoanProductRepaymentStartDateTypeAsSubmittedOnDateScheduleTest() {
// create loan account with product with repayment start date type configuration as submitted on date, verify
// repayment schedule is according to submitted on date, before and after disbursements
try {
// Set business date
LocalDate businessDate = LocalDate.of(2023, 03, 3);
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate);
// Loan ExternalId
String loanExternalIdStr = UUID.randomUUID().toString();
final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
// set repayment start date type as submittedOn date
final Integer repaymentStartDateType = 2;
// Loan Product creation with repayment start date type configuration
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithRepaymentStartDateTypeConfigurationAndMultipleDisbursements(
loanTransactionHelper, repaymentStartDateType);
assertNotNull(getLoanProductsProductResponse);
assertEquals(repaymentStartDateType, getLoanProductsProductResponse.getRepaymentStartDateType().getId().intValue());
assertEquals("repaymentStartDateType.submittedOnDate", getLoanProductsProductResponse.getRepaymentStartDateType().getCode());
// create loan account with submitted date as business date (03 March 2023) and expected disbursement date
// as future date (07 March 2023)
final Integer loanId = createLoanAccountMultipleRepaymentsDisbursement(clientId, getLoanProductsProductResponse.getId(),
loanExternalIdStr);
// Retrieve Loan with loanId
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue());
assertNotNull(loanDetails);
// verify loan schedule is according to submitted on date
assertNotNull(loanDetails.getRepaymentSchedule());
// loan term
assertEquals(92, loanDetails.getRepaymentSchedule().getLoanTermInDays());
assertEquals(4, loanDetails.getRepaymentSchedule().getPeriods().size());
// verify amounts
assertEquals(1000.0, loanDetails.getRepaymentSchedule().getTotalPrincipalExpected());
// first period [2023-03-03 to 2023-04-03]
assertEquals(1, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPeriod());
assertEquals(LocalDate.of(2023, 03, 3), loanDetails.getRepaymentSchedule().getPeriods().get(1).getFromDate());
assertEquals(LocalDate.of(2023, 04, 3), loanDetails.getRepaymentSchedule().getPeriods().get(1).getDueDate());
assertEquals(333.33, loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalInstallmentAmountForPeriod());
// second period [2023-04-03 to 2023-05-03]
assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPeriod());
assertEquals(LocalDate.of(2023, 04, 3), loanDetails.getRepaymentSchedule().getPeriods().get(2).getFromDate());
assertEquals(LocalDate.of(2023, 05, 3), loanDetails.getRepaymentSchedule().getPeriods().get(2).getDueDate());
assertEquals(333.33, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalInstallmentAmountForPeriod());
// third period [2023-05-03 to 2023-06-03]
assertEquals(3, loanDetails.getRepaymentSchedule().getPeriods().get(3).getPeriod());
assertEquals(LocalDate.of(2023, 05, 3), loanDetails.getRepaymentSchedule().getPeriods().get(3).getFromDate());
assertEquals(LocalDate.of(2023, 06, 3), loanDetails.getRepaymentSchedule().getPeriods().get(3).getDueDate());
assertEquals(333.34, loanDetails.getRepaymentSchedule().getPeriods().get(3).getTotalInstallmentAmountForPeriod());
// first disbursement on a future date (7 March 2023)
LocalDate disbursementDate = LocalDate.of(2023, 03, 7);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate);
loanTransactionHelper.disburseLoanWithTransactionAmount("07 March 2023", loanId, "500");
loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue());
// verify loan schedule is according to submitted on date after first disbursement
assertNotNull(loanDetails);
assertNotNull(loanDetails.getRepaymentSchedule());
// loan term
assertEquals(92, loanDetails.getRepaymentSchedule().getLoanTermInDays());
assertEquals(4, loanDetails.getRepaymentSchedule().getPeriods().size());
// verify amounts
assertEquals(500.0, loanDetails.getRepaymentSchedule().getTotalPrincipalExpected());
assertEquals(500.0, loanDetails.getRepaymentSchedule().getTotalPrincipalDisbursed());
// first period [2023-03-03 to 2023-04-03]
assertEquals(1, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPeriod());
assertEquals(LocalDate.of(2023, 03, 3), loanDetails.getRepaymentSchedule().getPeriods().get(1).getFromDate());
assertEquals(LocalDate.of(2023, 04, 3), loanDetails.getRepaymentSchedule().getPeriods().get(1).getDueDate());
assertEquals(166.67, loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalInstallmentAmountForPeriod());
// second period [2023-04-03 to 2023-05-03]
assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPeriod());
assertEquals(LocalDate.of(2023, 04, 3), loanDetails.getRepaymentSchedule().getPeriods().get(2).getFromDate());
assertEquals(LocalDate.of(2023, 05, 3), loanDetails.getRepaymentSchedule().getPeriods().get(2).getDueDate());
assertEquals(166.67, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalInstallmentAmountForPeriod());
// third period [2023-05-03 to 2023-06-03]
assertEquals(3, loanDetails.getRepaymentSchedule().getPeriods().get(3).getPeriod());
assertEquals(LocalDate.of(2023, 05, 3), loanDetails.getRepaymentSchedule().getPeriods().get(3).getFromDate());
assertEquals(LocalDate.of(2023, 06, 3), loanDetails.getRepaymentSchedule().getPeriods().get(3).getDueDate());
assertEquals(166.66, loanDetails.getRepaymentSchedule().getPeriods().get(3).getTotalInstallmentAmountForPeriod());
// second disbursement next month (7 April 2023)
disbursementDate = LocalDate.of(2023, 04, 7);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate);
loanTransactionHelper.disburseLoanWithTransactionAmount("07 April 2023", loanId, "500");
loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue());
// verify loan schedule is according to submitted on date after second disbursement
assertNotNull(loanDetails);
assertNotNull(loanDetails.getRepaymentSchedule());
// loan term
assertEquals(92, loanDetails.getRepaymentSchedule().getLoanTermInDays());
assertEquals(5, loanDetails.getRepaymentSchedule().getPeriods().size());
// verify amounts
assertEquals(1000.0, loanDetails.getRepaymentSchedule().getTotalPrincipalExpected());
assertEquals(1000.0, loanDetails.getRepaymentSchedule().getTotalPrincipalDisbursed());
// first period [2023-03-03 to 2023-04-03]
assertEquals(1, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPeriod());
assertEquals(LocalDate.of(2023, 03, 3), loanDetails.getRepaymentSchedule().getPeriods().get(1).getFromDate());
assertEquals(LocalDate.of(2023, 04, 3), loanDetails.getRepaymentSchedule().getPeriods().get(1).getDueDate());
assertEquals(166.67, loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalInstallmentAmountForPeriod());
// second period [2023-04-03 to 2023-05-03]
assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().get(3).getPeriod());
assertEquals(LocalDate.of(2023, 04, 3), loanDetails.getRepaymentSchedule().getPeriods().get(3).getFromDate());
assertEquals(LocalDate.of(2023, 05, 3), loanDetails.getRepaymentSchedule().getPeriods().get(3).getDueDate());
assertEquals(333.33, loanDetails.getRepaymentSchedule().getPeriods().get(3).getTotalInstallmentAmountForPeriod());
// third period [2023-05-03 to 2023-06-03]
assertEquals(3, loanDetails.getRepaymentSchedule().getPeriods().get(4).getPeriod());
assertEquals(LocalDate.of(2023, 05, 3), loanDetails.getRepaymentSchedule().getPeriods().get(4).getFromDate());
assertEquals(LocalDate.of(2023, 06, 3), loanDetails.getRepaymentSchedule().getPeriods().get(4).getDueDate());
assertEquals(500.00, loanDetails.getRepaymentSchedule().getPeriods().get(4).getTotalInstallmentAmountForPeriod());
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
@Test
public void loanAccountWithLoanProductRepaymentStartDateTypeAsDisbursementDateScheduleTest() {
// create loan account with loan product with repayment start date type configuration as disbursement date ,
// verify repayment schedule is as per disbursement date before and after disbursements
try {
// Set business date
LocalDate businessDate = LocalDate.of(2023, 03, 3);
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate);
// Loan ExternalId
String loanExternalIdStr = UUID.randomUUID().toString();
final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
// set repayment start date type as default, disbursement date
final Integer repaymentStartDateType = 1;
// Loan Product creation with repayment date type configuration
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithRepaymentStartDateTypeConfigurationAndMultipleDisbursements(
loanTransactionHelper, repaymentStartDateType);
assertNotNull(getLoanProductsProductResponse);
assertEquals(repaymentStartDateType, getLoanProductsProductResponse.getRepaymentStartDateType().getId().intValue());
assertEquals("repaymentStartDateType.disbursementDate", getLoanProductsProductResponse.getRepaymentStartDateType().getCode());
// create loan account with submitted date as business date (03 March 2023) and expected disbursement date
// (07 March 2023)
final Integer loanId = createLoanAccountMultipleRepaymentsDisbursement(clientId, getLoanProductsProductResponse.getId(),
loanExternalIdStr);
// Retrieve Loan with loanId
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue());
assertNotNull(loanDetails);
// verify loan schedule is according to disbursement date
assertNotNull(loanDetails.getRepaymentSchedule());
// loan term
assertEquals(92, loanDetails.getRepaymentSchedule().getLoanTermInDays());
assertEquals(4, loanDetails.getRepaymentSchedule().getPeriods().size());
// verify amounts
assertEquals(1000.0, loanDetails.getRepaymentSchedule().getTotalPrincipalExpected());
// first period [2023-03-07 to 2023-04-07]
assertEquals(1, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPeriod());
assertEquals(LocalDate.of(2023, 03, 7), loanDetails.getRepaymentSchedule().getPeriods().get(1).getFromDate());
assertEquals(LocalDate.of(2023, 04, 7), loanDetails.getRepaymentSchedule().getPeriods().get(1).getDueDate());
assertEquals(333.33, loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalInstallmentAmountForPeriod());
// second period [2023-04-07 to 2023-05-07]
assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPeriod());
assertEquals(LocalDate.of(2023, 04, 7), loanDetails.getRepaymentSchedule().getPeriods().get(2).getFromDate());
assertEquals(LocalDate.of(2023, 05, 7), loanDetails.getRepaymentSchedule().getPeriods().get(2).getDueDate());
assertEquals(333.33, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalInstallmentAmountForPeriod());
// third period [2023-05-07 to 2023-06-07]
assertEquals(3, loanDetails.getRepaymentSchedule().getPeriods().get(3).getPeriod());
assertEquals(LocalDate.of(2023, 05, 7), loanDetails.getRepaymentSchedule().getPeriods().get(3).getFromDate());
assertEquals(LocalDate.of(2023, 06, 7), loanDetails.getRepaymentSchedule().getPeriods().get(3).getDueDate());
assertEquals(333.34, loanDetails.getRepaymentSchedule().getPeriods().get(3).getTotalInstallmentAmountForPeriod());
// first disbursement (7 March 2023)
LocalDate disbursementDate = LocalDate.of(2023, 03, 7);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate);
loanTransactionHelper.disburseLoanWithTransactionAmount("07 March 2023", loanId, "500");
loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue());
// verify loan schedule is according to disbursement date
assertNotNull(loanDetails);
assertNotNull(loanDetails.getRepaymentSchedule());
// loan term
assertEquals(92, loanDetails.getRepaymentSchedule().getLoanTermInDays());
assertEquals(4, loanDetails.getRepaymentSchedule().getPeriods().size());
// verify amounts
assertEquals(500.0, loanDetails.getRepaymentSchedule().getTotalPrincipalExpected());
assertEquals(500.0, loanDetails.getRepaymentSchedule().getTotalPrincipalDisbursed());
// first period [2023-03-07 to 2023-04-07]
assertEquals(1, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPeriod());
assertEquals(LocalDate.of(2023, 03, 7), loanDetails.getRepaymentSchedule().getPeriods().get(1).getFromDate());
assertEquals(LocalDate.of(2023, 04, 7), loanDetails.getRepaymentSchedule().getPeriods().get(1).getDueDate());
assertEquals(166.67, loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalInstallmentAmountForPeriod());
// second period [2023-04-07 to 2023-05-07]
assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPeriod());
assertEquals(LocalDate.of(2023, 04, 7), loanDetails.getRepaymentSchedule().getPeriods().get(2).getFromDate());
assertEquals(LocalDate.of(2023, 05, 7), loanDetails.getRepaymentSchedule().getPeriods().get(2).getDueDate());
assertEquals(166.67, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalInstallmentAmountForPeriod());
// third period [2023-05-07 to 2023-06-07]
assertEquals(3, loanDetails.getRepaymentSchedule().getPeriods().get(3).getPeriod());
assertEquals(LocalDate.of(2023, 05, 7), loanDetails.getRepaymentSchedule().getPeriods().get(3).getFromDate());
assertEquals(LocalDate.of(2023, 06, 7), loanDetails.getRepaymentSchedule().getPeriods().get(3).getDueDate());
assertEquals(166.66, loanDetails.getRepaymentSchedule().getPeriods().get(3).getTotalInstallmentAmountForPeriod());
// second disbursement next month (7 April 2023)
disbursementDate = LocalDate.of(2023, 04, 7);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate);
loanTransactionHelper.disburseLoanWithTransactionAmount("07 April 2023", loanId, "500");
loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue());
// verify loan schedule is according to disbursement after second disbursement
assertNotNull(loanDetails);
assertNotNull(loanDetails.getRepaymentSchedule());
// loan term
assertEquals(92, loanDetails.getRepaymentSchedule().getLoanTermInDays());
assertEquals(5, loanDetails.getRepaymentSchedule().getPeriods().size());
// verify amounts
assertEquals(1000.0, loanDetails.getRepaymentSchedule().getTotalPrincipalExpected());
assertEquals(1000.0, loanDetails.getRepaymentSchedule().getTotalPrincipalDisbursed());
// first period [2023-03-07 to 2023-04-07]
assertEquals(1, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPeriod());
assertEquals(LocalDate.of(2023, 03, 7), loanDetails.getRepaymentSchedule().getPeriods().get(2).getFromDate());
assertEquals(LocalDate.of(2023, 04, 7), loanDetails.getRepaymentSchedule().getPeriods().get(2).getDueDate());
assertEquals(333.33, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalInstallmentAmountForPeriod());
// second period [2023-04-07 to 2023-05-07]
assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().get(3).getPeriod());
assertEquals(LocalDate.of(2023, 04, 7), loanDetails.getRepaymentSchedule().getPeriods().get(3).getFromDate());
assertEquals(LocalDate.of(2023, 05, 7), loanDetails.getRepaymentSchedule().getPeriods().get(3).getDueDate());
assertEquals(333.33, loanDetails.getRepaymentSchedule().getPeriods().get(3).getTotalInstallmentAmountForPeriod());
// third period [2023-05-07 to 2023-06-07]
assertEquals(3, loanDetails.getRepaymentSchedule().getPeriods().get(4).getPeriod());
assertEquals(LocalDate.of(2023, 05, 7), loanDetails.getRepaymentSchedule().getPeriods().get(4).getFromDate());
assertEquals(LocalDate.of(2023, 06, 7), loanDetails.getRepaymentSchedule().getPeriods().get(4).getDueDate());
assertEquals(333.34, loanDetails.getRepaymentSchedule().getPeriods().get(4).getTotalInstallmentAmountForPeriod());
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
private PutLoanProductsProductIdResponse updateLoanProduct(LoanTransactionHelper loanTransactionHelper, Long id) {
// repayment start date configuration
final Integer repaymentStartDateType = 1;
final PutLoanProductsProductIdRequest requestModifyLoan = new PutLoanProductsProductIdRequest()
.repaymentStartDateType(repaymentStartDateType).locale("en");
return loanTransactionHelper.updateLoanProduct(id, requestModifyLoan);
}
private Integer createLoanProductWithRepaymentStartDateTypeConfiguration(final LoanTransactionHelper loanTransactionHelper,
final Integer delinquencyBucketId, final Integer repaymentStartDateType) {
final HashMap<String, Object> loanProductMap = new LoanProductTestBuilder().withRepaymentStartDateType(repaymentStartDateType)
.build(null, delinquencyBucketId);
final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap));
return loanProductId;
}
private Integer createLoanAccountMultipleRepaymentsDisbursement(final Integer clientID, final Long loanProductID,
final String externalId) {
String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("3")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("3").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance()
.withAmortizationTypeAsEqualPrincipalPayments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod()
.withExpectedDisbursementDate("07 March 2023").withSubmittedOnDate("03 March 2023").withLoanType("individual")
.withExternalId(externalId).build(clientID.toString(), loanProductID.toString(), null);
final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON);
loanTransactionHelper.approveLoan("03 March 2023", "1000", loanId, null);
return loanId;
}
private GetLoanProductsProductIdResponse createLoanProductWithRepaymentStartDateTypeConfigurationAndMultipleDisbursements(
LoanTransactionHelper loanTransactionHelper, final Integer repaymentStartDateType) {
final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentTypeAsMonth()
.withRepaymentAfterEvery("1").withNumberOfRepayments("3").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0")
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance()
.withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withDaysInMonth("30").withDaysInYear("365")
.withMoratorium("0", "0").withMultiDisburse().withDisallowExpectedDisbursements(true)
.withRepaymentStartDateType(repaymentStartDateType).build(null);
final Integer loanProductId = loanTransactionHelper.getLoanProductId(loanProductJSON);
return loanTransactionHelper.getLoanProduct(loanProductId);
}
}
@@ -149,6 +149,7 @@ public class LoanProductTestBuilder {
private boolean enableDownPayment = false;
private String disbursedAmountPercentageForDownPayment = null;
private boolean enableAutoRepaymentForDownPayment = false;
private Integer repaymentStartDateType = null;
public String build() {
final HashMap<String, Object> map = build(null, null);
@@ -299,6 +300,10 @@ public class LoanProductTestBuilder {
map.put("enableAutoRepaymentForDownPayment", enableAutoRepaymentForDownPayment);
}
if (this.repaymentStartDateType != null) {
map.put("repaymentStartDateType", repaymentStartDateType);
}
return map;
}
@@ -723,4 +728,9 @@ public class LoanProductTestBuilder {
return this;
}
public LoanProductTestBuilder withRepaymentStartDateType(final Integer repaymentStartDateType) {
this.repaymentStartDateType = repaymentStartDateType;
return this;
}
}