FINERACT-2162: Chargeback implementation for progressive interest recalculation enabled loan.
This commit is contained in:
committed by
Adam Saghy
parent
22f7972f60
commit
cdbf0df571
+187
-41
@@ -86,12 +86,12 @@ 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.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
|
||||
@@ -405,7 +405,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Long toId = chargebackId;
|
||||
Collection<LoanTransaction> updatedTransactions = changedTransactionDetail.getNewTransactionMappings().values();
|
||||
Optional<LoanTransaction> fromTransaction = updatedTransactions.stream()
|
||||
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(hasMatchingToLoanTransaction(toId, CHARGEBACK)))
|
||||
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(hasMatchingToLoanTransaction(toId, CHARGEBACK))
|
||||
|| tr.getLoanTransactionRelations().stream()
|
||||
.anyMatch(this.hasMatchingToLoanTransaction(chargebackTransaction, CHARGEBACK)))
|
||||
.findFirst();
|
||||
if (fromTransaction.isPresent()) {
|
||||
return fromTransaction.get();
|
||||
@@ -414,7 +416,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Long toId = chargebackId;
|
||||
// if the original transaction is not in the ctx, then it means that it has not changed during reverse replay
|
||||
Optional<LoanTransaction> fromTransaction = chargebackTransaction.getLoan().getLoanTransactions().stream()
|
||||
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(this.hasMatchingToLoanTransaction(toId, CHARGEBACK)))
|
||||
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(this.hasMatchingToLoanTransaction(toId, CHARGEBACK))
|
||||
|| tr.getLoanTransactionRelations().stream()
|
||||
.anyMatch(this.hasMatchingToLoanTransaction(chargebackTransaction, CHARGEBACK)))
|
||||
.findFirst();
|
||||
if (fromTransaction.isEmpty()) {
|
||||
throw new RuntimeException("Chargeback transaction must have an original transaction");
|
||||
@@ -422,8 +426,141 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return fromTransaction.get();
|
||||
}
|
||||
|
||||
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
private Map<AllocationType, Money> calculateChargebackAllocationMapPrincipalOnly(Money transactionAmount, MonetaryCurrency currency) {
|
||||
Map<AllocationType, Money> chargebackAllocation = new HashMap<>();
|
||||
chargebackAllocation.put(PRINCIPAL, transactionAmount);
|
||||
chargebackAllocation.put(INTEREST, Money.zero(currency));
|
||||
chargebackAllocation.put(PENALTY, Money.zero(currency));
|
||||
chargebackAllocation.put(FEE, Money.zero(currency));
|
||||
return chargebackAllocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns installment from the provided context which has the given due date.
|
||||
*
|
||||
* @param ctx
|
||||
* Progressive transaction context
|
||||
* @param dueDate
|
||||
* installment's due date
|
||||
* @return found installment or null if there is no matching installment.
|
||||
*/
|
||||
private LoanRepaymentScheduleInstallment getInstallmentWithDueDate(@NotNull final ProgressiveTransactionCtx ctx,
|
||||
@NotNull final LocalDate dueDate) {
|
||||
return ctx.getInstallments().stream().filter(i -> i.getDueDate().isEqual(dueDate)).findAny().orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns installment from the provided context which has from date <= given date < due date.
|
||||
*
|
||||
* @param ctx
|
||||
* Progressive transaction context
|
||||
* @param date
|
||||
* the given date
|
||||
* @return found installment or null if there is no matching installment.
|
||||
*/
|
||||
private LoanRepaymentScheduleInstallment getRelatedInstallmentByFromDateInclusiveAndToDateExclusive(
|
||||
@NotNull final ProgressiveTransactionCtx ctx, @NotNull final LocalDate date) {
|
||||
if (ctx.getInstallments() == null) {
|
||||
return null;
|
||||
}
|
||||
return ctx.getInstallments().stream().filter(i -> !date.isBefore(i.getFromDate()) && date.isBefore(i.getDueDate())).findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
protected void processCreditTransactionWithEmiCalculator(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) {
|
||||
|
||||
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
loanTransaction.resetDerivedComponents();
|
||||
Money transactionAmount = loanTransaction.getAmount(currency);
|
||||
Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject();
|
||||
loanTransaction.setOverPayments(totalOverpaid);
|
||||
if (!transactionAmount.isGreaterThanZero()) {
|
||||
return;
|
||||
}
|
||||
if (!loanTransaction.isChargeback()) {
|
||||
throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name());
|
||||
}
|
||||
Map<AllocationType, Money> chargebackAllocation;
|
||||
|
||||
if (hasNoCustomCreditAllocationRule(loanTransaction)) {
|
||||
// whole amount should allocate as principal no need to check previous chargebacks.
|
||||
chargebackAllocation = calculateChargebackAllocationMapPrincipalOnly(transactionAmount, currency);
|
||||
} else {
|
||||
chargebackAllocation = calculateChargebackAllocationMapByCreditAllocationRule(loanTransaction, ctx);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
|
||||
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
|
||||
|
||||
LocalDate lastInstallmentDueDate = model.getMaturityDate();
|
||||
if (!loanTransaction.getTransactionDate().isAfter(lastInstallmentDueDate)) {
|
||||
// handle charge back before or on last installments due date
|
||||
|
||||
if (chargebackAllocation.get(PRINCIPAL).isGreaterThanZero()) {
|
||||
emiCalculator.chargebackPrincipal(model, loanTransaction.getTransactionDate(), chargebackAllocation.get(PRINCIPAL));
|
||||
}
|
||||
|
||||
if (chargebackAllocation.get(INTEREST).isGreaterThanZero()) {
|
||||
emiCalculator.chargebackInterest(model, loanTransaction.getTransactionDate(), chargebackAllocation.get(INTEREST));
|
||||
}
|
||||
|
||||
// update repayment periods until maturity date, for principal and interest portions
|
||||
updateRepaymentPeriods(loanTransaction, ctx);
|
||||
|
||||
// search last instalment if transaction is posted on last installment's due date
|
||||
// otherwise search installment where transaction date is in from date (inclusive) to due date (exclusive)
|
||||
// interval
|
||||
LoanRepaymentScheduleInstallment instalment = lastInstallmentDueDate.isEqual(loanTransaction.getTransactionDate())
|
||||
? getInstallmentWithDueDate(ctx, loanTransaction.getTransactionDate())
|
||||
: getRelatedInstallmentByFromDateInclusiveAndToDateExclusive(ctx, loanTransaction.getTransactionDate());
|
||||
// recognize fees and penalties only because principal and interest portions are already updated.
|
||||
recognizeFeePenaltiesAmountsAfterChargeback(ctx, instalment, chargebackAllocation);
|
||||
} else {
|
||||
// (N+1)th installment case for post maturity date charge back
|
||||
LoanRepaymentScheduleInstallment instalment = ctx.getInstallments().stream()
|
||||
.filter(LoanRepaymentScheduleInstallment::isAdditional).findAny()
|
||||
.orElseGet(() -> createAdditionalInstalment(loanTransaction, ctx));
|
||||
// recognize principal, interest, fees and penalties portions
|
||||
recognizeAmountsAfterChargeback(ctx, loanTransaction.getTransactionDate(), instalment, chargebackAllocation);
|
||||
if (instalment.getDueDate().isBefore(loanTransaction.getTransactionDate())) {
|
||||
instalment.updateDueDate(loanTransaction.getTransactionDate());
|
||||
}
|
||||
}
|
||||
|
||||
allocateOverpayment(loanTransaction, ctx);
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment createAdditionalInstalment(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) {
|
||||
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loanTransaction.getLoan(),
|
||||
(ctx.getInstallments().size() + 1), ctx.getModel().getMaturityDate(), loanTransaction.getTransactionDate(), ZERO, ZERO,
|
||||
ZERO, ZERO, false, null);
|
||||
installment.markAsAdditional();
|
||||
loanTransaction.getLoan().addLoanRepaymentScheduleInstallment(installment);
|
||||
return installment;
|
||||
}
|
||||
|
||||
private Map<AllocationType, Money> calculateChargebackAllocationMapByCreditAllocationRule(LoanTransaction loanTransaction,
|
||||
TransactionCtx ctx) {
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
|
||||
// get the original allocation from the original transaction
|
||||
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, currency);
|
||||
LoanCreditAllocationRule chargebackAllocationRule = getChargebackAllocationRules(loanTransaction);
|
||||
|
||||
// if there were earlier chargebacks then let's calculate the remaining amounts for each portion
|
||||
Map<AllocationType, Money> originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction,
|
||||
originalAllocationNotAdjusted, loanTransaction, ctx, chargebackAllocationRule);
|
||||
|
||||
// calculate the current chargeback allocation
|
||||
return calculateChargebackAllocationMap(originalAllocation, loanTransaction.getAmount(currency).getAmount(),
|
||||
chargebackAllocationRule.getAllocationTypes(), currency);
|
||||
}
|
||||
|
||||
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
if (loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
processCreditTransactionWithEmiCalculator(loanTransaction, (ProgressiveTransactionCtx) ctx);
|
||||
} else if (hasNoCustomCreditAllocationRule(loanTransaction)) {
|
||||
super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
|
||||
} else {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
@@ -444,17 +581,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
|
||||
// get the original allocation from the opriginal transaction
|
||||
// get the original allocation from the original transaction
|
||||
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, currency);
|
||||
LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction);
|
||||
LoanCreditAllocationRule chargebackAllocationRule = getChargebackAllocationRules(loanTransaction);
|
||||
|
||||
// if there were earlier chargebacks then let's calculate the remaining amounts for each portion
|
||||
Map<AllocationType, Money> originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction,
|
||||
originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule);
|
||||
originalAllocationNotAdjusted, loanTransaction, ctx, chargebackAllocationRule);
|
||||
|
||||
// calculate the current chargeback allocation
|
||||
Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation,
|
||||
transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), currency);
|
||||
transactionAmount.getAmount(), chargebackAllocationRule.getAllocationTypes(), currency);
|
||||
|
||||
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
|
||||
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
|
||||
@@ -501,23 +638,22 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
private Map<AllocationType, Money> adjustOriginalAllocationWithFormerChargebacks(LoanTransaction originalTransaction,
|
||||
Map<AllocationType, Money> originalAllocation, LoanTransaction chargeBackTransaction, TransactionCtx ctx,
|
||||
LoanCreditAllocationRule chargeBackAllocationRule) {
|
||||
Map<AllocationType, Money> originalAllocation, LoanTransaction chargebackTransaction, TransactionCtx ctx,
|
||||
LoanCreditAllocationRule chargebackAllocationRule) {
|
||||
// these are the list of existing transactions
|
||||
List<LoanTransaction> allTransactions = new ArrayList<>(chargeBackTransaction.getLoan().getLoanTransactions());
|
||||
List<LoanTransaction> allTransactions = new ArrayList<>(chargebackTransaction.getLoan().getLoanTransactions());
|
||||
|
||||
// Remove the current chargeback from the list
|
||||
if (chargeBackTransaction.getId() != null) {
|
||||
allTransactions.remove(chargeBackTransaction);
|
||||
} else {
|
||||
Long oldId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(chargeBackTransaction);
|
||||
allTransactions.remove(allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), oldId)).findFirst().get());
|
||||
allTransactions.remove(chargebackTransaction);
|
||||
if (ctx.getChangedTransactionDetail() != null) {
|
||||
Long oldId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(chargebackTransaction);
|
||||
allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), oldId)).findFirst().ifPresent(allTransactions::remove);
|
||||
}
|
||||
|
||||
// Add the replayed transactions and remove their old version before the replay
|
||||
if (ctx.getChangedTransactionDetail() != null && ctx.getChangedTransactionDetail().getNewTransactionMappings() != null) {
|
||||
for (Long id : ctx.getChangedTransactionDetail().getNewTransactionMappings().keySet()) {
|
||||
allTransactions.remove(allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), id)).findFirst().get());
|
||||
allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), id)).findFirst().ifPresent(allTransactions::remove);
|
||||
allTransactions.add(ctx.getChangedTransactionDetail().getNewTransactionMappings().get(id));
|
||||
}
|
||||
}
|
||||
@@ -529,18 +665,36 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Comparator<LoanTransaction> comparator = LoanTransactionComparator.INSTANCE;
|
||||
List<LoanTransaction> chargebacksForTheSameOriginal = chargebacks.stream()
|
||||
.filter(tr -> findChargebackOriginalTransaction(tr, ctx) == originalTransaction
|
||||
&& comparator.compare(tr, chargeBackTransaction) < 0)
|
||||
&& comparator.compare(tr, chargebackTransaction) < 0)
|
||||
.sorted(comparator).toList();
|
||||
|
||||
Map<AllocationType, Money> allocation = new HashMap<>(originalAllocation);
|
||||
for (LoanTransaction loanTransaction : chargebacksForTheSameOriginal) {
|
||||
Map<AllocationType, Money> temp = calculateChargebackAllocationMap(allocation, loanTransaction.getAmount(),
|
||||
chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency());
|
||||
chargebackAllocationRule.getAllocationTypes(), ctx.getCurrency());
|
||||
allocation.keySet().forEach(k -> allocation.put(k, allocation.get(k).minus(temp.get(k))));
|
||||
}
|
||||
return allocation;
|
||||
}
|
||||
|
||||
private void recognizeFeePenaltiesAmountsAfterChargeback(TransactionCtx ctx, LoanRepaymentScheduleInstallment installment,
|
||||
Map<AllocationType, Money> chargebackAllocation) {
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
Money fee = chargebackAllocation.get(FEE);
|
||||
if (fee.isGreaterThanZero()) {
|
||||
installment.addToCreditedFee(fee.getAmount());
|
||||
installment.addToChargePortion(fee, Money.zero(currency), Money.zero(currency), Money.zero(currency), Money.zero(currency),
|
||||
Money.zero(currency));
|
||||
}
|
||||
|
||||
Money penalty = chargebackAllocation.get(PENALTY);
|
||||
if (penalty.isGreaterThanZero()) {
|
||||
installment.addToCreditedPenalty(penalty.getAmount());
|
||||
installment.addToChargePortion(Money.zero(currency), Money.zero(currency), Money.zero(currency), penalty, Money.zero(currency),
|
||||
Money.zero(currency));
|
||||
}
|
||||
}
|
||||
|
||||
private void recognizeAmountsAfterChargeback(final TransactionCtx ctx, final LocalDate transactionDate,
|
||||
final LoanRepaymentScheduleInstallment installment, final Map<AllocationType, Money> chargebackAllocation) {
|
||||
final Money principal = chargebackAllocation.get(PRINCIPAL);
|
||||
@@ -554,21 +708,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
installment.addToCreditedInterest(interest.getAmount());
|
||||
installment.addToInterest(transactionDate, interest);
|
||||
}
|
||||
|
||||
final MonetaryCurrency currency = ctx.getCurrency();
|
||||
final Money fee = chargebackAllocation.get(FEE);
|
||||
if (fee != null && fee.isGreaterThanZero()) {
|
||||
installment.addToCreditedFee(fee.getAmount());
|
||||
installment.addToChargePortion(fee, Money.zero(currency), Money.zero(currency), Money.zero(currency), Money.zero(currency),
|
||||
Money.zero(currency));
|
||||
}
|
||||
|
||||
final Money penalty = chargebackAllocation.get(PENALTY);
|
||||
if (penalty != null && penalty.isGreaterThanZero()) {
|
||||
installment.addToCreditedPenalty(penalty.getAmount());
|
||||
installment.addToChargePortion(Money.zero(currency), Money.zero(currency), Money.zero(currency), penalty, Money.zero(currency),
|
||||
Money.zero(currency));
|
||||
}
|
||||
recognizeFeePenaltiesAmountsAfterChargeback(ctx, installment, chargebackAllocation);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -613,6 +753,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return relation -> relation.getRelationType().equals(typeEnum) && Objects.equals(relation.getToTransaction().getId(), id);
|
||||
}
|
||||
|
||||
private Predicate<LoanTransactionRelation> hasMatchingToLoanTransaction(LoanTransaction loanTransaction,
|
||||
LoanTransactionRelationTypeEnum typeEnum) {
|
||||
return relation -> relation.getRelationType().equals(typeEnum) && relation.getToTransaction() == loanTransaction;
|
||||
}
|
||||
|
||||
protected void handleRefund(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
Money zero = Money.zero(currency);
|
||||
@@ -1228,9 +1373,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
if (!installments.isEmpty() && transactionDate.isBefore(loan.getMaturityDate())) {
|
||||
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
|
||||
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
final BigDecimal newInterest = emiCalculator
|
||||
.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(), currentInstallment.getDueDate(), transactionDate)
|
||||
.getAmount();
|
||||
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
|
||||
currentInstallment.getDueDate(), transactionDate, true).getAmount();
|
||||
currentInstallment.updateInterestCharged(newInterest);
|
||||
} else {
|
||||
final BigDecimal totalInterest = currentInstallment.getInterestOutstanding(transactionCtx.getCurrency()).getAmount();
|
||||
@@ -1315,7 +1459,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
installments.stream().filter(installment -> !installment.getFromDate().isAfter(transactionDate)
|
||||
&& installment.getDueDate().isAfter(transactionDate)).forEach(installment -> {
|
||||
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
|
||||
installment.getDueDate(), transactionDate).getAmount();
|
||||
installment.getDueDate(), transactionDate, true).getAmount();
|
||||
final BigDecimal interestRemoved = installment.getInterestCharged().subtract(newInterest);
|
||||
installment.updatePrincipal(MathUtil.nullToZero(installment.getPrincipal()).add(interestRemoved));
|
||||
installment.updateInterestCharged(newInterest);
|
||||
@@ -1826,6 +1970,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
if (installment != null) {
|
||||
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
|
||||
installment.updateInterestCharged(rm.getDueInterest().getAmount());
|
||||
installment.setCreditedInterest(rm.getChargebackInterest().getAmount());
|
||||
installment.setCreditedPrincipal(rm.getChargebackPrincipal().getAmount());
|
||||
installment.updateObligationsMet(ctx.getCurrency(), loanTransaction.getTransactionDate());
|
||||
}
|
||||
});
|
||||
@@ -2099,7 +2245,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
BigDecimal::add);
|
||||
|
||||
final BigDecimal newInterest = emiCalculator
|
||||
.getPeriodInterestTillDate(transactionCtx.getModel(), lastPeriod.getDueDate(), chargeOffDate).getAmount();
|
||||
.getPeriodInterestTillDate(transactionCtx.getModel(), lastPeriod.getDueDate(), chargeOffDate, false).getAmount();
|
||||
|
||||
lastPeriod.setEmi(lastPeriod.getDuePrincipal().add(totalPrincipal).add(newInterest));
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleIns
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
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.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
|
||||
@Getter
|
||||
public class ProgressiveTransactionCtx extends TransactionCtx {
|
||||
|
||||
+12
-6
@@ -48,10 +48,10 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleD
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -182,9 +182,15 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
.principal(outstandingAmounts.getOutstandingPrincipal()) //
|
||||
.interest(outstandingAmounts.getOutstandingInterest());//
|
||||
|
||||
installments.forEach(installment -> result //
|
||||
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
|
||||
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));
|
||||
installments.forEach(installment -> {
|
||||
if (installment.isAdditional()) {
|
||||
result.plusPrincipal(installment.getPrincipalOutstanding(currency))
|
||||
.plusInterest(installment.getInterestOutstanding(currency));
|
||||
}
|
||||
result //
|
||||
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
|
||||
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -200,7 +206,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
return Money.zero(loan.getCurrency());
|
||||
}
|
||||
ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), targetDate);
|
||||
return emiCalculator.getPeriodInterestTillDate(model, installment.getDueDate(), targetDate);
|
||||
return emiCalculator.getPeriodInterestTillDate(model, installment.getDueDate(), targetDate, false);
|
||||
}
|
||||
|
||||
// Private, internal methods
|
||||
|
||||
+57
-5
@@ -27,49 +27,97 @@ import java.util.Optional;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
|
||||
|
||||
public interface EMICalculator {
|
||||
|
||||
/**
|
||||
* This method creates an Interest model with repayment periods from the schedule periods which generated by
|
||||
* schedule generator.
|
||||
*/
|
||||
@NotNull
|
||||
ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
|
||||
@NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
|
||||
List<LoanTermVariationsData> loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc);
|
||||
|
||||
/**
|
||||
* This method creates an Interest model with repayment periods from the installments which retrieved from the
|
||||
* database.
|
||||
*/
|
||||
@NotNull
|
||||
ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel(
|
||||
@NotNull List<LoanRepaymentScheduleInstallment> installments,
|
||||
@NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
|
||||
List<LoanTermVariationsData> loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc);
|
||||
|
||||
/**
|
||||
* Find repayment period based on Due Date.
|
||||
*/
|
||||
Optional<RepaymentPeriod> findRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate dueDate);
|
||||
|
||||
/**
|
||||
* Applies the disbursement on the interest model. This method recalculates the EMI amounts from the action date.
|
||||
*/
|
||||
void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount);
|
||||
|
||||
/**
|
||||
* Applies the interest rate change on the interest model. This method recalculates the EMI amounts from the action
|
||||
* date.
|
||||
*/
|
||||
void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestSubmittedOnDate,
|
||||
BigDecimal newInterestRate);
|
||||
|
||||
/**
|
||||
* This method applies outstanding balance correction on the interest model. Negative amount decreases the
|
||||
* outstanding balance while positive amounts are increasing that. Typically used for late repayment or to count
|
||||
* repayments.
|
||||
*/
|
||||
void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
|
||||
Money balanceCorrectionAmount);
|
||||
|
||||
/**
|
||||
* This method used for pay interest portion during the repayment transaction.
|
||||
*/
|
||||
void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
|
||||
Money interestAmount);
|
||||
|
||||
/**
|
||||
* This method used for pay principal portion during the repayment transaction.
|
||||
*/
|
||||
void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
|
||||
Money principalAmount);
|
||||
|
||||
/**
|
||||
* This method used for charge back principal portion. This method increases the outstanding balance. This method
|
||||
* creates a calculated "virtual" EMI for the applied period.
|
||||
*/
|
||||
void chargebackPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
|
||||
Money chargebackPrincipalAmount);
|
||||
|
||||
/**
|
||||
* This method used for charge back interest portion. This method adds extra interest due. This method creates a
|
||||
* calculated "virtual" EMI for the applied period.
|
||||
*/
|
||||
void chargebackInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money chargebackInterestAmount);
|
||||
|
||||
/**
|
||||
* This method gives back the maximum of the due principal and maximum of the due interest for a requested day.
|
||||
*/
|
||||
@NotNull
|
||||
PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
|
||||
@NotNull LocalDate targetDate);
|
||||
|
||||
/**
|
||||
* Gives back the sum of the interest from the whole model on the given date.
|
||||
*/
|
||||
@NotNull
|
||||
Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
|
||||
@NotNull LocalDate targetDate);
|
||||
@NotNull LocalDate targetDate, boolean includeChargebackInterest);
|
||||
|
||||
Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
LocalDate targetDate);
|
||||
@@ -80,5 +128,9 @@ public interface EMICalculator {
|
||||
|
||||
Money getSumOfDueInterestsOnDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate subjectDate);
|
||||
|
||||
/**
|
||||
* This method stops the interest counting for the given range. Chargeback interest counts even if the normal
|
||||
* interest paused.
|
||||
*/
|
||||
void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate);
|
||||
}
|
||||
|
||||
+83
-24
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanproduct.calc;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
@@ -30,6 +31,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
@@ -40,13 +42,14 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.EmiAdjustment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiAdjustment;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -188,13 +191,60 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
repaymentPeriod.ifPresent(rp -> {
|
||||
// If any period total paid > calculated EMI, then set EMI to total paid -> effectively it is marked as
|
||||
// fully paid
|
||||
if (rp.getTotalPaidAmount().compareTo(rp.getEmi()) > 0) {
|
||||
rp.setEmi(rp.getTotalPaidAmount());
|
||||
if (rp.getTotalPaidAmount().isGreaterThan(rp.getEmiPlusChargeback())) {
|
||||
rp.setEmi(rp.getTotalPaidAmount().minus(rp.getTotalChargebackAmount()));
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addChargebackAmountsToInterestPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
|
||||
Money chargebackPrincipalAmount, Money chargeBackInterestAmount) {
|
||||
scheduleModel.repaymentPeriods().stream().filter(checkRepaymentPeriodIsInChargebackRange(scheduleModel, transactionDate))
|
||||
.findFirst()
|
||||
.flatMap(repaymentPeriod -> repaymentPeriod.getInterestPeriods().stream()
|
||||
.filter(interestPeriod -> interestPeriod.getFromDate().equals(transactionDate)).reduce((v1, v2) -> v2))
|
||||
.ifPresent(interestPeriod -> {
|
||||
interestPeriod.addChargebackPrincipalAmount(chargebackPrincipalAmount);
|
||||
interestPeriod.addChargebackInterestAmount(chargeBackInterestAmount);
|
||||
});
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static Predicate<RepaymentPeriod> checkRepaymentPeriodIsInChargebackRange(ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
LocalDate transactionDate) {
|
||||
return repaymentPeriod -> scheduleModel.isLastRepaymentPeriod(repaymentPeriod)
|
||||
? !transactionDate.isBefore(repaymentPeriod.getFromDate()) && !transactionDate.isAfter(repaymentPeriod.getDueDate())
|
||||
: !transactionDate.isBefore(repaymentPeriod.getFromDate()) && transactionDate.isBefore(repaymentPeriod.getDueDate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chargebackPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
|
||||
Money chargebackPrincipalAmount) {
|
||||
addChargeback(scheduleModel, transactionDate, chargebackPrincipalAmount, scheduleModel.zero());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chargebackInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
|
||||
Money chargebackInterestAmount) {
|
||||
addChargeback(scheduleModel, transactionDate, scheduleModel.zero(), chargebackInterestAmount);
|
||||
}
|
||||
|
||||
private void addChargeback(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
|
||||
Money chargebackPrincipalAmount, Money chargebackInterestAmount) {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), chargebackPrincipalAmount)
|
||||
.ifPresent(repaymentPeriod -> {
|
||||
addChargebackAmountsToInterestPeriod(scheduleModel, transactionDate, chargebackPrincipalAmount,
|
||||
chargebackInterestAmount);
|
||||
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
||||
calculateOutstandingBalance(scheduleModel);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gives back the maximum of the due principal and maximum of the due interest for a requested day.
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
public PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
|
||||
@@ -216,11 +266,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
@Override
|
||||
@NotNull
|
||||
public Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
|
||||
@NotNull LocalDate targetDate) {
|
||||
@NotNull LocalDate targetDate, boolean includeChargebackInterest) {
|
||||
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
|
||||
periodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
|
||||
return repaymentPeriod.getCalculatedDueInterest();
|
||||
return includeChargebackInterest ? repaymentPeriod.getCalculatedDueInterest()
|
||||
: repaymentPeriod.getCalculatedDueInterest().minus(repaymentPeriod.getChargebackInterest(),
|
||||
recalculatedScheduleModelTillDate.mc());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -288,20 +340,25 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
interestPeriod = repaymentPeriod.getLastInterestPeriod();
|
||||
adjustedTargetDate = repaymentPeriod.getDueDate();
|
||||
} else {
|
||||
// TODO use findInterestPeriod
|
||||
interestPeriod = repaymentPeriod.getInterestPeriods().stream()
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow();
|
||||
interestPeriod = repaymentPeriod.findInterestPeriod(targetDate).orElseThrow();
|
||||
}
|
||||
// TODO use findInterestPeriod
|
||||
scheduleModelCopy.repaymentPeriods().stream()//
|
||||
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream()//
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) //
|
||||
.reduce((one, two) -> two))
|
||||
.ifPresent(ip -> ip.setDueDate(targetDate)); //
|
||||
scheduleModelCopy.findRepaymentPeriod(targetDate)//
|
||||
.flatMap(rp -> rp.findInterestPeriod(targetDate)).ifPresent(ip -> ip.setDueDate(targetDate)); //
|
||||
interestPeriod.setDueDate(adjustedTargetDate);
|
||||
int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod);
|
||||
repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear();
|
||||
int nextIdx = index + 1;
|
||||
boolean thereIsInterestPeriodFromDateOnTargetDate = repaymentPeriod.getInterestPeriods().size() > nextIdx
|
||||
&& repaymentPeriod.getInterestPeriods().get(nextIdx).getFromDate().isEqual(targetDate);
|
||||
if (thereIsInterestPeriodFromDateOnTargetDate) {
|
||||
// NOTE: If there is a next interest period with fromDate on the target date
|
||||
// then the related chargeback amounts comes from the next interest period too.
|
||||
InterestPeriod nextInterestPeriod = repaymentPeriod.getInterestPeriods().get(nextIdx);
|
||||
interestPeriod.addChargebackPrincipalAmount(nextInterestPeriod.getChargebackPrincipal());
|
||||
interestPeriod.addChargebackInterestAmount(nextInterestPeriod.getChargebackInterest());
|
||||
}
|
||||
|
||||
repaymentPeriod.getInterestPeriods().subList(nextIdx, repaymentPeriod.getInterestPeriods().size()).clear();
|
||||
scheduleModelCopy.repaymentPeriods().forEach(rp -> rp.getInterestPeriods().removeIf(ip -> ip.getDueDate().isAfter(targetDate)));
|
||||
calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy);
|
||||
calculateOutstandingBalance(scheduleModelCopy);
|
||||
@@ -338,19 +395,21 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
Money totalDueInterest = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getDueInterest).reduce(scheduleModel.zero(),
|
||||
(m1, m2) -> m1.plus(m2, mc)); // 1.46
|
||||
Money totalEMI = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getEmi).reduce(scheduleModel.zero(),
|
||||
Money totalEMI = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getEmiPlusChargeback).reduce(scheduleModel.zero(),
|
||||
(m1, m2) -> m1.plus(m2, mc)); // 101.48
|
||||
Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream()
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
|
||||
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100
|
||||
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)) // 100
|
||||
.plus(scheduleModel.getTotalChargebackPrincipal(), mc); //
|
||||
|
||||
Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc);
|
||||
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
|
||||
.reduce((first, second) -> second);
|
||||
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
|
||||
repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff, mc));
|
||||
if (repaymentPeriod.getEmi().isLessThan(repaymentPeriod.getTotalPaidAmount())) {
|
||||
repaymentPeriod.setEmi(repaymentPeriod.getTotalPaidAmount());
|
||||
if (repaymentPeriod.getEmi()
|
||||
.isLessThan(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalChargebackAmount(), mc))) {
|
||||
repaymentPeriod.setEmi(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalChargebackAmount(), mc));
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
package org.apache.fineract.portfolio.loanproduct.calc.data;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanproduct.calc;
|
||||
package org.apache.fineract.portfolio.loanproduct.calc.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
+50
-12
@@ -16,13 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
package org.apache.fineract.portfolio.loanproduct.calc.data;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
@@ -35,11 +36,13 @@ import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
@Getter
|
||||
@ToString(exclude = { "repaymentPeriod" })
|
||||
@EqualsAndHashCode(exclude = { "repaymentPeriod" })
|
||||
@AllArgsConstructor
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
|
||||
private final RepaymentPeriod repaymentPeriod;
|
||||
private final LocalDate fromDate;
|
||||
@Setter
|
||||
@NotNull
|
||||
private LocalDate fromDate;
|
||||
@Setter
|
||||
@NotNull
|
||||
private LocalDate dueDate;
|
||||
@@ -47,17 +50,36 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
private BigDecimal rateFactor;
|
||||
@Setter
|
||||
private BigDecimal rateFactorTillPeriodDueDate;
|
||||
|
||||
private Money chargebackPrincipal;
|
||||
private Money chargebackInterest;
|
||||
|
||||
private Money disbursementAmount;
|
||||
private Money balanceCorrectionAmount;
|
||||
private Money outstandingLoanBalance;
|
||||
|
||||
private final MathContext mc;
|
||||
private final boolean isPaused;
|
||||
|
||||
public InterestPeriod(RepaymentPeriod repaymentPeriod, InterestPeriod interestPeriod) {
|
||||
this(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), interestPeriod.getRateFactor(),
|
||||
interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getDisbursementAmount(),
|
||||
interestPeriod.getBalanceCorrectionAmount(), interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(),
|
||||
interestPeriod.isPaused());
|
||||
public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) {
|
||||
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getChargebackPrincipal(),
|
||||
interestPeriod.getChargebackInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
|
||||
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), interestPeriod.isPaused());
|
||||
}
|
||||
|
||||
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
||||
LocalDate dueDate) {
|
||||
final Money zero = repaymentPeriod.getEmi().zero();
|
||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
|
||||
zero.getMc(), false);
|
||||
}
|
||||
|
||||
public static InterestPeriod withPausedAndEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
||||
LocalDate dueDate) {
|
||||
final Money zero = repaymentPeriod.getEmi().zero();
|
||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
|
||||
zero.getMc(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,19 +95,28 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
this.disbursementAmount = MathUtil.plus(this.disbursementAmount, disbursementAmount, mc);
|
||||
}
|
||||
|
||||
public void addChargebackPrincipalAmount(final Money chargebackPrincipal) {
|
||||
this.chargebackPrincipal = MathUtil.plus(this.chargebackPrincipal, chargebackPrincipal, mc);
|
||||
}
|
||||
|
||||
public void addChargebackInterestAmount(final Money chargebackInterest) {
|
||||
this.chargebackInterest = MathUtil.plus(this.chargebackInterest, chargebackInterest, mc);
|
||||
}
|
||||
|
||||
public Money getCalculatedDueInterest() {
|
||||
if (isPaused) {
|
||||
return Money.zero(outstandingLoanBalance.getCurrencyData(), mc);
|
||||
return chargebackInterest;
|
||||
}
|
||||
|
||||
long lengthTillPeriodDueDate = getLengthTillPeriodDueDate();
|
||||
final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 //
|
||||
final Money interestDueTillRepaymentDueDate = Money.of(outstandingLoanBalance.getCurrencyData(), lengthTillPeriodDueDate == 0 //
|
||||
? BigDecimal.ZERO //
|
||||
: getOutstandingLoanBalance().getAmount() //
|
||||
.multiply(getRateFactorTillPeriodDueDate(), mc) //
|
||||
.divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) //
|
||||
.multiply(BigDecimal.valueOf(getLength()), mc); //
|
||||
return Money.of(outstandingLoanBalance.getCurrencyData(), interestDueTillRepaymentDueDate, mc);
|
||||
.multiply(BigDecimal.valueOf(getLength()), mc),
|
||||
mc); //
|
||||
return MathUtil.plus(chargebackInterest, interestDueTillRepaymentDueDate, mc);
|
||||
}
|
||||
|
||||
public long getLength() {
|
||||
@@ -116,6 +147,13 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Include principal like amounts (all disbursement amount + chargeback principal)
|
||||
*/
|
||||
public Money getCreditedAmounts() {
|
||||
return getDisbursementAmount().plus(getChargebackPrincipal(), mc);
|
||||
}
|
||||
|
||||
public boolean isFirstInterestPeriod() {
|
||||
return this.equals(getRepaymentPeriod().getFirstInterestPeriod());
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
package org.apache.fineract.portfolio.loanproduct.calc.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
package org.apache.fineract.portfolio.loanproduct.calc.data;
|
||||
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
package org.apache.fineract.portfolio.loanproduct.calc.data;
|
||||
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
+68
-20
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
package org.apache.fineract.portfolio.loanproduct.calc.data;
|
||||
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
|
||||
|
||||
@@ -147,7 +147,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
return 0;
|
||||
}
|
||||
final RepaymentPeriod firstPeriod = repaymentPeriods.get(0);
|
||||
final RepaymentPeriod lastPeriod = repaymentPeriods.size() > 1 ? repaymentPeriods.get(repaymentPeriods.size() - 1) : firstPeriod;
|
||||
final RepaymentPeriod lastPeriod = repaymentPeriods.size() > 1 ? getLastRepaymentPeriod() : firstPeriod;
|
||||
return DateUtils.getExactDifferenceInDays(firstPeriod.getFromDate(), lastPeriod.getDueDate());
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
}
|
||||
|
||||
public LocalDate getMaturityDate() {
|
||||
return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(repaymentPeriods.size() - 1).getDueDate() : null;
|
||||
return !repaymentPeriods.isEmpty() ? getLastRepaymentPeriod().getDueDate() : null;
|
||||
}
|
||||
|
||||
public Optional<RepaymentPeriod> changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate,
|
||||
@@ -201,7 +201,10 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
private Consumer<RepaymentPeriod> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount,
|
||||
final Money correctionAmount) {
|
||||
return repaymentPeriod -> {
|
||||
final Optional<InterestPeriod> interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate);
|
||||
final boolean isChangeOnMaturityDate = isLastRepaymentPeriod(repaymentPeriod)
|
||||
&& balanceChangeDate.isEqual(repaymentPeriod.getDueDate());
|
||||
final Optional<InterestPeriod> interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate,
|
||||
isChangeOnMaturityDate);
|
||||
if (interestPeriodOptional.isPresent()) {
|
||||
interestPeriodOptional.get().addDisbursementAmount(disbursedAmount);
|
||||
interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount);
|
||||
@@ -212,10 +215,16 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
}
|
||||
|
||||
private Optional<InterestPeriod> findInterestPeriodForBalanceChange(final RepaymentPeriod repaymentPeriod,
|
||||
final LocalDate balanceChangeDate) {
|
||||
final LocalDate balanceChangeDate, final boolean isChangeOnMaturityDate) {
|
||||
if (repaymentPeriod == null || balanceChangeDate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// We want to create a 0 length interest period (if not existed yet) for any credit activity occurs on maturity
|
||||
// date
|
||||
if (isChangeOnMaturityDate) {
|
||||
var lastInterestPeriod = repaymentPeriod.getLastInterestPeriod();
|
||||
return lastInterestPeriod.getLength() == 0 ? Optional.of(lastInterestPeriod) : Optional.empty();
|
||||
}
|
||||
return repaymentPeriod.getInterestPeriods().stream()//
|
||||
.filter(interestPeriod -> balanceChangeDate.isEqual(interestPeriod.getDueDate()))//
|
||||
.findFirst();
|
||||
@@ -231,8 +240,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
previousInterestPeriod.addDisbursementAmount(disbursedAmount);
|
||||
previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount);
|
||||
|
||||
final InterestPeriod interestPeriod = new InterestPeriod(repaymentPeriod, newDueDate, originalDueDate, BigDecimal.ZERO,
|
||||
BigDecimal.ZERO, zero, zero, zero, mc, false);
|
||||
final InterestPeriod interestPeriod = InterestPeriod.withEmptyAmounts(repaymentPeriod, newDueDate, originalDueDate);
|
||||
repaymentPeriod.getInterestPeriods().add(interestPeriod);
|
||||
}
|
||||
|
||||
@@ -248,24 +256,20 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
newInterestPeriods.add(interestPeriod);
|
||||
} else {
|
||||
if (interestPeriod.getFromDate().isBefore(finalPauseStart)) {
|
||||
final InterestPeriod leftSlice = new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), finalPauseStart,
|
||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(),
|
||||
interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
|
||||
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), false);
|
||||
final InterestPeriod leftSlice = InterestPeriod.copy(repaymentPeriod, interestPeriod);
|
||||
leftSlice.setDueDate(finalPauseStart);
|
||||
|
||||
newInterestPeriods.add(leftSlice);
|
||||
}
|
||||
if (interestPeriod.getDueDate().isAfter(finalPauseEnd)) {
|
||||
final InterestPeriod rightSlice = new InterestPeriod(repaymentPeriod, finalPauseEnd, interestPeriod.getDueDate(),
|
||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(),
|
||||
interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
|
||||
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), false);
|
||||
final InterestPeriod rightSlice = InterestPeriod.copy(repaymentPeriod, interestPeriod);
|
||||
rightSlice.setFromDate(finalPauseEnd);
|
||||
newInterestPeriods.add(rightSlice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final InterestPeriod pausedSlice = new InterestPeriod(repaymentPeriod, finalPauseStart, finalPauseEnd, BigDecimal.ZERO,
|
||||
BigDecimal.ZERO, zero, zero, zero, mc, true);
|
||||
final InterestPeriod pausedSlice = InterestPeriod.withPausedAndEmptyAmounts(repaymentPeriod, finalPauseStart, finalPauseEnd);
|
||||
newInterestPeriods.add(pausedSlice);
|
||||
|
||||
newInterestPeriods.sort(Comparator.comparing(InterestPeriod::getFromDate));
|
||||
@@ -275,7 +279,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
|
||||
private InterestPeriod findPreviousInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate date) {
|
||||
if (date.isAfter(repaymentPeriod.getFromDate())) {
|
||||
return repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1);
|
||||
return repaymentPeriod.getLastInterestPeriod();
|
||||
} else {
|
||||
return repaymentPeriod.getInterestPeriods().stream()
|
||||
.filter(ip -> date.isAfter(ip.getFromDate()) && !date.isAfter(ip.getDueDate())).reduce((first, second) -> second)
|
||||
@@ -283,30 +287,65 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back the total due interest amount in the whole repayment schedule. Also includes chargeback interest
|
||||
* amount.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getTotalDueInterest() {
|
||||
return repaymentPeriods().stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest))
|
||||
.reduce(zero(), Money::plus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back the total due principal amount in the whole repayment schedule based on disbursements. Do not contain
|
||||
* chargeback principal amount.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getTotalDuePrincipal() {
|
||||
return repaymentPeriods.stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
|
||||
.reduce(zero(), Money::plus);
|
||||
return repaymentPeriods.stream().map(RepaymentPeriod::getCreditedAmounts).reduce(zero(), Money::plus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back the total paid interest amount in the whole repayment schedule.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getTotalPaidInterest() {
|
||||
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidInterest).reduce(zero, Money::plus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back the total paid principal amount in the whole repayment schedule.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getTotalPaidPrincipal() {
|
||||
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidPrincipal).reduce(zero, Money::plus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back the total chargeback principal amount in the whole repayment schedule.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getTotalChargebackPrincipal() {
|
||||
return repaymentPeriods().stream().map(RepaymentPeriod::getChargebackPrincipal).reduce(zero, Money::plus);
|
||||
}
|
||||
|
||||
public Optional<RepaymentPeriod> findRepaymentPeriod(@NotNull LocalDate transactionDate) {
|
||||
return repaymentPeriods.stream() //
|
||||
.filter(period -> isInPeriod(transactionDate, period.getFromDate(), period.getDueDate(), period.isFirstRepaymentPeriod()))//
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a disbursement in the model.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return repaymentPeriods.stream() //
|
||||
.filter(rp -> !rp.getEmi().isZero()) //
|
||||
@@ -314,6 +353,15 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
.isEmpty(); //
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public RepaymentPeriod getLastRepaymentPeriod() {
|
||||
return repaymentPeriods.get(repaymentPeriods.size() - 1);
|
||||
}
|
||||
|
||||
public boolean isLastRepaymentPeriod(@NotNull RepaymentPeriod repaymentPeriod) {
|
||||
return getLastRepaymentPeriod().equals(repaymentPeriod);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gives you repayment pairs to copy attributes.
|
||||
*
|
||||
+89
-9
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
package org.apache.fineract.portfolio.loanproduct.calc.data;
|
||||
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
|
||||
|
||||
@@ -74,8 +74,7 @@ public class RepaymentPeriod {
|
||||
this.mc = mc;
|
||||
this.interestPeriods = new ArrayList<>();
|
||||
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
||||
getInterestPeriods().add(new InterestPeriod(this, getFromDate(), getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, getZero(mc),
|
||||
getZero(mc), getZero(mc), mc, false));
|
||||
getInterestPeriods().add(InterestPeriod.withEmptyAmounts(this, getFromDate(), getDueDate()));
|
||||
this.paidInterest = getZero(mc);
|
||||
this.paidPrincipal = getZero(mc);
|
||||
}
|
||||
@@ -92,7 +91,7 @@ public class RepaymentPeriod {
|
||||
this.mc = mc;
|
||||
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
||||
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
|
||||
interestPeriods.add(new InterestPeriod(this, interestPeriod));
|
||||
interestPeriods.add(InterestPeriod.copy(this, interestPeriod));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +99,11 @@ public class RepaymentPeriod {
|
||||
return Optional.ofNullable(previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gives back sum of (Rate Factor +1) from the interest periods
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public BigDecimal getRateFactorPlus1() {
|
||||
if (rateFactorPlus1Calculation == null) {
|
||||
rateFactorPlus1Calculation = Memo.of(this::calculateRateFactorPlus1, () -> this.interestPeriods);
|
||||
@@ -111,6 +115,11 @@ public class RepaymentPeriod {
|
||||
return interestPeriods.stream().map(InterestPeriod::getRateFactor).reduce(BigDecimal.ONE, BigDecimal::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back calculated due interest + chargeback interest
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@NotNull
|
||||
public Money getCalculatedDueInterest() {
|
||||
if (calculatedDueInterestCalculation == null) {
|
||||
@@ -129,6 +138,11 @@ public class RepaymentPeriod {
|
||||
return calculatedDueInterest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back due interest + chargeback interest OR paid interest
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getDueInterest() {
|
||||
if (dueInterestCalculation == null) {
|
||||
// Due interest might be the maximum paid if there is pay-off or early repayment
|
||||
@@ -139,15 +153,70 @@ public class RepaymentPeriod {
|
||||
return dueInterestCalculation.get();
|
||||
}
|
||||
|
||||
public Money getCalculatedDuePrincipal() {
|
||||
return getEmi().minus(getCalculatedDueInterest(), mc);
|
||||
/**
|
||||
* Gives back an EMI amount which includes chargeback amounts as well
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getEmiPlusChargeback() {
|
||||
return getEmi().plus(getTotalChargebackAmount(), mc); //
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back principal due + charge back principal based on (EMI - Calculated Due Interest)
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getCalculatedDuePrincipal() {
|
||||
return getEmiPlusChargeback().minus(getCalculatedDueInterest(), mc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum of chargeback principals
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getChargebackPrincipal() {
|
||||
return interestPeriods.stream() //
|
||||
.map(InterestPeriod::getChargebackPrincipal) //
|
||||
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)); //
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum of chargeback interests
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getChargebackInterest() {
|
||||
return interestPeriods.stream() //
|
||||
.map(InterestPeriod::getChargebackInterest) //
|
||||
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)); //
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back due principal + chargeback principal or paid principal
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getDuePrincipal() {
|
||||
// Due principal might be the maximum paid if there is pay-off or early repayment
|
||||
return MathUtil.max(getEmi().minus(getDueInterest(), mc), getPaidPrincipal(), false);
|
||||
return MathUtil.max(getEmiPlusChargeback().minus(getDueInterest(), mc), getPaidPrincipal(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back sum of all chargeback principal + chargeback interest
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getTotalChargebackAmount() {
|
||||
return getChargebackPrincipal().plus(getChargebackInterest(), mc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Total paid amounts has everything: paid principal + paid interest + paid charge principal + paid charge interest
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getTotalPaidAmount() {
|
||||
return getPaidPrincipal().plus(getPaidInterest());
|
||||
}
|
||||
@@ -156,10 +225,20 @@ public class RepaymentPeriod {
|
||||
return getEmi().isEqualTo(getTotalPaidAmount());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method counts those interest amounts when there is no place in EMI. Which typically can happen if there is a
|
||||
* not full paid early repayment. In this case we can count in the next repayment period.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getUnrecognizedInterest() {
|
||||
return getCalculatedDueInterest().minus(getDueInterest(), mc);
|
||||
}
|
||||
|
||||
public Money getCreditedAmounts() {
|
||||
return interestPeriods.stream().map(InterestPeriod::getCreditedAmounts).reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc));
|
||||
}
|
||||
|
||||
public Money getOutstandingLoanBalance() {
|
||||
if (outstandingBalanceCalculation == null) {
|
||||
outstandingBalanceCalculation = Memo.of(() -> {
|
||||
@@ -190,8 +269,9 @@ public class RepaymentPeriod {
|
||||
} else {
|
||||
initialBalance = getZero(mc);
|
||||
}
|
||||
Money totalDisbursedAmount = getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount).reduce(getZero(mc),
|
||||
(m1, m2) -> m1.plus(m2, mc));
|
||||
Money totalDisbursedAmount = getInterestPeriods().stream() //
|
||||
.map(InterestPeriod::getDisbursementAmount) //
|
||||
.reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); //
|
||||
return initialBalance.add(totalDisbursedAmount, mc);
|
||||
}
|
||||
|
||||
+13
-13
@@ -67,9 +67,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationT
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
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.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
@@ -266,11 +266,11 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
public void testProcessCreditTransactionWithAllocationRulePrincipalPenaltyFeeInterest() {
|
||||
// given
|
||||
Loan loan = mock(Loan.class);
|
||||
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
|
||||
LoanTransaction chargebackTransaction = createChargebackTransaction(loan, 25.0);
|
||||
|
||||
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
|
||||
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
||||
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
|
||||
LoanTransaction repayment = createRepayment(loan, chargebackTransaction, 10, 0, 20, 5);
|
||||
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
|
||||
|
||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
||||
@@ -280,7 +280,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
|
||||
// when
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
|
||||
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||
underTest.processCreditTransaction(chargebackTransaction, ctx);
|
||||
|
||||
// verify principal
|
||||
Mockito.verify(installment, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
|
||||
@@ -311,7 +311,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
|
||||
Mockito.verify(chargebackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
|
||||
penalty.capture());
|
||||
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
@@ -323,11 +323,11 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
public void testProcessCreditTransactionWithAllocationRulePenaltyFeePrincipalInterest() {
|
||||
// given
|
||||
Loan loan = mock(Loan.class);
|
||||
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
|
||||
LoanTransaction chargebackTransaction = createChargebackTransaction(loan, 25.0);
|
||||
|
||||
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PENALTY, FEE, PRINCIPAL, INTEREST);
|
||||
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
||||
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
|
||||
LoanTransaction repayment = createRepayment(loan, chargebackTransaction, 10, 0, 20, 5);
|
||||
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
|
||||
|
||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
||||
@@ -337,7 +337,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
|
||||
// when
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
|
||||
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||
underTest.processCreditTransaction(chargebackTransaction, ctx);
|
||||
|
||||
// verify charges on installment
|
||||
Mockito.verify(installment, times(1)).addToCreditedFee(new BigDecimal("20.00"));
|
||||
@@ -360,7 +360,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
|
||||
Mockito.verify(chargebackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
|
||||
penalty.capture());
|
||||
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(0)));
|
||||
assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.valueOf(0)));
|
||||
@@ -372,11 +372,11 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment() {
|
||||
// given
|
||||
Loan loan = mock(Loan.class);
|
||||
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
|
||||
LoanTransaction chargebackTransaction = createChargebackTransaction(loan, 25.0);
|
||||
|
||||
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
|
||||
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
||||
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
|
||||
LoanTransaction repayment = createRepayment(loan, chargebackTransaction, 10, 0, 20, 5);
|
||||
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
|
||||
|
||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
||||
@@ -389,7 +389,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
|
||||
// when
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
|
||||
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||
underTest.processCreditTransaction(chargebackTransaction, ctx);
|
||||
|
||||
// verify principal
|
||||
Mockito.verify(installment2, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
|
||||
@@ -420,7 +420,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
|
||||
Mockito.verify(chargebackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
|
||||
penalty.capture());
|
||||
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
|
||||
+605
-10
@@ -35,16 +35,17 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -1145,7 +1146,7 @@ class ProgressiveEMICalculatorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_disbursedAmt1000_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() {
|
||||
public void test_disbursedAmt1000_actual_actual_repayEvery1Month_verify_due_principal_amounts() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
@@ -1264,6 +1265,73 @@ class ProgressiveEMICalculatorTest {
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 31, 0.18, 5.83);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_dailyInterest_chargeback_disbursedAmt1000_dayInYears360_daysInMonth30_repayIn1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(1000.0);
|
||||
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestModel, 0, 0, 1005.83, 0.0, 0.0, 5.83, 1000.0, 0.0);
|
||||
checkPeriod(interestModel, 0, 1, 1005.83, 0.005833333333, 5.83, 1000.0, 0.0);
|
||||
|
||||
final LocalDate dueDate = LocalDate.of(2024, 2, 1);
|
||||
final LocalDate startDay = LocalDate.of(2024, 1, 1);
|
||||
|
||||
// TODO: work on interest calculation
|
||||
// emiCalculator.payInterest(interestModel, dueDate, startDay.plusDays(3), toMoney(0.56));
|
||||
// emiCalculator.chargebackInterest(interestModel, startDay.plusDays(3), toMoney(0.0));
|
||||
// emiCalculator.addBalanceCorrection(interestModel, startDay.plusDays(3), toMoney(0.0));
|
||||
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 1, 0.19, 0.19);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 2, 0.19, 0.38);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 3, 0.18, 0.56);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 4, 0.19, 0.75);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 5, 0.19, 0.94);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 6, 0.19, 1.13);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 7, 0.19, 1.32);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 8, 0.19, 1.51);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 9, 0.18, 1.69);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 10, 0.19, 1.88);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 11, 0.19, 2.07);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 12, 0.19, 2.26);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 13, 0.19, 2.45);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 14, 0.18, 2.63);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 15, 0.19, 2.82);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 16, 0.19, 3.01);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 17, 0.19, 3.20);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 18, 0.19, 3.39);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 19, 0.19, 3.58);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 20, 0.18, 3.76);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 21, 0.19, 3.95);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 22, 0.19, 4.14);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 23, 0.19, 4.33);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 24, 0.19, 4.52);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 25, 0.18, 4.7);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 26, 0.19, 4.89);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 27, 0.19, 5.08);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 28, 0.19, 5.27);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 29, 0.19, 5.46);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 30, 0.19, 5.65);
|
||||
checkDailyInterest(interestModel, dueDate, startDay, 31, 0.18, 5.83);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_dailyInterest_disbursedAmt2000_dayInYears360_daysInMonth30_repayIn2Month() {
|
||||
|
||||
@@ -2216,6 +2284,530 @@ class ProgressiveEMICalculatorTest {
|
||||
checkPeriod(interestSchedule, 5, 867.65, 10.03, 857.62, 0.00, false);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ChargeBackPrincipalAmt100DayInYears360DaysInMonth30RepayEvery1MonthTests {
|
||||
|
||||
@Test
|
||||
public void test_S1_full_chargeback_on_due_date_before_maturity_date() {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
|
||||
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
|
||||
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
|
||||
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
|
||||
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
|
||||
|
||||
// repay 1st period
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(16.43));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(0.58));
|
||||
// repay 2nd period
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(16.52));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(0.49));
|
||||
|
||||
// full chargeback on duedate
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(17.01));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 84.06);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.49, 33.53, 50.53);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.81);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 17.00);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.10, 0.005833333333, 0.1, 17.00, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_S2_S3_partial_and_full_chargeback_on_due_date_before_maturity_date() {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
|
||||
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
|
||||
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
|
||||
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
|
||||
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
|
||||
|
||||
// repay 1st period
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(16.43));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(0.58));
|
||||
// repay 2nd period
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(16.52));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(0.49));
|
||||
|
||||
// partial chargeback
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(15.0));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 82.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.48, 31.53, 50.52);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.80);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.99);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.09, 0.005833333333, 0.1, 16.99, 0.0);
|
||||
|
||||
// full chargeback
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(17.01));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 99.06);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.58, 48.44, 50.62);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.3, 16.71, 33.91);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 17.1);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.20, 0.005833333333, 0.1, 17.1, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_S4_full_chargeback_in_middle_of_instalment_before_maturity_date() {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
|
||||
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
|
||||
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
|
||||
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
|
||||
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
|
||||
|
||||
// repay 1st period
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(16.43));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(0.58));
|
||||
// repay 2nd period
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(16.52));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(0.49));
|
||||
|
||||
// full chargeback on duedate
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 15), toMoney(17.01));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.002634408602, 0.18, 0.45, 33.57, 50.49);
|
||||
checkPeriod(interestSchedule, 2, 1, 17.01, 0.003198924731, 0.27, 0.45, 33.57, 50.49);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.77);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.96);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.06, 0.005833333333, 0.1, 16.96, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_chargeback_principalAndInterest_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
|
||||
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
|
||||
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
|
||||
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
|
||||
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
|
||||
|
||||
// repay 1st period
|
||||
LocalDate txnDate = LocalDate.of(2024, 2, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
|
||||
|
||||
// repay 2nd period
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
|
||||
|
||||
// chargeback
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52));
|
||||
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 83.57);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.98, 33.04, 50.53);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.81);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 17.0);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.10, 0.005833333333, 0.1, 17.0, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_chargeback_principalAndInterest_Amt100_dayInYears360_daysInMonth30_repayEvery1Month__() {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
|
||||
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
|
||||
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
|
||||
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
|
||||
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
|
||||
|
||||
// repay 1st period
|
||||
LocalDate txnDate = LocalDate.of(2024, 2, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
|
||||
|
||||
// repay 2nd period
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
|
||||
|
||||
// chargeback
|
||||
txnDate = LocalDate.of(2024, 7, 1);
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52));
|
||||
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
|
||||
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 0.59, 33.42, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_chargeback_less_principal_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
|
||||
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
|
||||
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
|
||||
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
|
||||
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
|
||||
|
||||
// repay 1st period
|
||||
LocalDate txnDate = LocalDate.of(2024, 2, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
|
||||
|
||||
// repay 2nd period
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
|
||||
|
||||
// chargeback
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(14.51));
|
||||
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 81.56);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.97, 31.04, 50.52);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.80);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.99);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.09, 0.005833333333, 0.1, 16.99, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_chargeback_less_principal_and_no_chargeback_interest_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
|
||||
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
|
||||
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
|
||||
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
|
||||
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
|
||||
|
||||
// repay 1st period
|
||||
LocalDate txnDate = LocalDate.of(2024, 2, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
|
||||
|
||||
// repay 2nd period
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
|
||||
|
||||
// chargeback
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(15.0));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 82.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.48, 31.53, 50.52);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.80);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.99);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.09, 0.005833333333, 0.1, 16.99, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_multi_chargeback_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
|
||||
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
|
||||
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
|
||||
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
|
||||
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
|
||||
|
||||
// repay 1st period
|
||||
LocalDate txnDate = LocalDate.of(2024, 2, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
|
||||
|
||||
// repay 2nd period
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
|
||||
|
||||
// chargeback 1st
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(15.0));
|
||||
// chargeback 2nd
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52));
|
||||
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 98.57);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 1.06, 47.96, 50.61);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.30, 16.71, 33.90);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 17.09);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.19, 0.005833333333, 0.10, 17.09, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_s5_chargeback_in_period_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
|
||||
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
|
||||
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
|
||||
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
|
||||
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
|
||||
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
|
||||
|
||||
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
|
||||
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
|
||||
|
||||
// repay 1st period
|
||||
LocalDate txnDate = LocalDate.of(2024, 2, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
|
||||
|
||||
// repay 2nd period
|
||||
txnDate = LocalDate.of(2024, 3, 1);
|
||||
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
|
||||
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
|
||||
|
||||
// chargeback
|
||||
txnDate = LocalDate.of(2024, 3, 15);
|
||||
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52));
|
||||
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
|
||||
|
||||
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 4, 1), txnDate);
|
||||
Assertions.assertEquals(33.08, toDouble(interestSchedule.repaymentPeriods().get(2).getDuePrincipal()));
|
||||
Assertions.assertEquals(33.35, toDouble(dueAmounts.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.67, toDouble(dueAmounts.getDueInterest()));
|
||||
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
|
||||
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
|
||||
checkPeriod(interestSchedule, 2, 0, 17.01, 0.002634408602, 0.18, 0.94, 33.08, 50.49);
|
||||
checkPeriod(interestSchedule, 2, 1, 17.01, 0.003198924731, 0.76, 0.94, 33.08, 50.49);
|
||||
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.77);
|
||||
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 16.96);
|
||||
checkPeriod(interestSchedule, 5, 0, 17.06, 0.005833333333, 0.10, 16.96, 0.0);
|
||||
}
|
||||
|
||||
private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
|
||||
final Money zeroAmount = Money.zero(currency);
|
||||
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
|
||||
@@ -2234,11 +2826,10 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
private static void checkDailyInterest(final ProgressiveLoanInterestScheduleModel interestModel, final LocalDate repaymentPeriodDueDate,
|
||||
final LocalDate interestStartDay, final int dayOffset, final double dailyInterest, final double interest) {
|
||||
Money previousInterest = emiCalculator
|
||||
.getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset - 1)).getDueInterest();
|
||||
Money currentInterest = emiCalculator.getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset))
|
||||
.getDueInterest();
|
||||
|
||||
Money previousInterest = emiCalculator.getPeriodInterestTillDate(interestModel, repaymentPeriodDueDate,
|
||||
interestStartDay.plusDays(dayOffset - 1), true);
|
||||
Money currentInterest = emiCalculator.getPeriodInterestTillDate(interestModel, repaymentPeriodDueDate,
|
||||
interestStartDay.plusDays(dayOffset), true);
|
||||
Assertions.assertEquals(dailyInterest, toDouble(currentInterest.minus(previousInterest)));
|
||||
Assertions.assertEquals(interest, toDouble(currentInterest));
|
||||
}
|
||||
@@ -2275,7 +2866,11 @@ class ProgressiveEMICalculatorTest {
|
||||
private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx,
|
||||
final int interestIdx, final double emiValue, final double rateFactor, final double interestDue,
|
||||
final double interestDueCumulated, final double principalDue, final double remaingBalance) {
|
||||
Assertions.assertTrue(repaymentIdx < interestScheduleModel.repaymentPeriods().size(),
|
||||
repaymentIdx + "th repaymentPeriod is not found.");
|
||||
final RepaymentPeriod repaymentPeriod = interestScheduleModel.repaymentPeriods().get(repaymentIdx);
|
||||
Assertions.assertTrue(interestIdx < repaymentPeriod.getInterestPeriods().size(),
|
||||
repaymentIdx + "th repaymentPeriod's " + interestIdx + "th interest period is not found.");
|
||||
final InterestPeriod interestPeriod = repaymentPeriod.getInterestPeriods().get(interestIdx);
|
||||
|
||||
Assertions.assertAll("Check period", () -> Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi())),
|
||||
|
||||
Reference in New Issue
Block a user