FINERACT-2412: Full term tranche - Schedule handling and Calculations

This commit is contained in:
mariiaKraievska
2026-01-13 18:06:12 +01:00
committed by Adam Saghy
parent 3e197e367b
commit 09031aa5a7
24 changed files with 739 additions and 51 deletions
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
/**
@@ -34,10 +35,19 @@ public class RepaymentScheduleRelatedLoanData {
private final BigDecimal netDisbursalAmount;
private final BigDecimal inArrearsTolerance;
private final BigDecimal totalFeeChargesAtDisbursement;
@Getter
private final boolean allowFullTermForTranche;
public RepaymentScheduleRelatedLoanData(final LocalDate expectedDisbursementDate, final LocalDate actualDisbursementDate,
final CurrencyData currency, final BigDecimal principal, final BigDecimal inArrearsTolerance,
final BigDecimal totalFeeChargesAtDisbursement) {
this(expectedDisbursementDate, actualDisbursementDate, currency, principal, inArrearsTolerance, totalFeeChargesAtDisbursement,
false);
}
public RepaymentScheduleRelatedLoanData(final LocalDate expectedDisbursementDate, final LocalDate actualDisbursementDate,
final CurrencyData currency, final BigDecimal principal, final BigDecimal inArrearsTolerance,
final BigDecimal totalFeeChargesAtDisbursement, final boolean allowFullTermForTranche) {
this.expectedDisbursementDate = expectedDisbursementDate;
this.actualDisbursementDate = actualDisbursementDate;
this.currency = currency;
@@ -45,6 +55,7 @@ public class RepaymentScheduleRelatedLoanData {
this.inArrearsTolerance = inArrearsTolerance;
this.totalFeeChargesAtDisbursement = totalFeeChargesAtDisbursement;
this.netDisbursalAmount = this.principal.subtract(this.totalFeeChargesAtDisbursement);
this.allowFullTermForTranche = allowFullTermForTranche;
}
public LocalDate disbursementDate() {
@@ -427,6 +427,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "enable_installment_level_delinquency", nullable = false)
private boolean enableInstallmentLevelDelinquency = false;
@Getter
@Column(name = "allow_full_term_for_tranche", nullable = false)
private boolean allowFullTermForTranche = false;
@@ -1787,10 +1788,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
}
public boolean isAllowFullTermForTranche() {
return this.allowFullTermForTranche;
}
public void updateAllowFullTermForTranche(boolean allowFullTermForTranche) {
this.allowFullTermForTranche = allowFullTermForTranche;
}
@@ -253,6 +253,10 @@ public class LoanRepaymentScheduleProcessingWrapper {
: DateUtils.isDateInRangeFromExclusiveToInclusive(targetDate, fromDate, toDate);
}
public static boolean isInPeriodFromInclusiveToExclusive(final LocalDate targetDate, final LocalDate fromDate, final LocalDate toDate) {
return DateUtils.isDateInRangeFromInclusiveToExclusive(fromDate, toDate, targetDate);
}
public static boolean isBeforePeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod) {
LocalDate fromDate = installment.getFromDate();
return isFirstPeriod ? DateUtils.isBefore(targetDate, fromDate) : !DateUtils.isAfter(targetDate, fromDate);
@@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
@@ -247,6 +248,8 @@ public final class LoanApplicationTerms {
private LoanBuyDownFeeStrategy buyDownFeeStrategy;
private LoanBuyDownFeeIncomeType buyDownFeeIncomeType;
private boolean merchantBuyDownFee;
@Getter
private boolean allowFullTermForTranche = false;
private LoanApplicationTerms(Builder builder) {
this.currency = builder.currency;
@@ -292,6 +295,7 @@ public final class LoanApplicationTerms {
this.buyDownFeeStrategy = builder.buyDownFeeStrategy;
this.buyDownFeeIncomeType = builder.buyDownFeeIncomeType;
this.merchantBuyDownFee = builder.merchantBuyDownFee;
this.allowFullTermForTranche = builder.allowFullTermForTranche;
this.interestMethod = builder.interestMethod;
this.allowPartialPeriodInterestCalculation = builder.allowPartialPeriodInterestCalculation;
}
@@ -333,6 +337,7 @@ public final class LoanApplicationTerms {
private LoanBuyDownFeeStrategy buyDownFeeStrategy;
private LoanBuyDownFeeIncomeType buyDownFeeIncomeType;
private boolean merchantBuyDownFee;
private boolean allowFullTermForTranche;
private boolean allowPartialPeriodInterestCalculation;
public Builder interestMethod(InterestMethod interestMethod) {
@@ -500,6 +505,11 @@ public final class LoanApplicationTerms {
return this;
}
public Builder allowFullTermForTranche(boolean value) {
this.allowFullTermForTranche = value;
return this;
}
public LoanApplicationTerms build() {
return new LoanApplicationTerms(this);
}
@@ -542,7 +552,8 @@ public final class LoanApplicationTerms {
.submittedOnDate(modelData.scheduleGenerationStartDate()).seedDate(seedDate)
.interestRecognitionOnDisbursementDate(modelData.interestRecognitionOnDisbursementDate())
.daysInYearCustomStrategy(modelData.daysInYearCustomStrategy()).interestMethod(modelData.interestMethod())
.allowPartialPeriodInterestCalculation(modelData.allowPartialPeriodInterestCalculation()).mc(mc).build();
.allowPartialPeriodInterestCalculation(modelData.allowPartialPeriodInterestCalculation())
.allowFullTermForTranche(modelData.allowFullTermForTranche()).mc(mc).build();
}
public static LoanApplicationTerms assembleFrom(final CurrencyData currency, final Integer loanTermFrequency,
@@ -579,7 +590,7 @@ public final class LoanApplicationTerms {
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final boolean enableBuyDownFee, final LoanBuyDownFeeCalculationType buyDownFeeCalculationType,
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
final boolean merchantBuyDownFee, final boolean allowFullTermForTranche) {
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
@@ -601,7 +612,7 @@ public final class LoanApplicationTerms {
fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, allowFullTermForTranche);
}
@@ -622,7 +633,7 @@ public final class LoanApplicationTerms {
final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod,
final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI,
final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans,
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate) {
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final boolean allowFullTermForTranche) {
final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments();
final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery();
@@ -680,7 +691,7 @@ public final class LoanApplicationTerms {
loanProductRelatedDetail.getCapitalizedIncomeStrategy(), loanProductRelatedDetail.getCapitalizedIncomeType(),
loanProductRelatedDetail.isEnableBuyDownFee(), loanProductRelatedDetail.getBuyDownFeeCalculationType(),
loanProductRelatedDetail.getBuyDownFeeStrategy(), loanProductRelatedDetail.getBuyDownFeeIncomeType(),
loanProductRelatedDetail.isMerchantBuyDownFee());
loanProductRelatedDetail.isMerchantBuyDownFee(), allowFullTermForTranche);
}
private LoanApplicationTerms(final CurrencyData currency, final Integer loanTermFrequency,
@@ -716,7 +727,7 @@ public final class LoanApplicationTerms {
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final boolean enableBuyDownFee, final LoanBuyDownFeeCalculationType buyDownFeeCalculationType,
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
final boolean merchantBuyDownFee, final boolean allowFullTermForTranche) {
this.currency = currency;
this.loanTermFrequency = loanTermFrequency;
@@ -827,6 +838,7 @@ public final class LoanApplicationTerms {
this.buyDownFeeStrategy = buyDownFeeStrategy;
this.buyDownFeeIncomeType = buyDownFeeIncomeType;
this.merchantBuyDownFee = merchantBuyDownFee;
this.allowFullTermForTranche = allowFullTermForTranche;
}
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
@@ -1703,7 +1715,7 @@ public final class LoanApplicationTerms {
repaymentEvery, numberOfRepayments,
isInterestChargedFromDateSameAsDisbursalDateEnabled != null && isInterestChargedFromDateSameAsDisbursalDateEnabled,
daysInYearCustomStrategy, allowPartialPeriodInterestCalculation, interestRecalculationEnabled, recalculationFrequencyType,
preClosureInterestCalculationStrategy);
preClosureInterestCalculationStrategy, allowFullTermForTranche);
}
public Integer getLoanTermFrequency() {
@@ -35,5 +35,6 @@ public record LoanRepaymentScheduleModelData(@NotNull LocalDate scheduleGenerati
@NotNull boolean downPaymentEnabled, @NotNull DaysInMonthType daysInMonth, @NotNull DaysInYearType daysInYear,
BigDecimal downPaymentPercentage, Integer installmentAmountInMultiplesOf, Integer fixedLength,
@NotNull Boolean interestRecognitionOnDisbursementDate, @Nullable DaysInYearCustomStrategyType daysInYearCustomStrategy,
@NotNull InterestMethod interestMethod, @NotNull boolean allowPartialPeriodInterestCalculation) {
@NotNull InterestMethod interestMethod, @NotNull boolean allowPartialPeriodInterestCalculation,
@NotNull boolean allowFullTermForTranche) {
}
@@ -120,7 +120,8 @@ public class LoanTermVariationsMapper {
scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), holidayDetailDTO,
allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(),
scheduleGeneratorDTO.isInterestToBeRecoveredFirstWhenGreaterThanEMI(), loan.getFixedPrincipalPercentagePerInstallment(),
scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans(), repaymentStartDateType, loan.getSubmittedOnDate());
scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans(), repaymentStartDateType, loan.getSubmittedOnDate(),
loan.isAllowFullTermForTranche());
}
private BigDecimal constructFloatingInterestRates(final BigDecimal annualNominalInterestRate, final FloatingRateDTO floatingRateDTO,
@@ -58,6 +58,8 @@ public class LoanConfigurationDetails implements ILoanConfigurationDetails {
private final RecalculationFrequencyType restFrequencyType;
@Getter
private final LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy;
@Getter
private final boolean allowFullTermForTranche;
public LoanConfigurationDetails(CurrencyData currency, BigDecimal interestRatePerPeriod, BigDecimal annualNominalInterestRate,
Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace,
@@ -67,7 +69,7 @@ public class LoanConfigurationDetails implements ILoanConfigurationDetails {
Integer numberOfRepayments, boolean interestRecognitionOnDisbursementDate,
DaysInYearCustomStrategyType daysInYearCustomStrategy, boolean allowPartialPeriodInterestCalculation,
boolean isInterestRecalculationEnabled, RecalculationFrequencyType restFrequencyType,
LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy) {
LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy, boolean allowFullTermForTranche) {
this.currency = currency;
this.interestRatePerPeriod = interestRatePerPeriod;
this.annualNominalInterestRate = annualNominalInterestRate;
@@ -89,6 +91,7 @@ public class LoanConfigurationDetails implements ILoanConfigurationDetails {
this.isInterestRecalculationEnabled = isInterestRecalculationEnabled;
this.restFrequencyType = restFrequencyType;
this.preCloseInterestCalculationStrategy = preCloseInterestCalculationStrategy;
this.allowFullTermForTranche = allowFullTermForTranche;
}
private Integer defaultToNullIfZero(final Integer value) {
@@ -73,4 +73,6 @@ public interface ILoanConfigurationDetails {
RecalculationFrequencyType getRestFrequencyType();
LoanPreCloseInterestCalculationStrategy getPreCloseInterestCalculationStrategy();
boolean isAllowFullTermForTranche();
}