FINERACT-1981: Progressive Loan schedule handling - Reschedule installment (Extend repayment period)

This commit is contained in:
Soma Sörös
2025-08-21 10:22:07 +02:00
committed by Adam Saghy
parent 47b55b5ee6
commit 14589e2600
10 changed files with 752 additions and 95 deletions
@@ -53,6 +53,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@@ -234,9 +235,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanTransaction> overpaidTransactions = new ArrayList<>();
for (final ChangeOperation changeOperation : changeOperations) {
if (changeOperation.isInterestRateChange()) {
final LoanTermVariationsData interestRateChange = changeOperation.getInterestRateChange().get();
processInterestRateChange(installments, interestRateChange, scheduleModel);
if (changeOperation.isLoanTermVariationsData()) {
final LoanTermVariationsData interestRateChange = changeOperation.getLoanTermVariationsData().get();
processLoanTermVariation(installments, interestRateChange, scheduleModel);
} else if (changeOperation.isTransaction()) {
LoanTransaction transaction = changeOperation.getLoanTransaction().get();
processSingleTransaction(transaction, ctx);
@@ -301,16 +302,55 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.map(TransactionChangeData::getNewTransaction).findFirst().orElse(transaction);
}
private void processInterestRateChange(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
if (interestRateChange.getTermVariationType().isInterestPauseVariation()) {
final LocalDate pauseEndDate = interestRateChange.getDateValue();
emiCalculator.applyInterestPause(scheduleModel, interestRateChangeSubmittedOnDate, pauseEndDate);
} else {
emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate);
private void processLoanTermVariation(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
switch (termVariationsData.getTermVariationType()) {
case INTEREST_PAUSE -> handleInterestPause(installments, termVariationsData, scheduleModel);
case INTEREST_RATE_FROM_INSTALLMENT -> handleChangeInterestRate(installments, termVariationsData, scheduleModel);
case EXTEND_REPAYMENT_PERIOD -> handleExtraRepaymentPeriod(installments, termVariationsData, scheduleModel);
default -> throw new IllegalStateException("Unhandled LoanTermVariationType.");
}
}
private void handleExtraRepaymentPeriod(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = termVariationsData.getTermVariationApplicableFrom();
final int repaymentPeriodsToAdd = termVariationsData.getDecimalValue().intValue();
emiCalculator.addRepaymentPeriods(scheduleModel, interestRateChangeSubmittedOnDate, repaymentPeriodsToAdd);
final Loan loan = installments.getFirst().getLoan();
int nextInstallmentNumber = installments.stream().mapToInt(LoanRepaymentScheduleInstallment::getInstallmentNumber).max().orElse(0)
+ 1;
for (int i = 0; i < scheduleModel.repaymentPeriods().size(); i++) {
final RepaymentPeriod rp = scheduleModel.repaymentPeriods().get(i);
// Check if this period already exists in installments
if (installments.stream().noneMatch(installment -> installment.getFromDate().equals(rp.getFromDate())
&& installment.getDueDate().equals(rp.getDueDate()))) {
final LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(loan, nextInstallmentNumber,
rp.getFromDate(), rp.getDueDate(), rp.getDuePrincipal().getAmount(), rp.getDueInterest().getAmount(), ZERO, ZERO,
false, null, ZERO);
installments.add(newInstallment);
nextInstallmentNumber++;
}
}
processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments);
}
private void handleChangeInterestRate(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = termVariationsData.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = termVariationsData.getDecimalValue();
emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate);
processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments);
}
private void handleInterestPause(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = termVariationsData.getTermVariationApplicableFrom();
final LocalDate pauseEndDate = termVariationsData.getDateValue();
emiCalculator.applyInterestPause(scheduleModel, interestRateChangeSubmittedOnDate, pauseEndDate);
processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments);
}
@@ -1204,14 +1244,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<ChangeOperation> changeOperations = new ArrayList<>();
Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariationsMap = loanTermVariations.stream()
.collect(Collectors.groupingBy(ltvd -> LoanTermVariationType.fromInt(ltvd.getTermType().getId().intValue())));
if (loanTermVariationsMap.get(LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT) != null) {
changeOperations.addAll(loanTermVariationsMap.get(LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT).stream()
.map(ChangeOperation::new).toList());
}
if (loanTermVariationsMap.get(LoanTermVariationType.INTEREST_PAUSE) != null) {
changeOperations
.addAll(loanTermVariationsMap.get(LoanTermVariationType.INTEREST_PAUSE).stream().map(ChangeOperation::new).toList());
}
Stream.of(LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT, LoanTermVariationType.INTEREST_PAUSE,
LoanTermVariationType.EXTEND_REPAYMENT_PERIOD).forEach(key -> {
if (loanTermVariationsMap.get(key) != null) {
changeOperations.addAll(loanTermVariationsMap.get(key).stream().map(ChangeOperation::new).toList());
}
});
if (charges != null) {
changeOperations.addAll(charges.stream().map(ChangeOperation::new).toList());
}
@@ -32,30 +32,30 @@ import org.springframework.lang.NonNull;
@Getter
public class ChangeOperation implements Comparable<ChangeOperation> {
private final Optional<LoanTermVariationsData> interestRateChange;
private final Optional<LoanTermVariationsData> loanTermVariationsData;
private final Optional<LoanCharge> loanCharge;
private final Optional<LoanTransaction> loanTransaction;
public ChangeOperation(LoanCharge loanCharge) {
this.interestRateChange = Optional.empty();
this.loanTermVariationsData = Optional.empty();
this.loanCharge = Optional.of(loanCharge);
this.loanTransaction = Optional.empty();
}
public ChangeOperation(LoanTransaction loanTransaction) {
this.interestRateChange = Optional.empty();
this.loanTermVariationsData = Optional.empty();
this.loanTransaction = Optional.of(loanTransaction);
this.loanCharge = Optional.empty();
}
public ChangeOperation(LoanTermVariationsData interestRateChange) {
this.interestRateChange = Optional.of(interestRateChange);
public ChangeOperation(LoanTermVariationsData loanTermVariationsData) {
this.loanTermVariationsData = Optional.of(loanTermVariationsData);
this.loanTransaction = Optional.empty();
this.loanCharge = Optional.empty();
}
public boolean isInterestRateChange() {
return interestRateChange.isPresent();
public boolean isLoanTermVariationsData() {
return loanTermVariationsData.isPresent();
}
public boolean isTransaction() {
@@ -80,7 +80,7 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
}
private LocalDate getEffectiveDate() {
if (interestRateChange.isPresent()) {
if (loanTermVariationsData.isPresent()) {
return getSubmittedOnDate();
} else if (loanCharge.isPresent()) {
if (isBackdatedCharge()) {
@@ -98,8 +98,8 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
}
private LocalDate getSubmittedOnDate() {
if (interestRateChange.isPresent()) {
return interestRateChange.get().getTermVariationApplicableFrom();
if (loanTermVariationsData.isPresent()) {
return loanTermVariationsData.get().getTermVariationApplicableFrom();
} else if (loanCharge.isPresent()) {
return loanCharge.get().getSubmittedOnDate();
} else if (loanTransaction.isPresent()) {
@@ -110,7 +110,7 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
}
private OffsetDateTime getCreatedDateTime() {
if (interestRateChange.isPresent()) {
if (loanTermVariationsData.isPresent()) {
return DateUtils.getOffsetDateTimeOfTenantFromLocalDate(getSubmittedOnDate());
} else if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) {
return loanCharge.get().getCreatedDate().get();
@@ -38,6 +38,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -78,42 +79,73 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder, loan);
BigDecimal interestRate = validateInterestRateParam(fromJsonHelper, jsonElement, dataValidatorBuilder, loan);
Integer extraTerms = validateExtraTermsParam(fromJsonHelper, jsonElement, dataValidatorBuilder, loan);
validateUnsupportedParams(jsonElement, dataValidatorBuilder);
boolean hasInterestRateChange = interestRate != null;
boolean hasAdjustDueDateChange = adjustedDueDate != null;
if (hasInterestRateChange) {
validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder);
} else {
validateLoanIsActive(loan, dataValidatorBuilder);
}
boolean hasExtraTermsChange = extraTerms != null;
if (hasInterestRateChange && hasAdjustDueDateChange) {
if (Stream.of(hasInterestRateChange, hasAdjustDueDateChange, hasExtraTermsChange).filter(f -> f).count() > 1) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.adjustedDueDateParamName).failWithCode(
RescheduleLoansApiConstants.rescheduleMultipleOperationsNotSupportedErrorCode,
"Only one operation is supported at a time during Loan Rescheduling");
}
if (rescheduleFromDate != null && hasInterestRateChange) {
validateInterestRateChangeRescheduleFromDate(loan, rescheduleFromDate);
if (hasExtraTermsChange) {
validateExtraTerms(dataValidatorBuilder, loan);
} else if (hasAdjustDueDateChange) {
validateAdjustDueDateChange(dataValidatorBuilder, loan, rescheduleFromDate);
} else if (hasInterestRateChange) {
validateInterestRate(dataValidatorBuilder, loan, rescheduleFromDate);
}
LoanRepaymentScheduleInstallment installment;
if (hasInterestRateChange) {
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
} else {
installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate);
}
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
private void validateAdjustDueDateChange(DataValidatorBuilder dataValidatorBuilder, Loan loan, LocalDate rescheduleFromDate) {
validateLoanIsActive(loan, dataValidatorBuilder);
LoanRepaymentScheduleInstallment installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate);
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
}
private void validateInterestRate(DataValidatorBuilder dataValidatorBuilder, Loan loan, LocalDate rescheduleFromDate) {
validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder);
if (rescheduleFromDate != null) {
validateInterestRateChangeRescheduleFromDate(loan, rescheduleFromDate);
}
LoanRepaymentScheduleInstallment installment;
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
}
private void validateExtraTerms(DataValidatorBuilder dataValidatorBuilder, Loan loan) {
validateLoanIsActive(loan, dataValidatorBuilder);
}
private Integer validateExtraTermsParam(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder, Loan loan) {
final Integer extraTerms = fromJsonHelper.extractIntegerWithLocaleNamed(RescheduleLoansApiConstants.extraTermsParamName,
jsonElement);
DataValidatorBuilder extraTermsDataValidator = dataValidatorBuilder.reset()
.parameter(RescheduleLoansApiConstants.extraTermsParamName).value(extraTerms).ignoreIfNull().integerGreaterThanZero();
if (extraTerms != null) {
Integer maxNumberOfRepayments = loan.getLoanProduct().getMaxNumberOfRepayments();
if (maxNumberOfRepayments != null) {
Integer numberOfRepayments = loan.getLoanProductRelatedDetail().getNumberOfRepayments();
extraTermsDataValidator.notGreaterThanMax(maxNumberOfRepayments - numberOfRepayments);
}
}
return extraTerms;
}
@Override
public void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBuilder, LoanRepaymentScheduleInstallment installment) {
if (installment == null) {
@@ -125,7 +157,6 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
private void validateUnsupportedParams(JsonElement jsonElement, DataValidatorBuilder dataValidatorBuilder) {
final var unsupportedFields = List.of(RescheduleLoansApiConstants.graceOnPrincipalParamName, //
RescheduleLoansApiConstants.graceOnInterestParamName, //
RescheduleLoansApiConstants.extraTermsParamName, //
RescheduleLoansApiConstants.emiParamName//
);
@@ -152,6 +183,7 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
final Loan loan = loanRescheduleRequest.getLoan();
LoanRepaymentScheduleInstallment installment;
boolean hasExtraTerms = false;
boolean hasInterestRateChange = false;
for (LoanRescheduleRequestToTermVariationMapping mapping : loanRescheduleRequest
.getLoanRescheduleRequestToTermVariationMappings()) {
@@ -159,6 +191,9 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
if (termType.isInterestRateVariation() || termType.isInterestRateFromInstallment()) {
hasInterestRateChange = true;
}
if (termType.isExtendRepaymentPeriod()) {
hasExtraTerms = true;
}
}
if (hasInterestRateChange) {
validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder);
@@ -166,13 +201,15 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
validateLoanIsActive(loan, dataValidatorBuilder);
}
if (loanRescheduleRequest.getInterestRateFromInstallmentTermVariationIfExists() != null) {
if (loanRescheduleRequest.getInterestRateFromInstallmentTermVariationIfExists() != null || hasExtraTerms) {
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
} else {
installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate);
}
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
if (!hasExtraTerms) {
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
@@ -198,7 +235,7 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
}
}
private BigDecimal validateInterestRate(final FromJsonHelper fromJsonHelper, final JsonElement jsonElement,
private BigDecimal validateInterestRateParam(final FromJsonHelper fromJsonHelper, final JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder, Loan loan) {
final BigDecimal interestRate = fromJsonHelper
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement);
@@ -78,6 +78,9 @@ public interface EMICalculator {
void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestSubmittedOnDate,
BigDecimal newInterestRate);
void addRepaymentPeriods(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate submittedOnDate,
int numberOfRepaymentPeriodsToAdd);
/**
* This method applies outstanding balance correction on the interest model. Negative amount decreases the
* outstanding balance while positive amounts are increasing that. Typically used for late repayment or to count
@@ -26,13 +26,16 @@ import java.time.LocalDate;
import java.time.Year;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
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;
@@ -178,6 +181,58 @@ public final class ProgressiveEMICalculator implements EMICalculator {
changeInterestRate(scheduleModel, EmiChangeOperation.changeInterestRate(newInterestSubmittedOnDate, newInterestRate));
}
@Override
public void addRepaymentPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate submittedOnDate,
final int numberOfRepaymentPeriodsToAdd) {
addRepaymentPeriods(scheduleModel,
EmiChangeOperation.addRepaymentPeriods(submittedOnDate, scheduleModel.zero(), numberOfRepaymentPeriodsToAdd));
}
public void addRepaymentPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
LocalDate disbursementDate = scheduleModel.getStartDate();
int repaymentPeriodCount = scheduleModel.repaymentPeriods().size();
final LocalDate interestRateChangeEffectiveDate = operation.getSubmittedOnDate().minusDays(1);
List<LocalDateInterval> periods2 = generateAdditionalRepaymentPeriodDueDates(scheduleModel, operation.getPeriodsToAdd(),
repaymentPeriodCount, scheduleModel.resolveRepaymentPEriodLengthGeneratorFunction(disbursementDate));
updateModel(scheduleModel, periods2, LocalDateInterval::startDate, LocalDateInterval::endDate);
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.zero(),
scheduleModel.zero(), scheduleModel.zero())
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel,
operation));
}
public <T> void updateModel(ProgressiveLoanInterestScheduleModel scheduleModel, List<T> updateExpectedRepaymentPeriods,
Function<T, LocalDate> from, Function<T, LocalDate> to) {
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
MathContext mc = scheduleModel.mc();
final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
RepaymentPeriod originalLAstRepaymentPeriod = scheduleModel.getLastRepaymentPeriod();
prev.set(originalLAstRepaymentPeriod);
List<RepaymentPeriod> repaymentPeriods = updateExpectedRepaymentPeriods.stream().map(e -> {
RepaymentPeriod rp = RepaymentPeriod.create(prev.get(), from.apply(e), to.apply(e), zero, mc, loanProductRelatedDetail);
rp.setTotalDisbursedAmount(originalLAstRepaymentPeriod.getTotalDisbursedAmount());
rp.setTotalCapitalizedIncomeAmount(originalLAstRepaymentPeriod.getTotalCapitalizedIncomeAmount());
prev.set(rp);
return rp;
}).toList();
scheduleModel.repaymentPeriods().addAll(repaymentPeriods);
}
List<LocalDateInterval> generateAdditionalRepaymentPeriodDueDates(final ProgressiveLoanInterestScheduleModel scheduleModel,
final int periods, final int existingRepayments, Function<Long, LocalDate> repaymentPeriodLengthResolver) {
final List<LocalDateInterval> expectedRepaymentPeriods = new ArrayList<>();
Integer repayEvery = scheduleModel.loanProductRelatedDetail().getRepayEvery();
IntStream.range(existingRepayments, existingRepayments + periods).forEach(
i -> expectedRepaymentPeriods.add(LocalDateInterval.create(repaymentPeriodLengthResolver.apply((long) i * repayEvery),
repaymentPeriodLengthResolver.apply((long) (i + 1) * repayEvery))));
return expectedRepaymentPeriods;
}
private void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
final LocalDate interestRateChangeEffectiveDate = operation.getSubmittedOnDate().minusDays(1);
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, operation.getInterestRate());
@@ -486,7 +541,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
final boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty()
|| operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE || scheduleModel.isCopy();
|| operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE
|| operation.getAction() == EmiChangeOperation.Action.ADD_REPAYMENT_PERIODS || scheduleModel.isCopy();
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
calculateOutstandingBalance(scheduleModel);
@@ -932,13 +988,40 @@ public final class ProgressiveEMICalculator implements EMICalculator {
Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc);
Money periodEmiInMultiplesOf = applyInstallmentAmountInMultiplesOf(scheduleModel, periodEmi);
Money remainder = total.minus(periodEmiInMultiplesOf.multipliedBy(repaymentPeriods.size(), mc));
AtomicReference<Money> reallocationAmount = new AtomicReference<>(Money.zero(currency));
repaymentPeriods.forEach(rp -> {
Money emi = rp.equals(lastRepaymentPeriod) ? periodEmiInMultiplesOf.add(remainder) : periodEmiInMultiplesOf;
rp.setEmi(emi);
rp.setOriginalEmi(emi);
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
if (emi.plus(rp.getTotalCreditedAmount(), mc).plus(rp.getFutureUnrecognizedInterest(), mc)
.isGreaterThanOrEqualTo(rp.getTotalPaidAmount())) {
rp.setEmi(emi);
rp.setOriginalEmi(emi);
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
} else {
Money adjustment = rp.getTotalPaidAmount()
.minus(emi.plus(rp.getTotalCreditedAmount(), mc).plus(rp.getFutureUnrecognizedInterest(), mc));
reallocationAmount.set(reallocationAmount.get().add(adjustment));
rp.setEmi(rp.getTotalPaidAmount());
rp.setOriginalEmi(rp.getTotalPaidAmount());
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
}
});
if (reallocationAmount.get().isGreaterThanZero()) {
repaymentPeriods.reversed().forEach(rp -> {
if (reallocationAmount.get().isGreaterThanZero() && !rp.isFullyPaid()) {
Money minimumNewEmi = MathUtil.max(rp.getEmi().minus(reallocationAmount.get()), Money.zero(currency), true);
Money alreadyPaidEmi = rp.getTotalPaidAmount().minus(rp.getTotalCreditedAmount(), mc)
.minus(rp.getFutureUnrecognizedInterest(), mc);
Money newEmi = MathUtil.max(rp.getCalculatedDueInterest(), MathUtil.max(minimumNewEmi, alreadyPaidEmi, true), true);
reallocationAmount.set(reallocationAmount.get().minus(rp.getEmi().minus(newEmi)));
rp.setEmi(newEmi);
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
}
});
}
}
private void calculateEMIOnActualModel(List<RepaymentPeriod> repaymentPeriods, ProgressiveLoanInterestScheduleModel scheduleModel) {
@@ -33,7 +33,7 @@ public class EmiChangeOperation {
DISBURSEMENT, //
INTEREST_RATE_CHANGE, //
CAPITALIZED_INCOME, //
;
ADD_REPAYMENT_PERIODS, //
}
private final Action action;
@@ -42,22 +42,37 @@ public class EmiChangeOperation {
private final Money amount;
private final BigDecimal interestRate;
private final int periodsToAdd;
public static EmiChangeOperation disburse(final LocalDate disbursementDueDate, final Money disbursedAmount) {
return new EmiChangeOperation(EmiChangeOperation.Action.DISBURSEMENT, disbursementDueDate, disbursedAmount, null);
return new EmiChangeOperation(EmiChangeOperation.Action.DISBURSEMENT, disbursementDueDate, disbursedAmount, null, 0);
}
public static EmiChangeOperation changeInterestRate(final LocalDate newInterestSubmittedOnDate, final BigDecimal newInterestRate) {
return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate);
return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate, 0);
}
public static EmiChangeOperation capitalizedIncome(final LocalDate transactionDueDate, final Money transactionAmount) {
return new EmiChangeOperation(Action.CAPITALIZED_INCOME, transactionDueDate, transactionAmount, null);
return new EmiChangeOperation(Action.CAPITALIZED_INCOME, transactionDueDate, transactionAmount, null, 0);
}
public static EmiChangeOperation addRepaymentPeriods(final LocalDate transactionDueDate, final Money transactionAmount,
final int numPeriods) {
return new EmiChangeOperation(Action.ADD_REPAYMENT_PERIODS, transactionDueDate, transactionAmount, null, numPeriods);
}
public EmiChangeOperation withZeroAmount() {
if (action == Action.DISBURSEMENT || action == Action.CAPITALIZED_INCOME) {
return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null);
return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null, 0);
}
return null;
}
public boolean isAddRepaymentPeriods() {
return action == Action.ADD_REPAYMENT_PERIODS;
}
public boolean isInterestRateChange() {
return action == Action.INTEREST_RATE_CHANGE;
}
}
@@ -38,6 +38,7 @@ import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -430,4 +431,13 @@ public class ProgressiveLoanInterestScheduleModel {
public boolean isCopy() {
return this.modifiers.get(COPY);
}
public Function<Long, LocalDate> resolveRepaymentPEriodLengthGeneratorFunction(LocalDate instance) {
return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
case MONTHS -> instance::plusMonths;
case WEEKS -> instance::plusWeeks;
case DAYS -> instance::plusDays;
default -> throw new UnsupportedOperationException();
};
}
}