FINERACT-2042 Handling overpayment of chargeback with credit allocations

This commit is contained in:
Peter Bagrij
2024-02-28 10:09:11 +01:00
committed by Peter Bagrij
parent 5be1001fd5
commit 4bc6755514
17 changed files with 1337 additions and 415 deletions
@@ -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.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod; 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.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
@@ -5959,10 +5960,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
.minus(loanTransaction.getOverPaymentPortion(getCurrency())); .minus(loanTransaction.getOverPaymentPortion(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount()); loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) { } else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
Money transactionOutstanding = loanTransaction.getAmount(getCurrency()); Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency());
if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) { if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) {
transactionOutstanding = loanTransaction.getAmount(getCurrency()) // in case of advanced payment strategy and creditAllocations the full amount is recognized first
.minus(loanTransaction.getOverPaymentPortion(getCurrency())); 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()) { if (transactionOutstanding.isLessThanZero()) {
transactionOutstanding = Money.zero(getCurrency()); transactionOutstanding = Money.zero(getCurrency());
} }
@@ -7198,6 +7207,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return paymentAllocationRules; return paymentAllocationRules;
} }
public LoanPaymentAllocationRule getPaymentAllocationRuleOrDefault(PaymentAllocationTransactionType transactionType) {
Optional<LoanPaymentAllocationRule> 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<LoanPaymentAllocationRule> loanPaymentAllocationRules) { public void setPaymentAllocationRules(List<LoanPaymentAllocationRule> loanPaymentAllocationRules) {
this.paymentAllocationRules = loanPaymentAllocationRules; this.paymentAllocationRules = loanPaymentAllocationRules;
} }
@@ -130,8 +130,14 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
@Column(name = "is_additional", nullable = false) @Column(name = "is_additional", nullable = false)
private boolean additional; private boolean additional;
@Column(name = "credits_amount", scale = 6, precision = 19, nullable = true) @Column(name = "credited_principal", scale = 6, precision = 19, nullable = true)
private BigDecimal credits; 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) @Column(name = "is_down_payment", nullable = false)
private boolean isDownPayment; private boolean isDownPayment;
@@ -246,8 +252,16 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
return this.dueDate; return this.dueDate;
} }
public Money getCredits(final MonetaryCurrency currency) { public Money getCreditedPrincipal(final MonetaryCurrency currency) {
return Money.of(currency, this.credits); 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) { public Money getPrincipal(final MonetaryCurrency currency) {
@@ -408,9 +422,17 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
this.obligationsMet = false; this.obligationsMet = false;
this.obligationsMetOnDate = null; this.obligationsMetOnDate = null;
if (this.credits != null) { if (this.creditedPrincipal != null) {
this.principal = this.principal.subtract(this.credits); this.principal = this.principal.subtract(this.creditedPrincipal);
this.credits = null; 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()); checkIfRepaymentPeriodObligationsAreMet(transactionDate, transactionAmount.getCurrency());
} }
public void addToCredits(final BigDecimal amount) { public void addToCreditedPrincipal(final BigDecimal amount) {
if (this.credits == null) { if (this.creditedPrincipal == null) {
this.credits = amount; this.creditedPrincipal = amount;
} else { } 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) { public void updateCredits(final LocalDate transactionDate, final Money transactionAmount) {
addToCredits(transactionAmount.getAmount()); addToCreditedPrincipal(transactionAmount.getAmount());
addToPrincipal(transactionDate, transactionAmount); addToPrincipal(transactionDate, transactionAmount);
} }
@@ -74,6 +74,9 @@ public class LoanSummary {
@Column(name = "total_charges_due_at_disbursement_derived", scale = 6, precision = 19) @Column(name = "total_charges_due_at_disbursement_derived", scale = 6, precision = 19)
private BigDecimal totalFeeChargesDueAtDisbursement; 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) @Column(name = "fee_charges_repaid_derived", scale = 6, precision = 19)
private BigDecimal totalFeeChargesRepaid; private BigDecimal totalFeeChargesRepaid;
@@ -89,6 +92,9 @@ public class LoanSummary {
@Column(name = "penalty_charges_charged_derived", scale = 6, precision = 19) @Column(name = "penalty_charges_charged_derived", scale = 6, precision = 19)
private BigDecimal totalPenaltyChargesCharged; 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) @Column(name = "penalty_charges_repaid_derived", scale = 6, precision = 19)
private BigDecimal totalPenaltyChargesRepaid; private BigDecimal totalPenaltyChargesRepaid;
@@ -204,6 +210,8 @@ public class LoanSummary {
public void zeroFields() { public void zeroFields() {
this.totalPrincipalDisbursed = BigDecimal.ZERO; this.totalPrincipalDisbursed = BigDecimal.ZERO;
this.totalPrincipalAdjustments = BigDecimal.ZERO; this.totalPrincipalAdjustments = BigDecimal.ZERO;
this.totalFeeAdjustments = BigDecimal.ZERO;
this.totalPenaltyAdjustments = BigDecimal.ZERO;
this.totalPrincipalRepaid = BigDecimal.ZERO; this.totalPrincipalRepaid = BigDecimal.ZERO;
this.totalPrincipalWrittenOff = BigDecimal.ZERO; this.totalPrincipalWrittenOff = BigDecimal.ZERO;
this.totalPrincipalOutstanding = BigDecimal.ZERO; this.totalPrincipalOutstanding = BigDecimal.ZERO;
@@ -238,6 +246,8 @@ public class LoanSummary {
this.totalPrincipalDisbursed = principal.getAmount(); this.totalPrincipalDisbursed = principal.getAmount();
this.totalPrincipalAdjustments = summaryWrapper.calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency) this.totalPrincipalAdjustments = summaryWrapper.calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency)
.getAmount(); .getAmount();
this.totalFeeAdjustments = summaryWrapper.calculateTotalFeeAdjusted(repaymentScheduleInstallments, currency).getAmount();
this.totalPenaltyAdjustments = summaryWrapper.calculateTotalPenaltyAdjusted(repaymentScheduleInstallments, currency).getAmount();
this.totalPrincipalRepaid = summaryWrapper.calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount(); this.totalPrincipalRepaid = summaryWrapper.calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount();
this.totalPrincipalWrittenOff = summaryWrapper.calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency) this.totalPrincipalWrittenOff = summaryWrapper.calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency)
.getAmount(); .getAmount();
@@ -259,7 +269,9 @@ public class LoanSummary {
this.totalFeeChargesCharged = totalFeeChargesCharged.getAmount(); this.totalFeeChargesCharged = totalFeeChargesCharged.getAmount();
Money totalFeeChargesRepaidAtDisbursement = summaryWrapper.calculateTotalChargesRepaidAtDisbursement(charges, currency); 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) { if (charges != null) {
this.totalFeeChargesWaived = summaryWrapper.calculateTotalFeeChargesWaived(charges, currency).getAmount(); this.totalFeeChargesWaived = summaryWrapper.calculateTotalFeeChargesWaived(charges, currency).getAmount();
@@ -45,7 +45,25 @@ public final class LoanSummaryWrapper {
final MonetaryCurrency currency) { final MonetaryCurrency currency) {
Money total = Money.zero(currency); Money total = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) { for (final LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) {
total = total.plus(installment.getCredits(currency)); total = total.plus(installment.getCreditedPrincipal(currency));
}
return total;
}
public Money calculateTotalFeeAdjusted(final List<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments,
final MonetaryCurrency currency) {
Money total = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) {
total = total.plus(installment.getCreditedPenalty(currency));
} }
return total; return total;
} }
@@ -247,7 +265,8 @@ public final class LoanSummaryWrapper {
return total; return total;
} }
for (final LoanCharge loanCharge : charges) { 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)); total = total.plus(loanCharge.getAmountPaid(currency));
} }
} }
@@ -494,7 +494,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
pastDueDate = currentInstallment.getDueDate(); pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) { if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
currentInstallment.addToCredits(transactionAmount.getAmount()); currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, transactionAmount); currentInstallment.addToPrincipal(transactionDate, transactionAmount);
if (repaidAmount.isGreaterThanZero()) { if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
@@ -526,7 +526,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (!loanTransactionMapped) { if (!loanTransactionMapped) {
if (loanTransaction.getTransactionDate().equals(pastDueDate)) { if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
LoanRepaymentScheduleInstallment currentInstallment = installments.get(installments.size() - 1); LoanRepaymentScheduleInstallment currentInstallment = installments.get(installments.size() - 1);
currentInstallment.addToCredits(transactionAmount.getAmount()); currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, transactionAmount); currentInstallment.addToPrincipal(transactionDate, transactionAmount);
if (repaidAmount.isGreaterThanZero()) { if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount); currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
@@ -539,7 +539,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
pastDueDate, transactionDate, transactionAmount.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), pastDueDate, transactionDate, transactionAmount.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(),
zeroMoney.getAmount(), false, null); zeroMoney.getAmount(), false, null);
installment.markAsAdditional(); installment.markAsAdditional();
installment.addToCredits(transactionAmount.getAmount()); installment.addToCreditedPrincipal(transactionAmount.getAmount());
loan.addLoanRepaymentScheduleInstallment(installment); loan.addLoanRepaymentScheduleInstallment(installment);
if (repaidAmount.isGreaterThanZero()) { if (repaidAmount.isGreaterThanZero()) {
@@ -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.INTEREST;
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; 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.AllocationType.PRINCIPAL;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext; 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.DueType;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; 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.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -235,27 +235,31 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (hasNoCustomCreditAllocationRule(loanTransaction)) { if (hasNoCustomCreditAllocationRule(loanTransaction)) {
super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments()); super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
} else { } else {
log.debug("Processing credit transaction with custom credit allocation rules");
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate); final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
ctx.getInstallments().sort(byDate); ctx.getInstallments().sort(byDate);
final Money zeroMoney = Money.zero(ctx.getCurrency()); final Money zeroMoney = Money.zero(ctx.getCurrency());
Money transactionAmount = loanTransaction.getAmount(ctx.getCurrency()); Money transactionAmount = loanTransaction.getAmount(ctx.getCurrency());
Money amountToDistribute = MathUtil Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject();
.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(ctx.getOverpaymentHolder().getMoneyObject())); Money amountToDistribute = MathUtil.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(totalOverpaid));
Money repaidAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute)); Money overpaymentAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute));
loanTransaction.setOverPayments(repaidAmount); loanTransaction.setOverPayments(overpaymentAmount);
ctx.getOverpaymentHolder().setMoneyObject(ctx.getOverpaymentHolder().getMoneyObject().minus(repaidAmount));
if (amountToDistribute.isGreaterThanZero()) { if (transactionAmount.isGreaterThanZero()) {
if (loanTransaction.isChargeback()) { if (loanTransaction.isChargeback()) {
LoanTransaction originalTransaction = findOriginalTransaction(loanTransaction, ctx); LoanTransaction originalTransaction = findOriginalTransaction(loanTransaction, ctx);
Map<AllocationType, BigDecimal> originalAllocation = getOriginalAllocation(originalTransaction); // get the original allocation from the opriginal transaction
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction,
ctx.getCurrency());
LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction); LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction);
// if there were earlier chargebacks then let's calculate the remaining amounts for each portion
Map<AllocationType, Money> originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction,
originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule);
// calculate the current chargeback allocation
Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation, Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation,
amountToDistribute.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency()); transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency());
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST), loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY)); chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
@@ -266,18 +270,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) { for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) {
pastDueDate = currentInstallment.getDueDate(); pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) { if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
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));
}
loanTransactionMapped = true; loanTransactionMapped = true;
break; break;
@@ -287,16 +280,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) { if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) {
currentInstallment.updateDueDate(transactionDate); currentInstallment.updateDueDate(transactionDate);
} }
currentInstallment.addToCredits(transactionAmount.getAmount()); recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
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));
}
loanTransactionMapped = true; loanTransactionMapped = true;
break; break;
} }
@@ -307,42 +291,99 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (loanTransaction.getTransactionDate().equals(pastDueDate)) { if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments() LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments()
.get(ctx.getInstallments().size() - 1); .get(ctx.getInstallments().size() - 1);
currentInstallment.addToCredits(transactionAmount.getAmount()); recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
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));
}
} else { } else {
Loan loan = loanTransaction.getLoan(); Loan loan = loanTransaction.getLoan();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
(ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(), (ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(),
zeroMoney.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), false, null); zeroMoney.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), false, null);
installment.markAsAdditional(); recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, installment, chargebackAllocation);
installment.addToCredits(transactionAmount.getAmount());
installment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
Money originalInterest = installment.getInterestCharged(ctx.getCurrency());
installment.updateInterestCharged(
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
loan.addLoanRepaymentScheduleInstallment(installment); loan.addLoanRepaymentScheduleInstallment(installment);
if (repaidAmount.isGreaterThanZero()) {
installment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment,
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
}
} }
} }
allocateOverpayment(loanTransaction, ctx.getCurrency(), loanTransaction.getLoan().getRepaymentScheduleInstallments(),
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); ctx.getOverpaymentHolder());
} else {
throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name());
} }
} }
} }
} }
private Map<AllocationType, Money> adjustOriginalAllocationWithFormerChargebacks(LoanTransaction originalTransaction,
Map<AllocationType, Money> originalAllocation, LoanTransaction chargeBackTransaction, TransactionCtx ctx,
LoanCreditAllocationRule chargeBackAllocationRule) {
// these are the list of existing transactions
List<LoanTransaction> allTransactions = new ArrayList<>(chargeBackTransaction.getLoan().getLoanTransactions());
// 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<LoanTransaction> chargebacks = allTransactions.stream().filter(LoanTransaction::isChargeback).toList();
// let's figure out the original transaction for these chargebacks, and order them by ascending order
List<LoanTransaction> chargebacksForTheSameOriginal = chargebacks.stream()
.filter(tr -> findOriginalTransaction(tr, ctx) == originalTransaction).sorted(loanTransactionDateComparator()).toList();
Map<AllocationType, Money> allocation = new HashMap<>(originalAllocation);
for (LoanTransaction loanTransaction : chargebacksForTheSameOriginal) {
Map<AllocationType, Money> 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<LoanTransaction> 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<AllocationType, Money> 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 @NotNull
private LoanCreditAllocationRule getChargebackAllocationRules(LoanTransaction loanTransaction) { private LoanCreditAllocationRule getChargebackAllocationRules(LoanTransaction loanTransaction) {
LoanCreditAllocationRule chargeBackAllocationRule = loanTransaction.getLoan().getCreditAllocationRules().stream() LoanCreditAllocationRule chargeBackAllocationRule = loanTransaction.getLoan().getCreditAllocationRules().stream()
@@ -351,23 +392,23 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
@NotNull @NotNull
private Map<AllocationType, BigDecimal> getOriginalAllocation(LoanTransaction originalLoanTransaction) { private Map<AllocationType, Money> getOriginalAllocation(LoanTransaction originalLoanTransaction, MonetaryCurrency currency) {
Map<AllocationType, BigDecimal> originalAllocation = new HashMap<>(); Map<AllocationType, Money> originalAllocation = new HashMap<>();
originalAllocation.put(PRINCIPAL, originalLoanTransaction.getPrincipalPortion()); originalAllocation.put(PRINCIPAL, Money.of(currency, originalLoanTransaction.getPrincipalPortion()));
originalAllocation.put(INTEREST, originalLoanTransaction.getInterestPortion()); originalAllocation.put(INTEREST, Money.of(currency, originalLoanTransaction.getInterestPortion()));
originalAllocation.put(PENALTY, originalLoanTransaction.getPenaltyChargesPortion()); originalAllocation.put(PENALTY, Money.of(currency, originalLoanTransaction.getPenaltyChargesPortion()));
originalAllocation.put(FEE, originalLoanTransaction.getFeeChargesPortion()); originalAllocation.put(FEE, Money.of(currency, originalLoanTransaction.getFeeChargesPortion()));
return originalAllocation; return originalAllocation;
} }
protected Map<AllocationType, Money> calculateChargebackAllocationMap(Map<AllocationType, BigDecimal> originalAllocation, protected Map<AllocationType, Money> calculateChargebackAllocationMap(Map<AllocationType, Money> originalAllocation,
BigDecimal amountToDistribute, List<AllocationType> allocationTypes, MonetaryCurrency currency) { BigDecimal amountToDistribute, List<AllocationType> allocationTypes, MonetaryCurrency currency) {
BigDecimal remainingAmount = amountToDistribute; BigDecimal remainingAmount = amountToDistribute;
Map<AllocationType, Money> result = new HashMap<>(); Map<AllocationType, Money> result = new HashMap<>();
Arrays.stream(AllocationType.values()).forEach(allocationType -> result.put(allocationType, Money.of(currency, BigDecimal.ZERO))); Arrays.stream(AllocationType.values()).forEach(allocationType -> result.put(allocationType, Money.of(currency, BigDecimal.ZERO)));
for (AllocationType allocationType : allocationTypes) { for (AllocationType allocationType : allocationTypes) {
if (remainingAmount.compareTo(BigDecimal.ZERO) > 0) { if (remainingAmount.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal originalAmount = originalAllocation.get(allocationType); BigDecimal originalAmount = originalAllocation.get(allocationType).getAmount();
if (originalAmount != null && remainingAmount.compareTo(originalAmount) > 0 if (originalAmount != null && remainingAmount.compareTo(originalAmount) > 0
&& originalAmount.compareTo(BigDecimal.ZERO) > 0) { && originalAmount.compareTo(BigDecimal.ZERO) > 0) {
result.put(allocationType, Money.of(currency, originalAmount)); result.put(allocationType, Money.of(currency, originalAmount));
@@ -395,14 +436,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules(); List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() 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() LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst() .filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
.orElse(defaultPaymentAllocationRule); .orElse(defaultPaymentAllocationRule);
Balances balances = new Balances(zero, zero, zero, zero); Balances balances = new Balances(zero, zero, zero, zero);
List<PaymentAllocationType> paymentAllocationTypes; List<PaymentAllocationType> paymentAllocationTypes;
FutureInstallmentAllocationRule futureInstallmentAllocationRule; 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 // if the allocation rule is not defined then the reverse order of the default allocation rule will be used
paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes()); paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes());
Collections.reverse(paymentAllocationTypes); Collections.reverse(paymentAllocationTypes);
@@ -560,7 +601,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>(); List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules(); List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() 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 transactionAmountUnprocessed = null;
Money zero = Money.zero(currency); Money zero = Money.zero(currency);
@@ -926,7 +967,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules(); List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() 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() LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst() .filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
.orElse(defaultPaymentAllocationRule); .orElse(defaultPaymentAllocationRule);
@@ -39,4 +39,5 @@
<include relativeToChangelogFile="true" file="parts/1014_add_loan_account_custom_snapshot_event.xml"/> <include relativeToChangelogFile="true" file="parts/1014_add_loan_account_custom_snapshot_event.xml"/>
<include relativeToChangelogFile="true" file="parts/1015_remove_disable_schedule_extension_column.xml"/> <include relativeToChangelogFile="true" file="parts/1015_remove_disable_schedule_extension_column.xml"/>
<include relativeToChangelogFile="true" file="parts/1016_add_credit_allocation_rule.xml"/> <include relativeToChangelogFile="true" file="parts/1016_add_credit_allocation_rule.xml"/>
<include relativeToChangelogFile="true" file="parts/1017_add_fee_and_penalty_adjustments_to_loan.xml"/>
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_loan">
<column name="fee_adjustments_derived" type="DECIMAL(19, 6)" defaultValueNumeric="0.000000">
<constraints nullable="false"/>
</column>
<column name="penalty_adjustments_derived" type="DECIMAL(19, 6)" defaultValueNumeric="0.000000">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="2">
<addColumn tableName="m_loan_repayment_schedule">
<column defaultValueComputed="NULL" name="credited_fee" type="DECIMAL(19, 6)"/>
<column defaultValueComputed="NULL" name="credited_penalty" type="DECIMAL(19, 6)"/>
</addColumn>
<renameColumn tableName="m_loan_repayment_schedule" oldColumnName="credits_amount" newColumnName="credited_principal" columnDataType="DECIMAL(19, 6)"/>
</changeSet>
</databaseChangeLog>
@@ -524,6 +524,8 @@ final class LoansApiResourceSwagger {
public Double interestOutstanding; public Double interestOutstanding;
@Schema(example = "200000.000000") @Schema(example = "200000.000000")
public Double interestOverdue; public Double interestOverdue;
@Schema(example = "0.00")
public Double feeAdjustments;
@Schema(example = "18000.000000") @Schema(example = "18000.000000")
public Double feeChargesCharged; public Double feeChargesCharged;
@Schema(example = "0.000000") @Schema(example = "0.000000")
@@ -538,6 +540,8 @@ final class LoansApiResourceSwagger {
public Double feeChargesOutstanding; public Double feeChargesOutstanding;
@Schema(example = "15000.000000") @Schema(example = "15000.000000")
public Double feeChargesOverdue; public Double feeChargesOverdue;
@Schema(example = "0.00")
public Double penaltyAdjustments;
@Schema(example = "0.000000") @Schema(example = "0.000000")
public Double penaltyChargesCharged; public Double penaltyChargesCharged;
@Schema(example = "0.000000") @Schema(example = "0.000000")
@@ -51,12 +51,15 @@ public class LoanScheduleAccrualData {
private BigDecimal dueDatePenaltyIncome; private BigDecimal dueDatePenaltyIncome;
private BigDecimal accruableIncome; private BigDecimal accruableIncome;
private BigDecimal creditedFee;
private BigDecimal creditedPenalty;
public LoanScheduleAccrualData(final Long loanId, final Long officeId, final Integer installmentNumber, final LocalDate accruedTill, 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 PeriodFrequencyType repaymentFrequency, final Integer repayEvery, final LocalDate dueDate, final LocalDate fromDate,
final Long repaymentScheduleId, final Long loanProductId, final BigDecimal interestIncome, final BigDecimal feeIncome, final Long repaymentScheduleId, final Long loanProductId, final BigDecimal interestIncome, final BigDecimal feeIncome,
final BigDecimal penaltyIncome, final BigDecimal accruedInterestIncome, final BigDecimal accruedFeeIncome, final BigDecimal penaltyIncome, final BigDecimal accruedInterestIncome, final BigDecimal accruedFeeIncome,
final BigDecimal accruedPenaltyIncome, final CurrencyData currencyData, final LocalDate interestCalculatedFrom, final BigDecimal accruedPenaltyIncome, final CurrencyData currencyData, final LocalDate interestCalculatedFrom,
final BigDecimal waivedInterestIncome) { final BigDecimal waivedInterestIncome, BigDecimal creditedFee, BigDecimal creditedPenalty) {
this.loanId = loanId; this.loanId = loanId;
this.installmentNumber = installmentNumber; this.installmentNumber = installmentNumber;
this.officeId = officeId; this.officeId = officeId;
@@ -76,6 +79,8 @@ public class LoanScheduleAccrualData {
this.repayEvery = repayEvery; this.repayEvery = repayEvery;
this.interestCalculatedFrom = interestCalculatedFrom; this.interestCalculatedFrom = interestCalculatedFrom;
this.waivedInterestIncome = waivedInterestIncome; this.waivedInterestIncome = waivedInterestIncome;
this.creditedFee = creditedFee;
this.creditedPenalty = creditedPenalty;
} }
public Long getLoanId() { public Long getLoanId() {
@@ -185,4 +190,12 @@ public class LoanScheduleAccrualData {
this.accruableIncome = accruableIncome; this.accruableIncome = accruableIncome;
} }
public BigDecimal getCreditedFee() {
return this.creditedFee;
}
public BigDecimal getCreditedPenalty() {
return this.creditedPenalty;
}
} }
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.data.CurrencyData;
@@ -31,6 +32,7 @@ import org.springframework.util.CollectionUtils;
* Immutable data object representing loan summary information. * Immutable data object representing loan summary information.
*/ */
@Data @Data
@Builder
@Accessors(chain = true) @Accessors(chain = true)
public class LoanSummaryData { public class LoanSummaryData {
@@ -48,6 +50,7 @@ public class LoanSummaryData {
private final BigDecimal interestOutstanding; private final BigDecimal interestOutstanding;
private final BigDecimal interestOverdue; private final BigDecimal interestOverdue;
private final BigDecimal feeChargesCharged; private final BigDecimal feeChargesCharged;
private final BigDecimal feeAdjustments;
private final BigDecimal feeChargesDueAtDisbursementCharged; private final BigDecimal feeChargesDueAtDisbursementCharged;
private final BigDecimal feeChargesPaid; private final BigDecimal feeChargesPaid;
private final BigDecimal feeChargesWaived; private final BigDecimal feeChargesWaived;
@@ -55,6 +58,7 @@ public class LoanSummaryData {
private final BigDecimal feeChargesOutstanding; private final BigDecimal feeChargesOutstanding;
private final BigDecimal feeChargesOverdue; private final BigDecimal feeChargesOverdue;
private final BigDecimal penaltyChargesCharged; private final BigDecimal penaltyChargesCharged;
private final BigDecimal penaltyAdjustments;
private final BigDecimal penaltyChargesPaid; private final BigDecimal penaltyChargesPaid;
private final BigDecimal penaltyChargesWaived; private final BigDecimal penaltyChargesWaived;
private final BigDecimal penaltyChargesWrittenOff; private final BigDecimal penaltyChargesWrittenOff;
@@ -90,61 +94,6 @@ public class LoanSummaryData {
private final Long chargeOffReasonId; private final Long chargeOffReasonId;
private final String chargeOffReason; 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, public static LoanSummaryData withTransactionAmountsSummary(final LoanSummaryData defaultSummaryData,
final Collection<LoanTransactionData> loanTransactions) { final Collection<LoanTransactionData> loanTransactions) {
@@ -184,34 +133,41 @@ public class LoanSummaryData {
totalRepaymentTransactionReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.REPAYMENT, loanTransactions); totalRepaymentTransactionReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.REPAYMENT, loanTransactions);
} }
return new LoanSummaryData(defaultSummaryData.currency, defaultSummaryData.principalDisbursed, return LoanSummaryData.builder().currency(defaultSummaryData.currency).principalDisbursed(defaultSummaryData.principalDisbursed)
defaultSummaryData.principalAdjustments, defaultSummaryData.principalPaid, defaultSummaryData.principalWrittenOff, .principalAdjustments(defaultSummaryData.principalAdjustments).principalPaid(defaultSummaryData.principalPaid)
defaultSummaryData.principalOutstanding, defaultSummaryData.principalOverdue, defaultSummaryData.interestCharged, .principalWrittenOff(defaultSummaryData.principalWrittenOff).principalOutstanding(defaultSummaryData.principalOutstanding)
defaultSummaryData.interestPaid, defaultSummaryData.interestWaived, defaultSummaryData.interestWrittenOff, .principalOverdue(defaultSummaryData.principalOverdue).interestCharged(defaultSummaryData.interestCharged)
defaultSummaryData.interestOutstanding, defaultSummaryData.interestOverdue, defaultSummaryData.feeChargesCharged, .interestPaid(defaultSummaryData.interestPaid).interestWaived(defaultSummaryData.interestWaived)
defaultSummaryData.feeChargesDueAtDisbursementCharged, defaultSummaryData.feeChargesPaid, .interestWrittenOff(defaultSummaryData.interestWrittenOff).interestOutstanding(defaultSummaryData.interestOutstanding)
defaultSummaryData.feeChargesWaived, defaultSummaryData.feeChargesWrittenOff, defaultSummaryData.feeChargesOutstanding, .interestOverdue(defaultSummaryData.interestOverdue).feeChargesCharged(defaultSummaryData.feeChargesCharged)
defaultSummaryData.feeChargesOverdue, defaultSummaryData.penaltyChargesCharged, defaultSummaryData.penaltyChargesPaid, .feeAdjustments(defaultSummaryData.feeAdjustments)
defaultSummaryData.penaltyChargesWaived, defaultSummaryData.penaltyChargesWrittenOff, .feeChargesDueAtDisbursementCharged(defaultSummaryData.feeChargesDueAtDisbursementCharged)
defaultSummaryData.penaltyChargesOutstanding, defaultSummaryData.penaltyChargesOverdue, .feeChargesPaid(defaultSummaryData.feeChargesPaid).feeChargesWaived(defaultSummaryData.feeChargesWaived)
defaultSummaryData.totalExpectedRepayment, defaultSummaryData.totalRepayment, defaultSummaryData.totalExpectedCostOfLoan, .feeChargesWrittenOff(defaultSummaryData.feeChargesWrittenOff)
defaultSummaryData.totalCostOfLoan, defaultSummaryData.totalWaived, defaultSummaryData.totalWrittenOff, .feeChargesOutstanding(defaultSummaryData.feeChargesOutstanding).feeChargesOverdue(defaultSummaryData.feeChargesOverdue)
defaultSummaryData.totalOutstanding, defaultSummaryData.totalOverdue, defaultSummaryData.overdueSinceDate, .penaltyChargesCharged(defaultSummaryData.penaltyChargesCharged).penaltyAdjustments(defaultSummaryData.penaltyAdjustments)
defaultSummaryData.writeoffReasonId, defaultSummaryData.writeoffReason, defaultSummaryData.totalRecovered, .penaltyChargesPaid(defaultSummaryData.penaltyChargesPaid).penaltyChargesWaived(defaultSummaryData.penaltyChargesWaived)
defaultSummaryData.chargeOffReasonId, defaultSummaryData.chargeOffReason).setTotalMerchantRefund(totalMerchantRefund) .penaltyChargesWrittenOff(defaultSummaryData.penaltyChargesWrittenOff)
.setTotalMerchantRefundReversed(totalMerchantRefundReversed).setTotalPayoutRefund(totalPayoutRefund) .penaltyChargesOutstanding(defaultSummaryData.penaltyChargesOutstanding)
.setTotalPayoutRefundReversed(totalPayoutRefundReversed).setTotalGoodwillCredit(totalGoodwillCredit) .penaltyChargesOverdue(defaultSummaryData.penaltyChargesOverdue)
.setTotalGoodwillCreditReversed(totalGoodwillCreditReversed).setTotalChargeAdjustment(totalChargeAdjustment) .totalExpectedRepayment(defaultSummaryData.totalExpectedRepayment).totalRepayment(defaultSummaryData.totalRepayment)
.setTotalChargeAdjustmentReversed(totalChargeAdjustmentReversed).setTotalChargeback(totalChargeback) .totalExpectedCostOfLoan(defaultSummaryData.totalExpectedCostOfLoan).totalCostOfLoan(defaultSummaryData.totalCostOfLoan)
.setTotalCreditBalanceRefund(totalCreditBalanceRefund).setTotalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed) .totalWaived(defaultSummaryData.totalWaived).totalWrittenOff(defaultSummaryData.totalWrittenOff)
.setTotalRepaymentTransaction(totalRepaymentTransaction) .totalOutstanding(defaultSummaryData.totalOutstanding).totalOverdue(defaultSummaryData.totalOverdue)
.setTotalRepaymentTransactionReversed(totalRepaymentTransactionReversed); .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) { 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, return LoanSummaryData.builder().currency(currencyData).build();
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null);
} }
private static BigDecimal computeTotalAmountForReversedTransactions(LoanTransactionType transactionType, private static BigDecimal computeTotalAmountForReversedTransactions(LoanTransactionType transactionType,
@@ -695,7 +695,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(), installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(),
installment.getInterestAccrued(currency).getAmount(), installment.getFeeAccrued(currency).getAmount(), installment.getInterestAccrued(currency).getAmount(), installment.getFeeAccrued(currency).getAmount(),
installment.getPenaltyAccrued(currency).getAmount(), currencyData, interestCalculatedFrom, 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); loanScheduleAccrualDatas.add(accrualData);
} }
@@ -940,10 +941,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
.minus(loanRepaymentScheduleInstallment.getInterestWaived(currency)); .minus(loanRepaymentScheduleInstallment.getInterestWaived(currency));
feePortion = feePortion.add(loanRepaymentScheduleInstallment.getFeeChargesCharged(currency)) feePortion = feePortion.add(loanRepaymentScheduleInstallment.getFeeChargesCharged(currency))
.minus(loanRepaymentScheduleInstallment.getFeeAccrued(currency)) .minus(loanRepaymentScheduleInstallment.getFeeAccrued(currency))
.minus(loanRepaymentScheduleInstallment.getFeeChargesWaived(currency)); .minus(loanRepaymentScheduleInstallment.getFeeChargesWaived(currency))
.minus(loanRepaymentScheduleInstallment.getCreditedFee(currency));
penaltyPortion = penaltyPortion.add(loanRepaymentScheduleInstallment.getPenaltyChargesCharged(currency)) penaltyPortion = penaltyPortion.add(loanRepaymentScheduleInstallment.getPenaltyChargesCharged(currency))
.minus(loanRepaymentScheduleInstallment.getPenaltyAccrued(currency)) .minus(loanRepaymentScheduleInstallment.getPenaltyAccrued(currency))
.minus(loanRepaymentScheduleInstallment.getPenaltyChargesWaived(currency)); .minus(loanRepaymentScheduleInstallment.getPenaltyChargesWaived(currency))
.minus(loanRepaymentScheduleInstallment.getCreditedPenalty(currency));
} }
Money total = interestPortion.plus(feePortion).plus(penaltyPortion); Money total = interestPortion.plus(feePortion).plus(penaltyPortion);
@@ -168,7 +168,9 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome(); BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome();
BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome(); BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome();
BigDecimal totalCreditedPenalty = accrualData.getCreditedPenalty();
BigDecimal totalAccFee = accrualData.getAccruedFeeIncome(); BigDecimal totalAccFee = accrualData.getAccruedFeeIncome();
BigDecimal totalCreditedFee = accrualData.getCreditedFee();
if (totalAccInterest == null) { if (totalAccInterest == null) {
totalAccInterest = BigDecimal.ZERO; totalAccInterest = BigDecimal.ZERO;
@@ -183,7 +185,10 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
if (totalAccFee == null) { if (totalAccFee == null) {
totalAccFee = BigDecimal.ZERO; totalAccFee = BigDecimal.ZERO;
} }
feePortion = feePortion.subtract(totalAccFee); if (totalCreditedFee == null) {
totalCreditedFee = BigDecimal.ZERO;
}
feePortion = feePortion.subtract(totalAccFee).subtract(totalCreditedFee);
amount = amount.add(feePortion); amount = amount.add(feePortion);
totalAccFee = totalAccFee.add(feePortion); totalAccFee = totalAccFee.add(feePortion);
if (feePortion.compareTo(BigDecimal.ZERO) == 0) { if (feePortion.compareTo(BigDecimal.ZERO) == 0) {
@@ -195,7 +200,10 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
if (totalAccPenalty == null) { if (totalAccPenalty == null) {
totalAccPenalty = BigDecimal.ZERO; totalAccPenalty = BigDecimal.ZERO;
} }
penaltyPortion = penaltyPortion.subtract(totalAccPenalty); if (totalCreditedPenalty == null) {
totalCreditedPenalty = BigDecimal.ZERO;
}
penaltyPortion = penaltyPortion.subtract(totalAccPenalty).subtract(totalCreditedPenalty);
amount = amount.add(penaltyPortion); amount = amount.add(penaltyPortion);
totalAccPenalty = totalAccPenalty.add(penaltyPortion); totalAccPenalty = totalAccPenalty.add(penaltyPortion);
if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) { if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) {
@@ -234,6 +242,9 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
if (scheduleAccrualData.getAccruedFeeIncome() != null) { if (scheduleAccrualData.getAccruedFeeIncome() != null) {
feePortion = feePortion.subtract(scheduleAccrualData.getAccruedFeeIncome()); feePortion = feePortion.subtract(scheduleAccrualData.getAccruedFeeIncome());
} }
if (scheduleAccrualData.getCreditedFee() != null) {
feePortion = feePortion.subtract(scheduleAccrualData.getCreditedFee());
}
amount = amount.add(feePortion); amount = amount.add(feePortion);
if (feePortion.compareTo(BigDecimal.ZERO) == 0) { if (feePortion.compareTo(BigDecimal.ZERO) == 0) {
feePortion = null; feePortion = null;
@@ -248,6 +259,9 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
if (scheduleAccrualData.getAccruedPenaltyIncome() != null) { if (scheduleAccrualData.getAccruedPenaltyIncome() != null) {
penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getAccruedPenaltyIncome()); penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getAccruedPenaltyIncome());
} }
if (scheduleAccrualData.getCreditedPenalty() != null) {
penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getCreditedPenalty());
}
amount = amount.add(penaltyPortion); amount = amount.add(penaltyPortion);
if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) { if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) {
penaltyPortion = null; penaltyPortion = null;
@@ -649,6 +649,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " l.loan_officer_id as loanOfficerId, s.display_name as loanOfficerName, " + " 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_disbursed_derived as principalDisbursed, l.principal_repaid_derived as principalPaid,"
+ " l.principal_adjustments_derived as principalAdjustments, l.principal_writtenoff_derived as principalWrittenOff," + " 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.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_repaid_derived as interestPaid, l.interest_waived_derived as interestWaived,"
+ " l.interest_writtenoff_derived as interestWrittenOff, l.interest_outstanding_derived as interestOutstanding," + " 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 interestOverdue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestOverdue");
final BigDecimal feeChargesCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesCharged"); final BigDecimal feeChargesCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesCharged");
final BigDecimal feeAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeAdjustments");
final BigDecimal feeChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesPaid"); final BigDecimal feeChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesPaid");
final BigDecimal feeChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesWaived"); final BigDecimal feeChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesWaived");
final BigDecimal feeChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesWrittenOff"); 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 feeChargesOverdue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesOverdue");
final BigDecimal penaltyChargesCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesCharged"); final BigDecimal penaltyChargesCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesCharged");
final BigDecimal penaltyAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyAdjustments");
final BigDecimal penaltyChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesPaid"); final BigDecimal penaltyChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesPaid");
final BigDecimal penaltyChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWaived"); final BigDecimal penaltyChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWaived");
final BigDecimal penaltyChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWrittenOff"); final BigDecimal penaltyChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWrittenOff");
@@ -938,14 +941,24 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final LocalDate overdueSinceDate = JdbcSupport.getLocalDate(rs, "overdueSinceDate"); final LocalDate overdueSinceDate = JdbcSupport.getLocalDate(rs, "overdueSinceDate");
inArrears = (overdueSinceDate != null); inArrears = (overdueSinceDate != null);
loanSummary = new LoanSummaryData(currencyData, principalDisbursed, principalAdjustments, principalPaid, loanSummary = LoanSummaryData.builder().currency(currencyData).principalDisbursed(principalDisbursed)
principalWrittenOff, principalOutstanding, principalOverdue, interestCharged, interestPaid, interestWaived, .principalAdjustments(principalAdjustments).principalPaid(principalPaid).principalWrittenOff(principalWrittenOff)
interestWrittenOff, interestOutstanding, interestOverdue, feeChargesCharged, feeChargesDueAtDisbursementCharged, .principalOutstanding(principalOutstanding).principalOverdue(principalOverdue).interestCharged(interestCharged)
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, feeChargesOverdue, .interestPaid(interestPaid).interestWaived(interestWaived).interestWrittenOff(interestWrittenOff)
penaltyChargesCharged, penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff, .interestOutstanding(interestOutstanding).interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged)
penaltyChargesOutstanding, penaltyChargesOverdue, totalExpectedRepayment, totalRepayment, totalExpectedCostOfLoan, .feeAdjustments(feeAdjustments).feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged)
totalCostOfLoan, totalWaived, totalWrittenOff, totalOutstanding, totalOverdue, overdueSinceDate, writeoffReasonId, .feeChargesPaid(feeChargesPaid).feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff)
writeoffReason, totalRecovered, chargeOffReasonId, chargeOffReason); .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; 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.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_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.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 "; + " 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.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.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.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("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(" 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") .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 accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest");
final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege"); final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege");
final BigDecimal accruedPenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accpenalty"); 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 currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName"); 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, return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromDate,
repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome, 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.installment as installmentNumber, ")
.append("ls.interest_amount as interest, ls.interest_waived_derived as interestWaived,") .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.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("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(" 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") .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 interestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interest");
final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges"); final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges");
final BigDecimal penaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalty"); 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 interestIncomeWaived = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interestWaived");
final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest"); final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest");
final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege"); final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege");
@@ -2015,7 +2035,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final LocalDate interestCalculatedFrom = null; final LocalDate interestCalculatedFrom = null;
return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromdate, return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromdate,
repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome, repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome,
accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived); accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived, creditedFee, creditedPenalty);
} }
} }
@@ -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.PENALTY;
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL; import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.eq;
import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock; 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.times;
import static org.mockito.Mockito.when;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
@@ -83,7 +87,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
private final LocalDate transactionDate = LocalDate.of(2023, 7, 11); private final LocalDate transactionDate = LocalDate.of(2023, 7, 11);
private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1); private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1);
private static final MockedStatic<MoneyHelper> MONEY_HELPER = Mockito.mockStatic(MoneyHelper.class); private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
private AdvancedPaymentScheduleTransactionProcessor underTest; private AdvancedPaymentScheduleTransactionProcessor underTest;
@BeforeAll @BeforeAll
@@ -123,34 +127,33 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Money zero = Money.zero(currency); Money zero = Money.zero(currency);
Loan loan = mock(Loan.class); Loan loan = mock(Loan.class);
Money chargeAmountMoney = Money.of(currency, chargeAmount); Money chargeAmountMoney = Money.of(currency, chargeAmount);
LoanRepaymentScheduleInstallment installment = Mockito LoanRepaymentScheduleInstallment installment = spy(
.spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L), new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L),
BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
Mockito.when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT); when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
Mockito.when(chargePaidBy.getLoanCharge()).thenReturn(charge); when(chargePaidBy.getLoanCharge()).thenReturn(charge);
Mockito.when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy)); when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
Mockito.when(loanTransaction.getAmount()).thenReturn(chargeAmount); when(loanTransaction.getAmount()).thenReturn(chargeAmount);
Mockito.when(loanTransaction.getAmount(currency)).thenReturn(chargeAmountMoney); when(loanTransaction.getAmount(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getLoan()).thenReturn(loan); when(loanTransaction.getLoan()).thenReturn(loan);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true);
.thenReturn(true); when(installment.getInstallmentNumber()).thenReturn(1);
Mockito.when(installment.getInstallmentNumber()).thenReturn(1); when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney); when(loanTransaction.isPenaltyPayment()).thenReturn(false);
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
underTest.processLatestTransaction(loanTransaction, underTest.processLatestTransaction(loanTransaction,
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount))); 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(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(chargeAmountMoney).getAmount()); assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(chargeAmountMoney).getAmount());
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount())); assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(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 @Test
@@ -168,35 +171,33 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Money chargeAmountMoney = Money.of(currency, chargeAmount); Money chargeAmountMoney = Money.of(currency, chargeAmount);
BigDecimal transactionAmount = BigDecimal.valueOf(20.00); BigDecimal transactionAmount = BigDecimal.valueOf(20.00);
Money transactionAmountMoney = Money.of(currency, transactionAmount); Money transactionAmountMoney = Money.of(currency, transactionAmount);
LoanRepaymentScheduleInstallment installment = Mockito LoanRepaymentScheduleInstallment installment = spy(
.spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L), new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L),
BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
Mockito.when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT); when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
Mockito.when(chargePaidBy.getLoanCharge()).thenReturn(charge); when(chargePaidBy.getLoanCharge()).thenReturn(charge);
Mockito.when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy)); when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
Mockito.when(loanTransaction.getAmount()).thenReturn(transactionAmount); when(loanTransaction.getAmount()).thenReturn(transactionAmount);
Mockito.when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney); when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney);
Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getLoan()).thenReturn(loan); when(loanTransaction.getLoan()).thenReturn(loan);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true);
.thenReturn(true); when(installment.getInstallmentNumber()).thenReturn(1);
Mockito.when(installment.getInstallmentNumber()).thenReturn(1); when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
Mockito.when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney); when(loanTransaction.isPenaltyPayment()).thenReturn(false);
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
underTest.processLatestTransaction(loanTransaction, underTest.processLatestTransaction(loanTransaction,
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount))); 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(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), refEq(zero));
refEq(zero));
assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(transactionAmountMoney).getAmount()); assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(transactionAmountMoney).getAmount());
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount())); assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
assertEquals(0, BigDecimal.valueOf(80.00).compareTo(installment.getFeeChargesOutstanding(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 @Test
@@ -216,94 +217,50 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
BigDecimal transactionAmount = BigDecimal.valueOf(120.00); BigDecimal transactionAmount = BigDecimal.valueOf(120.00);
Money transactionAmountMoney = Money.of(currency, transactionAmount); Money transactionAmountMoney = Money.of(currency, transactionAmount);
LoanPaymentAllocationRule loanPaymentAllocationRule = mock(LoanPaymentAllocationRule.class); LoanPaymentAllocationRule loanPaymentAllocationRule = mock(LoanPaymentAllocationRule.class);
LoanRepaymentScheduleInstallment installment = Mockito LoanRepaymentScheduleInstallment installment = spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, transactionDate,
.spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, transactionDate, BigDecimal.valueOf(100L), BigDecimal.valueOf(100L), BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
Mockito.when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT); when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
Mockito.when(chargePaidBy.getLoanCharge()).thenReturn(charge); when(chargePaidBy.getLoanCharge()).thenReturn(charge);
Mockito.when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy)); when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
Mockito.when(loanTransaction.getAmount()).thenReturn(transactionAmount); when(loanTransaction.getAmount()).thenReturn(transactionAmount);
Mockito.when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney); when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney);
Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getLoan()).thenReturn(loan); when(loanTransaction.getLoan()).thenReturn(loan);
Mockito.when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
Mockito.when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL); when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true);
.thenReturn(true); when(installment.getInstallmentNumber()).thenReturn(1);
Mockito.when(installment.getInstallmentNumber()).thenReturn(1); when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney); when(loanTransaction.isPenaltyPayment()).thenReturn(false);
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false); when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule));
Mockito.when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule)); when(loanPaymentAllocationRule.getTransactionType()).thenReturn(PaymentAllocationTransactionType.DEFAULT);
Mockito.when(loanPaymentAllocationRule.getTransactionType()).thenReturn(PaymentAllocationTransactionType.DEFAULT); when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
Mockito.when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL)); when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true);
Mockito.when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true);
underTest.processLatestTransaction(loanTransaction, underTest.processLatestTransaction(loanTransaction,
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount))); 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(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); 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, BigDecimal.valueOf(20).compareTo(loanTransaction.getAmount(currency).minus(chargeAmountMoney).getAmount()));
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount())); assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount())); assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
assertEquals(0, BigDecimal.valueOf(80).compareTo(installment.getPrincipalOutstanding(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 @Test
public void testProcessCreditTransactionWithAllocationRuleInterestAndPrincipal() { public void testProcessCreditTransactionWithAllocationRulePrincipalPenaltyFeeInterest() {
// given // given
Loan loan = mock(Loan.class); Loan loan = mock(Loan.class);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan); LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(INTEREST, PRINCIPAL, PENALTY, FEE); LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule)); when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction); 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<LoanRepaymentScheduleInstallment> 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<LocalDate> localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class);
ArgumentCaptor<Money> 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<Money> principal = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> 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);
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment)); lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY)); MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
@@ -315,15 +272,31 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder); TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
underTest.processCreditTransaction(chargeBackTransaction, ctx); underTest.processCreditTransaction(chargeBackTransaction, ctx);
// then // verify principal
Mockito.verify(installment, Mockito.times(1)).addToCredits(new BigDecimal("25.00")); Mockito.verify(installment, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
Mockito.verify(installment, Mockito.times(1)).updateInterestCharged(new BigDecimal("15.00"));
ArgumentCaptor<LocalDate> localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class); ArgumentCaptor<LocalDate> localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class);
ArgumentCaptor<Money> moneyCaptor = ArgumentCaptor.forClass(Money.class); ArgumentCaptor<Money> 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()); Assertions.assertEquals(LocalDate.of(2023, 1, 1), localDateArgumentCaptor.getValue());
assertEquals(0, moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); assertEquals(0, moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
// verify charges on installment
Mockito.verify(installment, times(1)).addToCreditedFee(new BigDecimal("10.00"));
Mockito.verify(installment, times(1)).addToCreditedPenalty(new BigDecimal("5.00"));
ArgumentCaptor<Money> feeCaptor = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> 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<Money> principal = ArgumentCaptor.forClass(Money.class); ArgumentCaptor<Money> principal = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class); ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class); ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
@@ -331,24 +304,74 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(), Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
penalty.capture()); penalty.capture());
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); 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.valueOf(10.0)));
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO)); assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0)));
assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.ZERO)); 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<LoanRepaymentScheduleInstallment> 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<Money> feeCaptor = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> 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<Money> principal = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> 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 @Test
public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment() { public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment() {
// given // given
Loan loan = mock(Loan.class); Loan loan = mock(Loan.class);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan); LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE); LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule)); when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction); LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment)); lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY)); MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>(); List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
LoanRepaymentScheduleInstallment installment1 = createMockInstallment(LocalDate.of(2022, 12, 20), false); LoanRepaymentScheduleInstallment installment1 = createMockInstallment(LocalDate.of(2022, 12, 20), false);
LoanRepaymentScheduleInstallment installment2 = createMockInstallment(LocalDate.of(2022, 12, 27), true); LoanRepaymentScheduleInstallment installment2 = createMockInstallment(LocalDate.of(2022, 12, 27), true);
installments.add(installment1); installments.add(installment1);
@@ -358,15 +381,31 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder); TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
underTest.processCreditTransaction(chargeBackTransaction, ctx); underTest.processCreditTransaction(chargeBackTransaction, ctx);
// then // verify principal
Mockito.verify(installment2, Mockito.times(1)).addToCredits(new BigDecimal("25.00")); Mockito.verify(installment2, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
Mockito.verify(installment2, Mockito.times(1)).updateInterestCharged(new BigDecimal("15.00"));
ArgumentCaptor<LocalDate> localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class); ArgumentCaptor<LocalDate> localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class);
ArgumentCaptor<Money> moneyCaptor = ArgumentCaptor.forClass(Money.class); ArgumentCaptor<Money> 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()); Assertions.assertEquals(LocalDate.of(2023, 1, 1), localDateArgumentCaptor.getValue());
assertEquals(0, moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); 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<Money> feeCaptor = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> 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<Money> principal = ArgumentCaptor.forClass(Money.class); ArgumentCaptor<Money> principal = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class); ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class); ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
@@ -374,9 +413,9 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(), Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
penalty.capture()); penalty.capture());
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0))); 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.valueOf(10.0)));
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO)); assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0)));
assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.ZERO)); assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.ZERO));
} }
private LoanRepaymentScheduleInstallment createMockInstallment(LocalDate localDate, boolean isAdditional) { private LoanRepaymentScheduleInstallment createMockInstallment(LocalDate localDate, boolean isAdditional) {
@@ -396,15 +435,16 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
return mockCreditAllocationRule; 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); LoanTransaction repayment = mock(LoanTransaction.class);
lenient().when(repayment.getLoan()).thenReturn(loan); lenient().when(repayment.getLoan()).thenReturn(loan);
lenient().when(repayment.isRepayment()).thenReturn(true); lenient().when(repayment.isRepayment()).thenReturn(true);
lenient().when(repayment.getTypeOf()).thenReturn(REPAYMENT); lenient().when(repayment.getTypeOf()).thenReturn(REPAYMENT);
lenient().when(repayment.getPrincipalPortion()).thenReturn(BigDecimal.valueOf(10)); lenient().when(repayment.getPrincipalPortion()).thenReturn(BigDecimal.valueOf(principalPortion));
lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(20)); lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(interestPortion));
lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.ZERO); lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.valueOf(feePortion));
lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.ZERO); lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.valueOf(penaltyPortion));
LoanTransactionRelation relation = mock(LoanTransactionRelation.class); LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
lenient().when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); lenient().when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
@@ -414,14 +454,14 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
return repayment; return repayment;
} }
private LoanTransaction createChargebackTransaction(Loan loan) { private LoanTransaction createChargebackTransaction(Loan loan, double transactionAmount) {
LoanTransaction chargeback = mock(LoanTransaction.class); LoanTransaction chargeback = mock(LoanTransaction.class);
lenient().when(chargeback.isChargeback()).thenReturn(true); lenient().when(chargeback.isChargeback()).thenReturn(true);
lenient().when(chargeback.getTypeOf()).thenReturn(LoanTransactionType.CHARGEBACK); lenient().when(chargeback.getTypeOf()).thenReturn(LoanTransactionType.CHARGEBACK);
lenient().when(chargeback.getLoan()).thenReturn(loan); lenient().when(chargeback.getLoan()).thenReturn(loan);
lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(25)); lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(transactionAmount));
Money amount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(25)); Money money = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(transactionAmount));
lenient().when(chargeback.getAmount(MONETARY_CURRENCY)).thenReturn(amount); lenient().when(chargeback.getAmount(MONETARY_CURRENCY)).thenReturn(money);
lenient().when(chargeback.getTransactionDate()).thenReturn(LocalDate.of(2023, 1, 1)); lenient().when(chargeback.getTransactionDate()).thenReturn(LocalDate.of(2023, 1, 1));
return chargeback; return chargeback;
} }
@@ -431,40 +471,41 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Map<AllocationType, Money> result; Map<AllocationType, Money> result;
MonetaryCurrency currency = mock(MonetaryCurrency.class); 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); 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); 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); 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); 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); 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<AllocationType, BigDecimal> expected, Map<AllocationType, Money> actual) { private void verify(Map<AllocationType, Money> expected, Map<AllocationType, Money> actual) {
Assertions.assertEquals(expected.size(), actual.size()); Assertions.assertEquals(expected.size(), actual.size());
expected.forEach((k, v) -> { 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<AllocationType, BigDecimal> allocationMap(double principal, double interest, double fee, double penalty) { private Map<AllocationType, Money> allocationMap(double principal, double interest, double fee, double penalty,
Map<AllocationType, BigDecimal> allocationMap = new HashMap<>(); MonetaryCurrency currency) {
allocationMap.put(AllocationType.PRINCIPAL, BigDecimal.valueOf(principal)); Map<AllocationType, Money> allocationMap = new HashMap<>();
allocationMap.put(AllocationType.INTEREST, BigDecimal.valueOf(interest)); allocationMap.put(AllocationType.PRINCIPAL, Money.of(currency, BigDecimal.valueOf(principal)));
allocationMap.put(AllocationType.FEE, BigDecimal.valueOf(fee)); allocationMap.put(AllocationType.INTEREST, Money.of(currency, BigDecimal.valueOf(interest)));
allocationMap.put(AllocationType.PENALTY, BigDecimal.valueOf(penalty)); allocationMap.put(AllocationType.FEE, Money.of(currency, BigDecimal.valueOf(fee)));
allocationMap.put(AllocationType.PENALTY, Money.of(currency, BigDecimal.valueOf(penalty)));
return allocationMap; return allocationMap;
} }
@@ -472,17 +513,17 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionWhenIdProvided() { public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionWhenIdProvided() {
// given // given
LoanTransaction chargebackTransaction = mock(LoanTransaction.class); LoanTransaction chargebackTransaction = mock(LoanTransaction.class);
Mockito.when(chargebackTransaction.getId()).thenReturn(123L); when(chargebackTransaction.getId()).thenReturn(123L);
Loan loan = mock(Loan.class); Loan loan = mock(Loan.class);
Mockito.when(chargebackTransaction.getLoan()).thenReturn(loan); when(chargebackTransaction.getLoan()).thenReturn(loan);
LoanTransaction repayment1 = mock(LoanTransaction.class); LoanTransaction repayment1 = mock(LoanTransaction.class);
LoanTransaction repayment2 = 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); LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
Mockito.when(relation.getToTransaction()).thenReturn(chargebackTransaction); when(relation.getToTransaction()).thenReturn(chargebackTransaction);
Mockito.when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
TransactionCtx ctx = mock(TransactionCtx.class); TransactionCtx ctx = mock(TransactionCtx.class);
// when // when
@@ -496,14 +537,14 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testFindOriginalTransactionThrowsRuntimeExceptionWhenIdProvidedAndRelationsAreMissing() { public void testFindOriginalTransactionThrowsRuntimeExceptionWhenIdProvidedAndRelationsAreMissing() {
// given // given
LoanTransaction chargebackTransaction = mock(LoanTransaction.class); LoanTransaction chargebackTransaction = mock(LoanTransaction.class);
Mockito.when(chargebackTransaction.getId()).thenReturn(123L); when(chargebackTransaction.getId()).thenReturn(123L);
Loan loan = mock(Loan.class); Loan loan = mock(Loan.class);
Mockito.when(chargebackTransaction.getLoan()).thenReturn(loan); when(chargebackTransaction.getLoan()).thenReturn(loan);
LoanTransaction repayment1 = mock(LoanTransaction.class); LoanTransaction repayment1 = mock(LoanTransaction.class);
LoanTransaction repayment2 = 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); TransactionCtx ctx = mock(TransactionCtx.class);
@@ -517,22 +558,22 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvided() { public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvided() {
// given // given
LoanTransaction chargebackReplayed = mock(LoanTransaction.class); LoanTransaction chargebackReplayed = mock(LoanTransaction.class);
Mockito.when(chargebackReplayed.getId()).thenReturn(null); when(chargebackReplayed.getId()).thenReturn(null);
LoanTransaction repayment1 = mock(LoanTransaction.class); LoanTransaction repayment1 = mock(LoanTransaction.class);
LoanTransaction repayment2 = mock(LoanTransaction.class); LoanTransaction repayment2 = mock(LoanTransaction.class);
LoanTransaction originalChargeback = mock(LoanTransaction.class); LoanTransaction originalChargeback = mock(LoanTransaction.class);
Mockito.when(originalChargeback.getId()).thenReturn(123L); when(originalChargeback.getId()).thenReturn(123L);
LoanTransactionRelation relation = mock(LoanTransactionRelation.class); LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
Mockito.when(relation.getToTransaction()).thenReturn(originalChargeback); when(relation.getToTransaction()).thenReturn(originalChargeback);
Mockito.when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
TransactionCtx ctx = mock(TransactionCtx.class); TransactionCtx ctx = mock(TransactionCtx.class);
ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class); ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class);
Mockito.when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail); when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail);
Mockito.when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L)); when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L));
Mockito.when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2)); when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2));
// when // when
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx); LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
@@ -545,25 +586,25 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvidedFallbackToPersistedTransactions() { public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvidedFallbackToPersistedTransactions() {
// given // given
LoanTransaction chargebackReplayed = mock(LoanTransaction.class); LoanTransaction chargebackReplayed = mock(LoanTransaction.class);
Mockito.when(chargebackReplayed.getId()).thenReturn(null); when(chargebackReplayed.getId()).thenReturn(null);
LoanTransaction repayment1 = mock(LoanTransaction.class); LoanTransaction repayment1 = mock(LoanTransaction.class);
LoanTransaction repayment2 = mock(LoanTransaction.class); LoanTransaction repayment2 = mock(LoanTransaction.class);
Loan loan = mock(Loan.class); Loan loan = mock(Loan.class);
Mockito.when(chargebackReplayed.getLoan()).thenReturn(loan); when(chargebackReplayed.getLoan()).thenReturn(loan);
Mockito.when(loan.getLoanTransactions()).thenReturn(List.of(repayment1, repayment2)); when(loan.getLoanTransactions()).thenReturn(List.of(repayment1, repayment2));
LoanTransaction originalChargeback = mock(LoanTransaction.class); LoanTransaction originalChargeback = mock(LoanTransaction.class);
Mockito.when(originalChargeback.getId()).thenReturn(123L); when(originalChargeback.getId()).thenReturn(123L);
LoanTransactionRelation relation = mock(LoanTransactionRelation.class); LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
Mockito.when(relation.getToTransaction()).thenReturn(originalChargeback); when(relation.getToTransaction()).thenReturn(originalChargeback);
Mockito.when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK); when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation)); when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
TransactionCtx ctx = mock(TransactionCtx.class); TransactionCtx ctx = mock(TransactionCtx.class);
ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class); ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class);
Mockito.when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail); when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail);
Mockito.when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L)); when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L));
Mockito.when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of()); when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of());
// when // when
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx); LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
@@ -476,6 +476,46 @@ public abstract class BaseLoanIntegrationTest {
totalOutstanding)); 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(installments[i].completed, period.getComplete());
Assertions.assertEquals(LocalDate.parse(installments[i].dueDate, dateTimeFormatter), period.getDueDate()); 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) { 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, protected Installment installment(double principalAmount, double interestAmount, double totalOutstandingAmount, Boolean completed,
String dueDate) { 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, protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double totalOutstandingAmount,
Boolean completed, String dueDate) { 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, protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double penaltyAmount,
double totalOutstandingAmount, Boolean completed, String dueDate) { 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() { protected BatchRequestBuilder batchRequest() {
@@ -735,6 +784,17 @@ public abstract class BaseLoanIntegrationTest {
Double totalOutstandingAmount; Double totalOutstandingAmount;
Boolean completed; Boolean completed;
String dueDate; String dueDate;
OutstandingAmounts outstandingAmounts;
}
@AllArgsConstructor
@ToString
public static class OutstandingAmounts {
Double principalOutstanding;
Double feeOutstanding;
Double penaltyOutstanding;
Double totalOutstanding;
} }
public static class AmortizationType { public static class AmortizationType {
@@ -26,6 +26,8 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.CreditAllocationData; import org.apache.fineract.client.models.CreditAllocationData;
import org.apache.fineract.client.models.CreditAllocationOrder; 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.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse; 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.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -46,13 +49,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoanIntegrationTest { public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoanIntegrationTest {
@Test @Test
public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrincipal() { public void simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipal() {
runAt("01 January 2023", () -> { runAt("01 January 2023", () -> {
// Create Client // Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product // Create Loan Product
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")); Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan // Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId); Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -92,15 +97,13 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // 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(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 // Verify Repayment Schedule
verifyRepaymentSchedule(loanId, // verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), // 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 installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"),
// fee and the penalty to the
// outstanding
installment(313.0, 0, 0, 0, 313.0, false, "01 March 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(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
@@ -109,13 +112,145 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
} }
@Test @Test
public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrincipalOnNPlusOneInstallment() { public void simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipalOnTheLastDayOfTheInstallment() {
runAt("01 January 2023", () -> { runAt("01 January 2023", () -> {
// Create Client // Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product // 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 // Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId); 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(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(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(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, // 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 March 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 April 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(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 installment(30.0, 0, 50, 20, 100.0, false, "02 May 2023") //
// after chargeback
); );
}); });
} }
@Test @Test
public void createLoanWithCreditAllocationAndChargebackReverseReplayWithBackdatedPayment() { public void chargebackWithCreditAllocationAndReverseReplayWithBackdatedPayment() {
runAt("01 January 2023", () -> { runAt("01 January 2023", () -> {
// Create Client // Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product // Create Loan Product
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")); Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan // Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId); Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -232,15 +368,13 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // 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(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 // Verify Repayment Schedule
verifyRepaymentSchedule(loanId, // verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), // 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 installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"), //
// fee and the penalty to the
// outstanding
installment(313.0, 0, 0, 0, 313.0, false, "01 March 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(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") // installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
@@ -260,13 +394,15 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
} }
@Test @Test
public void createLoanWithCreditAllocationAndOnlyTheChargebackReverseReplayedWithBackdatedPayment() { public void chargebackWithCreditAllocationReverseReplayedWithBackdatedPayment() {
runAt("01 January 2023", () -> { runAt("01 January 2023", () -> {
// Create Client // Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product // Create Loan Product
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")); Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan // Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId); Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -307,15 +443,13 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // 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(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 // Verify Repayment Schedule
verifyRepaymentSchedule(loanId, // verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), // 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 installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"),
// fee and the penalty to the
// outstanding
installment(313.0, 0, 0, 0, 313.0, false, "01 March 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(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 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(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(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(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 @Test
public void createLoanWithCreditAllocationAndChargebackPrincipalInterestFeePenalty() { public void chargebackWithCreditAllocationPrincipalInterestFeePenalty() {
runAt("01 January 2023", () -> { runAt("01 January 2023", () -> {
// Create Client // Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product // Create Loan Product
Long loanProductId = createLoanProduct(chargebackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY")); Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY")//
);
// Apply and Approve Loan // Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId); Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -387,16 +527,501 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
// Verify Repayment Schedule // Verify Repayment Schedule
verifyRepaymentSchedule(loanId, // verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), // 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 installment(413.0, 0, 50, 20, 100.0, false, "01 February 2023"),
// fee and the penalty to the
// outstanding
installment(313.0, 0, 0, 0, 313.0, false, "01 March 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(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 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 @Nullable
private Long applyAndApproveLoan(Long clientId, Long loanProductId) { private Long applyAndApproveLoan(Long clientId, Long loanProductId) {
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", 1250.0, 4)// PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", 1250.0, 4)//
@@ -415,22 +1040,23 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
return loanId; return loanId;
} }
public Long createLoanProduct(CreditAllocationData... creditAllocationData) { public Long createLoanProduct(AdvancedPaymentData defaultAllocation, CreditAllocationData creditAllocationData) {
PostLoanProductsRequest postLoanProductsRequest = loanProductWithAdvancedPaymentAllocationWith4Installments(creditAllocationData); PostLoanProductsRequest postLoanProductsRequest = loanProductWithAdvancedPaymentAllocationWith4Installments(defaultAllocation,
creditAllocationData);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(postLoanProductsRequest); PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(postLoanProductsRequest);
return loanProductResponse.getResourceId(); return loanProductResponse.getResourceId();
} }
private PostLoanProductsRequest loanProductWithAdvancedPaymentAllocationWith4Installments( private PostLoanProductsRequest loanProductWithAdvancedPaymentAllocationWith4Installments(AdvancedPaymentData defaultAllocation,
CreditAllocationData... creditAllocationData) { CreditAllocationData creditAllocationData) {
return createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().numberOfRepayments(4)// return createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().numberOfRepayments(4)//
.repaymentEvery(1)// .repaymentEvery(1)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())// .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
.loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) // .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) //
.loanScheduleProcessingType(LoanScheduleProcessingType.VERTICAL.toString()) // .loanScheduleProcessingType(LoanScheduleProcessingType.VERTICAL.toString()) //
.transactionProcessingStrategyCode("advanced-payment-allocation-strategy") .transactionProcessingStrategyCode("advanced-payment-allocation-strategy")
.paymentAllocation(List.of(createDefaultPaymentAllocation(), createRepaymentPaymentAllocation())) .paymentAllocation(List.of(defaultAllocation, createRepaymentPaymentAllocation()))
.creditAllocation(Arrays.asList(creditAllocationData)); .creditAllocation(List.of(creditAllocationData));
} }
private AdvancedPaymentData createDefaultPaymentAllocation() { private AdvancedPaymentData createDefaultPaymentAllocation() {
@@ -448,6 +1074,21 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
return advancedPaymentData; return advancedPaymentData;
} }
private AdvancedPaymentData createDefaultPaymentAllocationPrincipalFirst() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
List<PaymentAllocationOrder> 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() { private AdvancedPaymentData createRepaymentPaymentAllocation() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("REPAYMENT"); advancedPaymentData.setTransactionType("REPAYMENT");