FINERACT-2114: Interest rate modification - Advanced payment allocation processor support
This commit is contained in:
committed by
Adam Saghy
parent
3e64cc1536
commit
94360c9f8a
+52
-15
@@ -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) {
|
||||
|
||||
+37
-17
@@ -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;
|
||||
}
|
||||
+3
-3
@@ -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
|
||||
|
||||
+20
-10
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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() {
|
||||
|
||||
+30
-36
@@ -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();
|
||||
|
||||
+24
-15
@@ -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());//
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user