FINERACT-1981: Pay-off schedule handling
This commit is contained in:
committed by
Adam Saghy
parent
e94ce7f1df
commit
49e9005109
+86
-32
@@ -55,6 +55,7 @@ import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
@@ -76,6 +77,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
@@ -140,12 +142,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
// only for progressive loans
|
||||
public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessProgressiveLoanTransactions(
|
||||
LocalDate disbursementDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
if (loanTransactions.isEmpty()) {
|
||||
return changedTransactionDetail;
|
||||
return Pair.of(changedTransactionDetail, null);
|
||||
}
|
||||
if (charges != null) {
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
@@ -180,10 +183,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
chargeOrTransaction.getLoanCharge()
|
||||
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
|
||||
}
|
||||
List<LoanTransaction> txs = chargeOrTransactions.stream().map(ChargeOrTransaction::getLoanTransaction).filter(Optional::isPresent)
|
||||
List<LoanTransaction> txs = chargeOrTransactions.stream() //
|
||||
.map(ChargeOrTransaction::getLoanTransaction) //
|
||||
.filter(Optional::isPresent) //
|
||||
.map(Optional::get).toList();
|
||||
reprocessInstallments(disbursementDate, txs, installments, currency);
|
||||
return changedTransactionDetail;
|
||||
return Pair.of(changedTransactionDetail, scheduleModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -276,7 +287,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
return originalTransaction.get();
|
||||
} else { // when there is no id, then it might be that the original transaction is changed, so we need to look
|
||||
// it up from the Ctx.
|
||||
// it up from the Ctx.
|
||||
Long originalChargebackTransactionId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction);
|
||||
Collection<LoanTransaction> updatedTransactions = ctx.getChangedTransactionDetail().getNewTransactionMappings().values();
|
||||
Optional<LoanTransaction> updatedTransaction = updatedTransactions.stream().filter(tr -> tr.getLoanTransactionRelations()
|
||||
@@ -733,19 +744,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
|
||||
|
||||
Money transactionAmountUnprocessed = null;
|
||||
Money transactionAmountUnprocessed = transactionCtx.getOverpaymentHolder().getMoneyObject();
|
||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(),
|
||||
transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(),
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed,
|
||||
defaultPaymentAllocationRule, transactionMappings, Set.of(), balances);
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(),
|
||||
transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(),
|
||||
defaultPaymentAllocationRule, transactionMappings, Set.of(), balances);
|
||||
transactionCtx.getInstallments(), transactionAmountUnprocessed, defaultPaymentAllocationRule, transactionMappings,
|
||||
Set.of(), balances);
|
||||
}
|
||||
transactionCtx.getOverpaymentHolder().setMoneyObject(transactionAmountUnprocessed);
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
@@ -1104,9 +1114,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(),
|
||||
transactionCtx.getInstallments(), transactionAmountUnprocessed, paymentAllocationRule, transactionMappings,
|
||||
transactionCtx.getCharges(), balances);
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed,
|
||||
paymentAllocationRule, transactionMappings, transactionCtx.getCharges(), balances);
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(),
|
||||
@@ -1120,26 +1129,31 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder());
|
||||
}
|
||||
|
||||
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges, Balances balances) {
|
||||
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx,
|
||||
Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
|
||||
LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream()
|
||||
.collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new,
|
||||
mapping(Function.identity(), toList())));
|
||||
|
||||
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
|
||||
transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments,
|
||||
transactionAmountUnprocessed, paymentAllocationsEntry.getValue(),
|
||||
paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, charges, balances);
|
||||
transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed,
|
||||
paymentAllocationsEntry.getValue(), paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings,
|
||||
charges, balances);
|
||||
}
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
||||
private Money processAllocationsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx,
|
||||
Money transactionAmountUnprocessed, List<PaymentAllocationType> paymentAllocationTypes,
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
|
||||
if (transactionAmountUnprocessed.isZero()) {
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
|
||||
Money paidPortion;
|
||||
boolean exit = false;
|
||||
do {
|
||||
@@ -1209,15 +1223,55 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
// Adjust the portion for the last installment
|
||||
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
||||
evenPortion = evenPortion.add(balanceAdjustment);
|
||||
}
|
||||
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
|
||||
evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
|
||||
Loan loan = loanTransaction.getLoan();
|
||||
if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing()
|
||||
&& loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
|
||||
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
|
||||
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
LocalDate payDate = inAdvanceInstallment.getFromDate().isAfter(transactionDate)
|
||||
? inAdvanceInstallment.getFromDate()
|
||||
: transactionDate;
|
||||
ProgressiveLoanInterestRepaymentModel payableDetails = emiCalculator
|
||||
.getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate).orElseThrow();
|
||||
|
||||
switch (paymentAllocationType) {
|
||||
case IN_ADVANCE_INTEREST ->
|
||||
inAdvanceInstallment.updateInterestCharged(payableDetails.getInterestDue().getAmount());
|
||||
case IN_ADVANCE_PRINCIPAL ->
|
||||
inAdvanceInstallment.updatePrincipal(payableDetails.getPrincipalDue().getAmount());
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
|
||||
inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
|
||||
switch (paymentAllocationType) {
|
||||
case IN_ADVANCE_PRINCIPAL -> {
|
||||
emiCalculator.addBalanceCorrection(model, payDate,
|
||||
payableDetails.getOutstandingBalance().multipliedBy(-1));
|
||||
emiCalculator.addBalanceCorrection(model, payDate,
|
||||
payableDetails.getPrincipalDue().minus(paidPortion));
|
||||
}
|
||||
case IN_ADVANCE_INTEREST -> emiCalculator.addBalanceCorrection(model, payDate,
|
||||
payableDetails.getInterestDue().minus(paidPortion));
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Adjust the portion for the last installment
|
||||
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
||||
evenPortion = evenPortion.add(balanceAdjustment);
|
||||
}
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
|
||||
evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
}
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
}
|
||||
} else {
|
||||
|
||||
+28
-69
@@ -18,11 +18,10 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import static java.time.temporal.ChronoUnit.DAYS;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -30,14 +29,12 @@ import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
|
||||
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.DisbursementData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
@@ -46,6 +43,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
|
||||
@@ -247,77 +245,38 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
|
||||
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
|
||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
|
||||
return switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) {
|
||||
case TILL_PRE_CLOSURE_DATE -> {
|
||||
log.debug("calculating prepayment amount till pre closure date (Strategy A)");
|
||||
OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency);
|
||||
AtomicBoolean firstAfterPayoff = new AtomicBoolean(true);
|
||||
loan.getRepaymentScheduleInstallments().forEach(installment -> {
|
||||
boolean isInstallmentAfterPayoff = installment.getDueDate().isAfter(onDate);
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
|
||||
outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency));
|
||||
if (isInstallmentAfterPayoff) {
|
||||
if (firstAfterPayoff.getAndSet(false)) {
|
||||
outstandingAmounts.plusInterest(calculatePayableInterest(loan, installment, onDate));
|
||||
} else {
|
||||
log.debug("Installment {} - {} is after payoff, not counting interest", installment.getFromDate(),
|
||||
installment.getDueDate());
|
||||
}
|
||||
} else {
|
||||
log.debug("adding interest for {} - {}: {}", installment.getFromDate(), installment.getDueDate(),
|
||||
installment.getInterestOutstanding(currency));
|
||||
outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency));
|
||||
}
|
||||
outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency));
|
||||
outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency));
|
||||
});
|
||||
yield outstandingAmounts;
|
||||
}
|
||||
|
||||
case TILL_REST_FREQUENCY_DATE -> {
|
||||
log.debug("calculating prepayment amount till rest frequency date (Strategy B)");
|
||||
OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency);
|
||||
loan.getRepaymentScheduleInstallments().forEach(installment -> {
|
||||
boolean isPayoffBeforeInstallment = installment.getFromDate().isBefore(onDate);
|
||||
|
||||
outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency));
|
||||
if (isPayoffBeforeInstallment) {
|
||||
outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency));
|
||||
} else {
|
||||
log.debug("Payoff after installment {}, not counting interest", installment.getDueDate());
|
||||
}
|
||||
outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency));
|
||||
outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency));
|
||||
});
|
||||
|
||||
yield outstandingAmounts;
|
||||
}
|
||||
case NONE -> throw new UnsupportedOperationException("Pre-closure interest calculation strategy not supported");
|
||||
LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) {
|
||||
case TILL_PRE_CLOSURE_DATE -> onDate;
|
||||
case TILL_REST_FREQUENCY_DATE -> // find due date of current installment
|
||||
installments.stream().filter(it -> it.getFromDate().isBefore(onDate) && it.getDueDate().isAfter(onDate)).findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("No installment found for transaction date: " + onDate)).getDueDate();
|
||||
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
|
||||
};
|
||||
}
|
||||
|
||||
private Money calculatePayableInterest(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate onDate) {
|
||||
RoundingMode roundingMode = MoneyHelper.getRoundingMode();
|
||||
MonetaryCurrency currency = loan.getCurrency();
|
||||
Money originalInterest = installment.getInterestCharged(currency);
|
||||
log.debug("calculating interest for {} from {} to {}", originalInterest, installment.getFromDate(), installment.getDueDate());
|
||||
|
||||
LocalDate start = installment.getFromDate();
|
||||
Money payableInterest = Money.zero(currency);
|
||||
|
||||
while (!start.isEqual(onDate)) {
|
||||
long between = DAYS.between(start, installment.getDueDate());
|
||||
Money dailyInterest = originalInterest.minus(payableInterest).dividedBy(between, roundingMode);
|
||||
log.debug("Daily interest is {}: {} / {}, total: {}", dailyInterest, originalInterest.minus(payableInterest), between,
|
||||
payableInterest.add(dailyInterest));
|
||||
payableInterest = payableInterest.add(dailyInterest);
|
||||
start = start.plusDays(1);
|
||||
if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) {
|
||||
throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor");
|
||||
}
|
||||
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(),
|
||||
loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight();
|
||||
|
||||
payableInterest = payableInterest.minus(installment.getInterestPaid(currency).minus(installment.getInterestWaived(currency)));
|
||||
LoanRepaymentScheduleInstallment actualInstallment = installments.stream()
|
||||
.filter(it -> transactionDate.isAfter(it.getFromDate()) && !transactionDate.isAfter(it.getDueDate())).findFirst()
|
||||
.orElse(installments.get(0));
|
||||
|
||||
log.debug("Payable interest is {}", payableInterest);
|
||||
return payableInterest;
|
||||
ProgressiveLoanInterestRepaymentModel result = emiCalculator
|
||||
.getPayableDetails(model, actualInstallment.getDueDate(), transactionDate).orElseThrow();
|
||||
|
||||
OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) //
|
||||
.principal(result.getOutstandingBalance()) //
|
||||
.interest(result.getInterestDue());
|
||||
|
||||
installments.forEach(installment -> amounts //
|
||||
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
|
||||
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));
|
||||
|
||||
return amounts;
|
||||
}
|
||||
|
||||
// Private, internal methods
|
||||
|
||||
-192
@@ -1,192 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import static java.math.BigDecimal.ZERO;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_REST_FREQUENCY_DATE;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
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.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
class ProgressiveLoanScheduleGeneratorTest {
|
||||
|
||||
static class TestRow {
|
||||
|
||||
LocalDate fromDate;
|
||||
LocalDate dueDate;
|
||||
BigDecimal balance;
|
||||
BigDecimal principal;
|
||||
BigDecimal interest;
|
||||
BigDecimal fee;
|
||||
BigDecimal penalty;
|
||||
boolean paid;
|
||||
|
||||
TestRow(LocalDate fromDate, LocalDate dueDate, BigDecimal balance, BigDecimal principal, BigDecimal interest, BigDecimal fee,
|
||||
BigDecimal penalty, boolean paid) {
|
||||
this.fromDate = fromDate;
|
||||
this.dueDate = dueDate;
|
||||
this.balance = balance;
|
||||
this.principal = principal;
|
||||
this.interest = interest;
|
||||
this.fee = fee;
|
||||
this.penalty = penalty;
|
||||
this.paid = paid;
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(null, null);
|
||||
private MonetaryCurrency usd = new MonetaryCurrency("USD", 2, null);
|
||||
private HolidayDetailDTO holidays = new HolidayDetailDTO(false, null, null);
|
||||
LoanRepaymentScheduleTransactionProcessor processor = mock(LoanRepaymentScheduleTransactionProcessor.class);
|
||||
|
||||
static {
|
||||
ConfigurationDomainService domainService = mock(ConfigurationDomainService.class);
|
||||
when(domainService.getRoundingMode()).thenReturn(RoundingMode.HALF_UP.ordinal());
|
||||
ReflectionTestUtils.setField(MoneyHelper.class, "staticConfigurationDomainService", domainService);
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() {
|
||||
((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(Logger.ROOT_LOGGER_NAME).setLevel(ch.qos.logback.classic.Level.DEBUG);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(Logger.ROOT_LOGGER_NAME).setLevel(Level.INFO);
|
||||
}
|
||||
|
||||
public List<TestRow> testRows() {
|
||||
return List.of(
|
||||
new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(83.57), BigDecimal.valueOf(16.43),
|
||||
BigDecimal.valueOf(0.58), ZERO, ZERO, true),
|
||||
new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(67.05), BigDecimal.valueOf(16.52),
|
||||
BigDecimal.valueOf(0.49), ZERO, ZERO, false),
|
||||
new TestRow(LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1), BigDecimal.valueOf(50.43), BigDecimal.valueOf(16.62),
|
||||
BigDecimal.valueOf(0.39), ZERO, ZERO, false),
|
||||
new TestRow(LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1), BigDecimal.valueOf(33.71), BigDecimal.valueOf(16.72),
|
||||
BigDecimal.valueOf(0.29), ZERO, ZERO, false),
|
||||
new TestRow(LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1), BigDecimal.valueOf(16.90), BigDecimal.valueOf(16.81),
|
||||
BigDecimal.valueOf(0.20), ZERO, ZERO, false),
|
||||
new TestRow(LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1), BigDecimal.valueOf(00.90), BigDecimal.valueOf(16.90),
|
||||
BigDecimal.valueOf(0.10), ZERO, ZERO, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatePrepaymentAmount_TILL_PRE_CLOSURE_DATE() {
|
||||
LoanApplicationTerms terms = mock(LoanApplicationTerms.class);
|
||||
when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_PRE_CLOSURE_DATE);
|
||||
Loan loan = prepareLoanWithInstallments(testRows());
|
||||
|
||||
OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms, MathContext.DECIMAL32,
|
||||
loan, holidays, processor);
|
||||
assertEquals(BigDecimal.valueOf(83.84), amounts.getTotalOutstanding().getAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculatePrepaymentAmount_TILL_REST_FREQUENCY_DATE() {
|
||||
LoanApplicationTerms terms = mock(LoanApplicationTerms.class);
|
||||
when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_REST_FREQUENCY_DATE);
|
||||
Loan loan = prepareLoanWithInstallments(testRows());
|
||||
|
||||
OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms, MathContext.DECIMAL32,
|
||||
loan, holidays, processor);
|
||||
assertEquals(BigDecimal.valueOf(84.06), amounts.getTotalOutstanding().getAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateSameDayPayoff_TILL_PRE_CLOSURE_DATE() {
|
||||
LoanApplicationTerms terms = mock(LoanApplicationTerms.class);
|
||||
when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_PRE_CLOSURE_DATE);
|
||||
|
||||
Loan loan = prepareLoanWithInstallments(List.of(
|
||||
new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100),
|
||||
BigDecimal.valueOf(2), ZERO, ZERO, false),
|
||||
new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100),
|
||||
BigDecimal.valueOf(2), ZERO, ZERO, false)));
|
||||
|
||||
OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 1, 1), terms, MathContext.DECIMAL32,
|
||||
loan, holidays, processor);
|
||||
assertEquals(BigDecimal.valueOf(200.0).longValue(), amounts.getTotalOutstanding().getAmount().longValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateSameDayPayoff_TILL_REST_FREQUENCY_DATE() {
|
||||
LoanApplicationTerms terms = mock(LoanApplicationTerms.class);
|
||||
when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_REST_FREQUENCY_DATE);
|
||||
|
||||
Loan loan = prepareLoanWithInstallments(List.of(
|
||||
new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100),
|
||||
BigDecimal.valueOf(2), ZERO, ZERO, false),
|
||||
new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100),
|
||||
BigDecimal.valueOf(2), ZERO, ZERO, false)));
|
||||
|
||||
OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 1, 1), terms, MathContext.DECIMAL32,
|
||||
loan, holidays, processor);
|
||||
assertEquals(BigDecimal.valueOf(200.0).longValue(), amounts.getTotalOutstanding().getAmount().longValue());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Loan prepareLoanWithInstallments(List<TestRow> rows) {
|
||||
Loan loan = mock(Loan.class);
|
||||
List<LoanRepaymentScheduleInstallment> installments = createInstallments(rows, loan, usd);
|
||||
when(loan.getRepaymentScheduleInstallments()).thenReturn(installments);
|
||||
when(loan.getCurrency()).thenReturn(usd);
|
||||
return loan;
|
||||
}
|
||||
|
||||
private List<LoanRepaymentScheduleInstallment> createInstallments(List<TestRow> rows, Loan loan, MonetaryCurrency usd) {
|
||||
AtomicInteger count = new AtomicInteger(1);
|
||||
return rows.stream().map(row -> {
|
||||
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, count.incrementAndGet(), row.fromDate,
|
||||
row.dueDate, row.principal, row.interest, row.fee, row.penalty, true, null, null, row.paid);
|
||||
if (row.paid) {
|
||||
installment.payPrincipalComponent(row.fromDate, Money.of(usd, row.principal));
|
||||
installment.payInterestComponent(row.fromDate, Money.of(usd, row.interest));
|
||||
installment.updateObligationMet(true);
|
||||
}
|
||||
return installment;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user