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 8c7b42477..46f49e853 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.InterestRecalculationCom 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; @@ -5959,10 +5960,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .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()); } @@ -7198,6 +7207,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 1431c8a8f..6d6456510 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 class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa 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 class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa 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 class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa 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.credits = this.credits.add(amount); + 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.creditedPenalty = this.creditedPenalty.add(amount); } } @@ -908,7 +946,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa } 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 2cbb06463..271f88f51 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 class LoanSummary { 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 class LoanSummary { 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 class LoanSummary { 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 947d17540..8ad10df39 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 final class LoanSummaryWrapper { final MonetaryCurrency currency) { Money total = Money.zero(currency); for (final LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) { - total = total.plus(installment.getCredits(currency)); + total = total.plus(installment.getCreditedPrincipal(currency)); + } + return total; + } + + public Money calculateTotalFeeAdjusted(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 final class LoanSummaryWrapper { 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 596513e7e..94592fed8 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 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen 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 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen 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 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen 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 53de2b0ef..60fb6caee 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.FE 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.CreditAllocationTransact 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } @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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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); @@ -560,7 +601,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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); @@ -926,7 +967,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 16760fc8b..71521ef69 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 000000000..faa79a4c0 --- /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 d93575ab6..375d506e2 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 @@ final class LoansApiResourceSwagger { 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 @@ final class LoansApiResourceSwagger { 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 6fcb359ca..21abaa153 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 class LoanScheduleAccrualData { this.repayEvery = repayEvery; this.interestCalculatedFrom = interestCalculatedFrom; this.waivedInterestIncome = waivedInterestIncome; + this.creditedFee = creditedFee; + this.creditedPenalty = creditedPenalty; } public Long getLoanId() { @@ -185,4 +190,12 @@ public class LoanScheduleAccrualData { 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 73e7be47a..42ebbd6be 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 @@ package org.apache.fineract.portfolio.loanaccount.data; 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 @@ import org.springframework.util.CollectionUtils; * 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 class LoanSummaryData { 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 f58785762..cc795819b 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 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { 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 class LoanAccountDomainServiceJpa implements LoanAccountDomainService { .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 27b4d167c..4172f5c81 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 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat 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 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat 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 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat 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 class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat 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 class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat 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 c804805a7..6d6643f9d 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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa + " 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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa 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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa 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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa 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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa + " 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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa .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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa 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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa 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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa .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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa 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 class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa 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 fb79dc290..e4ba465b7 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.IN 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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,94 +217,50 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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)); + LoanRepaymentScheduleInstallment installment = 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); + 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); - lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment)); - - MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY)); - List installments = new ArrayList<>(); - LoanRepaymentScheduleInstallment installment = createMockInstallment(LocalDate.of(2023, 1, 31), false); - 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")); - 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(5.0))); - - 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)); - } - - @Test - public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterest() { - // given - Loan loan = mock(Loan.class); - LoanTransaction chargeBackTransaction = createChargebackTransaction(loan); - - 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)); @@ -315,15 +272,31 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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")); + // 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(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); @@ -331,24 +304,74 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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)); + } + + @Test + public void testProcessCreditTransactionWithAllocationRulePenaltyFeePrincipalInterest() { + // given + Loan loan = mock(Loan.class); + LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0); + + LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PENALTY, FEE, PRINCIPAL, INTEREST); + when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule)); + LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5); + lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment)); + + MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY)); + List installments = new ArrayList<>(); + LoanRepaymentScheduleInstallment installment = createMockInstallment(LocalDate.of(2023, 1, 31), false); + installments.add(installment); + + // when + TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder); + underTest.processCreditTransaction(chargeBackTransaction, ctx); + + // 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(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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 a3073f75e..f0056b250 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 @@ -476,6 +476,46 @@ public abstract class BaseLoanIntegrationTest { 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()); @@ -603,22 +643,31 @@ public abstract class BaseLoanIntegrationTest { } 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() { @@ -735,6 +784,17 @@ public abstract class BaseLoanIntegrationTest { 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 1f761043f..63ae1b179 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 lombok.extern.slf4j.Slf4j; 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.LoanSchedul 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 @@ import org.junit.jupiter.api.extension.ExtendWith; 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,15 +97,13 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa 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") // @@ -109,13 +112,145 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa } @Test - public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrincipalOnNPlusOneInstallment() { + public void simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipalOnTheLastDayOfTheInstallment() { 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 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 simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipalOnTheLastDayOfTheLoan() { + 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 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 class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa 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 class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa 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 class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa 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 class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa } @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 class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa 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 class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa 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,16 +527,501 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa // 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); }); } + @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); + + // 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") // + ); + + 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") // + ); + }); + } + + @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); + }); + } + + @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); + + // 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)// @@ -415,22 +1040,23 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa return loanId; } - public Long createLoanProduct(CreditAllocationData... creditAllocationData) { - PostLoanProductsRequest postLoanProductsRequest = loanProductWithAdvancedPaymentAllocationWith4Installments(creditAllocationData); + public Long createLoanProduct(AdvancedPaymentData defaultAllocation, CreditAllocationData creditAllocationData) { + PostLoanProductsRequest postLoanProductsRequest = loanProductWithAdvancedPaymentAllocationWith4Installments(defaultAllocation, + creditAllocationData); PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(postLoanProductsRequest); return loanProductResponse.getResourceId(); } - private PostLoanProductsRequest loanProductWithAdvancedPaymentAllocationWith4Installments( - CreditAllocationData... creditAllocationData) { + 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(createDefaultPaymentAllocation(), createRepaymentPaymentAllocation())) - .creditAllocation(Arrays.asList(creditAllocationData)); + .paymentAllocation(List.of(defaultAllocation, createRepaymentPaymentAllocation())) + .creditAllocation(List.of(creditAllocationData)); } private AdvancedPaymentData createDefaultPaymentAllocation() { @@ -448,6 +1074,21 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa 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");