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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user