FINERACT-1981: Progressive Loan schedule handling - Reschedule installment (Extend repayment period)
This commit is contained in:
+60
-20
@@ -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());
|
||||
}
|
||||
|
||||
+11
-11
@@ -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();
|
||||
|
||||
+60
-23
@@ -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);
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
+87
-4
@@ -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) {
|
||||
|
||||
+20
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+219
@@ -3953,6 +3953,225 @@ class ProgressiveEMICalculatorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class RescheduleInstallmentExtendRepaymentPeriod {
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE);
|
||||
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(12.0));
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtraTermsDecliningBalance() {
|
||||
final LocalDate disbursementDate = LocalDate.of(2024, 6, 1);
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); //
|
||||
checkPeriod(interestSchedule, 1, 340.02, 6.70, 333.32, 336.66, false); //
|
||||
checkPeriod(interestSchedule, 2, 340.03, 3.37, 336.66, 0.00, false); //
|
||||
|
||||
// add extra repayment period 1
|
||||
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 7, 2), 1);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); //
|
||||
checkPeriod(interestSchedule, 1, 227.81, 6.70, 221.11, 448.87, false); //
|
||||
checkPeriod(interestSchedule, 2, 227.81, 4.49, 223.32, 225.55, false); //
|
||||
checkPeriod(interestSchedule, 3, 227.81, 2.26, 225.55, 0.00, false); //
|
||||
|
||||
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 8, 2), 2);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); //
|
||||
checkPeriod(interestSchedule, 1, 227.81, 6.70, 221.11, 448.87, false); //
|
||||
checkPeriod(interestSchedule, 2, 115.04, 4.49, 110.55, 338.32, false); //
|
||||
checkPeriod(interestSchedule, 3, 115.04, 3.38, 111.66, 226.66, false); //
|
||||
checkPeriod(interestSchedule, 4, 115.04, 2.27, 112.77, 113.89, false); //
|
||||
checkPeriod(interestSchedule, 5, 115.03, 1.14, 113.89, 0.00, false); //
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtraTermsFlat() {
|
||||
final LocalDate disbursementDate = LocalDate.of(2024, 6, 1);
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.FLAT);
|
||||
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod())
|
||||
.thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD);
|
||||
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
|
||||
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
|
||||
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
|
||||
|
||||
// add extra repayment period 1
|
||||
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 7, 2), 1);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
|
||||
checkPeriod(interestSchedule, 1, 232.22, 10.00, 222.22, 444.45, false); //
|
||||
checkPeriod(interestSchedule, 2, 232.22, 10.00, 222.22, 222.23, false); //
|
||||
checkPeriod(interestSchedule, 3, 232.23, 10.00, 222.23, 0.00, false); //
|
||||
|
||||
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 8, 2), 2);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
|
||||
checkPeriod(interestSchedule, 1, 232.22, 10.00, 222.22, 444.45, false); //
|
||||
checkPeriod(interestSchedule, 2, 121.11, 10.00, 111.11, 333.34, false); //
|
||||
checkPeriod(interestSchedule, 3, 121.11, 10.00, 111.11, 222.23, false); //
|
||||
checkPeriod(interestSchedule, 4, 121.11, 10.00, 111.11, 111.12, false); //
|
||||
checkPeriod(interestSchedule, 5, 121.12, 10.00, 111.12, 0.00, false); //
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtraTermsDecliningBalanceWithFullyRepaidPeriods() {
|
||||
final LocalDate disbursementDate = LocalDate.of(2024, 6, 1);
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(4);
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 256.28, 10.00, 246.28, 753.72, false); //
|
||||
checkPeriod(interestSchedule, 1, 256.28, 7.54, 248.74, 504.98, false); //
|
||||
checkPeriod(interestSchedule, 2, 256.28, 5.05, 251.23, 253.75, false); //
|
||||
checkPeriod(interestSchedule, 3, 256.29, 2.54, 253.75, 0.00, false); //
|
||||
|
||||
// simulate merchant issued refund allocated on last installment ( fully repaid ) on first date
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 10, 1), LocalDate.of(2024, 6, 1),
|
||||
Money.of(currency, BigDecimal.valueOf(256.29D)));
|
||||
|
||||
// simulate early repayment on day 0
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 6, 1),
|
||||
Money.of(currency, BigDecimal.valueOf(256.28D)));
|
||||
|
||||
// verify 1st and 4th installments are fully repaid
|
||||
checkPeriod(interestSchedule, 0, 256.28, 0.00, 256.28, 487.43, true); //
|
||||
checkPeriod(interestSchedule, 1, 256.28, 9.74, 246.54, 240.89, false); //
|
||||
checkPeriod(interestSchedule, 2, 243.30, 2.41, 240.89, 0.00, false); //
|
||||
checkPeriod(interestSchedule, 3, 256.29, 0.00, 256.29, 0.00, true); //
|
||||
|
||||
// add extra repayment period 1
|
||||
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 6, 15), 1);
|
||||
|
||||
// verify 1st and 4th installments are not modified, additional principal paid are substract from last
|
||||
// period
|
||||
checkPeriod(interestSchedule, 0, 256.28, 0.00, 256.28, 487.43, true); //
|
||||
checkPeriod(interestSchedule, 1, 206.04, 9.74, 196.30, 291.13, false); //
|
||||
checkPeriod(interestSchedule, 2, 206.04, 2.91, 203.13, 88.00, false); //
|
||||
checkPeriod(interestSchedule, 3, 256.29, 0.00, 256.29, 88.00, true); //
|
||||
checkPeriod(interestSchedule, 4, 89.76, 1.76, 88.00, 0.00, false); //
|
||||
|
||||
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 6, 15), 2);
|
||||
|
||||
// verify 1st and 4th installments are not modified, additional principal paid are substract from last
|
||||
// periods
|
||||
checkPeriod(interestSchedule, 0, 256.28, 0.00, 256.28, 487.43, true); //
|
||||
checkPeriod(interestSchedule, 1, 126.91, 9.74, 117.17, 370.26, false); //
|
||||
checkPeriod(interestSchedule, 2, 126.91, 3.70, 123.21, 247.05, false); //
|
||||
checkPeriod(interestSchedule, 3, 256.29, 0.00, 256.29, 247.05, true); //
|
||||
checkPeriod(interestSchedule, 4, 126.91, 4.94, 121.97, 125.08, false); //
|
||||
checkPeriod(interestSchedule, 5, 126.33, 1.25, 125.08, 0.00, false); //
|
||||
checkPeriod(interestSchedule, 6, 0.00, 0.00, 0.00, 0.00, true); //
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtraTermsFlatWithFullyRepaidPeriods() {
|
||||
final LocalDate disbursementDate = LocalDate.of(2024, 6, 1);
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(4);
|
||||
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.FLAT);
|
||||
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod())
|
||||
.thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD);
|
||||
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 750.00, false); //
|
||||
checkPeriod(interestSchedule, 1, 260.00, 10.00, 250.00, 500.00, false); //
|
||||
checkPeriod(interestSchedule, 2, 260.00, 10.00, 250.00, 250.00, false); //
|
||||
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 0.00, false); //
|
||||
|
||||
// simulate merchant issued refund allocated on last installment ( fully repaid ) on first date
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 10, 1), LocalDate.of(2024, 6, 1),
|
||||
Money.of(currency, BigDecimal.valueOf(250.0D)));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 10, 1), LocalDate.of(2024, 6, 1),
|
||||
Money.of(currency, BigDecimal.valueOf(10.0D)));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 500.00, false); //
|
||||
checkPeriod(interestSchedule, 1, 260.00, 10.00, 250.00, 250.00, false); //
|
||||
checkPeriod(interestSchedule, 2, 260.00, 10.00, 250.00, 0.00, false); //
|
||||
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 0.00, true); //
|
||||
|
||||
// simulate early repayment on day 0
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 6, 1),
|
||||
Money.of(currency, BigDecimal.valueOf(250.0D)));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 6, 1),
|
||||
Money.of(currency, BigDecimal.valueOf(10.0D)));
|
||||
|
||||
// verify 1st and 4th installments are fully repaid
|
||||
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 500.00, true); //
|
||||
checkPeriod(interestSchedule, 1, 260.00, 10.00, 250.00, 250.00, false); //
|
||||
checkPeriod(interestSchedule, 2, 260.00, 10.00, 250.00, 0.00, false); //
|
||||
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 0.00, true); //
|
||||
|
||||
// add extra repayment period 1
|
||||
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 6, 15), 1);
|
||||
|
||||
// verify 1st and 4th installments are not modified, additional principal paid are substract from last
|
||||
// period
|
||||
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 500.00, true); //
|
||||
checkPeriod(interestSchedule, 1, 210.00, 10.00, 200.00, 300.00, false); //
|
||||
checkPeriod(interestSchedule, 2, 210.00, 10.00, 200.00, 100.00, false); //
|
||||
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 100.00, true); //
|
||||
checkPeriod(interestSchedule, 4, 110.00, 10.00, 100.00, 0.00, false); //
|
||||
|
||||
// add extra repayment period 2
|
||||
emiCalculator.addRepaymentPeriods(interestSchedule, LocalDate.of(2024, 6, 15), 2);
|
||||
|
||||
// verify 1st and 4th installments are not modified, additional principal paid are substract from last
|
||||
// periods
|
||||
checkPeriod(interestSchedule, 0, 260.00, 10.00, 250.00, 500.00, true); //
|
||||
checkPeriod(interestSchedule, 1, 152.86, 10.00, 142.86, 357.14, false); //
|
||||
checkPeriod(interestSchedule, 2, 152.86, 10.00, 142.86, 214.28, false); //
|
||||
checkPeriod(interestSchedule, 3, 260.00, 10.00, 250.00, 214.28, true); //
|
||||
checkPeriod(interestSchedule, 4, 152.86, 10.00, 142.86, 71.42, false); //
|
||||
checkPeriod(interestSchedule, 5, 81.42, 10.00, 71.42, 0.00, false); //
|
||||
checkPeriod(interestSchedule, 6, 10.00, 10.00, 0.00, 0.00, false); //
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// utilities
|
||||
private List<LoanScheduleModelRepaymentPeriod> generateExpectedRepaymentPeriods(LocalDate disbursementDate) {
|
||||
return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
|
||||
|
||||
Reference in New Issue
Block a user