From 4371eda06ad41a3bdc962d81fb0dd25e8be96ab7 Mon Sep 17 00:00:00 2001 From: Peter Bagrij Date: Thu, 15 Feb 2024 18:55:27 +0100 Subject: [PATCH] FINERACT-2042 Handling overpayment of chargeback with credit allocations --- .../portfolio/loanaccount/domain/Loan.java | 22 +- .../LoanRepaymentScheduleInstallment.java | 62 +- .../loanaccount/domain/LoanSummary.java | 14 +- .../domain/LoanSummaryWrapper.java | 23 +- ...RepaymentScheduleTransactionProcessor.java | 6 +- ...edPaymentScheduleTransactionProcessor.java | 179 ++-- .../module/loan/module-changelog-master.xml | 1 + ...dd_fee_and_penalty_adjustments_to_loan.xml | 43 + .../api/LoansApiResourceSwagger.java | 4 + .../data/LoanScheduleAccrualData.java | 15 +- .../loanaccount/data/LoanSummaryData.java | 116 +-- .../domain/LoanAccountDomainServiceJpa.java | 9 +- .../LoanAccrualWritePlatformServiceImpl.java | 18 +- .../service/LoanReadPlatformServiceImpl.java | 42 +- ...ymentScheduleTransactionProcessorTest.java | 371 +++++---- .../BaseLoanIntegrationTest.java | 68 +- ...WithCreditAllocationsIntegrationTests.java | 777 ++++++++++++++++-- 17 files changed, 1346 insertions(+), 424 deletions(-) create mode 100644 fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1017_add_fee_and_penalty_adjustments_to_loan.xml diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 8d936944335..9075af107fe 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -140,6 +140,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod; +import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; @@ -5945,10 +5946,18 @@ private void updateLoanOutstandingBalances() { .minus(loanTransaction.getOverPaymentPortion(getCurrency())); loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount()); } else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) { - Money transactionOutstanding = loanTransaction.getAmount(getCurrency()); + Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency()); if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) { - transactionOutstanding = loanTransaction.getAmount(getCurrency()) - .minus(loanTransaction.getOverPaymentPortion(getCurrency())); + // in case of advanced payment strategy and creditAllocations the full amount is recognized first + if (this.getCreditAllocationRules() != null && this.getCreditAllocationRules().size() > 0) { + Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() + .map(mapping -> mapping.getPrincipalPortion(getCurrency())).reduce(Money.zero(getCurrency()), Money::plus); + transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency()).minus(payedPrincipal); + } else { + // in case legacy payment strategy + transactionOutstanding = loanTransaction.getAmount(getCurrency()) + .minus(loanTransaction.getOverPaymentPortion(getCurrency())); + } if (transactionOutstanding.isLessThanZero()) { transactionOutstanding = Money.zero(getCurrency()); } @@ -7184,6 +7193,13 @@ public List getPaymentAllocationRules() { return paymentAllocationRules; } + public LoanPaymentAllocationRule getPaymentAllocationRuleOrDefault(PaymentAllocationTransactionType transactionType) { + Optional paymentAllocationRule = this.getPaymentAllocationRules().stream() + .filter(rule -> rule.getTransactionType().equals(transactionType)).findFirst(); + return paymentAllocationRule.orElse(this.getPaymentAllocationRules().stream() + .filter(rule -> rule.getTransactionType().equals(PaymentAllocationTransactionType.DEFAULT)).findFirst().get()); + } + public void setPaymentAllocationRules(List loanPaymentAllocationRules) { this.paymentAllocationRules = loanPaymentAllocationRules; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 1431c8a8f2b..6d6456510f5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -130,8 +130,14 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa @Column(name = "is_additional", nullable = false) private boolean additional; - @Column(name = "credits_amount", scale = 6, precision = 19, nullable = true) - private BigDecimal credits; + @Column(name = "credited_principal", scale = 6, precision = 19, nullable = true) + private BigDecimal creditedPrincipal; + + @Column(name = "credited_fee", scale = 6, precision = 19, nullable = true) + private BigDecimal creditedFee; + + @Column(name = "credited_penalty", scale = 6, precision = 19, nullable = true) + private BigDecimal creditedPenalty; @Column(name = "is_down_payment", nullable = false) private boolean isDownPayment; @@ -246,8 +252,16 @@ public LocalDate getDueDate() { return this.dueDate; } - public Money getCredits(final MonetaryCurrency currency) { - return Money.of(currency, this.credits); + public Money getCreditedPrincipal(final MonetaryCurrency currency) { + return Money.of(currency, this.creditedPrincipal); + } + + public Money getCreditedFee(final MonetaryCurrency currency) { + return Money.of(currency, this.creditedFee); + } + + public Money getCreditedPenalty(final MonetaryCurrency currency) { + return Money.of(currency, this.creditedPenalty); } public Money getPrincipal(final MonetaryCurrency currency) { @@ -408,9 +422,17 @@ public void resetDerivedComponents() { this.obligationsMet = false; this.obligationsMetOnDate = null; - if (this.credits != null) { - this.principal = this.principal.subtract(this.credits); - this.credits = null; + if (this.creditedPrincipal != null) { + this.principal = this.principal.subtract(this.creditedPrincipal); + this.creditedPrincipal = null; + } + if (this.creditedFee != null) { + this.feeChargesCharged = this.feeChargesCharged.subtract(this.creditedFee); + this.creditedFee = null; + } + if (this.creditedPenalty != null) { + this.penaltyCharges = this.penaltyCharges.subtract(this.creditedPenalty); + this.creditedPenalty = null; } } @@ -780,11 +802,27 @@ public void addToPrincipal(final LocalDate transactionDate, final Money transact checkIfRepaymentPeriodObligationsAreMet(transactionDate, transactionAmount.getCurrency()); } - public void addToCredits(final BigDecimal amount) { - if (this.credits == null) { - this.credits = amount; + public void addToCreditedPrincipal(final BigDecimal amount) { + if (this.creditedPrincipal == null) { + this.creditedPrincipal = amount; + } else { + this.creditedPrincipal = this.creditedPrincipal.add(amount); + } + } + + public void addToCreditedFee(final BigDecimal amount) { + if (this.creditedFee == null) { + this.creditedFee = amount; + } else { + this.creditedFee = this.creditedFee.add(amount); + } + } + + public void addToCreditedPenalty(final BigDecimal amount) { + if (this.creditedPenalty == null) { + this.creditedPenalty = amount; } else { - this.credits = this.credits.add(amount); + this.creditedPenalty = this.creditedPenalty.add(amount); } } @@ -908,7 +946,7 @@ private void reduceAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transa } public void updateCredits(final LocalDate transactionDate, final Money transactionAmount) { - addToCredits(transactionAmount.getAmount()); + addToCreditedPrincipal(transactionAmount.getAmount()); addToPrincipal(transactionDate, transactionAmount); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java index 2cbb06463a9..271f88f513c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java @@ -74,6 +74,9 @@ public class LoanSummary { @Column(name = "total_charges_due_at_disbursement_derived", scale = 6, precision = 19) private BigDecimal totalFeeChargesDueAtDisbursement; + @Column(name = "fee_adjustments_derived", scale = 6, precision = 19) + private BigDecimal totalFeeAdjustments; + @Column(name = "fee_charges_repaid_derived", scale = 6, precision = 19) private BigDecimal totalFeeChargesRepaid; @@ -89,6 +92,9 @@ public class LoanSummary { @Column(name = "penalty_charges_charged_derived", scale = 6, precision = 19) private BigDecimal totalPenaltyChargesCharged; + @Column(name = "penalty_adjustments_derived", scale = 6, precision = 19) + private BigDecimal totalPenaltyAdjustments; + @Column(name = "penalty_charges_repaid_derived", scale = 6, precision = 19) private BigDecimal totalPenaltyChargesRepaid; @@ -204,6 +210,8 @@ public void updateTotalWaived(final BigDecimal totalWaived) { public void zeroFields() { this.totalPrincipalDisbursed = BigDecimal.ZERO; this.totalPrincipalAdjustments = BigDecimal.ZERO; + this.totalFeeAdjustments = BigDecimal.ZERO; + this.totalPenaltyAdjustments = BigDecimal.ZERO; this.totalPrincipalRepaid = BigDecimal.ZERO; this.totalPrincipalWrittenOff = BigDecimal.ZERO; this.totalPrincipalOutstanding = BigDecimal.ZERO; @@ -238,6 +246,8 @@ public void updateSummary(final MonetaryCurrency currency, final Money principal this.totalPrincipalDisbursed = principal.getAmount(); this.totalPrincipalAdjustments = summaryWrapper.calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency) .getAmount(); + this.totalFeeAdjustments = summaryWrapper.calculateTotalFeeAdjusted(repaymentScheduleInstallments, currency).getAmount(); + this.totalPenaltyAdjustments = summaryWrapper.calculateTotalPenaltyAdjusted(repaymentScheduleInstallments, currency).getAmount(); this.totalPrincipalRepaid = summaryWrapper.calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount(); this.totalPrincipalWrittenOff = summaryWrapper.calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency) .getAmount(); @@ -259,7 +269,9 @@ public void updateSummary(final MonetaryCurrency currency, final Money principal this.totalFeeChargesCharged = totalFeeChargesCharged.getAmount(); Money totalFeeChargesRepaidAtDisbursement = summaryWrapper.calculateTotalChargesRepaidAtDisbursement(charges, currency); - this.totalFeeChargesRepaid = totalFeeChargesRepaidAtDisbursement.getAmount(); + Money totalFeeChargesRepaidAfterDisbursement = summaryWrapper.calculateTotalFeeChargesRepaid(repaymentScheduleInstallments, + currency); + this.totalFeeChargesRepaid = totalFeeChargesRepaidAfterDisbursement.plus(totalFeeChargesRepaidAtDisbursement).getAmount(); if (charges != null) { this.totalFeeChargesWaived = summaryWrapper.calculateTotalFeeChargesWaived(charges, currency).getAmount(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java index 947d175407a..8ad10df39a8 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java @@ -45,7 +45,25 @@ public Money calculateTotalPrincipalAdjusted(final List repaymentScheduleInstallments, + final MonetaryCurrency currency) { + Money total = Money.zero(currency); + for (final LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) { + total = total.plus(installment.getCreditedFee(currency)); + } + return total; + } + + public Money calculateTotalPenaltyAdjusted(final List repaymentScheduleInstallments, + final MonetaryCurrency currency) { + Money total = Money.zero(currency); + for (final LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) { + total = total.plus(installment.getCreditedPenalty(currency)); } return total; } @@ -247,7 +265,8 @@ public Money calculateTotalChargesRepaidAtDisbursement(Set charges, return total; } for (final LoanCharge loanCharge : charges) { - if (!loanCharge.isPenaltyCharge() && loanCharge.getAmountPaid(currency).isGreaterThanZero()) { + if (!loanCharge.isPenaltyCharge() && loanCharge.getAmountPaid(currency).isGreaterThanZero() + && loanCharge.isDisbursementCharge()) { total = total.plus(loanCharge.getAmountPaid(currency)); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 596513e7e1c..94592fed8cb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -494,7 +494,7 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHo for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { pastDueDate = currentInstallment.getDueDate(); if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) { - currentInstallment.addToCredits(transactionAmount.getAmount()); + currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount()); currentInstallment.addToPrincipal(transactionDate, transactionAmount); if (repaidAmount.isGreaterThanZero()) { currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); @@ -526,7 +526,7 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHo if (!loanTransactionMapped) { if (loanTransaction.getTransactionDate().equals(pastDueDate)) { LoanRepaymentScheduleInstallment currentInstallment = installments.get(installments.size() - 1); - currentInstallment.addToCredits(transactionAmount.getAmount()); + currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount()); currentInstallment.addToPrincipal(transactionDate, transactionAmount); if (repaidAmount.isGreaterThanZero()) { currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); @@ -539,7 +539,7 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHo pastDueDate, transactionDate, transactionAmount.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), false, null); installment.markAsAdditional(); - installment.addToCredits(transactionAmount.getAmount()); + installment.addToCreditedPrincipal(transactionAmount.getAmount()); loan.addLoanRepaymentScheduleInstallment(installment); if (repaidAmount.isGreaterThanZero()) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 297f4efcaa7..64b3683f218 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -25,6 +25,7 @@ import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST; import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL; +import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT; import java.math.BigDecimal; import java.math.MathContext; @@ -76,7 +77,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.DueType; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -235,27 +235,31 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac if (hasNoCustomCreditAllocationRule(loanTransaction)) { super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments()); } else { - log.debug("Processing credit transaction with custom credit allocation rules"); - loanTransaction.resetDerivedComponents(); - List transactionMappings = new ArrayList<>(); final Comparator byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate); ctx.getInstallments().sort(byDate); final Money zeroMoney = Money.zero(ctx.getCurrency()); Money transactionAmount = loanTransaction.getAmount(ctx.getCurrency()); - Money amountToDistribute = MathUtil - .negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(ctx.getOverpaymentHolder().getMoneyObject())); - Money repaidAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute)); - loanTransaction.setOverPayments(repaidAmount); - ctx.getOverpaymentHolder().setMoneyObject(ctx.getOverpaymentHolder().getMoneyObject().minus(repaidAmount)); + Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject(); + Money amountToDistribute = MathUtil.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(totalOverpaid)); + Money overpaymentAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute)); + loanTransaction.setOverPayments(overpaymentAmount); - if (amountToDistribute.isGreaterThanZero()) { + if (transactionAmount.isGreaterThanZero()) { if (loanTransaction.isChargeback()) { LoanTransaction originalTransaction = findOriginalTransaction(loanTransaction, ctx); - Map originalAllocation = getOriginalAllocation(originalTransaction); + // get the original allocation from the opriginal transaction + Map originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, + ctx.getCurrency()); LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction); + + // if there were earlier chargebacks then let's calculate the remaining amounts for each portion + Map originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction, + originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule); + + // calculate the current chargeback allocation Map chargebackAllocation = calculateChargebackAllocationMap(originalAllocation, - amountToDistribute.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency()); + transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency()); loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST), chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY)); @@ -266,18 +270,7 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) { pastDueDate = currentInstallment.getDueDate(); if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) { - - currentInstallment.addToCredits(transactionAmount.getAmount()); - currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL)); - Money originalInterest = currentInstallment.getInterestCharged(ctx.getCurrency()); - currentInstallment.updateInterestCharged( - originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero()); - - if (repaidAmount.isGreaterThanZero()) { - currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); - transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, - currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney)); - } + recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation); loanTransactionMapped = true; break; @@ -287,16 +280,7 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) { currentInstallment.updateDueDate(transactionDate); } - currentInstallment.addToCredits(transactionAmount.getAmount()); - currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL)); - Money originalInterest = currentInstallment.getInterestCharged(ctx.getCurrency()); - currentInstallment.updateInterestCharged( - originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero()); - if (repaidAmount.isGreaterThanZero()) { - currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); - transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, - currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney)); - } + recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation); loanTransactionMapped = true; break; } @@ -307,42 +291,99 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac if (loanTransaction.getTransactionDate().equals(pastDueDate)) { LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments() .get(ctx.getInstallments().size() - 1); - currentInstallment.addToCredits(transactionAmount.getAmount()); - currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL)); - Money originalInterest = currentInstallment.getInterestCharged(ctx.getCurrency()); - currentInstallment.updateInterestCharged( - originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero()); - if (repaidAmount.isGreaterThanZero()) { - currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); - transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, - currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney)); - } + 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); - installment.markAsAdditional(); - installment.addToCredits(transactionAmount.getAmount()); - installment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL)); - Money originalInterest = installment.getInterestCharged(ctx.getCurrency()); - installment.updateInterestCharged( - originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero()); + recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, installment, chargebackAllocation); loan.addLoanRepaymentScheduleInstallment(installment); - if (repaidAmount.isGreaterThanZero()) { - installment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); - transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment, - repaidAmount, zeroMoney, zeroMoney, zeroMoney)); - } } } - - loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); + allocateOverpayment(loanTransaction, ctx.getCurrency(), loanTransaction.getLoan().getRepaymentScheduleInstallments(), + ctx.getOverpaymentHolder()); + } else { + throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name()); } } } } + private Map adjustOriginalAllocationWithFormerChargebacks(LoanTransaction originalTransaction, + Map originalAllocation, LoanTransaction chargeBackTransaction, TransactionCtx ctx, + LoanCreditAllocationRule chargeBackAllocationRule) { + // these are the list of existing transactions + List allTransactions = new ArrayList<>(chargeBackTransaction.getLoan().getLoanTransactions()); + + // Remove the current chargeback from the list + if (chargeBackTransaction.getId() != null) { + allTransactions.remove(chargeBackTransaction); + } else { + Long oldId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(chargeBackTransaction); + allTransactions.remove(allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), oldId)).findFirst().get()); + } + + // Add the replayed transactions and remove their old version before the replay + if (ctx.getChangedTransactionDetail() != null && ctx.getChangedTransactionDetail().getNewTransactionMappings() != null) { + for (Long id : ctx.getChangedTransactionDetail().getNewTransactionMappings().keySet()) { + allTransactions.remove(allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), id)).findFirst().get()); + allTransactions.add(ctx.getChangedTransactionDetail().getNewTransactionMappings().get(id)); + } + } + + // keep only the chargeback transactions + List chargebacks = allTransactions.stream().filter(LoanTransaction::isChargeback).toList(); + + // let's figure out the original transaction for these chargebacks, and order them by ascending order + List chargebacksForTheSameOriginal = chargebacks.stream() + .filter(tr -> findOriginalTransaction(tr, ctx) == originalTransaction).sorted(loanTransactionDateComparator()).toList(); + + Map allocation = new HashMap<>(originalAllocation); + for (LoanTransaction loanTransaction : chargebacksForTheSameOriginal) { + Map temp = calculateChargebackAllocationMap(allocation, loanTransaction.getAmount(), + chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency()); + allocation.keySet().forEach(k -> allocation.put(k, allocation.get(k).minus(temp.get(k)))); + } + return allocation; + } + + @NotNull + private Comparator loanTransactionDateComparator() { + return (tr1, tr2) -> { + if (tr1.getTransactionDate().compareTo(tr2.getTransactionDate()) != 0) { + return tr1.getTransactionDate().compareTo(tr2.getTransactionDate()); + } else if (tr1.getSubmittedOnDate().compareTo(tr2.getSubmittedOnDate()) != 0) { + return tr1.getSubmittedOnDate().compareTo(tr2.getSubmittedOnDate()); + } else { + return tr1.getCreatedDateTime().compareTo(tr2.getCreatedDateTime()); + } + }; + } + + private void recognizeAmountsAfterChargeback(MonetaryCurrency currency, LocalDate localDate, + LoanRepaymentScheduleInstallment installment, Map chargebackAllocation) { + Money principal = chargebackAllocation.get(PRINCIPAL); + if (principal.isGreaterThanZero()) { + installment.addToCreditedPrincipal(principal.getAmount()); + installment.addToPrincipal(localDate, principal); + } + + Money fee = chargebackAllocation.get(FEE); + if (fee.isGreaterThanZero()) { + installment.addToCreditedFee(fee.getAmount()); + installment.addToChargePortion(fee, Money.zero(currency), Money.zero(currency), Money.zero(currency), Money.zero(currency), + Money.zero(currency)); + } + + Money penalty = chargebackAllocation.get(PENALTY); + if (penalty.isGreaterThanZero()) { + installment.addToCreditedPenalty(penalty.getAmount()); + installment.addToChargePortion(Money.zero(currency), Money.zero(currency), Money.zero(currency), penalty, Money.zero(currency), + Money.zero(currency)); + } + } + @NotNull private LoanCreditAllocationRule getChargebackAllocationRules(LoanTransaction loanTransaction) { LoanCreditAllocationRule chargeBackAllocationRule = loanTransaction.getLoan().getCreditAllocationRules().stream() @@ -351,23 +392,23 @@ private LoanCreditAllocationRule getChargebackAllocationRules(LoanTransaction lo } @NotNull - private Map getOriginalAllocation(LoanTransaction originalLoanTransaction) { - Map originalAllocation = new HashMap<>(); - originalAllocation.put(PRINCIPAL, originalLoanTransaction.getPrincipalPortion()); - originalAllocation.put(INTEREST, originalLoanTransaction.getInterestPortion()); - originalAllocation.put(PENALTY, originalLoanTransaction.getPenaltyChargesPortion()); - originalAllocation.put(FEE, originalLoanTransaction.getFeeChargesPortion()); + private Map getOriginalAllocation(LoanTransaction originalLoanTransaction, MonetaryCurrency currency) { + Map originalAllocation = new HashMap<>(); + originalAllocation.put(PRINCIPAL, Money.of(currency, originalLoanTransaction.getPrincipalPortion())); + originalAllocation.put(INTEREST, Money.of(currency, originalLoanTransaction.getInterestPortion())); + originalAllocation.put(PENALTY, Money.of(currency, originalLoanTransaction.getPenaltyChargesPortion())); + originalAllocation.put(FEE, Money.of(currency, originalLoanTransaction.getFeeChargesPortion())); return originalAllocation; } - protected Map calculateChargebackAllocationMap(Map originalAllocation, + protected Map calculateChargebackAllocationMap(Map originalAllocation, BigDecimal amountToDistribute, List allocationTypes, MonetaryCurrency currency) { BigDecimal remainingAmount = amountToDistribute; Map result = new HashMap<>(); Arrays.stream(AllocationType.values()).forEach(allocationType -> result.put(allocationType, Money.of(currency, BigDecimal.ZERO))); for (AllocationType allocationType : allocationTypes) { if (remainingAmount.compareTo(BigDecimal.ZERO) > 0) { - BigDecimal originalAmount = originalAllocation.get(allocationType); + BigDecimal originalAmount = originalAllocation.get(allocationType).getAmount(); if (originalAmount != null && remainingAmount.compareTo(originalAmount) > 0 && originalAmount.compareTo(BigDecimal.ZERO) > 0) { result.put(allocationType, Money.of(currency, originalAmount)); @@ -395,14 +436,14 @@ protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency cu List paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules(); LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() - .filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); + .filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream() .filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst() .orElse(defaultPaymentAllocationRule); Balances balances = new Balances(zero, zero, zero, zero); List paymentAllocationTypes; FutureInstallmentAllocationRule futureInstallmentAllocationRule; - if (PaymentAllocationTransactionType.DEFAULT.equals(paymentAllocationRule.getTransactionType())) { + if (DEFAULT.equals(paymentAllocationRule.getTransactionType())) { // if the allocation rule is not defined then the reverse order of the default allocation rule will be used paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes()); Collections.reverse(paymentAllocationTypes); @@ -551,7 +592,7 @@ private void allocateOverpayment(LoanTransaction loanTransaction, MonetaryCurren List transactionMappings = new ArrayList<>(); List paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules(); LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() - .filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); + .filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); Money transactionAmountUnprocessed = null; Money zero = Money.zero(currency); @@ -919,7 +960,7 @@ private void processTransaction(LoanTransaction loanTransaction, MonetaryCurrenc List paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules(); LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() - .filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); + .filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream() .filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst() .orElse(defaultPaymentAllocationRule); diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml index 16760fc8b94..71521ef6927 100644 --- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml @@ -39,4 +39,5 @@ + diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1017_add_fee_and_penalty_adjustments_to_loan.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1017_add_fee_and_penalty_adjustments_to_loan.xml new file mode 100644 index 00000000000..faa79a4c08c --- /dev/null +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1017_add_fee_and_penalty_adjustments_to_loan.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index d93575ab667..375d506e2b9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -524,6 +524,8 @@ private GetLoansLoanIdFeeFrequency() {} public Double interestOutstanding; @Schema(example = "200000.000000") public Double interestOverdue; + @Schema(example = "0.00") + public Double feeAdjustments; @Schema(example = "18000.000000") public Double feeChargesCharged; @Schema(example = "0.000000") @@ -538,6 +540,8 @@ private GetLoansLoanIdFeeFrequency() {} public Double feeChargesOutstanding; @Schema(example = "15000.000000") public Double feeChargesOverdue; + @Schema(example = "0.00") + public Double penaltyAdjustments; @Schema(example = "0.000000") public Double penaltyChargesCharged; @Schema(example = "0.000000") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanScheduleAccrualData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanScheduleAccrualData.java index 6fcb359ca54..21abaa1531d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanScheduleAccrualData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanScheduleAccrualData.java @@ -51,12 +51,15 @@ public class LoanScheduleAccrualData { private BigDecimal dueDatePenaltyIncome; private BigDecimal accruableIncome; + private BigDecimal creditedFee; + private BigDecimal creditedPenalty; + public LoanScheduleAccrualData(final Long loanId, final Long officeId, final Integer installmentNumber, final LocalDate accruedTill, final PeriodFrequencyType repaymentFrequency, final Integer repayEvery, final LocalDate dueDate, final LocalDate fromDate, final Long repaymentScheduleId, final Long loanProductId, final BigDecimal interestIncome, final BigDecimal feeIncome, final BigDecimal penaltyIncome, final BigDecimal accruedInterestIncome, final BigDecimal accruedFeeIncome, final BigDecimal accruedPenaltyIncome, final CurrencyData currencyData, final LocalDate interestCalculatedFrom, - final BigDecimal waivedInterestIncome) { + final BigDecimal waivedInterestIncome, BigDecimal creditedFee, BigDecimal creditedPenalty) { this.loanId = loanId; this.installmentNumber = installmentNumber; this.officeId = officeId; @@ -76,6 +79,8 @@ public LoanScheduleAccrualData(final Long loanId, final Long officeId, final Int this.repayEvery = repayEvery; this.interestCalculatedFrom = interestCalculatedFrom; this.waivedInterestIncome = waivedInterestIncome; + this.creditedFee = creditedFee; + this.creditedPenalty = creditedPenalty; } public Long getLoanId() { @@ -185,4 +190,12 @@ public void updateAccruableIncome(BigDecimal accruableIncome) { this.accruableIncome = accruableIncome; } + public BigDecimal getCreditedFee() { + return this.creditedFee; + } + + public BigDecimal getCreditedPenalty() { + return this.creditedPenalty; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java index 73e7be47a33..42ebbd6bec4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java @@ -21,6 +21,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; +import lombok.Builder; import lombok.Data; import lombok.experimental.Accessors; import org.apache.fineract.organisation.monetary.data.CurrencyData; @@ -31,6 +32,7 @@ * Immutable data object representing loan summary information. */ @Data +@Builder @Accessors(chain = true) public class LoanSummaryData { @@ -48,6 +50,7 @@ public class LoanSummaryData { private final BigDecimal interestOutstanding; private final BigDecimal interestOverdue; private final BigDecimal feeChargesCharged; + private final BigDecimal feeAdjustments; private final BigDecimal feeChargesDueAtDisbursementCharged; private final BigDecimal feeChargesPaid; private final BigDecimal feeChargesWaived; @@ -55,6 +58,7 @@ public class LoanSummaryData { private final BigDecimal feeChargesOutstanding; private final BigDecimal feeChargesOverdue; private final BigDecimal penaltyChargesCharged; + private final BigDecimal penaltyAdjustments; private final BigDecimal penaltyChargesPaid; private final BigDecimal penaltyChargesWaived; private final BigDecimal penaltyChargesWrittenOff; @@ -90,61 +94,6 @@ public class LoanSummaryData { private final Long chargeOffReasonId; private final String chargeOffReason; - public LoanSummaryData(final CurrencyData currency, final BigDecimal principalDisbursed, final BigDecimal principalAdjustments, - final BigDecimal principalPaid, final BigDecimal principalWrittenOff, final BigDecimal principalOutstanding, - final BigDecimal principalOverdue, final BigDecimal interestCharged, final BigDecimal interestPaid, - final BigDecimal interestWaived, final BigDecimal interestWrittenOff, final BigDecimal interestOutstanding, - final BigDecimal interestOverdue, final BigDecimal feeChargesCharged, final BigDecimal feeChargesDueAtDisbursementCharged, - final BigDecimal feeChargesPaid, final BigDecimal feeChargesWaived, final BigDecimal feeChargesWrittenOff, - final BigDecimal feeChargesOutstanding, final BigDecimal feeChargesOverdue, final BigDecimal penaltyChargesCharged, - final BigDecimal penaltyChargesPaid, final BigDecimal penaltyChargesWaived, final BigDecimal penaltyChargesWrittenOff, - final BigDecimal penaltyChargesOutstanding, final BigDecimal penaltyChargesOverdue, final BigDecimal totalExpectedRepayment, - final BigDecimal totalRepayment, final BigDecimal totalExpectedCostOfLoan, final BigDecimal totalCostOfLoan, - final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding, - final BigDecimal totalOverdue, final LocalDate overdueSinceDate, final Long writeoffReasonId, final String writeoffReason, - final BigDecimal totalRecovered, final Long chargeOffReasonId, final String chargeOffReason) { - this.currency = currency; - this.principalDisbursed = principalDisbursed; - this.principalAdjustments = principalAdjustments; - this.principalPaid = principalPaid; - this.principalWrittenOff = principalWrittenOff; - this.principalOutstanding = principalOutstanding; - this.principalOverdue = principalOverdue; - this.interestCharged = interestCharged; - this.interestPaid = interestPaid; - this.interestWaived = interestWaived; - this.interestWrittenOff = interestWrittenOff; - this.interestOutstanding = interestOutstanding; - this.interestOverdue = interestOverdue; - this.feeChargesCharged = feeChargesCharged; - this.feeChargesDueAtDisbursementCharged = feeChargesDueAtDisbursementCharged; - this.feeChargesPaid = feeChargesPaid; - this.feeChargesWaived = feeChargesWaived; - this.feeChargesWrittenOff = feeChargesWrittenOff; - this.feeChargesOutstanding = feeChargesOutstanding; - this.feeChargesOverdue = feeChargesOverdue; - this.penaltyChargesCharged = penaltyChargesCharged; - this.penaltyChargesPaid = penaltyChargesPaid; - this.penaltyChargesWaived = penaltyChargesWaived; - this.penaltyChargesWrittenOff = penaltyChargesWrittenOff; - this.penaltyChargesOutstanding = penaltyChargesOutstanding; - this.penaltyChargesOverdue = penaltyChargesOverdue; - this.totalExpectedRepayment = totalExpectedRepayment; - this.totalRepayment = totalRepayment; - this.totalExpectedCostOfLoan = totalExpectedCostOfLoan; - this.totalCostOfLoan = totalCostOfLoan; - this.totalWaived = totalWaived; - this.totalWrittenOff = totalWrittenOff; - this.totalOutstanding = totalOutstanding; - this.totalOverdue = totalOverdue; - this.overdueSinceDate = overdueSinceDate; - this.writeoffReasonId = writeoffReasonId; - this.writeoffReason = writeoffReason; - this.totalRecovered = totalRecovered; - this.chargeOffReasonId = chargeOffReasonId; - this.chargeOffReason = chargeOffReason; - } - public static LoanSummaryData withTransactionAmountsSummary(final LoanSummaryData defaultSummaryData, final Collection loanTransactions) { @@ -184,34 +133,41 @@ public static LoanSummaryData withTransactionAmountsSummary(final LoanSummaryDat totalRepaymentTransactionReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.REPAYMENT, loanTransactions); } - return new LoanSummaryData(defaultSummaryData.currency, defaultSummaryData.principalDisbursed, - defaultSummaryData.principalAdjustments, defaultSummaryData.principalPaid, defaultSummaryData.principalWrittenOff, - defaultSummaryData.principalOutstanding, defaultSummaryData.principalOverdue, defaultSummaryData.interestCharged, - defaultSummaryData.interestPaid, defaultSummaryData.interestWaived, defaultSummaryData.interestWrittenOff, - defaultSummaryData.interestOutstanding, defaultSummaryData.interestOverdue, defaultSummaryData.feeChargesCharged, - defaultSummaryData.feeChargesDueAtDisbursementCharged, defaultSummaryData.feeChargesPaid, - defaultSummaryData.feeChargesWaived, defaultSummaryData.feeChargesWrittenOff, defaultSummaryData.feeChargesOutstanding, - defaultSummaryData.feeChargesOverdue, defaultSummaryData.penaltyChargesCharged, defaultSummaryData.penaltyChargesPaid, - defaultSummaryData.penaltyChargesWaived, defaultSummaryData.penaltyChargesWrittenOff, - defaultSummaryData.penaltyChargesOutstanding, defaultSummaryData.penaltyChargesOverdue, - defaultSummaryData.totalExpectedRepayment, defaultSummaryData.totalRepayment, defaultSummaryData.totalExpectedCostOfLoan, - defaultSummaryData.totalCostOfLoan, defaultSummaryData.totalWaived, defaultSummaryData.totalWrittenOff, - defaultSummaryData.totalOutstanding, defaultSummaryData.totalOverdue, defaultSummaryData.overdueSinceDate, - defaultSummaryData.writeoffReasonId, defaultSummaryData.writeoffReason, defaultSummaryData.totalRecovered, - defaultSummaryData.chargeOffReasonId, defaultSummaryData.chargeOffReason).setTotalMerchantRefund(totalMerchantRefund) - .setTotalMerchantRefundReversed(totalMerchantRefundReversed).setTotalPayoutRefund(totalPayoutRefund) - .setTotalPayoutRefundReversed(totalPayoutRefundReversed).setTotalGoodwillCredit(totalGoodwillCredit) - .setTotalGoodwillCreditReversed(totalGoodwillCreditReversed).setTotalChargeAdjustment(totalChargeAdjustment) - .setTotalChargeAdjustmentReversed(totalChargeAdjustmentReversed).setTotalChargeback(totalChargeback) - .setTotalCreditBalanceRefund(totalCreditBalanceRefund).setTotalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed) - .setTotalRepaymentTransaction(totalRepaymentTransaction) - .setTotalRepaymentTransactionReversed(totalRepaymentTransactionReversed); + return LoanSummaryData.builder().currency(defaultSummaryData.currency).principalDisbursed(defaultSummaryData.principalDisbursed) + .principalAdjustments(defaultSummaryData.principalAdjustments).principalPaid(defaultSummaryData.principalPaid) + .principalWrittenOff(defaultSummaryData.principalWrittenOff).principalOutstanding(defaultSummaryData.principalOutstanding) + .principalOverdue(defaultSummaryData.principalOverdue).interestCharged(defaultSummaryData.interestCharged) + .interestPaid(defaultSummaryData.interestPaid).interestWaived(defaultSummaryData.interestWaived) + .interestWrittenOff(defaultSummaryData.interestWrittenOff).interestOutstanding(defaultSummaryData.interestOutstanding) + .interestOverdue(defaultSummaryData.interestOverdue).feeChargesCharged(defaultSummaryData.feeChargesCharged) + .feeAdjustments(defaultSummaryData.feeAdjustments) + .feeChargesDueAtDisbursementCharged(defaultSummaryData.feeChargesDueAtDisbursementCharged) + .feeChargesPaid(defaultSummaryData.feeChargesPaid).feeChargesWaived(defaultSummaryData.feeChargesWaived) + .feeChargesWrittenOff(defaultSummaryData.feeChargesWrittenOff) + .feeChargesOutstanding(defaultSummaryData.feeChargesOutstanding).feeChargesOverdue(defaultSummaryData.feeChargesOverdue) + .penaltyChargesCharged(defaultSummaryData.penaltyChargesCharged).penaltyAdjustments(defaultSummaryData.penaltyAdjustments) + .penaltyChargesPaid(defaultSummaryData.penaltyChargesPaid).penaltyChargesWaived(defaultSummaryData.penaltyChargesWaived) + .penaltyChargesWrittenOff(defaultSummaryData.penaltyChargesWrittenOff) + .penaltyChargesOutstanding(defaultSummaryData.penaltyChargesOutstanding) + .penaltyChargesOverdue(defaultSummaryData.penaltyChargesOverdue) + .totalExpectedRepayment(defaultSummaryData.totalExpectedRepayment).totalRepayment(defaultSummaryData.totalRepayment) + .totalExpectedCostOfLoan(defaultSummaryData.totalExpectedCostOfLoan).totalCostOfLoan(defaultSummaryData.totalCostOfLoan) + .totalWaived(defaultSummaryData.totalWaived).totalWrittenOff(defaultSummaryData.totalWrittenOff) + .totalOutstanding(defaultSummaryData.totalOutstanding).totalOverdue(defaultSummaryData.totalOverdue) + .overdueSinceDate(defaultSummaryData.overdueSinceDate).writeoffReasonId(defaultSummaryData.writeoffReasonId) + .writeoffReason(defaultSummaryData.writeoffReason).totalRecovered(defaultSummaryData.totalRecovered) + .chargeOffReasonId(defaultSummaryData.chargeOffReasonId).chargeOffReason(defaultSummaryData.chargeOffReason) + .totalMerchantRefund(totalMerchantRefund).totalMerchantRefundReversed(totalMerchantRefundReversed) + .totalPayoutRefund(totalPayoutRefund).totalPayoutRefundReversed(totalPayoutRefundReversed) + .totalGoodwillCredit(totalGoodwillCredit).totalGoodwillCreditReversed(totalGoodwillCreditReversed) + .totalChargeAdjustment(totalChargeAdjustment).totalChargeAdjustmentReversed(totalChargeAdjustmentReversed) + .totalChargeback(totalChargeback).totalCreditBalanceRefund(totalCreditBalanceRefund) + .totalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed).totalRepaymentTransaction(totalRepaymentTransaction) + .totalRepaymentTransactionReversed(totalRepaymentTransactionReversed).build(); } public static LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData) { - return new LoanSummaryData(currencyData, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null); + return LoanSummaryData.builder().currency(currencyData).build(); } private static BigDecimal computeTotalAmountForReversedTransactions(LoanTransactionType transactionType, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index f5878576215..cc795819be0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -695,7 +695,8 @@ private void generateLoanScheduleAccrualData(final LocalDate accruedTill, installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(), installment.getInterestAccrued(currency).getAmount(), installment.getFeeAccrued(currency).getAmount(), installment.getPenaltyAccrued(currency).getAmount(), currencyData, interestCalculatedFrom, - installment.getInterestWaived(currency).getAmount()); + installment.getInterestWaived(currency).getAmount(), installment.getCreditedFee(currency).getAmount(), + installment.getCreditedPenalty(currency).getAmount()); loanScheduleAccrualDatas.add(accrualData); } @@ -940,10 +941,12 @@ public void applyFinalIncomeAccrualTransaction(Loan loan) { .minus(loanRepaymentScheduleInstallment.getInterestWaived(currency)); feePortion = feePortion.add(loanRepaymentScheduleInstallment.getFeeChargesCharged(currency)) .minus(loanRepaymentScheduleInstallment.getFeeAccrued(currency)) - .minus(loanRepaymentScheduleInstallment.getFeeChargesWaived(currency)); + .minus(loanRepaymentScheduleInstallment.getFeeChargesWaived(currency)) + .minus(loanRepaymentScheduleInstallment.getCreditedFee(currency)); penaltyPortion = penaltyPortion.add(loanRepaymentScheduleInstallment.getPenaltyChargesCharged(currency)) .minus(loanRepaymentScheduleInstallment.getPenaltyAccrued(currency)) - .minus(loanRepaymentScheduleInstallment.getPenaltyChargesWaived(currency)); + .minus(loanRepaymentScheduleInstallment.getPenaltyChargesWaived(currency)) + .minus(loanRepaymentScheduleInstallment.getCreditedPenalty(currency)); } Money total = interestPortion.plus(feePortion).plus(penaltyPortion); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java index 27b4d167c38..4172f5c81b9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java @@ -168,7 +168,9 @@ private void addAccrualTillSpecificDate(final LocalDate tillDate, final LoanSche BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome(); BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome(); + BigDecimal totalCreditedPenalty = accrualData.getCreditedPenalty(); BigDecimal totalAccFee = accrualData.getAccruedFeeIncome(); + BigDecimal totalCreditedFee = accrualData.getCreditedFee(); if (totalAccInterest == null) { totalAccInterest = BigDecimal.ZERO; @@ -183,7 +185,10 @@ private void addAccrualTillSpecificDate(final LocalDate tillDate, final LoanSche if (totalAccFee == null) { totalAccFee = BigDecimal.ZERO; } - feePortion = feePortion.subtract(totalAccFee); + if (totalCreditedFee == null) { + totalCreditedFee = BigDecimal.ZERO; + } + feePortion = feePortion.subtract(totalAccFee).subtract(totalCreditedFee); amount = amount.add(feePortion); totalAccFee = totalAccFee.add(feePortion); if (feePortion.compareTo(BigDecimal.ZERO) == 0) { @@ -195,7 +200,10 @@ private void addAccrualTillSpecificDate(final LocalDate tillDate, final LoanSche if (totalAccPenalty == null) { totalAccPenalty = BigDecimal.ZERO; } - penaltyPortion = penaltyPortion.subtract(totalAccPenalty); + if (totalCreditedPenalty == null) { + totalCreditedPenalty = BigDecimal.ZERO; + } + penaltyPortion = penaltyPortion.subtract(totalAccPenalty).subtract(totalCreditedPenalty); amount = amount.add(penaltyPortion); totalAccPenalty = totalAccPenalty.add(penaltyPortion); if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) { @@ -234,6 +242,9 @@ public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) { if (scheduleAccrualData.getAccruedFeeIncome() != null) { feePortion = feePortion.subtract(scheduleAccrualData.getAccruedFeeIncome()); } + if (scheduleAccrualData.getCreditedFee() != null) { + feePortion = feePortion.subtract(scheduleAccrualData.getCreditedFee()); + } amount = amount.add(feePortion); if (feePortion.compareTo(BigDecimal.ZERO) == 0) { feePortion = null; @@ -248,6 +259,9 @@ public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) { if (scheduleAccrualData.getAccruedPenaltyIncome() != null) { penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getAccruedPenaltyIncome()); } + if (scheduleAccrualData.getCreditedPenalty() != null) { + penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getCreditedPenalty()); + } amount = amount.add(penaltyPortion); if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) { penaltyPortion = null; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index c804805a7b1..6d6643f9d86 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -649,6 +649,7 @@ public String loanSchema() { + " l.loan_officer_id as loanOfficerId, s.display_name as loanOfficerName, " + " l.principal_disbursed_derived as principalDisbursed, l.principal_repaid_derived as principalPaid," + " l.principal_adjustments_derived as principalAdjustments, l.principal_writtenoff_derived as principalWrittenOff," + + " l.fee_adjustments_derived as feeAdjustments, l.penalty_adjustments_derived as penaltyAdjustments," + " l.principal_outstanding_derived as principalOutstanding, l.interest_charged_derived as interestCharged," + " l.interest_repaid_derived as interestPaid, l.interest_waived_derived as interestWaived," + " l.interest_writtenoff_derived as interestWrittenOff, l.interest_outstanding_derived as interestOutstanding," @@ -912,6 +913,7 @@ public LoanAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") fi final BigDecimal interestOverdue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestOverdue"); final BigDecimal feeChargesCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesCharged"); + final BigDecimal feeAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeAdjustments"); final BigDecimal feeChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesPaid"); final BigDecimal feeChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesWaived"); final BigDecimal feeChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesWrittenOff"); @@ -919,6 +921,7 @@ public LoanAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") fi final BigDecimal feeChargesOverdue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesOverdue"); final BigDecimal penaltyChargesCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesCharged"); + final BigDecimal penaltyAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyAdjustments"); final BigDecimal penaltyChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesPaid"); final BigDecimal penaltyChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWaived"); final BigDecimal penaltyChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWrittenOff"); @@ -938,14 +941,24 @@ public LoanAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") fi final LocalDate overdueSinceDate = JdbcSupport.getLocalDate(rs, "overdueSinceDate"); inArrears = (overdueSinceDate != null); - loanSummary = new LoanSummaryData(currencyData, principalDisbursed, principalAdjustments, principalPaid, - principalWrittenOff, principalOutstanding, principalOverdue, interestCharged, interestPaid, interestWaived, - interestWrittenOff, interestOutstanding, interestOverdue, feeChargesCharged, feeChargesDueAtDisbursementCharged, - feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, feeChargesOverdue, - penaltyChargesCharged, penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff, - penaltyChargesOutstanding, penaltyChargesOverdue, totalExpectedRepayment, totalRepayment, totalExpectedCostOfLoan, - totalCostOfLoan, totalWaived, totalWrittenOff, totalOutstanding, totalOverdue, overdueSinceDate, writeoffReasonId, - writeoffReason, totalRecovered, chargeOffReasonId, chargeOffReason); + loanSummary = LoanSummaryData.builder().currency(currencyData).principalDisbursed(principalDisbursed) + .principalAdjustments(principalAdjustments).principalPaid(principalPaid).principalWrittenOff(principalWrittenOff) + .principalOutstanding(principalOutstanding).principalOverdue(principalOverdue).interestCharged(interestCharged) + .interestPaid(interestPaid).interestWaived(interestWaived).interestWrittenOff(interestWrittenOff) + .interestOutstanding(interestOutstanding).interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged) + .feeAdjustments(feeAdjustments).feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged) + .feeChargesPaid(feeChargesPaid).feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff) + .feeChargesOutstanding(feeChargesOutstanding).feeChargesOverdue(feeChargesOverdue) + .penaltyChargesCharged(penaltyChargesCharged).penaltyAdjustments(penaltyAdjustments) + .penaltyChargesPaid(penaltyChargesPaid).penaltyChargesWaived(penaltyChargesWaived) + .penaltyChargesWrittenOff(penaltyChargesWrittenOff).penaltyChargesOutstanding(penaltyChargesOutstanding) + .penaltyChargesOverdue(penaltyChargesOverdue).totalExpectedRepayment(totalExpectedRepayment) + .totalRepayment(totalRepayment).totalExpectedCostOfLoan(totalExpectedCostOfLoan).totalCostOfLoan(totalCostOfLoan) + .totalWaived(totalWaived).totalWrittenOff(totalWrittenOff).totalOutstanding(totalOutstanding) + .totalOverdue(totalOverdue).overdueSinceDate(overdueSinceDate).writeoffReasonId(writeoffReasonId) + .writeoffReason(writeoffReason).totalRecovered(totalRecovered).chargeOffReasonId(chargeOffReasonId) + .chargeOffReason(chargeOffReason).build(); + } GroupGeneralData groupData = null; @@ -1144,7 +1157,7 @@ public String schema() { + " ls.fee_charges_amount as feeChargesDue, ls.fee_charges_completed_derived as feeChargesPaid, ls.fee_charges_waived_derived as feeChargesWaived, ls.fee_charges_writtenoff_derived as feeChargesWrittenOff, " + " ls.penalty_charges_amount as penaltyChargesDue, ls.penalty_charges_completed_derived as penaltyChargesPaid, ls.penalty_charges_waived_derived as penaltyChargesWaived, " + " ls.penalty_charges_writtenoff_derived as penaltyChargesWrittenOff, ls.total_paid_in_advance_derived as totalPaidInAdvanceForPeriod, " - + " ls.total_paid_late_derived as totalPaidLateForPeriod, ls.credits_amount as totalCredits, ls.is_down_payment isDownPayment " + + " ls.total_paid_late_derived as totalPaidLateForPeriod, (coalesce(ls.credited_principal,0) + coalesce(ls.credited_fee,0) + coalesce(ls.credited_penalty,0)) as totalCredits, ls.is_down_payment isDownPayment " + " from m_loan_repayment_schedule ls "; } @@ -1912,6 +1925,7 @@ public String schema() { .append("ls.duedate as duedate,ls.fromdate as fromdate ,ls.id as scheduleId,loan.product_id as productId,") .append("ls.interest_amount as interest, ls.interest_waived_derived as interestWaived,") .append("ls.penalty_charges_amount as penalty, ").append("ls.fee_charges_amount as charges, ") + .append("ls.credited_penalty as credited_penalty, ").append("ls.credited_fee as credited_fee, ") .append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,") .append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,") .append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode") @@ -1946,6 +1960,8 @@ public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest"); final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege"); final BigDecimal accruedPenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accpenalty"); + final BigDecimal creditedFee = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_fee"); + final BigDecimal creditedPenalty = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_penalty"); final String currencyCode = rs.getString("currencyCode"); final String currencyName = rs.getString("currencyName"); @@ -1958,7 +1974,7 @@ public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromDate, repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome, - accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived); + accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived, creditedFee, creditedPenalty); } } @@ -1972,6 +1988,7 @@ public String schema() { .append("ls.installment as installmentNumber, ") .append("ls.interest_amount as interest, ls.interest_waived_derived as interestWaived,") .append("ls.penalty_charges_amount as penalty, ").append("ls.fee_charges_amount as charges, ") + .append("ls.credited_penalty as credited_penalty, ").append("ls.credited_fee as credited_fee, ") .append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,") .append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,") .append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode") @@ -1996,6 +2013,9 @@ public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") final BigDecimal interestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interest"); final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges"); final BigDecimal penaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalty"); + final BigDecimal creditedFee = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_fee"); + final BigDecimal creditedPenalty = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_penalty"); + final BigDecimal interestIncomeWaived = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interestWaived"); final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest"); final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege"); @@ -2015,7 +2035,7 @@ public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") final LocalDate interestCalculatedFrom = null; return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromdate, repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome, - accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived); + accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived, creditedFee, creditedPenalty); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index fb79dc2900c..e4ba465b715 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -24,11 +24,15 @@ import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; import java.math.BigDecimal; import java.math.RoundingMode; @@ -83,7 +87,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest { private final LocalDate transactionDate = LocalDate.of(2023, 7, 11); private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1); - private static final MockedStatic MONEY_HELPER = Mockito.mockStatic(MoneyHelper.class); + private static final MockedStatic MONEY_HELPER = mockStatic(MoneyHelper.class); private AdvancedPaymentScheduleTransactionProcessor underTest; @BeforeAll @@ -123,34 +127,33 @@ public void chargePaymentTransactionTestWithExactAmount() { Money zero = Money.zero(currency); Loan loan = mock(Loan.class); Money chargeAmountMoney = Money.of(currency, chargeAmount); - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L), + LoanRepaymentScheduleInstallment installment = spy( + new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); - Mockito.when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT); - Mockito.when(chargePaidBy.getLoanCharge()).thenReturn(charge); - Mockito.when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy)); - Mockito.when(loanTransaction.getAmount()).thenReturn(chargeAmount); - Mockito.when(loanTransaction.getAmount(currency)).thenReturn(chargeAmountMoney); - Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); - Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); - Mockito.when(loanTransaction.getLoan()).thenReturn(loan); - Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); - Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) - .thenReturn(true); - Mockito.when(installment.getInstallmentNumber()).thenReturn(1); - Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney); - Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false); + when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT); + when(chargePaidBy.getLoanCharge()).thenReturn(charge); + when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy)); + when(loanTransaction.getAmount()).thenReturn(chargeAmount); + when(loanTransaction.getAmount(currency)).thenReturn(chargeAmountMoney); + when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); + when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); + when(loanTransaction.getLoan()).thenReturn(loan); + when(loan.getDisbursementDate()).thenReturn(disbursementDate); + when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true); + when(installment.getInstallmentNumber()).thenReturn(1); + when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney); + when(loanTransaction.isPenaltyPayment()).thenReturn(false); underTest.processLatestTransaction(loanTransaction, new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount))); - Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney)); - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); + Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney)); + Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(chargeAmountMoney).getAmount()); assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount())); assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount())); - Mockito.verify(loan, Mockito.times(0)).getPaymentAllocationRules(); + Mockito.verify(loan, times(0)).getPaymentAllocationRules(); } @Test @@ -168,35 +171,33 @@ public void chargePaymentTransactionTestWithLessTransactionAmount() { Money chargeAmountMoney = Money.of(currency, chargeAmount); BigDecimal transactionAmount = BigDecimal.valueOf(20.00); Money transactionAmountMoney = Money.of(currency, transactionAmount); - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L), + LoanRepaymentScheduleInstallment installment = spy( + new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); - Mockito.when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT); - Mockito.when(chargePaidBy.getLoanCharge()).thenReturn(charge); - Mockito.when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy)); - Mockito.when(loanTransaction.getAmount()).thenReturn(transactionAmount); - Mockito.when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney); - Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); - Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); - Mockito.when(loanTransaction.getLoan()).thenReturn(loan); - Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); - Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) - .thenReturn(true); - Mockito.when(installment.getInstallmentNumber()).thenReturn(1); - Mockito.when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney); - Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false); + when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT); + when(chargePaidBy.getLoanCharge()).thenReturn(charge); + when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy)); + when(loanTransaction.getAmount()).thenReturn(transactionAmount); + when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney); + when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); + when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); + when(loanTransaction.getLoan()).thenReturn(loan); + when(loan.getDisbursementDate()).thenReturn(disbursementDate); + when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true); + when(installment.getInstallmentNumber()).thenReturn(1); + when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney); + when(loanTransaction.isPenaltyPayment()).thenReturn(false); underTest.processLatestTransaction(loanTransaction, new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount))); - Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney)); - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), - refEq(zero)); + Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney)); + Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), refEq(zero)); assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(transactionAmountMoney).getAmount()); assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount())); assertEquals(0, BigDecimal.valueOf(80.00).compareTo(installment.getFeeChargesOutstanding(currency).getAmount())); - Mockito.verify(loan, Mockito.times(0)).getPaymentAllocationRules(); + Mockito.verify(loan, times(0)).getPaymentAllocationRules(); } @Test @@ -216,52 +217,50 @@ public void chargePaymentTransactionTestWithMoreTransactionAmount() { BigDecimal transactionAmount = BigDecimal.valueOf(120.00); Money transactionAmountMoney = Money.of(currency, transactionAmount); LoanPaymentAllocationRule loanPaymentAllocationRule = mock(LoanPaymentAllocationRule.class); - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, transactionDate, BigDecimal.valueOf(100L), - BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); - - Mockito.when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT); - Mockito.when(chargePaidBy.getLoanCharge()).thenReturn(charge); - Mockito.when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy)); - Mockito.when(loanTransaction.getAmount()).thenReturn(transactionAmount); - Mockito.when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney); - Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); - Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); - Mockito.when(loanTransaction.getLoan()).thenReturn(loan); - Mockito.when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); - Mockito.when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL); - Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); - Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) - .thenReturn(true); - Mockito.when(installment.getInstallmentNumber()).thenReturn(1); - Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney); - Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false); - Mockito.when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule)); - Mockito.when(loanPaymentAllocationRule.getTransactionType()).thenReturn(PaymentAllocationTransactionType.DEFAULT); - Mockito.when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL)); - Mockito.when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true); + LoanRepaymentScheduleInstallment installment = spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, transactionDate, + BigDecimal.valueOf(100L), BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); + + when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT); + when(chargePaidBy.getLoanCharge()).thenReturn(charge); + when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy)); + when(loanTransaction.getAmount()).thenReturn(transactionAmount); + when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney); + when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); + when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); + when(loanTransaction.getLoan()).thenReturn(loan); + when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); + when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL); + when(loan.getDisbursementDate()).thenReturn(disbursementDate); + when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true); + when(installment.getInstallmentNumber()).thenReturn(1); + when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney); + when(loanTransaction.isPenaltyPayment()).thenReturn(false); + when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule)); + when(loanPaymentAllocationRule.getTransactionType()).thenReturn(PaymentAllocationTransactionType.DEFAULT); + when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL)); + when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true); underTest.processLatestTransaction(loanTransaction, new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount))); - Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney)); - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); + Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney)); + Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); assertEquals(0, BigDecimal.valueOf(20).compareTo(loanTransaction.getAmount(currency).minus(chargeAmountMoney).getAmount())); assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount())); assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount())); assertEquals(0, BigDecimal.valueOf(80).compareTo(installment.getPrincipalOutstanding(currency).getAmount())); - Mockito.verify(loan, Mockito.times(1)).getPaymentAllocationRules(); + Mockito.verify(loan, times(1)).getPaymentAllocationRules(); } @Test - public void testProcessCreditTransactionWithAllocationRuleInterestAndPrincipal() { + public void testProcessCreditTransactionWithAllocationRulePrincipalPenaltyFeeInterest() { // given Loan loan = mock(Loan.class); - LoanTransaction chargeBackTransaction = createChargebackTransaction(loan); + LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0); - LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(INTEREST, PRINCIPAL, PENALTY, FEE); - Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule)); - LoanTransaction repayment = createRepayment(loan, chargeBackTransaction); + LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST); + when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule)); + LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5); lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment)); MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY)); @@ -270,40 +269,55 @@ public void testProcessCreditTransactionWithAllocationRuleInterestAndPrincipal() installments.add(installment); // when - TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder); underTest.processCreditTransaction(chargeBackTransaction, ctx); - // then - Mockito.verify(installment, Mockito.times(1)).addToCredits(new BigDecimal("25.00")); - Mockito.verify(installment, Mockito.times(1)).updateInterestCharged(new BigDecimal("20.00")); + // verify principal + Mockito.verify(installment, times(1)).addToCreditedPrincipal(new BigDecimal("10.00")); ArgumentCaptor localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class); ArgumentCaptor moneyCaptor = ArgumentCaptor.forClass(Money.class); - Mockito.verify(installment, Mockito.times(1)).addToPrincipal(localDateArgumentCaptor.capture(), moneyCaptor.capture()); + Mockito.verify(installment, times(1)).addToPrincipal(localDateArgumentCaptor.capture(), moneyCaptor.capture()); Assertions.assertEquals(LocalDate.of(2023, 1, 1), localDateArgumentCaptor.getValue()); - assertEquals(0, moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0))); + assertEquals(0, moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); + + // verify charges on installment + Mockito.verify(installment, times(1)).addToCreditedFee(new BigDecimal("10.00")); + Mockito.verify(installment, times(1)).addToCreditedPenalty(new BigDecimal("5.00")); + + ArgumentCaptor feeCaptor = ArgumentCaptor.forClass(Money.class); + ArgumentCaptor penaltyCaptor = ArgumentCaptor.forClass(Money.class); + Mockito.verify(installment, times(2)).addToChargePortion(feeCaptor.capture(), any(), any(), penaltyCaptor.capture(), any(), any()); + assertEquals(2, feeCaptor.getAllValues().size()); + assertEquals(2, penaltyCaptor.getAllValues().size()); + assertEquals(0, feeCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.valueOf(10.0))); + assertEquals(0, feeCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.ZERO)); + + assertEquals(0, penaltyCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.ZERO)); + assertEquals(0, penaltyCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.valueOf(5.0))); + + // verify transaction ArgumentCaptor principal = ArgumentCaptor.forClass(Money.class); ArgumentCaptor interest = ArgumentCaptor.forClass(Money.class); ArgumentCaptor fee = ArgumentCaptor.forClass(Money.class); ArgumentCaptor penalty = ArgumentCaptor.forClass(Money.class); Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(), penalty.capture()); - assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0))); - assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.valueOf(20.0))); - assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO)); - assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.ZERO)); + assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); + assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); + assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0))); + assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.ZERO)); } @Test - public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterest() { + public void testProcessCreditTransactionWithAllocationRulePenaltyFeePrincipalInterest() { // given Loan loan = mock(Loan.class); - LoanTransaction chargeBackTransaction = createChargebackTransaction(loan); + LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0); - LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE); - Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule)); - LoanTransaction repayment = createRepayment(loan, chargeBackTransaction); + LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PENALTY, FEE, PRINCIPAL, INTEREST); + when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule)); + LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5); lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment)); MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY)); @@ -315,40 +329,49 @@ public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterest() TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder); underTest.processCreditTransaction(chargeBackTransaction, ctx); - // then - Mockito.verify(installment, Mockito.times(1)).addToCredits(new BigDecimal("25.00")); - Mockito.verify(installment, Mockito.times(1)).updateInterestCharged(new BigDecimal("15.00")); - ArgumentCaptor localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class); - ArgumentCaptor moneyCaptor = ArgumentCaptor.forClass(Money.class); - Mockito.verify(installment, Mockito.times(1)).addToPrincipal(localDateArgumentCaptor.capture(), moneyCaptor.capture()); - Assertions.assertEquals(LocalDate.of(2023, 1, 1), localDateArgumentCaptor.getValue()); - assertEquals(0, moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); + // verify charges on installment + Mockito.verify(installment, times(1)).addToCreditedFee(new BigDecimal("20.00")); + Mockito.verify(installment, times(1)).addToCreditedPenalty(new BigDecimal("5.00")); + + ArgumentCaptor feeCaptor = ArgumentCaptor.forClass(Money.class); + ArgumentCaptor penaltyCaptor = ArgumentCaptor.forClass(Money.class); + Mockito.verify(installment, times(2)).addToChargePortion(feeCaptor.capture(), any(), any(), penaltyCaptor.capture(), any(), any()); + assertEquals(2, feeCaptor.getAllValues().size()); + assertEquals(2, penaltyCaptor.getAllValues().size()); + assertEquals(0, feeCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.valueOf(20.0))); + assertEquals(0, feeCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.ZERO)); + + assertEquals(0, penaltyCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.ZERO)); + assertEquals(0, penaltyCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.valueOf(5.0))); + + // verify transaction ArgumentCaptor principal = ArgumentCaptor.forClass(Money.class); ArgumentCaptor interest = ArgumentCaptor.forClass(Money.class); ArgumentCaptor fee = ArgumentCaptor.forClass(Money.class); ArgumentCaptor penalty = ArgumentCaptor.forClass(Money.class); Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(), penalty.capture()); - assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); - assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.valueOf(15.0))); - assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO)); - assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.ZERO)); + assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(0))); + assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.valueOf(0))); + assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(20.0))); + assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0))); } @Test public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment() { // given Loan loan = mock(Loan.class); - LoanTransaction chargeBackTransaction = createChargebackTransaction(loan); + LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0); - LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE); - Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule)); - LoanTransaction repayment = createRepayment(loan, chargeBackTransaction); + LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST); + when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule)); + LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5); lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment)); MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY)); List installments = new ArrayList<>(); + LoanRepaymentScheduleInstallment installment1 = createMockInstallment(LocalDate.of(2022, 12, 20), false); LoanRepaymentScheduleInstallment installment2 = createMockInstallment(LocalDate.of(2022, 12, 27), true); installments.add(installment1); @@ -358,15 +381,31 @@ public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWi TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder); underTest.processCreditTransaction(chargeBackTransaction, ctx); - // then - Mockito.verify(installment2, Mockito.times(1)).addToCredits(new BigDecimal("25.00")); - Mockito.verify(installment2, Mockito.times(1)).updateInterestCharged(new BigDecimal("15.00")); + // verify principal + Mockito.verify(installment2, times(1)).addToCreditedPrincipal(new BigDecimal("10.00")); ArgumentCaptor localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class); ArgumentCaptor moneyCaptor = ArgumentCaptor.forClass(Money.class); - Mockito.verify(installment2, Mockito.times(1)).addToPrincipal(localDateArgumentCaptor.capture(), moneyCaptor.capture()); + Mockito.verify(installment2, times(1)).addToPrincipal(localDateArgumentCaptor.capture(), moneyCaptor.capture()); Assertions.assertEquals(LocalDate.of(2023, 1, 1), localDateArgumentCaptor.getValue()); assertEquals(0, moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); + // verify charges on installment + Mockito.verify(installment2, times(1)).addToCreditedFee(new BigDecimal("10.00")); + Mockito.verify(installment2, times(1)).addToCreditedPenalty(new BigDecimal("5.00")); + + ArgumentCaptor feeCaptor = ArgumentCaptor.forClass(Money.class); + ArgumentCaptor penaltyCaptor = ArgumentCaptor.forClass(Money.class); + Mockito.verify(installment2, times(2)).addToChargePortion(feeCaptor.capture(), any(), any(), penaltyCaptor.capture(), any(), any()); + assertEquals(2, feeCaptor.getAllValues().size()); + assertEquals(2, penaltyCaptor.getAllValues().size()); + + assertEquals(0, feeCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.valueOf(10.0))); + assertEquals(0, feeCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.ZERO)); + + assertEquals(0, penaltyCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.ZERO)); + assertEquals(0, penaltyCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.valueOf(5.0))); + + // verify transaction ArgumentCaptor principal = ArgumentCaptor.forClass(Money.class); ArgumentCaptor interest = ArgumentCaptor.forClass(Money.class); ArgumentCaptor fee = ArgumentCaptor.forClass(Money.class); @@ -374,9 +413,9 @@ public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWi Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(), penalty.capture()); assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); - assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.valueOf(15.0))); - assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO)); - assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.ZERO)); + assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); + assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0))); + assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.ZERO)); } private LoanRepaymentScheduleInstallment createMockInstallment(LocalDate localDate, boolean isAdditional) { @@ -396,15 +435,16 @@ private LoanCreditAllocationRule createMockCreditAllocationRule(AllocationType.. return mockCreditAllocationRule; } - private LoanTransaction createRepayment(Loan loan, LoanTransaction toTransaction) { + private LoanTransaction createRepayment(Loan loan, LoanTransaction toTransaction, double principalPortion, double interestPortion, + double feePortion, double penaltyPortion) { LoanTransaction repayment = mock(LoanTransaction.class); lenient().when(repayment.getLoan()).thenReturn(loan); lenient().when(repayment.isRepayment()).thenReturn(true); lenient().when(repayment.getTypeOf()).thenReturn(REPAYMENT); - lenient().when(repayment.getPrincipalPortion()).thenReturn(BigDecimal.valueOf(10)); - lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(20)); - lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.ZERO); - lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.ZERO); + lenient().when(repayment.getPrincipalPortion()).thenReturn(BigDecimal.valueOf(principalPortion)); + lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(interestPortion)); + lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.valueOf(feePortion)); + lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.valueOf(penaltyPortion)); LoanTransactionRelation relation = mock(LoanTransactionRelation.class); lenient().when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); @@ -414,14 +454,14 @@ private LoanTransaction createRepayment(Loan loan, LoanTransaction toTransaction return repayment; } - private LoanTransaction createChargebackTransaction(Loan loan) { + private LoanTransaction createChargebackTransaction(Loan loan, double transactionAmount) { LoanTransaction chargeback = mock(LoanTransaction.class); lenient().when(chargeback.isChargeback()).thenReturn(true); lenient().when(chargeback.getTypeOf()).thenReturn(LoanTransactionType.CHARGEBACK); lenient().when(chargeback.getLoan()).thenReturn(loan); - lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(25)); - Money amount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(25)); - lenient().when(chargeback.getAmount(MONETARY_CURRENCY)).thenReturn(amount); + lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(transactionAmount)); + Money money = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(transactionAmount)); + lenient().when(chargeback.getAmount(MONETARY_CURRENCY)).thenReturn(money); lenient().when(chargeback.getTransactionDate()).thenReturn(LocalDate.of(2023, 1, 1)); return chargeback; } @@ -431,40 +471,41 @@ public void calculateChargebackAllocationMap() { Map result; MonetaryCurrency currency = mock(MonetaryCurrency.class); - result = underTest.calculateChargebackAllocationMap(allocationMap(50.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(50.0), + result = underTest.calculateChargebackAllocationMap(allocationMap(50.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(50.0), List.of(PRINCIPAL, INTEREST, FEE, PENALTY), currency); - verify(allocationMap(50.0, 0, 0, 0), result); + verify(allocationMap(50.0, 0, 0, 0, currency), result); - result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(50.0), + result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(50.0), List.of(PRINCIPAL, INTEREST, FEE, PENALTY), currency); - verify(allocationMap(40.0, 10, 0, 0), result); + verify(allocationMap(40.0, 10, 0, 0, currency), result); - result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(50.0), + result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(50.0), List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency); - verify(allocationMap(40.0, 0, 10, 0), result); + verify(allocationMap(40.0, 0, 10, 0, currency), result); - result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(340.0), + result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(340.0), List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency); - verify(allocationMap(40.0, 88.0, 200.0, 12.0), result); + verify(allocationMap(40.0, 88.0, 200.0, 12.0, currency), result); - result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(352.0), + result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(352.0), List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency); - verify(allocationMap(40.0, 100.0, 200.0, 12.0), result); + verify(allocationMap(40.0, 100.0, 200.0, 12.0, currency), result); } - private void verify(Map expected, Map actual) { + private void verify(Map expected, Map actual) { Assertions.assertEquals(expected.size(), actual.size()); expected.forEach((k, v) -> { - Assertions.assertEquals(0, v.compareTo(actual.get(k).getAmount()), "Not matching for " + k); + Assertions.assertEquals(0, v.getAmount().compareTo(actual.get(k).getAmount()), "Not matching for " + k); }); } - private Map allocationMap(double principal, double interest, double fee, double penalty) { - Map allocationMap = new HashMap<>(); - allocationMap.put(AllocationType.PRINCIPAL, BigDecimal.valueOf(principal)); - allocationMap.put(AllocationType.INTEREST, BigDecimal.valueOf(interest)); - allocationMap.put(AllocationType.FEE, BigDecimal.valueOf(fee)); - allocationMap.put(AllocationType.PENALTY, BigDecimal.valueOf(penalty)); + private Map allocationMap(double principal, double interest, double fee, double penalty, + MonetaryCurrency currency) { + Map allocationMap = new HashMap<>(); + allocationMap.put(AllocationType.PRINCIPAL, Money.of(currency, BigDecimal.valueOf(principal))); + allocationMap.put(AllocationType.INTEREST, Money.of(currency, BigDecimal.valueOf(interest))); + allocationMap.put(AllocationType.FEE, Money.of(currency, BigDecimal.valueOf(fee))); + allocationMap.put(AllocationType.PENALTY, Money.of(currency, BigDecimal.valueOf(penalty))); return allocationMap; } @@ -472,17 +513,17 @@ private Map allocationMap(double principal, double i public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionWhenIdProvided() { // given LoanTransaction chargebackTransaction = mock(LoanTransaction.class); - Mockito.when(chargebackTransaction.getId()).thenReturn(123L); + when(chargebackTransaction.getId()).thenReturn(123L); Loan loan = mock(Loan.class); - Mockito.when(chargebackTransaction.getLoan()).thenReturn(loan); + when(chargebackTransaction.getLoan()).thenReturn(loan); LoanTransaction repayment1 = mock(LoanTransaction.class); LoanTransaction repayment2 = mock(LoanTransaction.class); - Mockito.when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2)); + when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2)); LoanTransactionRelation relation = mock(LoanTransactionRelation.class); - Mockito.when(relation.getToTransaction()).thenReturn(chargebackTransaction); - Mockito.when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); - Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); + when(relation.getToTransaction()).thenReturn(chargebackTransaction); + when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); + when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); TransactionCtx ctx = mock(TransactionCtx.class); // when @@ -496,14 +537,14 @@ public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionWhenI public void testFindOriginalTransactionThrowsRuntimeExceptionWhenIdProvidedAndRelationsAreMissing() { // given LoanTransaction chargebackTransaction = mock(LoanTransaction.class); - Mockito.when(chargebackTransaction.getId()).thenReturn(123L); + when(chargebackTransaction.getId()).thenReturn(123L); Loan loan = mock(Loan.class); - Mockito.when(chargebackTransaction.getLoan()).thenReturn(loan); + when(chargebackTransaction.getLoan()).thenReturn(loan); LoanTransaction repayment1 = mock(LoanTransaction.class); LoanTransaction repayment2 = mock(LoanTransaction.class); - Mockito.when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2)); + when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2)); - Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of()); + when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of()); TransactionCtx ctx = mock(TransactionCtx.class); @@ -517,22 +558,22 @@ public void testFindOriginalTransactionThrowsRuntimeExceptionWhenIdProvidedAndRe public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvided() { // given LoanTransaction chargebackReplayed = mock(LoanTransaction.class); - Mockito.when(chargebackReplayed.getId()).thenReturn(null); + when(chargebackReplayed.getId()).thenReturn(null); LoanTransaction repayment1 = mock(LoanTransaction.class); LoanTransaction repayment2 = mock(LoanTransaction.class); LoanTransaction originalChargeback = mock(LoanTransaction.class); - Mockito.when(originalChargeback.getId()).thenReturn(123L); + when(originalChargeback.getId()).thenReturn(123L); LoanTransactionRelation relation = mock(LoanTransactionRelation.class); - Mockito.when(relation.getToTransaction()).thenReturn(originalChargeback); - Mockito.when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); - Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); + when(relation.getToTransaction()).thenReturn(originalChargeback); + when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); + when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); TransactionCtx ctx = mock(TransactionCtx.class); ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class); - Mockito.when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail); - Mockito.when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L)); - Mockito.when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2)); + when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail); + when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L)); + when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2)); // when LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx); @@ -545,25 +586,25 @@ public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromT public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvidedFallbackToPersistedTransactions() { // given LoanTransaction chargebackReplayed = mock(LoanTransaction.class); - Mockito.when(chargebackReplayed.getId()).thenReturn(null); + when(chargebackReplayed.getId()).thenReturn(null); LoanTransaction repayment1 = mock(LoanTransaction.class); LoanTransaction repayment2 = mock(LoanTransaction.class); Loan loan = mock(Loan.class); - Mockito.when(chargebackReplayed.getLoan()).thenReturn(loan); - Mockito.when(loan.getLoanTransactions()).thenReturn(List.of(repayment1, repayment2)); + when(chargebackReplayed.getLoan()).thenReturn(loan); + when(loan.getLoanTransactions()).thenReturn(List.of(repayment1, repayment2)); LoanTransaction originalChargeback = mock(LoanTransaction.class); - Mockito.when(originalChargeback.getId()).thenReturn(123L); + when(originalChargeback.getId()).thenReturn(123L); LoanTransactionRelation relation = mock(LoanTransactionRelation.class); - Mockito.when(relation.getToTransaction()).thenReturn(originalChargeback); - Mockito.when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); - Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); + when(relation.getToTransaction()).thenReturn(originalChargeback); + when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); + when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); TransactionCtx ctx = mock(TransactionCtx.class); ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class); - Mockito.when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail); - Mockito.when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L)); - Mockito.when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of()); + when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail); + when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L)); + when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of()); // when LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 7907515280b..1e584db85e4 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -441,6 +441,46 @@ protected void verifyRepaymentSchedule(Long loanId, Installment... installments) totalOutstanding)); } + Double outstandingPrincipalExpected = installments[i].outstandingAmounts != null + ? installments[i].outstandingAmounts.principalOutstanding + : null; + Double outstandingPrincipal = period.getPrincipalOutstanding(); + if (outstandingPrincipalExpected != null) { + Assertions.assertEquals(outstandingPrincipalExpected, outstandingPrincipal, + "%d. installment's outstanding principal is different, expected: %.2f, actual: %.2f".formatted(i, + outstandingPrincipalExpected, outstandingPrincipal)); + } + + Double outstandingFeeExpected = installments[i].outstandingAmounts != null + ? installments[i].outstandingAmounts.feeOutstanding + : null; + Double outstandingFee = period.getFeeChargesOutstanding(); + if (outstandingFeeExpected != null) { + Assertions.assertEquals(outstandingFeeExpected, outstandingFee, + "%d. installment's outstanding fee is different, expected: %.2f, actual: %.2f".formatted(i, + outstandingFeeExpected, outstandingFee)); + } + + Double outstandingPenaltyExpected = installments[i].outstandingAmounts != null + ? installments[i].outstandingAmounts.penaltyOutstanding + : null; + Double outstandingPenalty = period.getPenaltyChargesOutstanding(); + if (outstandingPenaltyExpected != null) { + Assertions.assertEquals(outstandingPenaltyExpected, outstandingPenalty, + "%d. installment's outstanding penalty is different, expected: %.2f, actual: %.2f".formatted(i, + outstandingPenaltyExpected, outstandingPenalty)); + } + + Double outstandingTotalExpected = installments[i].outstandingAmounts != null + ? installments[i].outstandingAmounts.totalOutstanding + : null; + Double outstandingTotal = period.getTotalOutstandingForPeriod(); + if (outstandingTotalExpected != null) { + Assertions.assertEquals(outstandingTotalExpected, outstandingTotal, + "%d. installment's total outstanding is different, expected: %.2f, actual: %.2f".formatted(i, + outstandingTotalExpected, outstandingTotal)); + } + } Assertions.assertEquals(installments[i].completed, period.getComplete()); Assertions.assertEquals(LocalDate.parse(installments[i].dueDate, dateTimeFormatter), period.getDueDate()); @@ -564,22 +604,31 @@ protected TransactionExt transaction(double amount, String type, String date, do } protected Installment installment(double principalAmount, Boolean completed, String dueDate) { - return new Installment(principalAmount, null, null, null, null, completed, dueDate); + return new Installment(principalAmount, null, null, null, null, completed, dueDate, null); } protected Installment installment(double principalAmount, double interestAmount, double totalOutstandingAmount, Boolean completed, String dueDate) { - return new Installment(principalAmount, interestAmount, null, null, totalOutstandingAmount, completed, dueDate); + return new Installment(principalAmount, interestAmount, null, null, totalOutstandingAmount, completed, dueDate, null); } protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double totalOutstandingAmount, Boolean completed, String dueDate) { - return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate); + return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate, null); } protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double penaltyAmount, double totalOutstandingAmount, Boolean completed, String dueDate) { - return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, totalOutstandingAmount, completed, dueDate); + return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, totalOutstandingAmount, completed, dueDate, null); + } + + protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double penaltyAmount, + OutstandingAmounts outstandingAmounts, Boolean completed, String dueDate) { + return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, null, completed, dueDate, outstandingAmounts); + } + + protected OutstandingAmounts outstanding(double principal, double fee, double penalty, double total) { + return new OutstandingAmounts(principal, fee, penalty, total); } protected BatchRequestBuilder batchRequest() { @@ -695,6 +744,17 @@ public static class Installment { Double totalOutstandingAmount; Boolean completed; String dueDate; + OutstandingAmounts outstandingAmounts; + } + + @AllArgsConstructor + @ToString + public static class OutstandingAmounts { + + Double principalOutstanding; + Double feeOutstanding; + Double penaltyOutstanding; + Double totalOutstanding; } public static class AmortizationType { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java index 1f761043f56..63ae1b179a7 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java @@ -26,6 +26,8 @@ import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.CreditAllocationData; import org.apache.fineract.client.models.CreditAllocationOrder; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdSummary; import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; @@ -38,6 +40,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -46,13 +49,15 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoanIntegrationTest { @Test - public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrincipal() { + public void simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipal() { runAt("01 January 2023", () -> { // Create Client Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); // Create Loan Product - Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")); - + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); // Apply and Approve Loan Long loanId = applyAndApproveLoan(clientId, loanProductId); @@ -92,30 +97,160 @@ public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrin verifyTransactions(loanId, // transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // - transaction(100.0, "Chargeback", "20 January 2023", 1037.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // ); // Verify Repayment Schedule verifyRepaymentSchedule(loanId, // installment(0, null, "01 January 2023"), // - installment(343.0, 0, 50, 20, 30.0, false, "01 February 2023"), // TODO: we still need to add the - // fee and the penalty to the - // outstanding + installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"), + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + }); + } + + @Test + public void simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipalOnTheLastDayOfTheInstallment() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + + // Add Charges + Long feeId = addCharge(loanId, false, 50, "15 January 2023"); + Long penaltyId = addCharge(loanId, true, 20, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Update Business Date + updateBusinessDate("20 January 2023"); + + // Add Repayment + Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), // installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // ); + + updateBusinessDate("01 February 2023"); + + // Add Chargeback + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30 + // principal + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "01 February 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + ); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0, true, "01 February 2023"), // + installment(343.0, 0, 50, 20, 413.0, false, "01 March 2023"), + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); }); } @Test - public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrincipalOnNPlusOneInstallment() { + public void simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipalOnTheLastDayOfTheLoan() { runAt("01 January 2023", () -> { // Create Client Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); // Create Loan Product - Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")); + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + // Add Charges + Long feeId = addCharge(loanId, false, 50, "15 January 2023"); + Long penaltyId = addCharge(loanId, true, 20, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Update Business Date + updateBusinessDate("20 January 2023"); + + // Add Repayment + Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + updateBusinessDate("01 May 2023"); + + // Add Chargeback + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30 + // principal + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "01 May 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + ); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(341.0, 0, 50, 20, 411.0, false, "01 May 2023") // + ); + }); + } + + @Test + public void chargebackWithCreditAllocationPenaltyFeeInterestAndPrincipalOnNPlusOneInstallment() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); // Apply and Approve Loan Long loanId = applyAndApproveLoan(clientId, loanProductId); @@ -169,7 +304,7 @@ public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrin transaction(313.0, "Repayment", "20 March 2023", 311.0, 313.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(381.0, "Repayment", "20 April 2023", 0.0, 311.0, 0.0, 50.0, 20.0, 0.0, 0.0), // transaction(70.0, "Accrual", "20 April 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), // - transaction(100.0, "Chargeback", "02 May 2023", 100.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + transaction(100.0, "Chargeback", "02 May 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // ); verifyRepaymentSchedule(loanId, // @@ -178,20 +313,21 @@ public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrin installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), // installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), // installment(311.0, 0, 50, 20, 0.0, true, "01 May 2023"), // - installment(30.0, 0, 0, 0, 30.0, false, "02 May 2023") // TODO: fee and penalty must be added here - // after chargeback + installment(30.0, 0, 50, 20, 100.0, false, "02 May 2023") // ); }); } @Test - public void createLoanWithCreditAllocationAndChargebackReverseReplayWithBackdatedPayment() { + public void chargebackWithCreditAllocationAndReverseReplayWithBackdatedPayment() { runAt("01 January 2023", () -> { // Create Client Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); // Create Loan Product - Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")); - + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); // Apply and Approve Loan Long loanId = applyAndApproveLoan(clientId, loanProductId); @@ -232,15 +368,13 @@ public void createLoanWithCreditAllocationAndChargebackReverseReplayWithBackdate verifyTransactions(loanId, // transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // - transaction(100.0, "Chargeback", "21 January 2023", 1037.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + transaction(100.0, "Chargeback", "21 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // ); // Verify Repayment Schedule verifyRepaymentSchedule(loanId, // installment(0, null, "01 January 2023"), // - installment(343.0, 0, 50, 20, 30.0, false, "01 February 2023"), // TODO: we still need to add the - // fee and the penalty to the - // outstanding + installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"), // installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // @@ -260,13 +394,15 @@ public void createLoanWithCreditAllocationAndChargebackReverseReplayWithBackdate } @Test - public void createLoanWithCreditAllocationAndOnlyTheChargebackReverseReplayedWithBackdatedPayment() { + public void chargebackWithCreditAllocationReverseReplayedWithBackdatedPayment() { runAt("01 January 2023", () -> { // Create Client Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); // Create Loan Product - Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")); - + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); // Apply and Approve Loan Long loanId = applyAndApproveLoan(clientId, loanProductId); @@ -307,15 +443,13 @@ public void createLoanWithCreditAllocationAndOnlyTheChargebackReverseReplayedWit verifyTransactions(loanId, // transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // - transaction(100.0, "Chargeback", "22 January 2023", 1037.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + transaction(100.0, "Chargeback", "22 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // ); // Verify Repayment Schedule verifyRepaymentSchedule(loanId, // installment(0, null, "01 January 2023"), // - installment(343.0, 0, 50, 20, 30.0, false, "01 February 2023"), // TODO: we still need to add the - // fee and the penalty to the - // outstanding + installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"), installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // @@ -330,18 +464,24 @@ public void createLoanWithCreditAllocationAndOnlyTheChargebackReverseReplayedWit transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // transaction(200.0, "Repayment", "21 January 2023", 737.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0), // - transaction(100.0, "Chargeback", "22 January 2023", 837.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + transaction(100.0, "Chargeback", "22 January 2023", 767.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // ); + + verifyLoanSummaryAmounts(loanId, 30.0, 50.0, 20.0, 837.0); }); } @Test - public void createLoanWithCreditAllocationAndChargebackPrincipalInterestFeePenalty() { + public void chargebackWithCreditAllocationPrincipalInterestFeePenalty() { runAt("01 January 2023", () -> { // Create Client Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product - Long loanProductId = createLoanProduct(chargebackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY")); + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY")// + ); // Apply and Approve Loan Long loanId = applyAndApproveLoan(clientId, loanProductId); @@ -387,67 +527,568 @@ public void createLoanWithCreditAllocationAndChargebackPrincipalInterestFeePenal // Verify Repayment Schedule verifyRepaymentSchedule(loanId, // installment(0, null, "01 January 2023"), // - installment(413.0, 0, 50, 20, 100.0, false, "01 February 2023"), // TODO: we still need to add the - // fee and the penalty to the - // outstanding + installment(413.0, 0, 50, 20, 100.0, false, "01 February 2023"), installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // ); + + verifyLoanSummaryAmounts(loanId, 100.0, 0.0, 0.0, 1037); }); } - @Nullable - private Long applyAndApproveLoan(Long clientId, Long loanProductId) { - PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", 1250.0, 4)// - .repaymentEvery(1)// - .loanTermFrequency(4)// - .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// - .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)// - .transactionProcessingStrategyCode("advanced-payment-allocation-strategy"); + @Test + public void chargebackWithCreditAllocationPrincipalInterestFeePenaltyWhenOverpaid() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId); - PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); - PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), - approveLoanRequest(1250.0, "01 January 2023")); + // Add Charges + Long feeId = addCharge(loanId, false, 50, "15 January 2023"); + Long penaltyId = addCharge(loanId, true, 20, "20 January 2023"); - Long loanId = approvedLoanResult.getLoanId(); - return loanId; - } + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); - public Long createLoanProduct(CreditAllocationData... creditAllocationData) { - PostLoanProductsRequest postLoanProductsRequest = loanProductWithAdvancedPaymentAllocationWith4Installments(creditAllocationData); - PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(postLoanProductsRequest); - return loanProductResponse.getResourceId(); + // Update Business Date + updateBusinessDate("20 January 2023"); + + // Add Repayment + Long repaymentTransaction = addRepaymentForLoan(loanId, 1370.0, "20 January 2023"); // 1250 + 70 = 1320; 50 + // overpayment + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), // + installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") // + ); + + updateBusinessDate("02 May 2023"); + + // Add Chargeback + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 principal, 0 interest, 0 fee 0 penalty + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "02 May 2023", 50.0, 100.0, 0.0, 0.0, 0.0, 0.0, 50.0) // + ); + + // Verify Repayment Schedule + // DEFAULT payment allocation is ..., DUE_PENALTY, DUE_FEE, DUE_PRINCIPAL, ... + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0, true, "01 February 2023"), // + installment(313.0, 0, 0, 0, 0, true, "01 March 2023"), // + installment(313.0, 0, 0, 0, 0, true, "01 April 2023"), // + installment(311.0, 0, 0, 0, 0, true, "01 May 2023"), // + installment(100.0, 0, 0, 0, outstanding(50.0, 0d, 0d, 50.0), false, "02 May 2023") // + ); + }); } - private PostLoanProductsRequest loanProductWithAdvancedPaymentAllocationWith4Installments( - CreditAllocationData... creditAllocationData) { - return createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().numberOfRepayments(4)// - .repaymentEvery(1)// - .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())// - .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) // - .loanScheduleProcessingType(LoanScheduleProcessingType.VERTICAL.toString()) // - .transactionProcessingStrategyCode("advanced-payment-allocation-strategy") - .paymentAllocation(List.of(createDefaultPaymentAllocation(), createRepaymentPaymentAllocation())) - .creditAllocation(Arrays.asList(creditAllocationData)); + @Test + public void chargebackWithCreditAllocationFeePenaltyPrincipalInterestWhenOverpaid() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("FEE", "PENALTY", "PRINCIPAL", "INTEREST")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + + // Add Charges + Long feeId = addCharge(loanId, false, 50, "15 January 2023"); + Long penaltyId = addCharge(loanId, true, 20, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Update Business Date + updateBusinessDate("20 January 2023"); + + // Add Repayment + Long repaymentTransaction = addRepaymentForLoan(loanId, 1370.0, "20 January 2023"); // 1250 + 70 = 1320; 50 + // overpayment + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), // + installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") // + ); + + // Add Chargeback + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 principal, 0 interest, 0 fee 0 penalty + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) // + ); + + // Verify Repayment Schedule, + // DEFAULT payment allocation is ..., DUE_PENALTY, DUE_FEE, DUE_PRINCIPAL, ... + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(343.0, 0, 100, 40, outstanding(30.0, 20.0, 0.0, 50.0), false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), // + installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") // + ); + + verifyLoanSummaryAmounts(loanId, 30.0, 50.0, 20.0, 50.0); + }); } - private AdvancedPaymentData createDefaultPaymentAllocation() { - AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); - advancedPaymentData.setTransactionType("DEFAULT"); - advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT"); + @Test + public void chargebackWithCreditAllocationFeePenaltyPrincipalInterestWhenOverpaidDefaultPaymentPrincipalFirst() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocationPrincipalFirst(), // + chargebackAllocation("FEE", "PENALTY", "PRINCIPAL", "INTEREST")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId); - List paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY, - PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST, - PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL, - PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE, + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + + // Add Charges + Long feeId = addCharge(loanId, false, 50, "15 January 2023"); + Long penaltyId = addCharge(loanId, true, 20, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Update Business Date + updateBusinessDate("20 January 2023"); + + // Add Repayment + Long repaymentTransaction = addRepaymentForLoan(loanId, 1370.0, "20 January 2023"); // 1250 + 70 = 1320; 50 + // overpayment + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), // + installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") // + ); + + // Add Chargeback + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 principal, 0 interest, 0 fee 0 penalty + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 0.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) // + ); + + // Verify Repayment Schedule, + // DEFAULT payment allocation is ..., DUE_PRINCIPAL, DUE_FEE, DUE_PENALTY ... + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(343.0, 0, 100, 40, outstanding(0.0, 30.0, 20.0, 50.0), false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), // + installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), // + installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") // + ); + + verifyLoanSummaryAmounts(loanId, 30.0, 50.0, 20.0, 50.0); + }); + } + + @Test + public void doubleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipal() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + + // Add Charges + Long feeId = addCharge(loanId, false, 50, "15 January 2023"); + Long penaltyId = addCharge(loanId, true, 20, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Update Business Date + updateBusinessDate("20 January 2023"); + + // Add Repayment + Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Add Chargeback + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30 + // principal + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + ); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"), + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + updateBusinessDate("21 January 2023"); + + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 to principal + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 967, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "21 January 2023", 1067.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(443.0, 0, 100, 40, 200.0, false, "01 February 2023"), + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + }); + } + + @Test + public void doubleChargebackReverseReplayedBothFeeAndPenaltyPayedWithCreditAllocationPenaltyFeeInterestAndPrincipal() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + + // Add Charges + Long feeId = addCharge(loanId, false, 50, "15 January 2023"); + Long penaltyId = addCharge(loanId, true, 20, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Update Business Date + updateBusinessDate("20 January 2023"); + + // Add Repayment + Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Add Chargeback + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30 + // principal + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + ); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"), + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + updateBusinessDate("21 January 2023"); + + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 to principal + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "21 January 2023", 1067.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(443.0, 0, 100, 40, 200.0, false, "01 February 2023"), + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Let's add repayment to trigger reverse replay for both chargebacks + addRepaymentForLoan(loanId, 200.0, "19 January 2023"); + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(200.0, "Repayment", "19 January 2023", 1120.0, 130.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 737.0, 383.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 837.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "21 January 2023", 937.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + }); + } + + @Test + public void doubleChargebackReverseReplayedOnlyPenaltyPayedWithCreditAllocationPenaltyFeeInterestAndPrincipal() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + + // Add Charges + Long feeId = addCharge(loanId, false, 50, "15 January 2023"); + Long penaltyId = addCharge(loanId, true, 20, "15 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Update Business Date + updateBusinessDate("20 January 2023"); + + // Add Repayment + Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Add Chargeback + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30 + // principal + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // + ); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"), + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + updateBusinessDate("21 January 2023"); + + addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 to principal + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "21 January 2023", 1067.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(443.0, 0, 100, 40, 200.0, false, "01 February 2023"), + installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), // + installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), // + installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // + ); + + // Let's add repayment to trigger reverse replay for both chargebacks + addRepaymentForLoan(loanId, 20.0, "19 January 2023"); + + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(20.0, "Repayment", "19 January 2023", 1250.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0), // + transaction(383.0, "Repayment", "20 January 2023", 917.0, 333.0, 0.0, 50.0, 0.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "20 January 2023", 967.0, 50.0, 0.0, 50.0, 0.0, 0.0, 0.0), // + transaction(100.0, "Chargeback", "21 January 2023", 1067.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + }); + } + + private void verifyLoanSummaryAmounts(Long loanId, double creditedPrincipal, double creditedFee, double creditedPenalty, + double totalOutstanding) { + GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); + GetLoansLoanIdSummary summary = loanResponse.getSummary(); + Assertions.assertNotNull(summary); + Assertions.assertEquals(creditedPrincipal, summary.getPrincipalAdjustments()); + Assertions.assertEquals(creditedFee, summary.getFeeAdjustments()); + Assertions.assertEquals(creditedPenalty, summary.getPenaltyAdjustments()); + Assertions.assertEquals(totalOutstanding, summary.getTotalOutstanding()); + } + + @Nullable + private Long applyAndApproveLoan(Long clientId, Long loanProductId) { + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", 1250.0, 4)// + .repaymentEvery(1)// + .loanTermFrequency(4)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// + .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)// + .transactionProcessingStrategyCode("advanced-payment-allocation-strategy"); + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + + PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), + approveLoanRequest(1250.0, "01 January 2023")); + + Long loanId = approvedLoanResult.getLoanId(); + return loanId; + } + + public Long createLoanProduct(AdvancedPaymentData defaultAllocation, CreditAllocationData creditAllocationData) { + PostLoanProductsRequest postLoanProductsRequest = loanProductWithAdvancedPaymentAllocationWith4Installments(defaultAllocation, + creditAllocationData); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(postLoanProductsRequest); + return loanProductResponse.getResourceId(); + } + + private PostLoanProductsRequest loanProductWithAdvancedPaymentAllocationWith4Installments(AdvancedPaymentData defaultAllocation, + CreditAllocationData creditAllocationData) { + return createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().numberOfRepayments(4)// + .repaymentEvery(1)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())// + .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) // + .loanScheduleProcessingType(LoanScheduleProcessingType.VERTICAL.toString()) // + .transactionProcessingStrategyCode("advanced-payment-allocation-strategy") + .paymentAllocation(List.of(defaultAllocation, createRepaymentPaymentAllocation())) + .creditAllocation(List.of(creditAllocationData)); + } + + private AdvancedPaymentData createDefaultPaymentAllocation() { + AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); + advancedPaymentData.setTransactionType("DEFAULT"); + advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT"); + + List paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY, + PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST, + PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL, + PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE, PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST); advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders); return advancedPaymentData; } + private AdvancedPaymentData createDefaultPaymentAllocationPrincipalFirst() { + AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); + advancedPaymentData.setTransactionType("DEFAULT"); + advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT"); + + List paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY, + PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST, + PaymentAllocationType.DUE_PRINCIPAL, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PENALTY, + PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_FEE, + PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_INTEREST); + + advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders); + return advancedPaymentData; + } + private AdvancedPaymentData createRepaymentPaymentAllocation() { AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); advancedPaymentData.setTransactionType("REPAYMENT");