FINERACT-2162: Chargeback implementation for progressive interest recalculation enabled loan.

This commit is contained in:
Janos Meszaros
2025-02-14 15:16:59 +01:00
committed by Adam Saghy
parent 22f7972f60
commit cdbf0df571
23 changed files with 2303 additions and 176 deletions
@@ -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));
@@ -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 {
@@ -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
@@ -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);
}
@@ -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);
}
});
@@ -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;
@@ -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;
@@ -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());
}
@@ -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;
@@ -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;
@@ -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;
@@ -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.
*
@@ -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);
}