FINERACT-2060: Accrual reverse replay logic and Handling

This commit is contained in:
Marta Jankovics
2024-11-29 19:17:23 +01:00
committed by Adam Saghy
parent 8d25b0c765
commit d57e72ec2f
80 changed files with 2882 additions and 2940 deletions
@@ -73,6 +73,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator;
@@ -95,6 +96,8 @@ import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocat
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
@@ -104,6 +107,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME = "Advanced payment allocation strategy";
public final EMICalculator emiCalculator;
public final LoanRepositoryWrapper loanRepositoryWrapper;
public final InterestRefundService interestRefundService;
@Override
@@ -152,7 +156,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// only for progressive loans
public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessProgressiveLoanTransactions(
LocalDate disbursementDate, LocalDate currentDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
LocalDate disbursementDate, LocalDate targetDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
if (loanTransactions.isEmpty()) {
@@ -179,8 +183,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
final Loan loan = loanTransactions.get(0).getLoan();
final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf();
final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail,
installmentAmountInMultiplesOf, installments, overpaymentHolder.getMoneyObject().getMc());
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments,
loanProductRelatedDetail, installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
changedTransactionDetail, scheduleModel);
@@ -217,7 +221,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId);
createNewTransaction(oldTransaction, newTransaction, ctx);
}
recalculateInterestForDate(currentDate, ctx);
recalculateInterestForDate(targetDate, ctx);
List<LoanTransaction> txs = changeOperations.stream() //
.filter(ChangeOperation::isTransaction) //
.map(e -> e.getLoanTransaction().get()).toList();
@@ -225,6 +229,32 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return Pair.of(changedTransactionDetail, scheduleModel);
}
@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
LocalDate currentDate = DateUtils.getBusinessLocalDate();
return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges)
.getLeft();
}
@NotNull
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) {
Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
List<LoanTransaction> transactions = loan.retrieveListOfTransactionsForReprocessing();
MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency();
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
Set<LoanCharge> charges = loan.getActiveCharges();
return reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), targetDate, transactions, currency, installments, charges)
.getRight();
}
@NotNull
private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) {
LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId());
return newTransaction == null ? transaction : newTransaction;
}
private void processInterestRateChange(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
@@ -249,29 +279,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
});
}
@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
LocalDate currentDate = DateUtils.getBusinessLocalDate();
return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges)
.getLeft();
}
@NotNull
private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) {
LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId());
return newTransaction == null ? transaction : newTransaction;
}
@Override
public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
switch (loanTransaction.getTypeOf()) {
case DISBURSEMENT -> handleDisbursement(loanTransaction, ctx);
case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
case WRITEOFF -> handleWriteOff(loanTransaction, ctx);
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx);
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
case CREDIT_BALANCE_REFUND ->
handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, ctx);
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
handleRepayment(loanTransaction, ctx);
@@ -281,7 +296,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx);
case REAGE -> handleReAge(loanTransaction, ctx);
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx);
// TODO: Cover rest of the transaction types
default -> {
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
@@ -357,9 +372,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
processCreditTransaction(loanTransaction, ctx);
}
protected void handleCreditBalanceRefund(LoanTransaction transaction, TransactionCtx ctx) {
super.handleCreditBalanceRefund(transaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
}
private boolean hasNoCustomCreditAllocationRule(LoanTransaction loanTransaction) {
return (loanTransaction.getLoan().getCreditAllocationRules() == null || !loanTransaction.getLoan().getCreditAllocationRules()
.stream().anyMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
List<LoanCreditAllocationRule> creditAllocationRules = loanTransaction.getLoan().getCreditAllocationRules();
return (creditAllocationRules == null || creditAllocationRules.stream()
.noneMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
}
protected LoanTransaction findChargebackOriginalTransaction(LoanTransaction chargebackTransaction, TransactionCtx ctx) {
@@ -396,77 +416,76 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
} else {
loanTransaction.resetDerivedComponents();
MonetaryCurrency currency = ctx.getCurrency();
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
ctx.getInstallments().sort(byDate);
final Money zeroMoney = Money.zero(ctx.getCurrency());
Money transactionAmount = loanTransaction.getAmount(ctx.getCurrency());
final Money zeroMoney = Money.zero(currency);
Money transactionAmount = loanTransaction.getAmount(currency);
Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject();
Money amountToDistribute = MathUtil.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(totalOverpaid));
Money amountToDistribute = MathUtil.negativeToZero(transactionAmount).minus(totalOverpaid);
Money overpaymentAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute));
loanTransaction.setOverPayments(overpaymentAmount);
if (!transactionAmount.isGreaterThanZero()) {
return;
}
if (!loanTransaction.isChargeback()) {
throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name());
}
if (transactionAmount.isGreaterThanZero()) {
if (loanTransaction.isChargeback()) {
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
// get the original allocation from the opriginal transaction
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction,
ctx.getCurrency());
LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction);
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
// get the original allocation from the opriginal 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);
// 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
Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation,
transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency());
// calculate the current chargeback allocation
Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation,
transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), currency);
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
final LocalDate transactionDate = loanTransaction.getTransactionDate();
boolean loanTransactionMapped = false;
LocalDate pastDueDate = null;
for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) {
pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
loanTransactionMapped = true;
break;
final LocalDate transactionDate = loanTransaction.getTransactionDate();
boolean loanTransactionMapped = false;
LocalDate pastDueDate = null;
for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) {
pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation);
loanTransactionMapped = true;
break;
// If already exists an additional installment just update the due date and
// principal from the Loan chargeback / CBR transaction
} else if (currentInstallment.isAdditional()) {
if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) {
currentInstallment.updateDueDate(transactionDate);
}
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
loanTransactionMapped = true;
break;
}
// If already exists an additional installment just update the due date and
// principal from the Loan chargeback / CBR transaction
} else if (currentInstallment.isAdditional()) {
if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) {
currentInstallment.updateDueDate(transactionDate);
}
// New installment will be added (N+1 scenario)
if (!loanTransactionMapped) {
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments()
.get(ctx.getInstallments().size() - 1);
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
} else {
Loan loan = loanTransaction.getLoan();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
(ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(),
zeroMoney.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), false, null);
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, installment, chargebackAllocation);
installment.markAsAdditional();
loan.addLoanRepaymentScheduleInstallment(installment);
}
}
allocateOverpayment(loanTransaction, ctx);
} else {
throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name());
recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation);
loanTransactionMapped = true;
break;
}
}
// New installment will be added (N+1 scenario)
if (!loanTransactionMapped) {
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments().get(ctx.getInstallments().size() - 1);
recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation);
} else {
Loan loan = loanTransaction.getLoan();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
(ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(), zeroMoney.getAmount(),
zeroMoney.getAmount(), zeroMoney.getAmount(), false, null);
recognizeAmountsAfterChargeback(ctx, transactionDate, installment, chargebackAllocation);
installment.markAsAdditional();
loan.addLoanRepaymentScheduleInstallment(installment);
}
}
allocateOverpayment(loanTransaction, ctx);
}
}
@@ -511,14 +530,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return allocation;
}
private void recognizeAmountsAfterChargeback(MonetaryCurrency currency, LocalDate localDate,
private void recognizeAmountsAfterChargeback(TransactionCtx ctx, LocalDate transactionDate,
LoanRepaymentScheduleInstallment installment, Map<AllocationType, Money> chargebackAllocation) {
Money principal = chargebackAllocation.get(PRINCIPAL);
if (principal.isGreaterThanZero()) {
installment.addToCreditedPrincipal(principal.getAmount());
installment.addToPrincipal(localDate, principal);
installment.addToPrincipal(transactionDate, principal);
}
MonetaryCurrency currency = ctx.getCurrency();
Money fee = chargebackAllocation.get(FEE);
if (fee.isGreaterThanZero()) {
installment.addToCreditedFee(fee.getAmount());
@@ -577,9 +597,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return relation -> relation.getRelationType().equals(typeEnum) && Objects.equals(relation.getToTransaction().getId(), id);
}
@Override
protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
protected void handleRefund(LoanTransaction loanTransaction, TransactionCtx ctx) {
MonetaryCurrency currency = ctx.getCurrency();
Money zero = Money.zero(currency);
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
@@ -604,18 +623,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList())));
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, currency, installments,
transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule,
transactionMappings, charges, balances);
transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, ctx, transactionAmountUnprocessed,
paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule, transactionMappings, balances);
if (!transactionAmountUnprocessed.isGreaterThanZero()) {
break;
}
}
} else if (scheduleProcessingType.isVertical()) {
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero,
transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, charges, balances,
paymentAllocationType);
transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, ctx, transactionMappings,
transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType);
if (!transactionAmountUnprocessed.isGreaterThanZero()) {
break;
}
@@ -639,7 +656,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// Reset derived component of new loan transaction and re-process transaction
processLatestTransaction(processTransaction, ctx);
if (loanTransaction.isInterestWaiver()) {
processTransaction.adjustInterestComponent(ctx.getCurrency());
processTransaction.adjustInterestComponent();
}
if (isNew) {
checkRegisteredNewTransaction(loanTransaction, ctx);
@@ -684,7 +701,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
processTransaction.addLoanTransactionToRepaymentScheduleMappings(transactionMappings);
if (processTransaction.isInterestWaiver()) {
processTransaction.adjustInterestComponent(currency);
processTransaction.adjustInterestComponent();
}
if (isNew) {
processTransaction = checkRegisteredNewTransaction(transaction, ctx);
@@ -810,51 +827,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return changeOperations;
}
private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) transactionCtx;
if (progressiveTransactionCtx.getModel() == null) {
throw new IllegalStateException("TransactionCtx has no model");
}
disbursementTransaction.resetDerivedComponents();
final MathContext mc = MoneyHelper.getMathContext();
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
Money downPaymentAmount = Money.zero(progressiveTransactionCtx.getCurrency());
if (loanProductRelatedDetail.isEnableDownPayment()) {
LoanRepaymentScheduleInstallment downPaymentInstallment = progressiveTransactionCtx.getInstallments().stream()
.filter(i -> i.isDownPayment() && i.getPrincipal(progressiveTransactionCtx.getCurrency()).isZero()).findFirst()
.orElseThrow();
BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(),
loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc);
if (installmentAmountInMultiplesOf != null) {
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
}
downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt);
downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount);
}
Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount);
emiCalculator.addDisbursement(progressiveTransactionCtx.getModel(), disbursementTransaction.getTransactionDate(),
amortizableAmount);
if (amortizableAmount.isGreaterThanZero()) {
progressiveTransactionCtx.getModel().repaymentPeriods().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = transactionCtx.getInstallments().stream()
.filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment()
&& !ri.getDueDate().isBefore(disbursementTransaction.getTransactionDate()))
.findFirst().orElse(null);
if (installment != null) {
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
installment.updateInterestCharged(rm.getDueInterest().getAmount());
installment.updateObligationsMet(progressiveTransactionCtx.getCurrency(), disbursementTransaction.getTransactionDate());
}
});
}
allocateOverpayment(disbursementTransaction, transactionCtx);
}
private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging
if (disbursementTransaction.getLoan().isInterestBearing()) {
@@ -864,43 +836,90 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
disbursementTransaction.resetDerivedComponents();
private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
ProgressiveLoanInterestScheduleModel model;
if (!(transactionCtx instanceof ProgressiveTransactionCtx)
|| (model = ((ProgressiveTransactionCtx) transactionCtx).getModel()) == null) {
throw new IllegalStateException("TransactionCtx has no model");
}
final MathContext mc = MoneyHelper.getMathContext();
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = transactionCtx.getInstallments().stream().filter(
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
.toList();
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
Money downPaymentAmount = Money.zero(transactionCtx.getCurrency());
Loan loan = disbursementTransaction.getLoan();
LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf();
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
LocalDate transactionDate = disbursementTransaction.getTransactionDate();
MonetaryCurrency currency = transactionCtx.getCurrency();
Money downPaymentAmount = Money.zero(currency);
if (loanProductRelatedDetail.isEnableDownPayment()) {
LoanRepaymentScheduleInstallment downPaymentInstallment = transactionCtx.getInstallments().stream()
.filter(i -> i.isDownPayment() && i.getPrincipal(transactionCtx.getCurrency()).isZero()).findFirst().orElseThrow();
BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(),
loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc);
if (installmentAmountInMultiplesOf != null) {
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
}
downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt);
downPaymentAmount = Money.of(currency, downPaymentAmt);
LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream()
.filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow();
downPaymentInstallment.addToPrincipal(transactionDate, downPaymentAmount);
}
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
emiCalculator.addDisbursement(model, transactionDate, amortizableAmount);
disbursementTransaction.resetDerivedComponents();
if (amortizableAmount.isGreaterThanZero()) {
model.repaymentPeriods().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
.findFirst().orElse(null);
if (installment != null) {
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
installment.updateInterestCharged(rm.getDueInterest().getAmount());
installment.updateObligationsMet(currency, transactionDate);
}
});
}
allocateOverpayment(disbursementTransaction, transactionCtx);
}
private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
disbursementTransaction.resetDerivedComponents();
final MathContext mc = MoneyHelper.getMathContext();
MonetaryCurrency currency = transactionCtx.getCurrency();
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
.toList();
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
Money downPaymentAmount = Money.zero(currency);
if (loanProductRelatedDetail.isEnableDownPayment()) {
LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream()
.filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow();
BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(),
loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc);
if (installmentAmountInMultiplesOf != null) {
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
}
downPaymentAmount = Money.of(currency, downPaymentAmt);
downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount);
}
Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount);
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
if (amortizableAmount.isGreaterThanZero()) {
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext());
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
candidateRepaymentInstallments.forEach(i -> {
Money previousPrincipal = i.getPrincipal(transactionCtx.getCurrency());
Money previousPrincipal = i.getPrincipal(currency);
Money newPrincipal = previousPrincipal.add(increasePrincipalBy);
if (installmentAmountInMultiplesOf != null) {
newPrincipal = Money.roundToMultiplesOf(newPrincipal, installmentAmountInMultiplesOf);
}
i.updatePrincipal(newPrincipal.getAmount());
moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal));
i.updateObligationsMet(transactionCtx.getCurrency(), disbursementTransaction.getTransactionDate());
i.updateObligationsMet(currency, disbursementTransaction.getTransactionDate());
});
// Hence the rounding, we might need to amend the last installment amount
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
@@ -930,26 +949,30 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
private List<LoanRepaymentScheduleInstallment> findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate currentDate,
protected void handleWriteOff(final LoanTransaction transaction, TransactionCtx ctx) {
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
}
private List<LoanRepaymentScheduleInstallment> findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate targetDate,
ProgressiveTransactionCtx transactionCtx) {
return transactionCtx.getInstallments().stream() //
.filter(installment -> !installment.isDownPayment() && !installment.isAdditional())
.filter(installment -> installment.isOverdueOn(currentDate))
.filter(installment -> installment.isOverdueOn(targetDate))
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList();
}
private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransactionCtx ctx) {
private void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()
&& ctx.getInstallments().get(0).getLoan().isInterestRecalculationEnabledForProduct()
&& ctx.getInstallments().get(0).getLoan().getLoanProductRelatedDetail().isInterestRecalculationEnabled()
&& !ctx.getInstallments().get(0).getLoan().isNpa() && !ctx.getInstallments().get(0).getLoan().isChargedOff()) {
List<LoanRepaymentScheduleInstallment> overdueInstallmentsSortedByInstallmentNumber = findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(
currentDate, ctx);
targetDate, ctx);
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
List<LoanRepaymentScheduleInstallment> normalInstallments = ctx.getInstallments().stream() //
.filter(installment -> !installment.isAdditional() && !installment.isDownPayment()).toList();
Optional<LoanRepaymentScheduleInstallment> currentInstallmentOptional = normalInstallments.stream().filter(
installment -> installment.getFromDate().isBefore(currentDate) && !installment.getDueDate().isBefore(currentDate))
installment -> installment.getFromDate().isBefore(targetDate) && !installment.getDueDate().isBefore(targetDate))
.findAny();
// get DUE installment or last installment
@@ -962,7 +985,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
for (LoanRepaymentScheduleInstallment processingInstallment : overdueInstallmentsSortedByInstallmentNumber) {
// add and subtract outstanding principal
if (!overDuePrincipal.isZero()) {
adjustOverduePrincipalForInstallment(currentDate, processingInstallment, overDuePrincipal,
adjustOverduePrincipalForInstallment(targetDate, processingInstallment, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
}
@@ -970,11 +993,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
}
boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(currentDate);
boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(targetDate);
if (adjustNeeded) {
adjustOverduePrincipalForInstallment(currentDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal,
ctx);
adjustOverduePrincipalForInstallment(targetDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal, ctx);
}
}
}
@@ -1078,15 +1099,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set<LoanCharge> chargesOfInstallment,
Balances balances, LoanRepaymentScheduleInstallment.PaymentAction action) {
AllocationType allocationType = paymentAllocationType.getAllocationType();
MonetaryCurrency currency = loanTransaction.getLoan().getCurrency();
Money zero = Money.zero(currency);
LocalDate transactionDate = loanTransaction.getTransactionDate();
Money zero = transactionAmountUnprocessed.zero();
LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment.getPaymentFunction(allocationType, action);
ChargesPaidByFunction chargesPaidByFunction = getChargesPaymentFunction(action);
Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed);
switch (paymentAllocationType.getAllocationType()) {
switch (allocationType) {
case PENALTY -> {
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion);
@@ -1109,7 +1130,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, loanTransaction.getLoan().loanCurrency());
currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return portion;
}
@@ -1130,13 +1151,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty);
}
private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction, MoneyHolder overpaymentHolder) {
if (overpaymentPortion.isGreaterThanZero()) {
private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
MoneyHolder overpaymentHolder = transactionCtx.getOverpaymentHolder();
if (MathUtil.isGreaterThanZero(overpaymentPortion)) {
onLoanOverpayment(loanTransaction, overpaymentPortion);
overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().add(overpaymentPortion));
loanTransaction.setOverPayments(overpaymentPortion);
} else {
overpaymentHolder.setMoneyObject(overpaymentPortion.zero());
overpaymentHolder.setMoneyObject(Money.zero(transactionCtx.getCurrency()));
}
}
@@ -1210,11 +1232,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
private Money refundTransactionHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
private Money refundTransactionHorizontally(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) {
MonetaryCurrency currency = ctx.getCurrency();
Money zero = Money.zero(currency);
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
Set<LoanCharge> charges = ctx.getCharges();
Money refundedPortion;
outerLoop: do {
LoanRepaymentScheduleInstallment latestPastDueInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency,
@@ -1284,13 +1308,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return transactionAmountUnprocessed;
}
private Money refundTransactionVertically(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money zero,
private Money refundTransactionVertically(LoanTransaction loanTransaction, TransactionCtx ctx,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed,
FutureInstallmentAllocationRule futureInstallmentAllocationRule, Set<LoanCharge> charges, Balances balances,
FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances,
PaymentAllocationType paymentAllocationType) {
LoanRepaymentScheduleInstallment currentInstallment = null;
MonetaryCurrency currency = ctx.getCurrency();
Money zero = Money.zero(currency);
Money refundedPortion = zero;
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
Set<LoanCharge> charges = ctx.getCharges();
LoanRepaymentScheduleInstallment currentInstallment = null;
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
do {
switch (paymentAllocationType.getDueType()) {
@@ -1401,7 +1428,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder());
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx);
}
private Money processPeriods(LoanTransaction transaction, Money processAmount, Set<LoanCharge> charges,
@@ -1413,7 +1440,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private Money processPeriods(LoanTransaction transaction, Money processAmount, LoanPaymentAllocationRule allocationRule,
Set<LoanCharge> charges, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances,
TransactionCtx transactionCtx) {
MonetaryCurrency currency = transactionCtx.getCurrency();
LoanScheduleProcessingType scheduleProcessingType = transaction.getLoan().getLoanProductRelatedDetail()
.getLoanScheduleProcessingType();
if (scheduleProcessingType.isHorizontal()) {
@@ -1421,8 +1447,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
balances);
}
if (scheduleProcessingType.isVertical()) {
return processPeriodsVertically(transaction, currency, transactionCtx.getInstallments(), processAmount, allocationRule,
transactionMappings, charges, balances);
return processPeriodsVertically(transaction, transactionCtx, processAmount, allocationRule, transactionMappings, charges,
balances);
}
return processAmount;
}
@@ -1446,7 +1472,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Money transactionAmountUnprocessed, List<PaymentAllocationType> paymentAllocationTypes,
FutureInstallmentAllocationRule futureInstallmentAllocationRule,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
if (transactionAmountUnprocessed.isZero()) {
if (MathUtil.isEmpty(transactionAmountUnprocessed)) {
return transactionAmountUnprocessed;
}
@@ -1589,7 +1615,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (DueType.IN_ADVANCE.equals(paymentAllocationType.getDueType())) {
payDate = calculateNewPayDateInCaseOfInAdvancePayment(loanTransaction, installment);
updateRepaymentPeriodBalances(paymentAllocationType, installment, model, payDate);
updateRepaymentPeriodBalances(paymentAllocationType, installment, ctx, payDate);
}
paidPortion = processPaymentAllocation(paymentAllocationType, installment, loanTransaction, transactionAmountUnprocessed,
@@ -1597,17 +1623,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (PRINCIPAL.equals(paymentAllocationType.getAllocationType())) {
emiCalculator.payPrincipal(model, installment.getDueDate(), payDate, paidPortion);
updateRepaymentPeriods(loanTransaction, ctx, model);
updateRepaymentPeriods(loanTransaction, ctx);
} else if (INTEREST.equals(paymentAllocationType.getAllocationType())) {
emiCalculator.payInterest(model, installment.getDueDate(), payDate, paidPortion);
updateRepaymentPeriods(loanTransaction, ctx, model);
updateRepaymentPeriods(loanTransaction, ctx);
}
return paidPortion;
}
private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx,
ProgressiveLoanInterestScheduleModel model) {
model.repaymentPeriods().forEach(rm -> {
private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) {
ctx.getModel().repaymentPeriods().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream()
.filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment()).findFirst().orElse(null);
if (installment != null) {
@@ -1619,8 +1644,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
private void updateRepaymentPeriodBalances(PaymentAllocationType paymentAllocationType,
LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveLoanInterestScheduleModel model, LocalDate payDate) {
PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(model, inAdvanceInstallment.getDueDate(), payDate);
LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveTransactionCtx ctx, LocalDate payDate) {
PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(ctx.getModel(), inAdvanceInstallment.getDueDate(), payDate);
switch (paymentAllocationType) {
case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getDueInterest().getAmount());
@@ -1637,6 +1662,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LocalDate payDate = switch (strategy) {
case TILL_PRE_CLOSURE_DATE -> loanTransaction.getTransactionDate();
// TODO use isInPeriod
case TILL_REST_FREQUENCY_DATE -> loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getFromDate()) //
&& !loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getDueDate()) //
? inAdvanceInstallment.getDueDate() //
@@ -1655,10 +1681,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.collect(Collectors.toSet());
}
private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
private Money processPeriodsVertically(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Set<LoanCharge> charges, Balances balances) {
MonetaryCurrency currency = ctx.getCurrency();
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) {
FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
@@ -1823,6 +1850,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
reprocessInstallmentsOrder(installments);
}
protected void calculateAccrualActivity(LoanTransaction transaction, TransactionCtx ctx) {
super.calculateAccrualActivity(transaction, ctx.getCurrency(), ctx.getInstallments());
}
private void reprocessInstallmentsOrder(List<LoanRepaymentScheduleInstallment> installments) {
AtomicInteger counter = new AtomicInteger(1);
installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate)
@@ -84,10 +84,13 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
}
public Money getCalculatedDueInterest() {
final BigDecimal interestDueTillRepaymentDueDate = getOutstandingLoanBalance()//
.multipliedBy(getRateFactorTillPeriodDueDate(), mc).getAmount() //
.divide(BigDecimal.valueOf(getLengthTillPeriodDueDate()), mc) //
.multiply(BigDecimal.valueOf(getLength()), mc); //
long lengthTillPeriodDueDate = getLengthTillPeriodDueDate();
final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 //
? BigDecimal.ZERO //
: getOutstandingLoanBalance() //
.multipliedBy(getRateFactorTillPeriodDueDate(), mc).getAmount() //
.divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) //
.multiply(BigDecimal.valueOf(getLength()), mc); //
return Money.of(outstandingLoanBalance.getCurrency(), interestDueTillRepaymentDueDate, mc);
}
@@ -103,8 +106,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
if (isFirstInterestPeriod()) {
Optional<RepaymentPeriod> previousRepaymentPeriod = getRepaymentPeriod().getPrevious();
if (previousRepaymentPeriod.isPresent()) {
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getInterestPeriods()
.get(previousRepaymentPeriod.get().getInterestPeriods().size() - 1);
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod();
this.outstandingLoanBalance = previousInterestPeriod.getOutstandingLoanBalance()//
.plus(previousInterestPeriod.getDisbursementAmount(), mc)//
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)//
@@ -120,7 +122,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
}
}
private boolean isFirstInterestPeriod() {
return getRepaymentPeriod().getInterestPeriods().get(0).equals(this);
public boolean isFirstInterestPeriod() {
return this.equals(getRepaymentPeriod().getFirstInterestPeriod());
}
}
@@ -18,6 +18,9 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
@@ -83,7 +86,7 @@ public class ProgressiveLoanInterestScheduleModel {
private BigDecimal findInterestRate(final LocalDate effectiveDate) {
return interestRates.stream() //
.filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)) //
.filter(ir -> !DateUtils.isAfter(ir.effectiveFrom(), effectiveDate)) //
.map(InterestRate::interestRate) //
.findFirst() //
.orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); //
@@ -93,12 +96,12 @@ public class ProgressiveLoanInterestScheduleModel {
interestRates.add(new InterestRate(newInterestEffectiveDate, newInterestRate));
}
public Optional<RepaymentPeriod> findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) {
public Optional<RepaymentPeriod> findRepaymentPeriodByDueDate(final LocalDate repaymentPeriodDueDate) {
if (repaymentPeriodDueDate == null) {
return Optional.empty();
}
return repaymentPeriods.stream()//
.filter(repaymentPeriodItem -> repaymentPeriodItem.getDueDate().isEqual(repaymentPeriodDueDate))//
.filter(repaymentPeriodItem -> DateUtils.isEqual(repaymentPeriodItem.getDueDate(), repaymentPeriodDueDate))//
.findFirst();
}
@@ -107,7 +110,7 @@ public class ProgressiveLoanInterestScheduleModel {
return repaymentPeriods;
}
return repaymentPeriods.stream()//
.filter(period -> !period.getDueDate().isBefore(calculateFromRepaymentPeriodDueDate))//
.filter(period -> !DateUtils.isBefore(period.getDueDate(), calculateFromRepaymentPeriodDueDate))//
.toList();//
}
@@ -120,6 +123,10 @@ public class ProgressiveLoanInterestScheduleModel {
return DateUtils.getExactDifferenceInDays(firstPeriod.getFromDate(), lastPeriod.getDueDate());
}
public LocalDate getStartDate() {
return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(0).getFromDate() : null;
}
public LocalDate getMaturityDate() {
return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(repaymentPeriods.size() - 1).getDueDate() : null;
}
@@ -135,6 +142,7 @@ public class ProgressiveLoanInterestScheduleModel {
if (balanceChangeDate == null) {
return Optional.empty();
}
// TODO use isInPeriod
return repaymentPeriods.stream()//
.filter(repaymentPeriod -> {
final boolean isFirstPeriod = repaymentPeriod.getPrevious().isEmpty();
@@ -214,4 +222,10 @@ public class ProgressiveLoanInterestScheduleModel {
public Money getTotalPaidPrincipal() {
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidPrincipal).reduce(getZero(), Money::plus);
}
public Optional<RepaymentPeriod> findRepaymentPeriod(@NotNull LocalDate transactionDate) {
return repaymentPeriods.stream() //
.filter(period -> isInPeriod(transactionDate, period.getFromDate(), period.getDueDate(), period.isFirstRepaymentPeriod()))//
.findFirst();
}
}
@@ -18,6 +18,9 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
@@ -106,6 +109,7 @@ public class RepaymentPeriod {
return interestPeriods.stream().map(InterestPeriod::getRateFactor).reduce(BigDecimal.ONE, BigDecimal::add);
}
@NotNull
public Money getCalculatedDueInterest() {
if (calculatedDueInterestCalculation == null) {
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest,
@@ -123,23 +127,6 @@ public class RepaymentPeriod {
return calculatedDueInterest;
}
private Money getZero(MathContext mc) {
// EMI is always initiated
return this.emi.zero(mc);
}
public Money getCalculatedDuePrincipal() {
return getEmi().minus(getCalculatedDueInterest(), mc);
}
public Money getTotalPaidAmount() {
return getPaidPrincipal().plus(getPaidInterest());
}
public boolean isFullyPaid() {
return getEmi().isEqualTo(getTotalPaidAmount());
}
public Money getDueInterest() {
if (dueInterestCalculation == null) {
// Due interest might be the maximum paid if there is pay-off or early repayment
@@ -150,11 +137,23 @@ public class RepaymentPeriod {
return dueInterestCalculation.get();
}
public Money getCalculatedDuePrincipal() {
return getEmi().minus(getCalculatedDueInterest(), mc);
}
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);
}
public Money getTotalPaidAmount() {
return getPaidPrincipal().plus(getPaidInterest());
}
public boolean isFullyPaid() {
return getEmi().isEqualTo(getTotalPaidAmount());
}
public Money getUnrecognizedInterest() {
return getCalculatedDueInterest().minus(getDueInterest(), mc);
}
@@ -193,4 +192,29 @@ public class RepaymentPeriod {
(m1, m2) -> m1.plus(m2, mc));
return initialBalance.add(totalDisbursedAmount, mc);
}
private Money getZero(MathContext mc) {
// EMI is always initiated
return this.emi.zero(mc);
}
public InterestPeriod getFirstInterestPeriod() {
return getInterestPeriods().get(0);
}
public InterestPeriod getLastInterestPeriod() {
List<InterestPeriod> interestPeriods = getInterestPeriods();
return interestPeriods.get(interestPeriods.size() - 1);
}
public Optional<InterestPeriod> findInterestPeriod(@NotNull LocalDate transactionDate) {
return interestPeriods.stream() //
.filter(interestPeriod -> isInPeriod(transactionDate, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
isFirstRepaymentPeriod() && interestPeriod.isFirstInterestPeriod()))//
.reduce((one, two) -> two);
}
public boolean isFirstRepaymentPeriod() {
return previous == null;
}
}
@@ -18,6 +18,9 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.findInPeriod;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
@@ -39,7 +42,6 @@ import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
@@ -89,7 +91,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
// generate list of proposed schedule due dates
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
periodStartDate, loanApplicationTerms, holidayDetailDTO);
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel(
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetail(),
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size());
@@ -147,6 +149,61 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
}
@Override
public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan,
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
LocalDate rescheduleFrom) {
LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO);
return LoanScheduleDTO.from(null, model);
}
@Override
public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) {
throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor");
}
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
LoanRepaymentScheduleInstallment actualInstallment = findInPeriod(onDate, installments).orElse(installments.get(0));
LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) {
case TILL_PRE_CLOSURE_DATE -> onDate;
case TILL_REST_FREQUENCY_DATE -> actualInstallment.getDueDate(); // due date of current installment
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
};
ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), onDate);
OutstandingDetails outstandingAmounts = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate);
// TODO: We should add all the past due outstanding amounts as well
OutstandingAmountsDTO result = new OutstandingAmountsDTO(currency) //
.principal(outstandingAmounts.getOutstandingPrincipal()) //
.interest(outstandingAmounts.getOutstandingInterest());//
installments.forEach(installment -> result //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));
return result;
}
@Override
public Money getPeriodInterestTillDate(@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate) {
Loan loan = installment.getLoan();
LoanRepaymentScheduleTransactionProcessor transactionProcessor = loan.getTransactionProcessor();
if (!(transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) {
throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor");
}
if (installment.isAdditional() || installment.isDownPayment() || installment.isReAged()) {
return Money.zero(loan.getCurrency());
}
ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), targetDate);
return emiCalculator.getPeriodInterestTillDate(model, installment.getDueDate(), targetDate);
}
// Private, internal methods
private List<DisbursementData> getSortedDisbursementList(LoanApplicationTerms loanApplicationTerms) {
final List<DisbursementData> disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas());
disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate));
@@ -238,49 +295,6 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
}
}
@Override
public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan,
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
LocalDate rescheduleFrom) {
LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO);
return LoanScheduleDTO.from(null, model);
}
@Override
public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) {
throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor");
}
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
LoanRepaymentScheduleInstallment actualInstallment = LoanRepaymentScheduleProcessingWrapper.findInPeriod(onDate, installments)
.orElse(installments.get(0));
LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) {
case TILL_PRE_CLOSURE_DATE -> onDate;
case TILL_REST_FREQUENCY_DATE -> actualInstallment.getDueDate(); // due date of current installment
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
};
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), onDate,
loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight();
OutstandingDetails result = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate);
// TODO: We should add all the past due outstanding amounts as well
OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) //
.principal(result.getOutstandingPrincipal()) //
.interest(result.getOutstandingInterest());//
installments.forEach(installment -> amounts //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));
return amounts;
}
// Private, internal methods
private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharge> loanCharges) {
BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO;
if (loanCharges != null) {
@@ -430,5 +444,4 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
}
return interestCharges;
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.calc;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
@@ -34,11 +35,14 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail
public interface EMICalculator {
ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(List<LoanScheduleModelRepaymentPeriod> periods,
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
@NotNull
ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
@NotNull LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc);
@NotNull
ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel(
@NotNull List<LoanRepaymentScheduleInstallment> installments, @NotNull LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf, MathContext mc);
Optional<RepaymentPeriod> findRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate dueDate);
@@ -56,7 +60,13 @@ public interface EMICalculator {
void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
Money principalAmount);
PeriodDueDetails getDueAmounts(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate);
@NotNull
PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
@NotNull LocalDate targetDate);
@NotNull
Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
@NotNull LocalDate targetDate);
Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
LocalDate targetDate);
@@ -18,14 +18,16 @@
*/
package org.apache.fineract.portfolio.loanproduct.calc;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.Year;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
@@ -42,7 +44,6 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLo
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
@Component
@@ -53,20 +54,36 @@ public final class ProgressiveEMICalculator implements EMICalculator {
private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7);
@Override
public ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(final List<LoanScheduleModelRepaymentPeriod> periods,
final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
final Money zeroAmount = Money.zero(loanProductRelatedDetail.getCurrency(), mc);
final ArrayList<RepaymentPeriod> interestRepaymentModelList = new ArrayList<>(periods.size());
RepaymentPeriod previousPeriod = null;
for (final LoanScheduleModelRepaymentPeriod period : periods) {
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, period.periodFromDate(), period.periodDueDate(), zeroAmount,
mc);
previousPeriod = currentPeriod;
interestRepaymentModelList.add(currentPeriod);
@NotNull
public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
@NotNull LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf,
final MathContext mc) {
return generateInterestScheduleModel(periods, LoanScheduleModelRepaymentPeriod::periodFromDate,
LoanScheduleModelRepaymentPeriod::periodDueDate, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
}
}
return new ProgressiveLoanInterestScheduleModel(interestRepaymentModelList, loanProductRelatedDetail,
installmentAmountInMultiplesOf, mc);
@Override
@NotNull
public ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel(
@NotNull List<LoanRepaymentScheduleInstallment> installments, @NotNull LoanProductRelatedDetail loanProductRelatedDetail,
final Integer installmentAmountInMultiplesOf, final MathContext mc) {
installments = installments.stream().filter(installment -> !installment.isDownPayment() && !installment.isAdditional()).toList();
return generateInterestScheduleModel(installments, LoanRepaymentScheduleInstallment::getFromDate,
LoanRepaymentScheduleInstallment::getDueDate, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
}
@NotNull
private <T> ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(@NotNull List<T> periods, Function<T, LocalDate> from,
Function<T, LocalDate> to, @NotNull LoanProductRelatedDetail loanProductRelatedDetail,
final Integer installmentAmountInMultiplesOf, final MathContext mc) {
final Money zero = Money.zero(loanProductRelatedDetail.getCurrency(), mc);
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
List<RepaymentPeriod> repaymentPeriods = periods.stream().map(e -> {
RepaymentPeriod rp = new RepaymentPeriod(prev.get(), from.apply(e), to.apply(e), zero, mc);
prev.set(rp);
return rp;
}).toList();
return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
}
@Override
@@ -75,7 +92,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
if (scheduleModel == null) {
return Optional.empty();
}
return scheduleModel.findRepaymentPeriod(repaymentPeriodDueDate);
return scheduleModel.findRepaymentPeriodByDueDate(repaymentPeriodDueDate);
}
/**
@@ -139,13 +156,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate,
LocalDate transactionDate, Money principalAmount) {
if (MathUtil.isEmpty(principalAmount)) {
return;
}
Optional<RepaymentPeriod> repaymentPeriod = findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate);
repaymentPeriod.ifPresent(rp -> rp.addPaidPrincipalAmount(principalAmount));
LocalDate balanceCorrectionDate = transactionDate;
if (repaymentPeriodDueDate.isBefore(transactionDate)) {
// If it is paid late, we need to calculate with the period due date
balanceCorrectionDate = repaymentPeriodDueDate;
}
// If it is paid late, we need to calculate with the period due date
LocalDate balanceCorrectionDate = DateUtils.isBefore(repaymentPeriodDueDate, transactionDate) ? repaymentPeriodDueDate
: transactionDate;
addBalanceCorrection(scheduleModel, balanceCorrectionDate, principalAmount.negated());
repaymentPeriod.ifPresent(rp -> {
@@ -159,11 +177,12 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
@Override
public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate,
final LocalDate targetDate) {
@NotNull
public PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
@NotNull LocalDate targetDate) {
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
repaymentPeriodDueDate, targetDate);
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
periodDueDate, targetDate);
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
boolean multiplePeriodIsUnpaid = recalculatedScheduleModelTillDate.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
.count() > 1L;
if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) {
@@ -176,11 +195,21 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
@Override
public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate,
@NotNull
public Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
@NotNull LocalDate targetDate) {
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
periodDueDate, targetDate);
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
return repaymentPeriod.getCalculatedDueInterest();
}
@Override
public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate,
LocalDate targetDate) {
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
repaymentPeriodDueDate, targetDate);
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
periodDueDate, targetDate);
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
return repaymentPeriod.getOutstandingLoanBalance();
}
@@ -189,7 +218,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate targetDate) {
MathContext mc = scheduleModel.mc();
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
// TODO use findInterestPeriod
scheduleModelCopy.repaymentPeriods().stream()//
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
.flatMap(rp -> rp.getInterestPeriods().stream()//
@@ -214,25 +243,26 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
@NotNull
private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(ProgressiveLoanInterestScheduleModel scheduleModel,
LocalDate repaymentPeriodDueDate, LocalDate targetDate) {
private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(
@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, @NotNull LocalDate targetDate) {
MathContext mc = scheduleModel.mc();
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream()
.filter(rp -> rp.getDueDate().equals(repaymentPeriodDueDate)).findFirst().orElseThrow();
RepaymentPeriod repaymentPeriod = scheduleModelCopy.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
LocalDate adjustedTargetDate = targetDate;
InterestPeriod interestPeriod;
if (!targetDate.isAfter(repaymentPeriod.getFromDate())) {
interestPeriod = repaymentPeriod.getInterestPeriods().get(0);
interestPeriod = repaymentPeriod.getFirstInterestPeriod();
adjustedTargetDate = repaymentPeriod.getFromDate();
} else if (targetDate.isAfter(repaymentPeriod.getDueDate())) {
interestPeriod = repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1);
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();
}
// TODO use findInterestPeriod
scheduleModelCopy.repaymentPeriods().stream()//
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
.flatMap(rp -> rp.getInterestPeriods().stream()//
@@ -645,6 +675,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, final BigDecimal repaymentPeriodMultiplierInDays,
final BigDecimal repaymentEvery, final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod,
final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
if (MathUtil.isZero(calculatedDaysInPeriod)) {
return BigDecimal.ZERO;
}
final BigDecimal interestFractionPerPeriod = repaymentPeriodMultiplierInDays//
.multiply(repaymentEvery, mc)//
.divide(daysInYear, mc);//
@@ -661,6 +694,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, final BigDecimal repaymentEvery,
final BigDecimal cumulatedPeriodRatio, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
final MathContext mc) {
if (MathUtil.isZero(calculatedDaysInPeriod)) {
return BigDecimal.ZERO;
}
final BigDecimal interestFractionPerPeriod = repaymentEvery.multiply(cumulatedPeriodRatio);
return interestRate//
.multiply(interestFractionPerPeriod, mc)//
@@ -682,24 +718,6 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, mc), mc);
}
@Override
public ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc) {
List<LoanRepaymentScheduleInstallment> repaymentModelsWithoutDownPayment = repaymentPeriods.stream()
.filter(period -> !period.isDownPayment() && !period.isAdditional()).toList();
List<RepaymentPeriod> repaymentModels = new ArrayList<>();
RepaymentPeriod previousPeriod = null;
for (LoanRepaymentScheduleInstallment repaymentModel : repaymentModelsWithoutDownPayment) {
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentModel.getFromDate(), repaymentModel.getDueDate(),
Money.zero(repaymentModel.getLoan().getCurrency(), mc), mc);
previousPeriod = currentPeriod;
repaymentModels.add(currentPeriod);
}
return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
}
/**
* Calculates the sum of due interests on interest periods.
*
@@ -60,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
@@ -95,6 +96,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
private AdvancedPaymentScheduleTransactionProcessor underTest;
private static final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class);
private static final LoanRepositoryWrapper loanRepositoryWrapper = Mockito.mock(LoanRepositoryWrapper.class);
@BeforeAll
public static void init() {
@@ -109,7 +111,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
@BeforeEach
public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, null);
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
@@ -235,7 +237,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
when(loanTransaction.getLoan()).thenReturn(loan);
when(loan.loanCurrency()).thenReturn(currency);
when(loan.getCurrency()).thenReturn(currency);
when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
when(loan.getDisbursementDate()).thenReturn(disbursementDate);
@@ -451,7 +453,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
when(loanProductRelatedDetail.isInterestRecalculationEnabled()).thenReturn(true);
when(loanTransaction.getLoan()).thenReturn(loan);
when(loan.loanCurrency()).thenReturn(currency);
when(loan.getCurrency()).thenReturn(currency);
when(loan.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule));
@@ -158,7 +158,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_generateInterestScheduleModel() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
final Integer installmentAmountInMultiplesOf = null;
@@ -169,8 +168,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator
.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Assertions.assertTrue(interestScheduleModel != null);
Assertions.assertTrue(interestScheduleModel.loanProductRelatedDetail() != null);
@@ -183,7 +182,6 @@ class ProgressiveEMICalculatorTest {
@Test
@Timeout(1) // seconds
public void test_emi_calculator_performance() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -209,8 +207,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -234,7 +232,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -254,8 +251,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -271,7 +268,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -291,8 +287,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -308,7 +304,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -328,8 +323,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -355,7 +350,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -375,8 +369,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -392,7 +386,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -414,8 +407,8 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -435,7 +428,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -457,8 +449,8 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -504,8 +496,8 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -529,7 +521,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -551,8 +542,8 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -576,7 +567,6 @@ class ProgressiveEMICalculatorTest {
*/
@Test
public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -598,8 +588,8 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -684,7 +674,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -706,8 +695,8 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -723,9 +712,8 @@ class ProgressiveEMICalculatorTest {
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal()));
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest()));
PeriodDueDetails details = null;
// check getdueAmounts forcast
details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
// check getDueAmounts forcast
PeriodDueDetails details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal()));
Assertions.assertEquals(0.49, toDouble(details.getDueInterest()));
@@ -757,7 +745,6 @@ class ProgressiveEMICalculatorTest {
@Test
public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
@@ -779,8 +766,8 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -856,8 +843,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Money disbursedAmount = toMoney(100);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -905,8 +892,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -954,8 +941,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount);
@@ -998,8 +985,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount);
@@ -1032,8 +1019,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(1000.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -1065,8 +1052,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -1098,8 +1085,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -1131,8 +1118,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -1163,8 +1150,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(1000.0);
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -1225,8 +1212,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount1st = toMoney(1000.0);
final Money disbursedAmount2nd = toMoney(1000.0);