FINERACT-2114: Interest rate modification - Advanced payment allocation processor support

This commit is contained in:
Janos Meszaros
2024-10-29 12:24:23 +01:00
committed by Adam Saghy
parent 3e64cc1536
commit 94360c9f8a
16 changed files with 633 additions and 363 deletions
@@ -63,6 +63,8 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
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.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
@@ -71,6 +73,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
@@ -169,8 +172,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
currentInstallment.updateObligationsMet(currency, disbursementDate);
}
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency));
final Loan loan = loanTransactions.get(0).getLoan();
final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf();
@@ -180,17 +181,26 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
changedTransactionDetail, scheduleModel);
LoanTermVariationsDataWrapper loanTermVariations = Optional
.ofNullable(loan.getActiveLoanTermVariations()).map(loanTermVariationsSet -> loanTermVariationsSet.stream()
.map(LoanTermVariations::toData).collect(Collectors.toCollection(ArrayList::new)))
.map(LoanTermVariationsDataWrapper::new).orElse(null);
List<ChangeOperation> changeOperations = createSortedChangeList(loanTermVariations, loanTransactions, charges);
List<LoanTransaction> overpaidTransactions = new ArrayList<>();
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
if (chargeOrTransaction.isTransaction()) {
LoanTransaction transaction = chargeOrTransaction.getLoanTransaction().get();
for (final ChangeOperation changeOperation : changeOperations) {
if (changeOperation.isInterestRateChange()) {
final LoanTermVariationsData interestRateChange = changeOperation.getInterestRateChange().get();
processInterestRateChange(installments, interestRateChange, scheduleModel);
} else if (changeOperation.isTransaction()) {
LoanTransaction transaction = changeOperation.getLoanTransaction().get();
processSingleTransaction(transaction, ctx);
transaction = getProcessedTransaction(changedTransactionDetail, transaction);
if (transaction.isOverPaid() && transaction.isRepaymentLikeType()) { // TODO CREDIT, DEBIT
overpaidTransactions.add(transaction);
}
} else {
LoanCharge loanCharge = chargeOrTransaction.getLoanCharge().get();
LoanCharge loanCharge = changeOperation.getLoanCharge().get();
processSingleCharge(loanCharge, currency, installments, disbursementDate);
if (!loanCharge.isFullyPaid() && !overpaidTransactions.isEmpty()) {
overpaidTransactions = processOverpaidTransactions(overpaidTransactions, currency, installments, charges,
@@ -205,13 +215,37 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
createNewTransaction(oldTransaction, newTransaction, ctx);
}
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx);
List<LoanTransaction> txs = chargeOrTransactions.stream() //
.filter(ChargeOrTransaction::isTransaction) //
List<LoanTransaction> txs = changeOperations.stream() //
.filter(ChangeOperation::isTransaction) //
.map(e -> e.getLoanTransaction().get()).toList();
reprocessInstallments(disbursementDate, txs, installments, currency);
return Pair.of(changedTransactionDetail, scheduleModel);
}
private void processInterestRateChange(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate);
processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments);
}
private void processInterestRateChangeOnInstallments(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate interestRateChangeSubmittedOnDate, final List<LoanRepaymentScheduleInstallment> installments) {
installments.stream() //
.filter(installment -> isNotObligationsMet(installment)
&& !interestRateChangeSubmittedOnDate.isAfter(installment.getDueDate()))
.forEach(installment -> updateInstallmentIfInterestPeriodPresent(scheduleModel, installment)); //
}
private void updateInstallmentIfInterestPeriodPresent(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LoanRepaymentScheduleInstallment installment) {
emiCalculator.findRepaymentPeriod(scheduleModel, installment.getDueDate()).ifPresent(interestRepaymentPeriod -> {
installment.updateInterestCharged(interestRepaymentPeriod.getDueInterest().getAmount());
installment.updatePrincipal(interestRepaymentPeriod.getDuePrincipal().getAmount());
});
}
@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
@@ -736,17 +770,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
@NotNull
private List<ChargeOrTransaction> createSortedChargesAndTransactionsList(List<LoanTransaction> loanTransactions,
Set<LoanCharge> charges) {
List<ChargeOrTransaction> chargeOrTransactions = new ArrayList<>();
private List<ChangeOperation> createSortedChangeList(final LoanTermVariationsDataWrapper loanTermVariations,
final List<LoanTransaction> loanTransactions, final Set<LoanCharge> charges) {
List<ChangeOperation> changeOperations = new ArrayList<>();
if (loanTermVariations != null && !loanTermVariations.getInterestRateFromInstallment().isEmpty()) {
changeOperations.addAll(loanTermVariations.getInterestRateFromInstallment().stream().map(ChangeOperation::new).toList());
}
if (charges != null) {
chargeOrTransactions.addAll(charges.stream().map(ChargeOrTransaction::new).toList());
changeOperations.addAll(charges.stream().map(ChangeOperation::new).toList());
}
if (loanTransactions != null) {
chargeOrTransactions.addAll(loanTransactions.stream().map(ChargeOrTransaction::new).toList());
changeOperations.addAll(loanTransactions.stream().map(ChangeOperation::new).toList());
}
Collections.sort(chargeOrTransactions);
return chargeOrTransactions;
Collections.sort(changeOperations);
return changeOperations;
}
private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
@@ -24,26 +24,40 @@ import java.time.OffsetDateTime;
import java.util.Optional;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.jetbrains.annotations.NotNull;
@Getter
public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
public class ChangeOperation implements Comparable<ChangeOperation> {
private final Optional<LoanTermVariationsData> interestRateChange;
private final Optional<LoanCharge> loanCharge;
private final Optional<LoanTransaction> loanTransaction;
public ChargeOrTransaction(LoanCharge loanCharge) {
public ChangeOperation(LoanCharge loanCharge) {
this.interestRateChange = Optional.empty();
this.loanCharge = Optional.of(loanCharge);
this.loanTransaction = Optional.empty();
}
public ChargeOrTransaction(LoanTransaction loanTransaction) {
public ChangeOperation(LoanTransaction loanTransaction) {
this.interestRateChange = Optional.empty();
this.loanTransaction = Optional.of(loanTransaction);
this.loanCharge = Optional.empty();
}
public ChangeOperation(LoanTermVariationsData interestRateChange) {
this.interestRateChange = Optional.of(interestRateChange);
this.loanTransaction = Optional.empty();
this.loanCharge = Optional.empty();
}
public boolean isInterestRateChange() {
return interestRateChange.isPresent();
}
public boolean isTransaction() {
return loanTransaction.isPresent();
}
@@ -52,8 +66,18 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
return loanCharge.isPresent();
}
private boolean isAccrualActivity() {
return isTransaction() && loanTransaction.get().isAccrualActivity();
}
private boolean isBackdatedCharge() {
return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
}
private LocalDate getEffectiveDate() {
if (loanCharge.isPresent()) {
if (interestRateChange.isPresent()) {
return getSubmittedOnDate();
} else if (loanCharge.isPresent()) {
if (isBackdatedCharge()) {
return loanCharge.get().getDueDate();
} else {
@@ -66,16 +90,10 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
}
}
private boolean isAccrualActivity() {
return isTransaction() && loanTransaction.get().isAccrualActivity();
}
private boolean isBackdatedCharge() {
return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
}
private LocalDate getSubmittedOnDate() {
if (loanCharge.isPresent()) {
if (interestRateChange.isPresent()) {
return interestRateChange.get().getTermVariationApplicableFrom();
} else if (loanCharge.isPresent()) {
return loanCharge.get().getSubmittedOnDate();
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getSubmittedOnDate();
@@ -85,7 +103,9 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
}
private OffsetDateTime getCreatedDateTime() {
if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) {
if (interestRateChange.isPresent()) {
return DateUtils.getOffsetDateTimeOfTenantFromLocalDate(getSubmittedOnDate());
} else if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) {
return loanCharge.get().getCreatedDate().get();
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getCreatedDateTime();
@@ -96,16 +116,16 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
@Override
@SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "TODO: fix this! See: https://stackoverflow.com/questions/2609037/findbugs-how-to-solve-eq-compareto-use-object-equals")
public int compareTo(@NotNull ChargeOrTransaction o) {
public int compareTo(@NotNull ChangeOperation o) {
int datePortion = DateUtils.compare(this.getEffectiveDate(), o.getEffectiveDate());
if (datePortion == 0) {
boolean isAccrual = isAccrualActivity();
final boolean isAccrual = isAccrualActivity();
if (isAccrual != o.isAccrualActivity()) {
return isAccrual ? 1 : -1;
}
int submittedDate = DateUtils.compare(getSubmittedOnDate(), o.getSubmittedOnDate());
if (submittedDate == 0) {
return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime());
return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime(), null);
}
return submittedDate;
}
@@ -22,9 +22,9 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import org.jetbrains.annotations.NotNull;
public record InterestRate(LocalDate effectiveFrom, //
LocalDate validFrom, //
BigDecimal interestRate //
public record InterestRate(//
LocalDate effectiveFrom, //
BigDecimal interestRate//
) implements Comparable<InterestRate> {
@Override
@@ -23,8 +23,10 @@ import java.math.MathContext;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.TreeSet;
import java.util.function.Consumer;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -36,7 +38,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail
public class ProgressiveLoanInterestScheduleModel {
private final List<RepaymentPeriod> repaymentPeriods;
private final List<InterestRate> interestRates;
private final TreeSet<InterestRate> interestRates;
private final LoanProductRelatedDetail loanProductRelatedDetail;
private final Integer installmentAmountInMultiplesOf;
private MathContext mc;
@@ -44,17 +46,17 @@ public class ProgressiveLoanInterestScheduleModel {
public ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf, MathContext mc) {
this.repaymentPeriods = repaymentPeriods;
this.interestRates = new ArrayList<>();
this.interestRates = new TreeSet<>(Collections.reverseOrder());
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
this.mc = mc;
}
private ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, final List<InterestRate> interestRates,
private ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, final TreeSet<InterestRate> interestRates,
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) {
this.mc = mc;
this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods);
this.interestRates = new ArrayList<>(interestRates);
this.interestRates = new TreeSet<>(interestRates);
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
}
@@ -80,8 +82,15 @@ public class ProgressiveLoanInterestScheduleModel {
}
private BigDecimal findInterestRate(final LocalDate effectiveDate) {
return interestRates.stream().filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)).map(InterestRate::interestRate).findFirst()
.orElse(loanProductRelatedDetail.getAnnualNominalInterestRate());
return interestRates.stream() //
.filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)) //
.map(InterestRate::interestRate) //
.findFirst() //
.orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); //
}
public void addInterestRate(final LocalDate newInterestEffectiveDate, final BigDecimal newInterestRate) {
interestRates.add(new InterestRate(newInterestEffectiveDate, newInterestRate));
}
public Optional<RepaymentPeriod> findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) {
@@ -128,11 +137,12 @@ public class ProgressiveLoanInterestScheduleModel {
}
return repaymentPeriods.stream()//
.filter(repaymentPeriod -> {
if (repaymentPeriod.getPrevious().isPresent()) {
return balanceChangeDate.isAfter(repaymentPeriod.getFromDate())
final boolean isFirstPeriod = repaymentPeriod.getPrevious().isEmpty();
if (isFirstPeriod) {
return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate())
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
} else {
return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate())
return balanceChangeDate.isAfter(repaymentPeriod.getFromDate())
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
}
})//
@@ -183,7 +193,7 @@ public class ProgressiveLoanInterestScheduleModel {
repaymentPeriod.getInterestPeriods().add(interestPeriod);
}
private Money getZero() {
public Money getZero() {
return Money.zero(loanProductRelatedDetail.getCurrency(), mc);
}
}
@@ -127,8 +127,12 @@ public class RepaymentPeriod {
return getEmi().minus(getCalculatedDueInterest(), mc);
}
public Money getTotalPaidAmount() {
return getPaidPrincipal().plus(getPaidInterest());
}
public boolean isFullyPaid() {
return getEmi().isEqualTo(getPaidPrincipal().plus(getPaidInterest()));
return getEmi().isEqualTo(getTotalPaidAmount());
}
public Money getDueInterest() {
@@ -35,7 +35,6 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
@@ -61,27 +60,19 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator;
private final EMICalculator emiCalculator;
public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc);
return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null));
}
@Override
public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO) {
final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency();
// generate list of proposed schedule due dates
LocalDate loanEndDate = scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO);
if (loanApplicationTerms.getLoanTermVariations() != null) {
LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations()
.fetchLoanTermDueDateVariationsData(loanEndDate);
if (lastDueDateVariation != null) {
loanEndDate = lastDueDateVariation.getDateValue();
}
}
// determine the total charges due at time of disbursement
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
// setup variables for tracking important facts required for loan
// schedule generation.
final MonetaryCurrency currency = loanApplicationTerms.getCurrency();
LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType())
? loanApplicationTerms.getExpectedDisbursementDate()
@@ -94,6 +85,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
// set and handled separately after all installments generated
final Set<LoanCharge> nonCompoundingCharges = separateTotalCompoundingPercentageCharges(loanCharges);
// generate list of proposed schedule due dates
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
periodStartDate, loanApplicationTerms, holidayDetailDTO);
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel(
@@ -102,29 +94,17 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size());
prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms);
final ArrayList<DisbursementData> disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas());
disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate));
final List<DisbursementData> disbursementDataList = getSortedDisbursementList(loanApplicationTerms);
for (LoanScheduleModelRepaymentPeriod repaymentPeriod : expectedRepaymentPeriods) {
scheduleParams.setPeriodStartDate(repaymentPeriod.getFromDate());
scheduleParams.setActualRepaymentDate(repaymentPeriod.getDueDate());
// in same repayment period the logic firstly applies interest rate changes and just after the disbursements
applyInterestRateChangesOnPeriod(loanApplicationTerms, repaymentPeriod, interestScheduleModel);
processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods,
chargesDueAtTimeOfDisbursement, false, mc);
repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber());
if (loanApplicationTerms.getLoanTermVariations() != null) {
for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) {
final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate())
&& !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) {
emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate);
}
}
}
emiCalculator.findRepaymentPeriod(interestScheduleModel, repaymentPeriod.getDueDate()).ifPresent(interestRepaymentPeriod -> {
final Money principalDue = interestRepaymentPeriod.getDuePrincipal();
final Money interestDue = interestRepaymentPeriod.getDueInterest();
@@ -166,10 +146,24 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
}
public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
private List<DisbursementData> getSortedDisbursementList(LoanApplicationTerms loanApplicationTerms) {
final List<DisbursementData> disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas());
disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate));
return disbursementDataList;
}
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc);
return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null));
private void applyInterestRateChangesOnPeriod(final LoanApplicationTerms loanApplicationTerms,
final LoanScheduleModelRepaymentPeriod repaymentPeriod, final ProgressiveLoanInterestScheduleModel interestScheduleModel) {
if (loanApplicationTerms.getLoanTermVariations() != null) {
for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) {
final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate())
&& !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) {
emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate);
}
}
}
}
private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) {
@@ -181,10 +175,10 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
}
}
private void processDisbursements(final LoanApplicationTerms loanApplicationTerms,
final ArrayList<DisbursementData> disbursementDataList, final LoanScheduleParams scheduleParams,
final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List<LoanScheduleModelPeriod> periods,
final BigDecimal chargesDueAtTimeOfDisbursement, final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) {
private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final List<DisbursementData> disbursementDataList,
final LoanScheduleParams scheduleParams, final ProgressiveLoanInterestScheduleModel interestScheduleModel,
final List<LoanScheduleModelPeriod> periods, final BigDecimal chargesDueAtTimeOfDisbursement,
final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) {
for (DisbursementData disbursementData : disbursementDataList) {
final LocalDate disbursementDate = disbursementData.disbursementDate();
@@ -81,16 +81,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate,
final Money disbursedAmount) {
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount,
Money.zero(disbursedAmount.getCurrency(), scheduleModel.mc()))
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount, scheduleModel.getZero())
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, disbursementDueDate), scheduleModel));
}
private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestScheduleModel scheduleModel,
final RepaymentPeriod changedRepaymentPeriod, final LocalDate disbursementDueDate) {
final boolean isRelatedToNextRepaymentPeriod = changedRepaymentPeriod.getDueDate().isEqual(disbursementDueDate);
final RepaymentPeriod changedRepaymentPeriod, final LocalDate operationDueDate) {
final boolean isRelatedToNextRepaymentPeriod = changedRepaymentPeriod.getDueDate().isEqual(operationDueDate);
if (isRelatedToNextRepaymentPeriod) {
final Optional<RepaymentPeriod> nextRepaymentPeriod = scheduleModel.repaymentPeriods().stream()
.filter(repaymentPeriod -> changedRepaymentPeriod.equals(repaymentPeriod.getPrevious().orElse(null))).findFirst();
@@ -106,14 +104,20 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate,
final BigDecimal newInterestRate) {
// TODO: impl
final LocalDate interestRateChangeEffectiveDate = newInterestSubmittedOnDate.minusDays(1);
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, newInterestRate);
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.getZero(),
scheduleModel.getZero())
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel));
}
@Override
public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
Money balanceCorrectionAmount) {
final Money zeroAmount = Money.zero(balanceCorrectionAmount.getCurrency());
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, zeroAmount, balanceCorrectionAmount)
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.getZero(), balanceCorrectionAmount)
.ifPresent(repaymentPeriod -> {
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculateOutstandingBalance(scheduleModel);
@@ -186,10 +190,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final ProgressiveLoanInterestScheduleModel scheduleModel) {
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
// TODO: optimalize
calculateOutstandingBalance(scheduleModel);
calculateEMIOnPeriods(relatedRepaymentPeriods, scheduleModel);
// TODO: optimalize
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods);
@@ -229,7 +231,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
.isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods), mc));
if (shouldBeAdjusted) {
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc);
long uncountablePeriods = relatedRepaymentPeriods.stream().filter(rp -> originalEmi.isLessThan(rp.getTotalPaidAmount()))
.count();
Money adjustment = emiDifference.dividedBy(Math.max(1, numberOfRelatedPeriods - uncountablePeriods), mc);
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel,
originalEmi.plus(adjustment, mc));
if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) {
@@ -238,7 +242,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final LocalDate relatedPeriodsFirstDueDate = relatedRepaymentPeriods.get(0).getDueDate();
final ProgressiveLoanInterestScheduleModel newScheduleModel = scheduleModel.deepCopy(mc);
newScheduleModel.repaymentPeriods().forEach(period -> {
if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate)) {
if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate)
&& !adjustedEqualMonthlyInstallmentValue.isLessThan(period.getTotalPaidAmount())) {
period.setEmi(adjustedEqualMonthlyInstallmentValue);
}
});
@@ -393,7 +398,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult, mc), mc);
final Money finalEqualMonthlyInstallment = applyInstallmentAmountInMultiplesOf(scheduleModel, equalMonthlyInstallment);
repaymentPeriods.forEach(period -> period.setEmi(finalEqualMonthlyInstallment));
repaymentPeriods.forEach(period -> {
if (!finalEqualMonthlyInstallment.isLessThan(period.getTotalPaidAmount())) {
period.setEmi(finalEqualMonthlyInstallment);
}
});
}
Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleModel scheduleModel,
@@ -571,7 +580,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return interestRate//
.multiply(interestFractionPerPeriod, mc)//
.multiply(actualDaysInPeriod, mc)//
.divide(calculatedDaysInPeriod, mc);//
.divide(calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());//
}
/**
@@ -585,7 +594,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return interestRate//
.multiply(interestFractionPerPeriod, mc)//
.multiply(actualDaysInPeriod, mc)//
.divide(calculatedDaysInPeriod, mc);//
.divide(calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());//
}
/**