FINERACT-2354: Work with DTOs instead of entities inside ProgressiveEMICalculator

This commit is contained in:
Adam Saghy
2025-11-13 08:44:22 +01:00
parent 068e398f90
commit 051571003e
23 changed files with 205 additions and 182 deletions
@@ -5432,6 +5432,7 @@ Feature: LoanReAging
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 01 April 2024 | Re-age | 67.44 | 67.05 | 0.39 | 0.0 | 0.0 | 0.0 | false | true |
@Skip
@TestRailId:C4154 @AdvancedPaymentAllocation
Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - re-aging on same day as disbursement - UC16.1
When Admin sets the business date to "01 January 2024"
@@ -53,13 +53,13 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCal
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductRelatedDetailMinimumData;
import org.apache.fineract.portfolio.loanproduct.data.LoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreCloseInterestCalculationStrategy;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
@@ -1694,15 +1694,16 @@ public final class LoanApplicationTerms {
this.buyDownFeeStrategy, this.buyDownFeeIncomeType, this.merchantBuyDownFee);
}
public LoanProductMinimumRepaymentScheduleRelatedDetail toLoanProductRelatedDetailMinimumData() {
public ILoanConfigurationDetails toLoanConfigurationDetails() {
final CurrencyData currency = new CurrencyData(this.currency.getCode(), this.currency.getDecimalPlaces(),
this.currency.getInMultiplesOf());
return new LoanProductRelatedDetailMinimumData(currency, interestRatePerPeriod, annualNominalInterestRate, interestChargingGrace,
return new LoanConfigurationDetails(currency, interestRatePerPeriod, annualNominalInterestRate, interestChargingGrace,
interestPaymentGrace, principalGrace, recurringMoratoriumOnPrincipalPeriods, interestMethod,
interestCalculationPeriodMethod, daysInYearType, daysInMonthType, amortizationMethod, repaymentPeriodFrequencyType,
repaymentEvery, numberOfRepayments,
isInterestChargedFromDateSameAsDisbursalDateEnabled != null && isInterestChargedFromDateSameAsDisbursalDateEnabled,
daysInYearCustomStrategy, allowPartialPeriodInterestCalcualtion);
daysInYearCustomStrategy, allowPartialPeriodInterestCalcualtion, interestRecalculationEnabled, recalculationFrequencyType,
preClosureInterestCalculationStrategy);
}
public Integer getLoanTermFrequency() {
@@ -19,17 +19,20 @@
package org.apache.fineract.portfolio.loanproduct.data;
import java.math.BigDecimal;
import lombok.Getter;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreCloseInterestCalculationStrategy;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRepaymentScheduleRelatedDetail {
public class LoanConfigurationDetails implements ILoanConfigurationDetails {
private final CurrencyData currency;
private final BigDecimal interestRatePerPeriod;
@@ -49,14 +52,22 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe
private final boolean interestRecognitionOnDisbursementDate;
private final DaysInYearCustomStrategyType daysInYearCustomStrategy;
private final boolean allowPartialPeriodInterestCalculation;
@Getter
private final boolean isInterestRecalculationEnabled;
@Getter
private final RecalculationFrequencyType restFrequencyType;
@Getter
private final LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy;
public LoanProductRelatedDetailMinimumData(CurrencyData currency, BigDecimal interestRatePerPeriod,
BigDecimal annualNominalInterestRate, Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace,
public LoanConfigurationDetails(CurrencyData currency, BigDecimal interestRatePerPeriod, BigDecimal annualNominalInterestRate,
Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace,
Integer recurringMoratoriumOnPrincipalPeriods, InterestMethod interestMethod,
InterestCalculationPeriodMethod interestCalculationPeriodMethod, DaysInYearType daysInYearType, DaysInMonthType daysInMonthType,
AmortizationMethod amortizationMethod, PeriodFrequencyType repaymentPeriodFrequencyType, Integer repaymentEvery,
Integer numberOfRepayments, boolean interestRecognitionOnDisbursementDate,
DaysInYearCustomStrategyType daysInYearCustomStrategy, boolean allowPartialPeriodInterestCalculation) {
DaysInYearCustomStrategyType daysInYearCustomStrategy, boolean allowPartialPeriodInterestCalculation,
boolean isInterestRecalculationEnabled, RecalculationFrequencyType restFrequencyType,
LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy) {
this.currency = currency;
this.interestRatePerPeriod = interestRatePerPeriod;
this.annualNominalInterestRate = annualNominalInterestRate;
@@ -75,6 +86,9 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe
this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate;
this.daysInYearCustomStrategy = daysInYearCustomStrategy;
this.allowPartialPeriodInterestCalculation = allowPartialPeriodInterestCalculation;
this.isInterestRecalculationEnabled = isInterestRecalculationEnabled;
this.restFrequencyType = restFrequencyType;
this.preCloseInterestCalculationStrategy = preCloseInterestCalculationStrategy;
}
private Integer defaultToNullIfZero(final Integer value) {
@@ -184,4 +198,5 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe
public DaysInYearCustomStrategyType getDaysInYearCustomStrategy() {
return daysInYearCustomStrategy;
}
}
@@ -26,7 +26,7 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
/**
* Represents the bare minimum repayment details needed for activities related to generating repayment schedules.
*/
public interface LoanProductMinimumRepaymentScheduleRelatedDetail {
public interface ILoanConfigurationDetails {
CurrencyData getCurrencyData();
@@ -67,4 +67,10 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail {
boolean isInterestRecognitionOnDisbursementDate();
DaysInYearCustomStrategyType getDaysInYearCustomStrategy();
boolean isInterestRecalculationEnabled();
RecalculationFrequencyType getRestFrequencyType();
LoanPreCloseInterestCalculationStrategy getPreCloseInterestCalculationStrategy();
}
@@ -53,7 +53,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
@Embeddable
@Getter
@Setter
public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentScheduleRelatedDetail {
public class LoanProductRelatedDetail {
@Embedded
private MonetaryCurrency currency;
@@ -329,7 +329,6 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
return this.currency.copy();
}
@Override
public CurrencyData getCurrencyData() {
return currency.toData();
}
@@ -342,27 +341,20 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
return Money.of(getCurrencyData(), this.inArrearsTolerance);
}
// TODO: REVIEW
@Override
public BigDecimal getNominalInterestRatePerPeriod() {
return this.nominalInterestRatePerPeriod == null ? null
: BigDecimal.valueOf(Double.parseDouble(this.nominalInterestRatePerPeriod.stripTrailingZeros().toString()));
}
// TODO: REVIEW
@Override
public PeriodFrequencyType getInterestPeriodFrequencyType() {
return this.interestPeriodFrequencyType == null ? PeriodFrequencyType.INVALID : this.interestPeriodFrequencyType;
}
// TODO: REVIEW
@Override
public BigDecimal getAnnualNominalInterestRate() {
return this.annualNominalInterestRate == null ? null
: BigDecimal.valueOf(Double.parseDouble(this.annualNominalInterestRate.stripTrailingZeros().toString()));
}
@Override
public DaysInYearCustomStrategyType getDaysInYearCustomStrategy() {
return daysInYearCustomStrategy;
}
@@ -28,7 +28,7 @@ import org.apache.fineract.portfolio.loanaccount.service.InterestScheduleModelRe
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
@SuppressWarnings("unused")
public class EmbeddableProgressiveLoanScheduleGenerator {
@@ -70,7 +70,7 @@ public class EmbeddableProgressiveLoanScheduleGenerator {
@Override
public Optional<ProgressiveLoanInterestScheduleModel> readProgressiveLoanInterestScheduleModel(Long loanId,
LoanProductMinimumRepaymentScheduleRelatedDetail detail, Integer installmentAmountInMultipliesOf) {
ILoanConfigurationDetails detail, Integer installmentAmountInMultipliesOf) {
return Optional.empty();
}
@@ -100,6 +100,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Mon
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanConfigurationDetailsMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
@@ -231,9 +232,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanTermVariationsData> loanTermVariations = loan.getActiveLoanTermVariations().stream().map(LoanTermVariations::toData)
.collect(Collectors.toCollection(ArrayList::new));
final Integer installmentAmountInMultiplesOf = loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf();
final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments,
loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
LoanConfigurationDetailsMapper.map(loan), loanTermVariations, installmentAmountInMultiplesOf,
overpaymentHolder.getMoneyObject().getMc());
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
changedTransactionDetail, scheduleModel);
@@ -1588,7 +1589,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (isInterestRecalculationSupported(ctx, loan) && !loan.isNpa()
&& !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
boolean modelHasUpdates = emiCalculator.recalculateModelOverdueAmountsTillDate(ctx, targetDate);
boolean modelHasUpdates = emiCalculator.recalculateModelOverdueAmountsTillDate(ctx.getModel(), targetDate,
ctx.isPrepayAttempt());
if (modelHasUpdates && updateInstallments) {
updateInstallmentsPrincipalAndInterestByModel(ctx);
}
@@ -3264,8 +3266,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
loan.getLoanProduct().getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation())
.mc(mc).build();
LocalDate reAgePeriodStartDate = calculateFirstReAgedPeriodStartDate(loanTransaction);
LocalDate reageFirstDueDate = loanTransaction.getLoanReAgeParameter().getStartDate();
// Update the existing model with re-aged periods
emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx, loanTransaction, loanApplicationTerms, mc);
emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx.getModel(), reAgePeriodStartDate, reageFirstDueDate,
loanTransaction.getTransactionDate(), loanApplicationTerms, mc);
updateInstallmentsByModelForReAging(loanTransaction, ctx);
@@ -3273,4 +3278,22 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Money.zero(currency));
reprocessInstallments(installments);
}
private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanTransaction loanTransaction) {
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
if (reAgingStartDate.isEqual(loanTransaction.getLoan().getDisbursementDate())) {
return reAgingStartDate;
}
return switch (loanReAgeParameter.getFrequencyType()) {
case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber());
case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber());
case MONTHS -> reAgingStartDate.minusMonths(loanReAgeParameter.getFrequencyNumber());
case YEARS -> reAgingStartDate.minusYears(loanReAgeParameter.getFrequencyNumber());
case WHOLE_TERM -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WHOLE_TERM");
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
};
}
}
@@ -110,7 +110,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
? loanApplicationTerms.getLoanTermVariations().getExceptionData()
: null;
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetailMinimumData(), loanTermVariations,
expectedRepaymentPeriods, loanApplicationTerms.toLoanConfigurationDetails(), loanTermVariations,
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size());
@@ -0,0 +1,42 @@
package org.apache.fineract.portfolio.loanaccount.mapper;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanproduct.data.LoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
public final class LoanConfigurationDetailsMapper {
private LoanConfigurationDetailsMapper() {}
public static ILoanConfigurationDetails map(Loan loan) {
if (loan == null) {
return null;
}
LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanProductRelatedDetail();
if (loanProductRelatedDetail == null) {
return null;
}
MonetaryCurrency currency = loan.getCurrency();
CurrencyData currencyData = currency.toData();
return new LoanConfigurationDetails(currencyData, loanProductRelatedDetail.getNominalInterestRatePerPeriod(),
loanProductRelatedDetail.getAnnualNominalInterestRate(), loanProductRelatedDetail.getGraceOnInterestCharged(),
loanProductRelatedDetail.getGraceOnPrincipalPayment(), loanProductRelatedDetail.getGraceOnPrincipalPayment(),
loanProductRelatedDetail.getRecurringMoratoriumOnPrincipalPeriods(), loanProductRelatedDetail.getInterestMethod(),
loanProductRelatedDetail.getInterestCalculationPeriodMethod(),
DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType()),
DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType()), loanProductRelatedDetail.getAmortizationMethod(),
loanProductRelatedDetail.getRepaymentPeriodFrequencyType(), loanProductRelatedDetail.getRepayEvery(),
loanProductRelatedDetail.getNumberOfRepayments(), loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate(),
loanProductRelatedDetail.getDaysInYearCustomStrategy(), loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation(),
loan.isInterestRecalculationEnabled(), loan.getLoanInterestRecalculationDetails().getRestFrequencyType(),
loan.getLoanInterestRecalculationDetails().getPreCloseInterestCalculationStrategy());
}
}
@@ -23,7 +23,7 @@ import java.util.Optional;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
public interface InterestScheduleModelRepositoryWrapper {
@@ -35,8 +35,8 @@ public interface InterestScheduleModelRepositoryWrapper {
ProgressiveLoanInterestScheduleModel writeInterestScheduleModel(Loan loan, ProgressiveLoanInterestScheduleModel model);
Optional<ProgressiveLoanInterestScheduleModel> readProgressiveLoanInterestScheduleModel(Long loanId,
LoanProductMinimumRepaymentScheduleRelatedDetail detail, Integer installmentAmountInMultipliesOf);
Optional<ProgressiveLoanInterestScheduleModel> readProgressiveLoanInterestScheduleModel(Long loanId, ILoanConfigurationDetails detail,
Integer installmentAmountInMultipliesOf);
boolean hasValidModelForDate(Long loanId, LocalDate targetDate);
@@ -35,9 +35,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanConfigurationDetailsMapper;
import org.apache.fineract.portfolio.loanaccount.repository.ProgressiveLoanModelRepository;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.springframework.stereotype.Service;
@Service
@@ -92,7 +93,7 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
public Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel) {
return progressiveLoanModel.map(ProgressiveLoanModel::getJsonModel) //
.map(jsonModel -> progressiveLoanInterestScheduleModelParserService.fromJson(jsonModel,
progressiveLoanModel.get().getLoan().getLoanProductRelatedDetail(), MoneyHelper.getMathContext(),
LoanConfigurationDetailsMapper.map(progressiveLoanModel.get().getLoan()), MoneyHelper.getMathContext(),
progressiveLoanModel.get().getLoan().getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf()));
}
@@ -125,7 +126,7 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
@Override
public Optional<ProgressiveLoanInterestScheduleModel> readProgressiveLoanInterestScheduleModel(final Long loanId,
final LoanProductMinimumRepaymentScheduleRelatedDetail detail, final Integer installmentAmountInMultipliesOf) {
final ILoanConfigurationDetails detail, final Integer installmentAmountInMultipliesOf) {
return loanModelRepository.findOneByLoanId(loanId) //
.map(ProgressiveLoanModel::getJsonModel) //
.map(jsonModel -> progressiveLoanInterestScheduleModelParserService.fromJson(jsonModel, detail,
@@ -27,7 +27,7 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
@Data
@RequiredArgsConstructor
@@ -35,7 +35,7 @@ public class InterestScheduleModelServiceGsonContext {
private final MonetaryCurrency currency;
private final MathContext mc;
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
private final ILoanConfigurationDetails loanProductRelatedDetail;
private RepaymentPeriod prev = null;
private final Integer installmentAmountInMultipliesOf;
@@ -42,7 +42,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanConfigurationDetailsMapper;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@@ -81,8 +83,8 @@ public class InternalProgressiveLoanApiResource implements InitializingBean {
if (!loan.isProgressiveSchedule()) {
throw new IllegalArgumentException("The loan is not progressive.");
}
return writePlatformService.readProgressiveLoanInterestScheduleModel(loanId, loan.getLoanRepaymentScheduleDetail(),
ILoanConfigurationDetails loanConfigurationDetails = LoanConfigurationDetailsMapper.map(loan);
return writePlatformService.readProgressiveLoanInterestScheduleModel(loanId, loanConfigurationDetails,
loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf()).orElse(null);
}
@@ -20,7 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.math.MathContext;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
public interface ProgressiveLoanInterestScheduleModelParserService {
@@ -32,6 +32,6 @@ public interface ProgressiveLoanInterestScheduleModelParserService {
/**
* Restore a ProgressiveLoanInterestScheduleModel from a JSON string.
*/
ProgressiveLoanInterestScheduleModel fromJson(String s, LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
MathContext mc, Integer installmentAmountInMultipliesOf);
ProgressiveLoanInterestScheduleModel fromJson(String s, ILoanConfigurationDetails loanProductRelatedDetail, MathContext mc,
Integer installmentAmountInMultipliesOf);
}
@@ -35,7 +35,7 @@ import org.apache.fineract.organisation.monetary.serialization.MoneySerializer;
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.springframework.lang.NonNull;
@Slf4j
@@ -51,7 +51,7 @@ public class ProgressiveLoanInterestScheduleModelParserServiceGsonImpl implement
.addSerializationExclusionStrategy(new JsonExcludeAnnotationBasedExclusionStrategy()).create();
}
private Gson createDeserializer(LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, MathContext mc,
private Gson createDeserializer(ILoanConfigurationDetails loanProductRelatedDetail, MathContext mc,
Integer installmentAmountInMultipliesOf) {
InterestScheduleModelServiceGsonContext ctx = new InterestScheduleModelServiceGsonContext(
new MonetaryCurrency(loanProductRelatedDetail.getCurrencyData()), mc, loanProductRelatedDetail,
@@ -73,9 +73,8 @@ public class ProgressiveLoanInterestScheduleModelParserServiceGsonImpl implement
}
@Override
public ProgressiveLoanInterestScheduleModel fromJson(String s,
@NonNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, @NonNull MathContext mc,
Integer installmentAmountInMultipliesOf) {
public ProgressiveLoanInterestScheduleModel fromJson(String s, @NonNull ILoanConfigurationDetails loanProductRelatedDetail,
@NonNull MathContext mc, Integer installmentAmountInMultipliesOf) {
if (s == null) {
return null;
}
@@ -27,15 +27,13 @@ import java.util.Optional;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
public interface EMICalculator {
@@ -45,8 +43,8 @@ public interface EMICalculator {
*/
@NotNull
ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
@NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
List<LoanTermVariationsData> loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc);
@NotNull ILoanConfigurationDetails loanProductRelatedDetail, List<LoanTermVariationsData> loanTermVariations,
Integer installmentAmountInMultiplesOf, MathContext mc);
/**
* This method creates an Interest model with repayment periods from the installments which retrieved from the
@@ -54,8 +52,7 @@ public interface EMICalculator {
*/
@NotNull
ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel(
@NotNull List<LoanRepaymentScheduleInstallment> installments,
@NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
@NotNull List<LoanRepaymentScheduleInstallment> installments, @NotNull ILoanConfigurationDetails loanProductRelatedDetail,
List<LoanTermVariationsData> loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc);
/**
@@ -144,10 +141,10 @@ public interface EMICalculator {
*/
void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate);
void updateModelRepaymentPeriodsDuringReAge(ProgressiveTransactionCtx ctx, LoanTransaction loanTransaction,
LoanApplicationTerms loanApplicationTerms, MathContext mc);
void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel ctx, LocalDate loanTransaction,
LocalDate reageFirstDueDate, LocalDate transactionDate, LoanApplicationTerms loanApplicationTerms, MathContext mc);
boolean recalculateModelOverdueAmountsTillDate(ProgressiveTransactionCtx ctx, LocalDate targetDate);
boolean recalculateModelOverdueAmountsTillDate(ProgressiveLoanInterestScheduleModel ctx, LocalDate targetDate, boolean prepayAttempt);
/**
* Gives back the sum of the outstanding interest from the whole model till the provided date.
@@ -39,21 +39,14 @@ import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
@@ -64,7 +57,7 @@ import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@@ -80,8 +73,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
@NotNull
public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
@NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
List<LoanTermVariationsData> loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
@NotNull ILoanConfigurationDetails loanProductRelatedDetail, List<LoanTermVariationsData> loanTermVariations,
final Integer installmentAmountInMultiplesOf, final MathContext mc) {
return generateInterestScheduleModel(periods, LoanScheduleModelRepaymentPeriod::periodFromDate,
LoanScheduleModelRepaymentPeriod::periodDueDate, loanProductRelatedDetail, loanTermVariations,
installmentAmountInMultiplesOf, mc);
@@ -90,8 +83,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
@NotNull
public ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel(
@NotNull List<LoanRepaymentScheduleInstallment> installments,
@NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
@NotNull List<LoanRepaymentScheduleInstallment> installments, @NotNull ILoanConfigurationDetails loanProductRelatedDetail,
List<LoanTermVariationsData> loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
installments = installments.stream().filter(installment -> !installment.isDownPayment() && !installment.isAdditional()).toList();
return generateInterestScheduleModel(installments, LoanRepaymentScheduleInstallment::getFromDate,
@@ -101,7 +93,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@NotNull
private <T> ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(@NotNull List<T> periods, Function<T, LocalDate> from,
Function<T, LocalDate> to, @NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
Function<T, LocalDate> to, @NotNull ILoanConfigurationDetails loanProductRelatedDetail,
List<LoanTermVariationsData> loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
@@ -218,7 +210,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
public <T> void updateModel(ProgressiveLoanInterestScheduleModel scheduleModel, List<T> updateExpectedRepaymentPeriods,
Function<T, LocalDate> from, Function<T, LocalDate> to) {
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
final ILoanConfigurationDetails loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
MathContext mc = scheduleModel.mc();
final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
@@ -575,36 +567,34 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
@Override
public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveTransactionCtx ctx, final LoanTransaction loanTransaction,
public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate reAgePeriodStartDate, LocalDate reageFirstDueDate, LocalDate targetDate,
final LoanApplicationTerms loanApplicationTerms, final MathContext mc) {
final List<RepaymentPeriod> existingRepaymentPeriods = ctx.getModel().repaymentPeriods();
moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, loanTransaction.getLoanReAgeParameter().getStartDate());
final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanTransaction);
moveOutstandingAmountsFromPeriodsBeforeReAging(scheduleModel.repaymentPeriods(), reageFirstDueDate);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryReAgedScheduleModel(loanApplicationTerms,
mc, periodStartDate);
mc, reAgePeriodStartDate);
mergeNewInterestScheduleModelWithExistingOne(ctx, temporaryReAgedScheduleModel, loanTransaction);
mergeNewInterestScheduleModelWithExistingOne(scheduleModel, temporaryReAgedScheduleModel, reageFirstDueDate, targetDate);
}
@Override
public boolean recalculateModelOverdueAmountsTillDate(final ProgressiveTransactionCtx ctx, final LocalDate targetDate) {
public boolean recalculateModelOverdueAmountsTillDate(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate targetDate, boolean prepayAttempt) {
boolean hasChange = false;
final ProgressiveLoanInterestScheduleModel model = ctx.getModel();
final List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate, model);
final List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate,
scheduleModel);
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
final RepaymentPeriod lastPeriod = model.getLastRepaymentPeriod();
final RepaymentPeriod currentPeriod = model.findRepaymentPeriod(targetDate).orElse(lastPeriod);
final MonetaryCurrency currency = model.zero().getCurrency();
Money overDuePrincipal = Money.zero(currency);
Money aggregatedOverDuePrincipal = Money.zero(currency);
final RepaymentPeriod lastPeriod = scheduleModel.getLastRepaymentPeriod();
final RepaymentPeriod currentPeriod = scheduleModel.findRepaymentPeriod(targetDate).orElse(lastPeriod);
Money overDuePrincipal = scheduleModel.zero();
Money aggregatedOverDuePrincipal = scheduleModel.zero();
for (RepaymentPeriod processingPeriod : overdueInstallmentsSortedByInstallmentNumber) {
// add and subtract outstanding principal
if (!overDuePrincipal.isZero()) {
final boolean currentChanges = adjustOverduePrincipal(targetDate, processingPeriod, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
aggregatedOverDuePrincipal, scheduleModel, prepayAttempt);
hasChange = hasChange || currentChanges;
}
@@ -615,13 +605,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
if (!currentPeriod.equals(lastPeriod) || !targetDate.isAfter(lastPeriod.getDueDate())) {
final boolean currentChanges = adjustOverduePrincipal(targetDate, currentPeriod, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
aggregatedOverDuePrincipal, scheduleModel, prepayAttempt);
hasChange = hasChange || currentChanges;
}
if (aggregatedOverDuePrincipal.isGreaterThanZero()
&& (model.lastOverdueBalanceChange() == null || model.lastOverdueBalanceChange().isBefore(targetDate))) {
model.lastOverdueBalanceChange(targetDate);
if (aggregatedOverDuePrincipal.isGreaterThanZero() && (scheduleModel.lastOverdueBalanceChange() == null
|| scheduleModel.lastOverdueBalanceChange().isBefore(targetDate))) {
scheduleModel.lastOverdueBalanceChange(targetDate);
}
}
@@ -644,13 +634,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
private boolean adjustOverduePrincipal(final LocalDate currentDate, final RepaymentPeriod currentInstallment,
final Money overduePrincipal, final Money aggregatedOverDuePrincipal, final ProgressiveTransactionCtx ctx) {
final Money overduePrincipal, final Money aggregatedOverDuePrincipal, final ProgressiveLoanInterestScheduleModel model,
boolean prepayAttempt) {
final LocalDate fromDate = currentInstallment.getFromDate();
final LocalDate toDate = currentInstallment.getDueDate();
final ProgressiveLoanInterestScheduleModel model = ctx.getModel();
final boolean prepayAttempt = ctx.isPrepayAttempt();
final LoanInterestRecalculationDetails loanInterestRecalculationDetails = ctx.getInstallments().getFirst().getLoan()
.getLoanInterestRecalculationDetails();
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
@@ -661,7 +648,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
LocalDate lastOverdueBalanceChange;
if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) {
if (shouldRecalculateTillInstallmentDueDate(model.loanProductRelatedDetail(), prepayAttempt)) {
lastOverdueBalanceChange = toDate;
} else {
lastOverdueBalanceChange = currentDate;
@@ -674,7 +661,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return false;
}
private boolean shouldRecalculateTillInstallmentDueDate(final LoanInterestRecalculationDetails recalculationDetails,
private boolean shouldRecalculateTillInstallmentDueDate(final ILoanConfigurationDetails loanProductRelatedDetails,
final boolean isPrepayAttempt) {
// Rest frequency type and pre close interest calculation strategy can be controversial
// if restFrequencyType == DAILY and preCloseInterestCalculationStrategy == TILL_PRE_CLOSURE_DATE
@@ -686,11 +673,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
// or restFrequencyType == SAME_AS_REPAYMENT_PERIOD and preCloseInterestCalculationStrategy ==
// TILL_PRE_CLOSURE_DATE
// we cannot harmonize the two configs. Behaviour should mimic prepay api.
return switch (recalculationDetails.getRestFrequencyType()) {
return switch (loanProductRelatedDetails.getRestFrequencyType()) {
case DAILY ->
isPrepayAttempt && recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
isPrepayAttempt && loanProductRelatedDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
case SAME_AS_REPAYMENT_PERIOD ->
recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
loanProductRelatedDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
case WEEKLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WEEKLY");
case MONTHLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: MONTHLY");
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
@@ -702,11 +689,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* the balances of the updated model and also recalculate the EMI if the EMI of the last repayment period differs
* significantly from other periods.
*/
private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveTransactionCtx ctx,
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel, final LoanTransaction loanTransaction) {
private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveLoanInterestScheduleModel scheduleModel,
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel, final LocalDate reageFirstDueDate,
LocalDate targetDate) {
final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods();
final ProgressiveLoanInterestScheduleModel scheduleModel = ctx.getModel();
final Loan loan = loanTransaction.getLoan();
if (newPeriods.isEmpty()) {
return;
@@ -716,10 +702,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final RepaymentPeriod firstRepaymentPeriod = existingRepaymentPeriods.getFirst();
final Money disbursedAmount = firstRepaymentPeriod.getTotalDisbursedAmount();
final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate();
final Optional<RepaymentPeriod> firstExistingReAgedRepaymentPeriodOpt = existingRepaymentPeriods.stream()
.filter(period -> period.getDueDate().equals(reAgingStartDate)).findFirst();
.filter(period -> period.getDueDate().equals(reageFirstDueDate)).findFirst();
for (final RepaymentPeriod newPeriod : newPeriods) {
final Optional<RepaymentPeriod> existingRepaymentPeriodOpt = existingRepaymentPeriods.stream().filter(
@@ -736,34 +720,18 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final RepaymentPeriod rp = RepaymentPeriod.create(
previousExistingRepaymentPeriodOpt.orElseGet(existingRepaymentPeriods::getLast), newPeriod.getFromDate(),
newPeriod.getDueDate(), newPrincipal.add(newInterest), MoneyHelper.getMathContext(),
loanTransaction.getLoan().getLoanProductRelatedDetail());
newPeriod.getDueDate(), newPrincipal.add(newInterest), scheduleModel.mc(), scheduleModel.loanProductRelatedDetail());
rp.setTotalDisbursedAmount(disbursedAmount);
if (existingRepaymentPeriodOpt.isPresent()) {
// Add an interest period on the disbursement date for the very first replaced period
if (existingRepaymentPeriodOpt.get().equals(firstRepaymentPeriod)) {
final LocalDate disbursementDate = loanTransaction.getLoan().getDisbursementDate();
rp.getInterestPeriods().getFirst().setDisbursementAmount(disbursedAmount);
rp.getInterestPeriods().getFirst().setFromDate(disbursementDate);
rp.getInterestPeriods().getFirst().setDueDate(disbursementDate);
rp.getInterestPeriods().add(InterestPeriod.withEmptyAmounts(rp, rp.getFromDate(), rp.getDueDate()));
}
existingRepaymentPeriods.remove(existingRepaymentPeriodOpt.get());
}
existingRepaymentPeriodOpt.ifPresent(existingRepaymentPeriods::remove);
existingRepaymentPeriods.add(rp);
calculateRateFactorForRepaymentPeriod(rp, scheduleModel);
}
if (reAgingStartDate.isBefore(DateUtils.getBusinessLocalDate()) && isInterestRecalculationIsAllowed(ctx, loan)) {
scheduleModel.lastOverdueBalanceChange(null);
recalculateModelOverdueAmountsTillDate(ctx, loanTransaction.getSubmittedOnDate());
}
final List<RepaymentPeriod> reAgedRepaymentPeriods = existingRepaymentPeriods.stream()
.filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reAgingStartDate)
|| repaymentPeriod.getDueDate().isEqual(reAgingStartDate))
.filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reageFirstDueDate)
|| repaymentPeriod.getDueDate().isEqual(reageFirstDueDate))
&& !repaymentPeriod.getDueDate().isAfter(newPeriods.getLast().getDueDate()))
.toList();
@@ -775,16 +743,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, loanTransaction.getTransactionDate());
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, targetDate);
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, reAgedRepaymentPeriods);
}
private boolean isInterestRecalculationIsAllowed(final ProgressiveTransactionCtx ctx, final Loan loan) {
return loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff() && !ctx.isWrittenOff()
&& !ctx.isContractTerminated() && !loan.isNpa()
&& !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue();
}
/**
* * Generates temporary interestScheduleModel with re-aged repayment periods
*/
@@ -794,36 +756,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
periodStartDate, loanApplicationTerms, null);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetailMinimumData(), null,
expectedRepaymentPeriods, loanApplicationTerms.toLoanConfigurationDetails(), null,
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
addDisbursement(temporaryReAgedScheduleModel, EmiChangeOperation.disburse(periodStartDate, loanApplicationTerms.getPrincipal()));
return temporaryReAgedScheduleModel;
}
/**
* * Based on the re-aging start date and frequency data calculates start date for the first re-aged period, which
* is used to generate re-aged repayment periods
*/
@NotNull
private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanTransaction loanTransaction) {
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
if (reAgingStartDate.isEqual(loanTransaction.getLoan().getDisbursementDate())) {
return reAgingStartDate;
}
return switch (loanReAgeParameter.getFrequencyType()) {
case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber());
case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber());
case MONTHS -> reAgingStartDate.minusMonths(loanReAgeParameter.getFrequencyNumber());
case YEARS -> reAgingStartDate.minusYears(loanReAgeParameter.getFrequencyNumber());
case WHOLE_TERM -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WHOLE_TERM");
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
};
}
/**
* * Zeroing out the EMI of the repayment periods, that are before re-aging and not been fully paid. And decreases
* the balance correction amount (added during interest recalculation for the business date) by the amount of the
@@ -1008,7 +947,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
BigDecimal calculateRateFactorPerPeriodForInterest(final ProgressiveLoanInterestScheduleModel scheduleModel,
final RepaymentPeriod repaymentPeriod, final LocalDate interestPeriodFromDate, final LocalDate interestPeriodDueDate) {
final MathContext mc = scheduleModel.mc();
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
final ILoanConfigurationDetails loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
final BigDecimal interestRate = calcNominalInterestRatePercentage(scheduleModel.getInterestRate(interestPeriodFromDate),
scheduleModel.mc());
final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
@@ -1136,7 +1075,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
private BigDecimal calculateRateFactorPerPeriod(final ProgressiveLoanInterestScheduleModel scheduleModel,
final RepaymentPeriod repaymentPeriod, final LocalDate interestPeriodFromDate, final LocalDate interestPeriodDueDate) {
final MathContext mc = scheduleModel.mc();
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
final ILoanConfigurationDetails loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
final BigDecimal interestRate = calcNominalInterestRatePercentage(scheduleModel.getInterestRate(interestPeriodFromDate),
scheduleModel.mc());
final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
@@ -20,5 +20,6 @@ package org.apache.fineract.portfolio.loanproduct.calc.data;
public enum LoanInterestScheduleModelModifiers { //
EMI_RECALCULATION, //
COPY //
COPY, //
INTEREST_RECALCULATION_ENABLED //
}
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanproduct.calc.data;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
import static org.apache.fineract.portfolio.loanproduct.calc.data.LoanInterestScheduleModelModifiers.COPY;
import static org.apache.fineract.portfolio.loanproduct.calc.data.LoanInterestScheduleModelModifiers.EMI_RECALCULATION;
import static org.apache.fineract.portfolio.loanproduct.calc.data.LoanInterestScheduleModelModifiers.INTEREST_RECALCULATION_ENABLED;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
@@ -50,7 +51,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
@Data
@Accessors(fluent = true)
@@ -60,7 +61,7 @@ public class ProgressiveLoanInterestScheduleModel {
private final List<RepaymentPeriod> repaymentPeriods;
private final TreeSet<InterestRate> interestRates;
@JsonExclude
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
private final ILoanConfigurationDetails loanProductRelatedDetail;
private final Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariations;
private final Integer installmentAmountInMultiplesOf;
@JsonExclude
@@ -73,8 +74,8 @@ public class ProgressiveLoanInterestScheduleModel {
private LocalDate lastOverdueBalanceChange;
public ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods,
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
final List<LoanTermVariationsData> loanTermVariations, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
final ILoanConfigurationDetails loanProductRelatedDetail, final List<LoanTermVariationsData> loanTermVariations,
final Integer installmentAmountInMultiplesOf, final MathContext mc) {
this.repaymentPeriods = new ArrayList<>(repaymentPeriods);
this.interestRates = new TreeSet<>(Collections.reverseOrder());
this.loanProductRelatedDetail = loanProductRelatedDetail;
@@ -82,11 +83,12 @@ public class ProgressiveLoanInterestScheduleModel {
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
this.mc = mc;
this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
modifiers = new HashMap<>(Map.of(EMI_RECALCULATION, true, COPY, false));
modifiers = new HashMap<>(Map.of(EMI_RECALCULATION, true, COPY, false, INTEREST_RECALCULATION_ENABLED,
loanProductRelatedDetail.isInterestRecalculationEnabled()));
}
private ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods, final TreeSet<InterestRate> interestRates,
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
final ILoanConfigurationDetails loanProductRelatedDetail,
final Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariations, final Integer installmentAmountInMultiplesOf,
final MathContext mc, final boolean isCopiedForCalculation) {
this.mc = mc;
@@ -97,7 +99,8 @@ public class ProgressiveLoanInterestScheduleModel {
this.loanTermVariations = loanTermVariations;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
modifiers = new HashMap<>(Map.of(EMI_RECALCULATION, true, COPY, isCopiedForCalculation));
modifiers = new HashMap<>(Map.of(EMI_RECALCULATION, true, COPY, isCopiedForCalculation, INTEREST_RECALCULATION_ENABLED,
loanProductRelatedDetail.isInterestRecalculationEnabled()));
}
public ProgressiveLoanInterestScheduleModel deepCopy(MathContext mc) {
@@ -440,4 +443,8 @@ public class ProgressiveLoanInterestScheduleModel {
default -> throw new UnsupportedOperationException();
};
}
public boolean isInterestRecalculationIsAllowed() {
return modifiers.get(INTEREST_RECALCULATION_ENABLED);
}
}
@@ -35,7 +35,7 @@ import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.apache.fineract.portfolio.util.Memo;
@ToString(exclude = { "previous" })
@@ -84,13 +84,13 @@ public class RepaymentPeriod {
private Money totalCapitalizedIncomeAmount;
@JsonExclude
@Getter
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
private final ILoanConfigurationDetails loanProductRelatedDetail;
@JsonExclude
private MonetaryCurrency currency;
protected RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods,
Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc,
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) {
ILoanConfigurationDetails loanProductRelatedDetail) {
this.previous = previous;
this.fromDate = fromDate;
this.dueDate = dueDate;
@@ -104,13 +104,12 @@ public class RepaymentPeriod {
this.loanProductRelatedDetail = loanProductRelatedDetail;
}
public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc,
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) {
public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc, ILoanConfigurationDetails loanProductRelatedDetail) {
return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, null, mc, loanProductRelatedDetail);
}
public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc,
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) {
ILoanConfigurationDetails loanProductRelatedDetail) {
final Money zero = emi.zero();
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero,
zero, mc, loanProductRelatedDetail);
@@ -47,9 +47,9 @@ import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
@@ -72,8 +72,7 @@ class ProgressiveEMICalculatorTest {
private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class);
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN);
private static LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = Mockito
.mock(LoanProductMinimumRepaymentScheduleRelatedDetail.class);
private static ILoanConfigurationDetails loanProductRelatedDetail = Mockito.mock(ILoanConfigurationDetails.class);
private static final CurrencyData currency = new CurrencyData("USD", "USD", 2, 1, "$", "USD");
@@ -32,9 +32,9 @@ import java.util.List;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -86,8 +86,7 @@ class InterestPeriodTest {
InterestPeriod period = InterestPeriod.empty(repaymentPeriod, MC);
when(repaymentPeriod.getInterestPeriods()).thenReturn(List.of(period));
when(repaymentPeriod.getFirstInterestPeriod()).thenReturn(period);
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = mock(
LoanProductMinimumRepaymentScheduleRelatedDetail.class);
ILoanConfigurationDetails loanProductRelatedDetail = mock(ILoanConfigurationDetails.class);
when(loanProductRelatedDetail.getCurrencyData()).thenReturn(USD);
when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE);
when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY);
@@ -31,9 +31,9 @@ import java.time.ZoneId;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -47,7 +47,7 @@ class RepaymentPeriodTest {
private static final Money ZERO = Money.of(USD, BigDecimal.ZERO, MC);
private static MockedStatic<MoneyHelper> moneyHelper;
private static LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
private static ILoanConfigurationDetails loanProductRelatedDetail;
@BeforeAll
static void init() {
@@ -55,7 +55,7 @@ class RepaymentPeriodTest {
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(MC);
loanProductRelatedDetail = mock(LoanProductMinimumRepaymentScheduleRelatedDetail.class);
loanProductRelatedDetail = mock(ILoanConfigurationDetails.class);
when(loanProductRelatedDetail.getCurrencyData()).thenReturn(USD);
when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE);
when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY);