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.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
@@ -5959,10 +5960,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
.minus(loanTransaction.getOverPaymentPortion(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
Money transactionOutstanding = loanTransaction.getAmount(getCurrency());
Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency());
if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) {
transactionOutstanding = loanTransaction.getAmount(getCurrency())
.minus(loanTransaction.getOverPaymentPortion(getCurrency()));
// in case of advanced payment strategy and creditAllocations the full amount is recognized first
if (this.getCreditAllocationRules() != null && this.getCreditAllocationRules().size() > 0) {
Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream()
.map(mapping -> mapping.getPrincipalPortion(getCurrency())).reduce(Money.zero(getCurrency()), Money::plus);
transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency()).minus(payedPrincipal);
} else {
// in case legacy payment strategy
transactionOutstanding = loanTransaction.getAmount(getCurrency())
.minus(loanTransaction.getOverPaymentPortion(getCurrency()));
}
if (transactionOutstanding.isLessThanZero()) {
transactionOutstanding = Money.zero(getCurrency());
}
@@ -7198,6 +7207,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return paymentAllocationRules;
}
public LoanPaymentAllocationRule getPaymentAllocationRuleOrDefault(PaymentAllocationTransactionType transactionType) {
Optional<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) {
this.paymentAllocationRules = loanPaymentAllocationRules;
}
@@ -130,8 +130,14 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
@Column(name = "is_additional", nullable = false)
private boolean additional;
@Column(name = "credits_amount", scale = 6, precision = 19, nullable = true)
private BigDecimal credits;
@Column(name = "credited_principal", scale = 6, precision = 19, nullable = true)
private BigDecimal creditedPrincipal;
@Column(name = "credited_fee", scale = 6, precision = 19, nullable = true)
private BigDecimal creditedFee;
@Column(name = "credited_penalty", scale = 6, precision = 19, nullable = true)
private BigDecimal creditedPenalty;
@Column(name = "is_down_payment", nullable = false)
private boolean isDownPayment;
@@ -246,8 +252,16 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
return this.dueDate;
}
public Money getCredits(final MonetaryCurrency currency) {
return Money.of(currency, this.credits);
public Money getCreditedPrincipal(final MonetaryCurrency currency) {
return Money.of(currency, this.creditedPrincipal);
}
public Money getCreditedFee(final MonetaryCurrency currency) {
return Money.of(currency, this.creditedFee);
}
public Money getCreditedPenalty(final MonetaryCurrency currency) {
return Money.of(currency, this.creditedPenalty);
}
public Money getPrincipal(final MonetaryCurrency currency) {
@@ -408,9 +422,17 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
this.obligationsMet = false;
this.obligationsMetOnDate = null;
if (this.credits != null) {
this.principal = this.principal.subtract(this.credits);
this.credits = null;
if (this.creditedPrincipal != null) {
this.principal = this.principal.subtract(this.creditedPrincipal);
this.creditedPrincipal = null;
}
if (this.creditedFee != null) {
this.feeChargesCharged = this.feeChargesCharged.subtract(this.creditedFee);
this.creditedFee = null;
}
if (this.creditedPenalty != null) {
this.penaltyCharges = this.penaltyCharges.subtract(this.creditedPenalty);
this.creditedPenalty = null;
}
}
@@ -780,11 +802,27 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
checkIfRepaymentPeriodObligationsAreMet(transactionDate, transactionAmount.getCurrency());
}
public void addToCredits(final BigDecimal amount) {
if (this.credits == null) {
this.credits = amount;
public void addToCreditedPrincipal(final BigDecimal amount) {
if (this.creditedPrincipal == null) {
this.creditedPrincipal = amount;
} else {
this.credits = this.credits.add(amount);
this.creditedPrincipal = this.creditedPrincipal.add(amount);
}
}
public void addToCreditedFee(final BigDecimal amount) {
if (this.creditedFee == null) {
this.creditedFee = amount;
} else {
this.creditedFee = this.creditedFee.add(amount);
}
}
public void addToCreditedPenalty(final BigDecimal amount) {
if (this.creditedPenalty == null) {
this.creditedPenalty = amount;
} else {
this.creditedPenalty = this.creditedPenalty.add(amount);
}
}
@@ -908,7 +946,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
}
public void updateCredits(final LocalDate transactionDate, final Money transactionAmount) {
addToCredits(transactionAmount.getAmount());
addToCreditedPrincipal(transactionAmount.getAmount());
addToPrincipal(transactionDate, transactionAmount);
}
@@ -74,6 +74,9 @@ public class LoanSummary {
@Column(name = "total_charges_due_at_disbursement_derived", scale = 6, precision = 19)
private BigDecimal totalFeeChargesDueAtDisbursement;
@Column(name = "fee_adjustments_derived", scale = 6, precision = 19)
private BigDecimal totalFeeAdjustments;
@Column(name = "fee_charges_repaid_derived", scale = 6, precision = 19)
private BigDecimal totalFeeChargesRepaid;
@@ -89,6 +92,9 @@ public class LoanSummary {
@Column(name = "penalty_charges_charged_derived", scale = 6, precision = 19)
private BigDecimal totalPenaltyChargesCharged;
@Column(name = "penalty_adjustments_derived", scale = 6, precision = 19)
private BigDecimal totalPenaltyAdjustments;
@Column(name = "penalty_charges_repaid_derived", scale = 6, precision = 19)
private BigDecimal totalPenaltyChargesRepaid;
@@ -204,6 +210,8 @@ public class LoanSummary {
public void zeroFields() {
this.totalPrincipalDisbursed = BigDecimal.ZERO;
this.totalPrincipalAdjustments = BigDecimal.ZERO;
this.totalFeeAdjustments = BigDecimal.ZERO;
this.totalPenaltyAdjustments = BigDecimal.ZERO;
this.totalPrincipalRepaid = BigDecimal.ZERO;
this.totalPrincipalWrittenOff = BigDecimal.ZERO;
this.totalPrincipalOutstanding = BigDecimal.ZERO;
@@ -238,6 +246,8 @@ public class LoanSummary {
this.totalPrincipalDisbursed = principal.getAmount();
this.totalPrincipalAdjustments = summaryWrapper.calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency)
.getAmount();
this.totalFeeAdjustments = summaryWrapper.calculateTotalFeeAdjusted(repaymentScheduleInstallments, currency).getAmount();
this.totalPenaltyAdjustments = summaryWrapper.calculateTotalPenaltyAdjusted(repaymentScheduleInstallments, currency).getAmount();
this.totalPrincipalRepaid = summaryWrapper.calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount();
this.totalPrincipalWrittenOff = summaryWrapper.calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency)
.getAmount();
@@ -259,7 +269,9 @@ public class LoanSummary {
this.totalFeeChargesCharged = totalFeeChargesCharged.getAmount();
Money totalFeeChargesRepaidAtDisbursement = summaryWrapper.calculateTotalChargesRepaidAtDisbursement(charges, currency);
this.totalFeeChargesRepaid = totalFeeChargesRepaidAtDisbursement.getAmount();
Money totalFeeChargesRepaidAfterDisbursement = summaryWrapper.calculateTotalFeeChargesRepaid(repaymentScheduleInstallments,
currency);
this.totalFeeChargesRepaid = totalFeeChargesRepaidAfterDisbursement.plus(totalFeeChargesRepaidAtDisbursement).getAmount();
if (charges != null) {
this.totalFeeChargesWaived = summaryWrapper.calculateTotalFeeChargesWaived(charges, currency).getAmount();
@@ -45,7 +45,25 @@ public final class LoanSummaryWrapper {
final MonetaryCurrency currency) {
Money total = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) {
total = total.plus(installment.getCredits(currency));
total = total.plus(installment.getCreditedPrincipal(currency));
}
return total;
}
public Money calculateTotalFeeAdjusted(final List<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;
}
@@ -247,7 +265,8 @@ public final class LoanSummaryWrapper {
return total;
}
for (final LoanCharge loanCharge : charges) {
if (!loanCharge.isPenaltyCharge() && loanCharge.getAmountPaid(currency).isGreaterThanZero()) {
if (!loanCharge.isPenaltyCharge() && loanCharge.getAmountPaid(currency).isGreaterThanZero()
&& loanCharge.isDisbursementCharge()) {
total = total.plus(loanCharge.getAmountPaid(currency));
}
}
@@ -494,7 +494,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
currentInstallment.addToCredits(transactionAmount.getAmount());
currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, transactionAmount);
if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
@@ -526,7 +526,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (!loanTransactionMapped) {
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
LoanRepaymentScheduleInstallment currentInstallment = installments.get(installments.size() - 1);
currentInstallment.addToCredits(transactionAmount.getAmount());
currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, transactionAmount);
if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
@@ -539,7 +539,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
pastDueDate, transactionDate, transactionAmount.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(),
zeroMoney.getAmount(), false, null);
installment.markAsAdditional();
installment.addToCredits(transactionAmount.getAmount());
installment.addToCreditedPrincipal(transactionAmount.getAmount());
loan.addLoanRepaymentScheduleInstallment(installment);
if (repaidAmount.isGreaterThanZero()) {
@@ -25,6 +25,7 @@ import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FE
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT;
import java.math.BigDecimal;
import java.math.MathContext;
@@ -76,7 +77,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransact
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -235,27 +235,31 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (hasNoCustomCreditAllocationRule(loanTransaction)) {
super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
} else {
log.debug("Processing credit transaction with custom credit allocation rules");
loanTransaction.resetDerivedComponents();
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
ctx.getInstallments().sort(byDate);
final Money zeroMoney = Money.zero(ctx.getCurrency());
Money transactionAmount = loanTransaction.getAmount(ctx.getCurrency());
Money amountToDistribute = MathUtil
.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(ctx.getOverpaymentHolder().getMoneyObject()));
Money repaidAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute));
loanTransaction.setOverPayments(repaidAmount);
ctx.getOverpaymentHolder().setMoneyObject(ctx.getOverpaymentHolder().getMoneyObject().minus(repaidAmount));
Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject();
Money amountToDistribute = MathUtil.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(totalOverpaid));
Money overpaymentAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute));
loanTransaction.setOverPayments(overpaymentAmount);
if (amountToDistribute.isGreaterThanZero()) {
if (transactionAmount.isGreaterThanZero()) {
if (loanTransaction.isChargeback()) {
LoanTransaction originalTransaction = findOriginalTransaction(loanTransaction, ctx);
Map<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);
// 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,
amountToDistribute.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency());
transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency());
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
@@ -266,18 +270,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) {
pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
currentInstallment.addToCredits(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
Money originalInterest = currentInstallment.getInterestCharged(ctx.getCurrency());
currentInstallment.updateInterestCharged(
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney));
}
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
loanTransactionMapped = true;
break;
@@ -287,16 +280,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) {
currentInstallment.updateDueDate(transactionDate);
}
currentInstallment.addToCredits(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
Money originalInterest = currentInstallment.getInterestCharged(ctx.getCurrency());
currentInstallment.updateInterestCharged(
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney));
}
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
loanTransactionMapped = true;
break;
}
@@ -307,42 +291,99 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments()
.get(ctx.getInstallments().size() - 1);
currentInstallment.addToCredits(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
Money originalInterest = currentInstallment.getInterestCharged(ctx.getCurrency());
currentInstallment.updateInterestCharged(
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
if (repaidAmount.isGreaterThanZero()) {
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment, repaidAmount, zeroMoney, zeroMoney, zeroMoney));
}
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
} else {
Loan loan = loanTransaction.getLoan();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
(ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(),
zeroMoney.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), false, null);
installment.markAsAdditional();
installment.addToCredits(transactionAmount.getAmount());
installment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
Money originalInterest = installment.getInterestCharged(ctx.getCurrency());
installment.updateInterestCharged(
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, installment, chargebackAllocation);
loan.addLoanRepaymentScheduleInstallment(installment);
if (repaidAmount.isGreaterThanZero()) {
installment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment,
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
}
}
}
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
allocateOverpayment(loanTransaction, ctx.getCurrency(), loanTransaction.getLoan().getRepaymentScheduleInstallments(),
ctx.getOverpaymentHolder());
} else {
throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name());
}
}
}
}
private Map<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
private LoanCreditAllocationRule getChargebackAllocationRules(LoanTransaction loanTransaction) {
LoanCreditAllocationRule chargeBackAllocationRule = loanTransaction.getLoan().getCreditAllocationRules().stream()
@@ -351,23 +392,23 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
@NotNull
private Map<AllocationType, BigDecimal> getOriginalAllocation(LoanTransaction originalLoanTransaction) {
Map<AllocationType, BigDecimal> originalAllocation = new HashMap<>();
originalAllocation.put(PRINCIPAL, originalLoanTransaction.getPrincipalPortion());
originalAllocation.put(INTEREST, originalLoanTransaction.getInterestPortion());
originalAllocation.put(PENALTY, originalLoanTransaction.getPenaltyChargesPortion());
originalAllocation.put(FEE, originalLoanTransaction.getFeeChargesPortion());
private Map<AllocationType, Money> getOriginalAllocation(LoanTransaction originalLoanTransaction, MonetaryCurrency currency) {
Map<AllocationType, Money> originalAllocation = new HashMap<>();
originalAllocation.put(PRINCIPAL, Money.of(currency, originalLoanTransaction.getPrincipalPortion()));
originalAllocation.put(INTEREST, Money.of(currency, originalLoanTransaction.getInterestPortion()));
originalAllocation.put(PENALTY, Money.of(currency, originalLoanTransaction.getPenaltyChargesPortion()));
originalAllocation.put(FEE, Money.of(currency, originalLoanTransaction.getFeeChargesPortion()));
return originalAllocation;
}
protected Map<AllocationType, Money> calculateChargebackAllocationMap(Map<AllocationType, BigDecimal> originalAllocation,
protected Map<AllocationType, Money> calculateChargebackAllocationMap(Map<AllocationType, Money> originalAllocation,
BigDecimal amountToDistribute, List<AllocationType> allocationTypes, MonetaryCurrency currency) {
BigDecimal remainingAmount = amountToDistribute;
Map<AllocationType, Money> result = new HashMap<>();
Arrays.stream(AllocationType.values()).forEach(allocationType -> result.put(allocationType, Money.of(currency, BigDecimal.ZERO)));
for (AllocationType allocationType : allocationTypes) {
if (remainingAmount.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal originalAmount = originalAllocation.get(allocationType);
BigDecimal originalAmount = originalAllocation.get(allocationType).getAmount();
if (originalAmount != null && remainingAmount.compareTo(originalAmount) > 0
&& originalAmount.compareTo(BigDecimal.ZERO) > 0) {
result.put(allocationType, Money.of(currency, originalAmount));
@@ -395,14 +436,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
.filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
.orElse(defaultPaymentAllocationRule);
Balances balances = new Balances(zero, zero, zero, zero);
List<PaymentAllocationType> paymentAllocationTypes;
FutureInstallmentAllocationRule futureInstallmentAllocationRule;
if (PaymentAllocationTransactionType.DEFAULT.equals(paymentAllocationRule.getTransactionType())) {
if (DEFAULT.equals(paymentAllocationRule.getTransactionType())) {
// if the allocation rule is not defined then the reverse order of the default allocation rule will be used
paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes());
Collections.reverse(paymentAllocationTypes);
@@ -560,7 +601,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
.filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
Money transactionAmountUnprocessed = null;
Money zero = Money.zero(currency);
@@ -926,7 +967,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
.filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
.orElse(defaultPaymentAllocationRule);
@@ -39,4 +39,5 @@
<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/1016_add_credit_allocation_rule.xml"/>
<include relativeToChangelogFile="true" file="parts/1017_add_fee_and_penalty_adjustments_to_loan.xml"/>
</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;
@Schema(example = "200000.000000")
public Double interestOverdue;
@Schema(example = "0.00")
public Double feeAdjustments;
@Schema(example = "18000.000000")
public Double feeChargesCharged;
@Schema(example = "0.000000")
@@ -538,6 +540,8 @@ final class LoansApiResourceSwagger {
public Double feeChargesOutstanding;
@Schema(example = "15000.000000")
public Double feeChargesOverdue;
@Schema(example = "0.00")
public Double penaltyAdjustments;
@Schema(example = "0.000000")
public Double penaltyChargesCharged;
@Schema(example = "0.000000")
@@ -51,12 +51,15 @@ public class LoanScheduleAccrualData {
private BigDecimal dueDatePenaltyIncome;
private BigDecimal accruableIncome;
private BigDecimal creditedFee;
private BigDecimal creditedPenalty;
public LoanScheduleAccrualData(final Long loanId, final Long officeId, final Integer installmentNumber, final LocalDate accruedTill,
final PeriodFrequencyType repaymentFrequency, final Integer repayEvery, final LocalDate dueDate, final LocalDate fromDate,
final Long repaymentScheduleId, final Long loanProductId, final BigDecimal interestIncome, final BigDecimal feeIncome,
final BigDecimal penaltyIncome, final BigDecimal accruedInterestIncome, final BigDecimal accruedFeeIncome,
final BigDecimal accruedPenaltyIncome, final CurrencyData currencyData, final LocalDate interestCalculatedFrom,
final BigDecimal waivedInterestIncome) {
final BigDecimal waivedInterestIncome, BigDecimal creditedFee, BigDecimal creditedPenalty) {
this.loanId = loanId;
this.installmentNumber = installmentNumber;
this.officeId = officeId;
@@ -76,6 +79,8 @@ public class LoanScheduleAccrualData {
this.repayEvery = repayEvery;
this.interestCalculatedFrom = interestCalculatedFrom;
this.waivedInterestIncome = waivedInterestIncome;
this.creditedFee = creditedFee;
this.creditedPenalty = creditedPenalty;
}
public Long getLoanId() {
@@ -185,4 +190,12 @@ public class LoanScheduleAccrualData {
this.accruableIncome = accruableIncome;
}
public BigDecimal getCreditedFee() {
return this.creditedFee;
}
public BigDecimal getCreditedPenalty() {
return this.creditedPenalty;
}
}
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
@@ -31,6 +32,7 @@ import org.springframework.util.CollectionUtils;
* Immutable data object representing loan summary information.
*/
@Data
@Builder
@Accessors(chain = true)
public class LoanSummaryData {
@@ -48,6 +50,7 @@ public class LoanSummaryData {
private final BigDecimal interestOutstanding;
private final BigDecimal interestOverdue;
private final BigDecimal feeChargesCharged;
private final BigDecimal feeAdjustments;
private final BigDecimal feeChargesDueAtDisbursementCharged;
private final BigDecimal feeChargesPaid;
private final BigDecimal feeChargesWaived;
@@ -55,6 +58,7 @@ public class LoanSummaryData {
private final BigDecimal feeChargesOutstanding;
private final BigDecimal feeChargesOverdue;
private final BigDecimal penaltyChargesCharged;
private final BigDecimal penaltyAdjustments;
private final BigDecimal penaltyChargesPaid;
private final BigDecimal penaltyChargesWaived;
private final BigDecimal penaltyChargesWrittenOff;
@@ -90,61 +94,6 @@ public class LoanSummaryData {
private final Long chargeOffReasonId;
private final String chargeOffReason;
public LoanSummaryData(final CurrencyData currency, final BigDecimal principalDisbursed, final BigDecimal principalAdjustments,
final BigDecimal principalPaid, final BigDecimal principalWrittenOff, final BigDecimal principalOutstanding,
final BigDecimal principalOverdue, final BigDecimal interestCharged, final BigDecimal interestPaid,
final BigDecimal interestWaived, final BigDecimal interestWrittenOff, final BigDecimal interestOutstanding,
final BigDecimal interestOverdue, final BigDecimal feeChargesCharged, final BigDecimal feeChargesDueAtDisbursementCharged,
final BigDecimal feeChargesPaid, final BigDecimal feeChargesWaived, final BigDecimal feeChargesWrittenOff,
final BigDecimal feeChargesOutstanding, final BigDecimal feeChargesOverdue, final BigDecimal penaltyChargesCharged,
final BigDecimal penaltyChargesPaid, final BigDecimal penaltyChargesWaived, final BigDecimal penaltyChargesWrittenOff,
final BigDecimal penaltyChargesOutstanding, final BigDecimal penaltyChargesOverdue, final BigDecimal totalExpectedRepayment,
final BigDecimal totalRepayment, final BigDecimal totalExpectedCostOfLoan, final BigDecimal totalCostOfLoan,
final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding,
final BigDecimal totalOverdue, final LocalDate overdueSinceDate, final Long writeoffReasonId, final String writeoffReason,
final BigDecimal totalRecovered, final Long chargeOffReasonId, final String chargeOffReason) {
this.currency = currency;
this.principalDisbursed = principalDisbursed;
this.principalAdjustments = principalAdjustments;
this.principalPaid = principalPaid;
this.principalWrittenOff = principalWrittenOff;
this.principalOutstanding = principalOutstanding;
this.principalOverdue = principalOverdue;
this.interestCharged = interestCharged;
this.interestPaid = interestPaid;
this.interestWaived = interestWaived;
this.interestWrittenOff = interestWrittenOff;
this.interestOutstanding = interestOutstanding;
this.interestOverdue = interestOverdue;
this.feeChargesCharged = feeChargesCharged;
this.feeChargesDueAtDisbursementCharged = feeChargesDueAtDisbursementCharged;
this.feeChargesPaid = feeChargesPaid;
this.feeChargesWaived = feeChargesWaived;
this.feeChargesWrittenOff = feeChargesWrittenOff;
this.feeChargesOutstanding = feeChargesOutstanding;
this.feeChargesOverdue = feeChargesOverdue;
this.penaltyChargesCharged = penaltyChargesCharged;
this.penaltyChargesPaid = penaltyChargesPaid;
this.penaltyChargesWaived = penaltyChargesWaived;
this.penaltyChargesWrittenOff = penaltyChargesWrittenOff;
this.penaltyChargesOutstanding = penaltyChargesOutstanding;
this.penaltyChargesOverdue = penaltyChargesOverdue;
this.totalExpectedRepayment = totalExpectedRepayment;
this.totalRepayment = totalRepayment;
this.totalExpectedCostOfLoan = totalExpectedCostOfLoan;
this.totalCostOfLoan = totalCostOfLoan;
this.totalWaived = totalWaived;
this.totalWrittenOff = totalWrittenOff;
this.totalOutstanding = totalOutstanding;
this.totalOverdue = totalOverdue;
this.overdueSinceDate = overdueSinceDate;
this.writeoffReasonId = writeoffReasonId;
this.writeoffReason = writeoffReason;
this.totalRecovered = totalRecovered;
this.chargeOffReasonId = chargeOffReasonId;
this.chargeOffReason = chargeOffReason;
}
public static LoanSummaryData withTransactionAmountsSummary(final LoanSummaryData defaultSummaryData,
final Collection<LoanTransactionData> loanTransactions) {
@@ -184,34 +133,41 @@ public class LoanSummaryData {
totalRepaymentTransactionReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.REPAYMENT, loanTransactions);
}
return new LoanSummaryData(defaultSummaryData.currency, defaultSummaryData.principalDisbursed,
defaultSummaryData.principalAdjustments, defaultSummaryData.principalPaid, defaultSummaryData.principalWrittenOff,
defaultSummaryData.principalOutstanding, defaultSummaryData.principalOverdue, defaultSummaryData.interestCharged,
defaultSummaryData.interestPaid, defaultSummaryData.interestWaived, defaultSummaryData.interestWrittenOff,
defaultSummaryData.interestOutstanding, defaultSummaryData.interestOverdue, defaultSummaryData.feeChargesCharged,
defaultSummaryData.feeChargesDueAtDisbursementCharged, defaultSummaryData.feeChargesPaid,
defaultSummaryData.feeChargesWaived, defaultSummaryData.feeChargesWrittenOff, defaultSummaryData.feeChargesOutstanding,
defaultSummaryData.feeChargesOverdue, defaultSummaryData.penaltyChargesCharged, defaultSummaryData.penaltyChargesPaid,
defaultSummaryData.penaltyChargesWaived, defaultSummaryData.penaltyChargesWrittenOff,
defaultSummaryData.penaltyChargesOutstanding, defaultSummaryData.penaltyChargesOverdue,
defaultSummaryData.totalExpectedRepayment, defaultSummaryData.totalRepayment, defaultSummaryData.totalExpectedCostOfLoan,
defaultSummaryData.totalCostOfLoan, defaultSummaryData.totalWaived, defaultSummaryData.totalWrittenOff,
defaultSummaryData.totalOutstanding, defaultSummaryData.totalOverdue, defaultSummaryData.overdueSinceDate,
defaultSummaryData.writeoffReasonId, defaultSummaryData.writeoffReason, defaultSummaryData.totalRecovered,
defaultSummaryData.chargeOffReasonId, defaultSummaryData.chargeOffReason).setTotalMerchantRefund(totalMerchantRefund)
.setTotalMerchantRefundReversed(totalMerchantRefundReversed).setTotalPayoutRefund(totalPayoutRefund)
.setTotalPayoutRefundReversed(totalPayoutRefundReversed).setTotalGoodwillCredit(totalGoodwillCredit)
.setTotalGoodwillCreditReversed(totalGoodwillCreditReversed).setTotalChargeAdjustment(totalChargeAdjustment)
.setTotalChargeAdjustmentReversed(totalChargeAdjustmentReversed).setTotalChargeback(totalChargeback)
.setTotalCreditBalanceRefund(totalCreditBalanceRefund).setTotalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed)
.setTotalRepaymentTransaction(totalRepaymentTransaction)
.setTotalRepaymentTransactionReversed(totalRepaymentTransactionReversed);
return LoanSummaryData.builder().currency(defaultSummaryData.currency).principalDisbursed(defaultSummaryData.principalDisbursed)
.principalAdjustments(defaultSummaryData.principalAdjustments).principalPaid(defaultSummaryData.principalPaid)
.principalWrittenOff(defaultSummaryData.principalWrittenOff).principalOutstanding(defaultSummaryData.principalOutstanding)
.principalOverdue(defaultSummaryData.principalOverdue).interestCharged(defaultSummaryData.interestCharged)
.interestPaid(defaultSummaryData.interestPaid).interestWaived(defaultSummaryData.interestWaived)
.interestWrittenOff(defaultSummaryData.interestWrittenOff).interestOutstanding(defaultSummaryData.interestOutstanding)
.interestOverdue(defaultSummaryData.interestOverdue).feeChargesCharged(defaultSummaryData.feeChargesCharged)
.feeAdjustments(defaultSummaryData.feeAdjustments)
.feeChargesDueAtDisbursementCharged(defaultSummaryData.feeChargesDueAtDisbursementCharged)
.feeChargesPaid(defaultSummaryData.feeChargesPaid).feeChargesWaived(defaultSummaryData.feeChargesWaived)
.feeChargesWrittenOff(defaultSummaryData.feeChargesWrittenOff)
.feeChargesOutstanding(defaultSummaryData.feeChargesOutstanding).feeChargesOverdue(defaultSummaryData.feeChargesOverdue)
.penaltyChargesCharged(defaultSummaryData.penaltyChargesCharged).penaltyAdjustments(defaultSummaryData.penaltyAdjustments)
.penaltyChargesPaid(defaultSummaryData.penaltyChargesPaid).penaltyChargesWaived(defaultSummaryData.penaltyChargesWaived)
.penaltyChargesWrittenOff(defaultSummaryData.penaltyChargesWrittenOff)
.penaltyChargesOutstanding(defaultSummaryData.penaltyChargesOutstanding)
.penaltyChargesOverdue(defaultSummaryData.penaltyChargesOverdue)
.totalExpectedRepayment(defaultSummaryData.totalExpectedRepayment).totalRepayment(defaultSummaryData.totalRepayment)
.totalExpectedCostOfLoan(defaultSummaryData.totalExpectedCostOfLoan).totalCostOfLoan(defaultSummaryData.totalCostOfLoan)
.totalWaived(defaultSummaryData.totalWaived).totalWrittenOff(defaultSummaryData.totalWrittenOff)
.totalOutstanding(defaultSummaryData.totalOutstanding).totalOverdue(defaultSummaryData.totalOverdue)
.overdueSinceDate(defaultSummaryData.overdueSinceDate).writeoffReasonId(defaultSummaryData.writeoffReasonId)
.writeoffReason(defaultSummaryData.writeoffReason).totalRecovered(defaultSummaryData.totalRecovered)
.chargeOffReasonId(defaultSummaryData.chargeOffReasonId).chargeOffReason(defaultSummaryData.chargeOffReason)
.totalMerchantRefund(totalMerchantRefund).totalMerchantRefundReversed(totalMerchantRefundReversed)
.totalPayoutRefund(totalPayoutRefund).totalPayoutRefundReversed(totalPayoutRefundReversed)
.totalGoodwillCredit(totalGoodwillCredit).totalGoodwillCreditReversed(totalGoodwillCreditReversed)
.totalChargeAdjustment(totalChargeAdjustment).totalChargeAdjustmentReversed(totalChargeAdjustmentReversed)
.totalChargeback(totalChargeback).totalCreditBalanceRefund(totalCreditBalanceRefund)
.totalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed).totalRepaymentTransaction(totalRepaymentTransaction)
.totalRepaymentTransactionReversed(totalRepaymentTransactionReversed).build();
}
public static LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData) {
return new LoanSummaryData(currencyData, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null);
return LoanSummaryData.builder().currency(currencyData).build();
}
private static BigDecimal computeTotalAmountForReversedTransactions(LoanTransactionType transactionType,
@@ -695,7 +695,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(),
installment.getInterestAccrued(currency).getAmount(), installment.getFeeAccrued(currency).getAmount(),
installment.getPenaltyAccrued(currency).getAmount(), currencyData, interestCalculatedFrom,
installment.getInterestWaived(currency).getAmount());
installment.getInterestWaived(currency).getAmount(), installment.getCreditedFee(currency).getAmount(),
installment.getCreditedPenalty(currency).getAmount());
loanScheduleAccrualDatas.add(accrualData);
}
@@ -940,10 +941,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
.minus(loanRepaymentScheduleInstallment.getInterestWaived(currency));
feePortion = feePortion.add(loanRepaymentScheduleInstallment.getFeeChargesCharged(currency))
.minus(loanRepaymentScheduleInstallment.getFeeAccrued(currency))
.minus(loanRepaymentScheduleInstallment.getFeeChargesWaived(currency));
.minus(loanRepaymentScheduleInstallment.getFeeChargesWaived(currency))
.minus(loanRepaymentScheduleInstallment.getCreditedFee(currency));
penaltyPortion = penaltyPortion.add(loanRepaymentScheduleInstallment.getPenaltyChargesCharged(currency))
.minus(loanRepaymentScheduleInstallment.getPenaltyAccrued(currency))
.minus(loanRepaymentScheduleInstallment.getPenaltyChargesWaived(currency));
.minus(loanRepaymentScheduleInstallment.getPenaltyChargesWaived(currency))
.minus(loanRepaymentScheduleInstallment.getCreditedPenalty(currency));
}
Money total = interestPortion.plus(feePortion).plus(penaltyPortion);
@@ -168,7 +168,9 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome();
BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome();
BigDecimal totalCreditedPenalty = accrualData.getCreditedPenalty();
BigDecimal totalAccFee = accrualData.getAccruedFeeIncome();
BigDecimal totalCreditedFee = accrualData.getCreditedFee();
if (totalAccInterest == null) {
totalAccInterest = BigDecimal.ZERO;
@@ -183,7 +185,10 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
if (totalAccFee == null) {
totalAccFee = BigDecimal.ZERO;
}
feePortion = feePortion.subtract(totalAccFee);
if (totalCreditedFee == null) {
totalCreditedFee = BigDecimal.ZERO;
}
feePortion = feePortion.subtract(totalAccFee).subtract(totalCreditedFee);
amount = amount.add(feePortion);
totalAccFee = totalAccFee.add(feePortion);
if (feePortion.compareTo(BigDecimal.ZERO) == 0) {
@@ -195,7 +200,10 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
if (totalAccPenalty == null) {
totalAccPenalty = BigDecimal.ZERO;
}
penaltyPortion = penaltyPortion.subtract(totalAccPenalty);
if (totalCreditedPenalty == null) {
totalCreditedPenalty = BigDecimal.ZERO;
}
penaltyPortion = penaltyPortion.subtract(totalAccPenalty).subtract(totalCreditedPenalty);
amount = amount.add(penaltyPortion);
totalAccPenalty = totalAccPenalty.add(penaltyPortion);
if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) {
@@ -234,6 +242,9 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
if (scheduleAccrualData.getAccruedFeeIncome() != null) {
feePortion = feePortion.subtract(scheduleAccrualData.getAccruedFeeIncome());
}
if (scheduleAccrualData.getCreditedFee() != null) {
feePortion = feePortion.subtract(scheduleAccrualData.getCreditedFee());
}
amount = amount.add(feePortion);
if (feePortion.compareTo(BigDecimal.ZERO) == 0) {
feePortion = null;
@@ -248,6 +259,9 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
if (scheduleAccrualData.getAccruedPenaltyIncome() != null) {
penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getAccruedPenaltyIncome());
}
if (scheduleAccrualData.getCreditedPenalty() != null) {
penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getCreditedPenalty());
}
amount = amount.add(penaltyPortion);
if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) {
penaltyPortion = null;
@@ -649,6 +649,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " l.loan_officer_id as loanOfficerId, s.display_name as loanOfficerName, "
+ " l.principal_disbursed_derived as principalDisbursed, l.principal_repaid_derived as principalPaid,"
+ " l.principal_adjustments_derived as principalAdjustments, l.principal_writtenoff_derived as principalWrittenOff,"
+ " l.fee_adjustments_derived as feeAdjustments, l.penalty_adjustments_derived as penaltyAdjustments,"
+ " l.principal_outstanding_derived as principalOutstanding, l.interest_charged_derived as interestCharged,"
+ " l.interest_repaid_derived as interestPaid, l.interest_waived_derived as interestWaived,"
+ " l.interest_writtenoff_derived as interestWrittenOff, l.interest_outstanding_derived as interestOutstanding,"
@@ -912,6 +913,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final BigDecimal interestOverdue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestOverdue");
final BigDecimal feeChargesCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesCharged");
final BigDecimal feeAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeAdjustments");
final BigDecimal feeChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesPaid");
final BigDecimal feeChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesWaived");
final BigDecimal feeChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesWrittenOff");
@@ -919,6 +921,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final BigDecimal feeChargesOverdue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesOverdue");
final BigDecimal penaltyChargesCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesCharged");
final BigDecimal penaltyAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyAdjustments");
final BigDecimal penaltyChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesPaid");
final BigDecimal penaltyChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWaived");
final BigDecimal penaltyChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWrittenOff");
@@ -938,14 +941,24 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final LocalDate overdueSinceDate = JdbcSupport.getLocalDate(rs, "overdueSinceDate");
inArrears = (overdueSinceDate != null);
loanSummary = new LoanSummaryData(currencyData, principalDisbursed, principalAdjustments, principalPaid,
principalWrittenOff, principalOutstanding, principalOverdue, interestCharged, interestPaid, interestWaived,
interestWrittenOff, interestOutstanding, interestOverdue, feeChargesCharged, feeChargesDueAtDisbursementCharged,
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, feeChargesOverdue,
penaltyChargesCharged, penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff,
penaltyChargesOutstanding, penaltyChargesOverdue, totalExpectedRepayment, totalRepayment, totalExpectedCostOfLoan,
totalCostOfLoan, totalWaived, totalWrittenOff, totalOutstanding, totalOverdue, overdueSinceDate, writeoffReasonId,
writeoffReason, totalRecovered, chargeOffReasonId, chargeOffReason);
loanSummary = LoanSummaryData.builder().currency(currencyData).principalDisbursed(principalDisbursed)
.principalAdjustments(principalAdjustments).principalPaid(principalPaid).principalWrittenOff(principalWrittenOff)
.principalOutstanding(principalOutstanding).principalOverdue(principalOverdue).interestCharged(interestCharged)
.interestPaid(interestPaid).interestWaived(interestWaived).interestWrittenOff(interestWrittenOff)
.interestOutstanding(interestOutstanding).interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged)
.feeAdjustments(feeAdjustments).feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged)
.feeChargesPaid(feeChargesPaid).feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff)
.feeChargesOutstanding(feeChargesOutstanding).feeChargesOverdue(feeChargesOverdue)
.penaltyChargesCharged(penaltyChargesCharged).penaltyAdjustments(penaltyAdjustments)
.penaltyChargesPaid(penaltyChargesPaid).penaltyChargesWaived(penaltyChargesWaived)
.penaltyChargesWrittenOff(penaltyChargesWrittenOff).penaltyChargesOutstanding(penaltyChargesOutstanding)
.penaltyChargesOverdue(penaltyChargesOverdue).totalExpectedRepayment(totalExpectedRepayment)
.totalRepayment(totalRepayment).totalExpectedCostOfLoan(totalExpectedCostOfLoan).totalCostOfLoan(totalCostOfLoan)
.totalWaived(totalWaived).totalWrittenOff(totalWrittenOff).totalOutstanding(totalOutstanding)
.totalOverdue(totalOverdue).overdueSinceDate(overdueSinceDate).writeoffReasonId(writeoffReasonId)
.writeoffReason(writeoffReason).totalRecovered(totalRecovered).chargeOffReasonId(chargeOffReasonId)
.chargeOffReason(chargeOffReason).build();
}
GroupGeneralData groupData = null;
@@ -1144,7 +1157,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " ls.fee_charges_amount as feeChargesDue, ls.fee_charges_completed_derived as feeChargesPaid, ls.fee_charges_waived_derived as feeChargesWaived, ls.fee_charges_writtenoff_derived as feeChargesWrittenOff, "
+ " ls.penalty_charges_amount as penaltyChargesDue, ls.penalty_charges_completed_derived as penaltyChargesPaid, ls.penalty_charges_waived_derived as penaltyChargesWaived, "
+ " ls.penalty_charges_writtenoff_derived as penaltyChargesWrittenOff, ls.total_paid_in_advance_derived as totalPaidInAdvanceForPeriod, "
+ " ls.total_paid_late_derived as totalPaidLateForPeriod, ls.credits_amount as totalCredits, ls.is_down_payment isDownPayment "
+ " ls.total_paid_late_derived as totalPaidLateForPeriod, (coalesce(ls.credited_principal,0) + coalesce(ls.credited_fee,0) + coalesce(ls.credited_penalty,0)) as totalCredits, ls.is_down_payment isDownPayment "
+ " from m_loan_repayment_schedule ls ";
}
@@ -1912,6 +1925,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
.append("ls.duedate as duedate,ls.fromdate as fromdate ,ls.id as scheduleId,loan.product_id as productId,")
.append("ls.interest_amount as interest, ls.interest_waived_derived as interestWaived,")
.append("ls.penalty_charges_amount as penalty, ").append("ls.fee_charges_amount as charges, ")
.append("ls.credited_penalty as credited_penalty, ").append("ls.credited_fee as credited_fee, ")
.append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,")
.append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,")
.append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode")
@@ -1946,6 +1960,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest");
final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege");
final BigDecimal accruedPenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accpenalty");
final BigDecimal creditedFee = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_fee");
final BigDecimal creditedPenalty = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_penalty");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
@@ -1958,7 +1974,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromDate,
repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome,
accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived);
accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived, creditedFee, creditedPenalty);
}
}
@@ -1972,6 +1988,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
.append("ls.installment as installmentNumber, ")
.append("ls.interest_amount as interest, ls.interest_waived_derived as interestWaived,")
.append("ls.penalty_charges_amount as penalty, ").append("ls.fee_charges_amount as charges, ")
.append("ls.credited_penalty as credited_penalty, ").append("ls.credited_fee as credited_fee, ")
.append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,")
.append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,")
.append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode")
@@ -1996,6 +2013,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final BigDecimal interestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interest");
final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges");
final BigDecimal penaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalty");
final BigDecimal creditedFee = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_fee");
final BigDecimal creditedPenalty = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_penalty");
final BigDecimal interestIncomeWaived = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interestWaived");
final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest");
final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege");
@@ -2015,7 +2035,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final LocalDate interestCalculatedFrom = null;
return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromdate,
repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome,
accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived);
accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived, creditedFee, creditedPenalty);
}
}
@@ -24,11 +24,15 @@ import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.IN
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -83,7 +87,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
private final LocalDate transactionDate = LocalDate.of(2023, 7, 11);
private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1);
private static final MockedStatic<MoneyHelper> MONEY_HELPER = Mockito.mockStatic(MoneyHelper.class);
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
private AdvancedPaymentScheduleTransactionProcessor underTest;
@BeforeAll
@@ -123,34 +127,33 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Money zero = Money.zero(currency);
Loan loan = mock(Loan.class);
Money chargeAmountMoney = Money.of(currency, chargeAmount);
LoanRepaymentScheduleInstallment installment = Mockito
.spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L),
LoanRepaymentScheduleInstallment installment = spy(
new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L),
BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
Mockito.when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
Mockito.when(chargePaidBy.getLoanCharge()).thenReturn(charge);
Mockito.when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
Mockito.when(loanTransaction.getAmount()).thenReturn(chargeAmount);
Mockito.when(loanTransaction.getAmount(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getLoan()).thenReturn(loan);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate()))
.thenReturn(true);
Mockito.when(installment.getInstallmentNumber()).thenReturn(1);
Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
when(chargePaidBy.getLoanCharge()).thenReturn(charge);
when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
when(loanTransaction.getAmount()).thenReturn(chargeAmount);
when(loanTransaction.getAmount(currency)).thenReturn(chargeAmountMoney);
when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
when(loanTransaction.getLoan()).thenReturn(loan);
when(loan.getDisbursementDate()).thenReturn(disbursementDate);
when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true);
when(installment.getInstallmentNumber()).thenReturn(1);
when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
underTest.processLatestTransaction(loanTransaction,
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(chargeAmountMoney).getAmount());
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
Mockito.verify(loan, Mockito.times(0)).getPaymentAllocationRules();
Mockito.verify(loan, times(0)).getPaymentAllocationRules();
}
@Test
@@ -168,35 +171,33 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Money chargeAmountMoney = Money.of(currency, chargeAmount);
BigDecimal transactionAmount = BigDecimal.valueOf(20.00);
Money transactionAmountMoney = Money.of(currency, transactionAmount);
LoanRepaymentScheduleInstallment installment = Mockito
.spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L),
LoanRepaymentScheduleInstallment installment = spy(
new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L),
BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
Mockito.when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
Mockito.when(chargePaidBy.getLoanCharge()).thenReturn(charge);
Mockito.when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
Mockito.when(loanTransaction.getAmount()).thenReturn(transactionAmount);
Mockito.when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney);
Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getLoan()).thenReturn(loan);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate()))
.thenReturn(true);
Mockito.when(installment.getInstallmentNumber()).thenReturn(1);
Mockito.when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
when(chargePaidBy.getLoanCharge()).thenReturn(charge);
when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
when(loanTransaction.getAmount()).thenReturn(transactionAmount);
when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney);
when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
when(loanTransaction.getLoan()).thenReturn(loan);
when(loan.getDisbursementDate()).thenReturn(disbursementDate);
when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true);
when(installment.getInstallmentNumber()).thenReturn(1);
when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
underTest.processLatestTransaction(loanTransaction,
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney),
refEq(zero));
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), refEq(zero));
assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(transactionAmountMoney).getAmount());
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
assertEquals(0, BigDecimal.valueOf(80.00).compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
Mockito.verify(loan, Mockito.times(0)).getPaymentAllocationRules();
Mockito.verify(loan, times(0)).getPaymentAllocationRules();
}
@Test
@@ -216,94 +217,50 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
BigDecimal transactionAmount = BigDecimal.valueOf(120.00);
Money transactionAmountMoney = Money.of(currency, transactionAmount);
LoanPaymentAllocationRule loanPaymentAllocationRule = mock(LoanPaymentAllocationRule.class);
LoanRepaymentScheduleInstallment installment = Mockito
.spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, transactionDate, BigDecimal.valueOf(100L),
BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
LoanRepaymentScheduleInstallment installment = spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, transactionDate,
BigDecimal.valueOf(100L), BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
Mockito.when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
Mockito.when(chargePaidBy.getLoanCharge()).thenReturn(charge);
Mockito.when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
Mockito.when(loanTransaction.getAmount()).thenReturn(transactionAmount);
Mockito.when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney);
Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getLoan()).thenReturn(loan);
Mockito.when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
Mockito.when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate()))
.thenReturn(true);
Mockito.when(installment.getInstallmentNumber()).thenReturn(1);
Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
Mockito.when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule));
Mockito.when(loanPaymentAllocationRule.getTransactionType()).thenReturn(PaymentAllocationTransactionType.DEFAULT);
Mockito.when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
Mockito.when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true);
when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
when(chargePaidBy.getLoanCharge()).thenReturn(charge);
when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
when(loanTransaction.getAmount()).thenReturn(transactionAmount);
when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney);
when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
when(loanTransaction.getLoan()).thenReturn(loan);
when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
when(loan.getDisbursementDate()).thenReturn(disbursementDate);
when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true);
when(installment.getInstallmentNumber()).thenReturn(1);
when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule));
when(loanPaymentAllocationRule.getTransactionType()).thenReturn(PaymentAllocationTransactionType.DEFAULT);
when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true);
underTest.processLatestTransaction(loanTransaction,
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
assertEquals(0, BigDecimal.valueOf(20).compareTo(loanTransaction.getAmount(currency).minus(chargeAmountMoney).getAmount()));
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
assertEquals(0, BigDecimal.valueOf(80).compareTo(installment.getPrincipalOutstanding(currency).getAmount()));
Mockito.verify(loan, Mockito.times(1)).getPaymentAllocationRules();
Mockito.verify(loan, times(1)).getPaymentAllocationRules();
}
@Test
public void testProcessCreditTransactionWithAllocationRuleInterestAndPrincipal() {
public void testProcessCreditTransactionWithAllocationRulePrincipalPenaltyFeeInterest() {
// given
Loan loan = mock(Loan.class);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(INTEREST, PRINCIPAL, PENALTY, FEE);
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction);
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
List<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);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
@@ -315,15 +272,31 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
underTest.processCreditTransaction(chargeBackTransaction, ctx);
// then
Mockito.verify(installment, Mockito.times(1)).addToCredits(new BigDecimal("25.00"));
Mockito.verify(installment, Mockito.times(1)).updateInterestCharged(new BigDecimal("15.00"));
// verify principal
Mockito.verify(installment, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
ArgumentCaptor<LocalDate> localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.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());
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> interest = 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(),
penalty.capture());
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.valueOf(15.0)));
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO));
assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.ZERO));
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0)));
assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.ZERO));
}
@Test
public void testProcessCreditTransactionWithAllocationRulePenaltyFeePrincipalInterest() {
// given
Loan loan = mock(Loan.class);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PENALTY, FEE, PRINCIPAL, INTEREST);
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
List<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
public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment() {
// given
Loan loan = mock(Loan.class);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE);
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
LoanRepaymentScheduleInstallment installment1 = createMockInstallment(LocalDate.of(2022, 12, 20), false);
LoanRepaymentScheduleInstallment installment2 = createMockInstallment(LocalDate.of(2022, 12, 27), true);
installments.add(installment1);
@@ -358,15 +381,31 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
underTest.processCreditTransaction(chargeBackTransaction, ctx);
// then
Mockito.verify(installment2, Mockito.times(1)).addToCredits(new BigDecimal("25.00"));
Mockito.verify(installment2, Mockito.times(1)).updateInterestCharged(new BigDecimal("15.00"));
// verify principal
Mockito.verify(installment2, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
ArgumentCaptor<LocalDate> localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.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());
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> interest = 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(),
penalty.capture());
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.valueOf(15.0)));
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO));
assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.ZERO));
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0)));
assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.ZERO));
}
private LoanRepaymentScheduleInstallment createMockInstallment(LocalDate localDate, boolean isAdditional) {
@@ -396,15 +435,16 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
return mockCreditAllocationRule;
}
private LoanTransaction createRepayment(Loan loan, LoanTransaction toTransaction) {
private LoanTransaction createRepayment(Loan loan, LoanTransaction toTransaction, double principalPortion, double interestPortion,
double feePortion, double penaltyPortion) {
LoanTransaction repayment = mock(LoanTransaction.class);
lenient().when(repayment.getLoan()).thenReturn(loan);
lenient().when(repayment.isRepayment()).thenReturn(true);
lenient().when(repayment.getTypeOf()).thenReturn(REPAYMENT);
lenient().when(repayment.getPrincipalPortion()).thenReturn(BigDecimal.valueOf(10));
lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(20));
lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.ZERO);
lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.ZERO);
lenient().when(repayment.getPrincipalPortion()).thenReturn(BigDecimal.valueOf(principalPortion));
lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(interestPortion));
lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.valueOf(feePortion));
lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.valueOf(penaltyPortion));
LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
lenient().when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
@@ -414,14 +454,14 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
return repayment;
}
private LoanTransaction createChargebackTransaction(Loan loan) {
private LoanTransaction createChargebackTransaction(Loan loan, double transactionAmount) {
LoanTransaction chargeback = mock(LoanTransaction.class);
lenient().when(chargeback.isChargeback()).thenReturn(true);
lenient().when(chargeback.getTypeOf()).thenReturn(LoanTransactionType.CHARGEBACK);
lenient().when(chargeback.getLoan()).thenReturn(loan);
lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(25));
Money amount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(25));
lenient().when(chargeback.getAmount(MONETARY_CURRENCY)).thenReturn(amount);
lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(transactionAmount));
Money money = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(transactionAmount));
lenient().when(chargeback.getAmount(MONETARY_CURRENCY)).thenReturn(money);
lenient().when(chargeback.getTransactionDate()).thenReturn(LocalDate.of(2023, 1, 1));
return chargeback;
}
@@ -431,40 +471,41 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Map<AllocationType, Money> result;
MonetaryCurrency currency = mock(MonetaryCurrency.class);
result = underTest.calculateChargebackAllocationMap(allocationMap(50.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(50.0),
result = underTest.calculateChargebackAllocationMap(allocationMap(50.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(50.0),
List.of(PRINCIPAL, INTEREST, FEE, PENALTY), currency);
verify(allocationMap(50.0, 0, 0, 0), result);
verify(allocationMap(50.0, 0, 0, 0, currency), result);
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(50.0),
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(50.0),
List.of(PRINCIPAL, INTEREST, FEE, PENALTY), currency);
verify(allocationMap(40.0, 10, 0, 0), result);
verify(allocationMap(40.0, 10, 0, 0, currency), result);
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(50.0),
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(50.0),
List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency);
verify(allocationMap(40.0, 0, 10, 0), result);
verify(allocationMap(40.0, 0, 10, 0, currency), result);
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(340.0),
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(340.0),
List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency);
verify(allocationMap(40.0, 88.0, 200.0, 12.0), result);
verify(allocationMap(40.0, 88.0, 200.0, 12.0, currency), result);
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0), BigDecimal.valueOf(352.0),
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(352.0),
List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency);
verify(allocationMap(40.0, 100.0, 200.0, 12.0), result);
verify(allocationMap(40.0, 100.0, 200.0, 12.0, currency), result);
}
private void verify(Map<AllocationType, BigDecimal> expected, Map<AllocationType, Money> actual) {
private void verify(Map<AllocationType, Money> expected, Map<AllocationType, Money> actual) {
Assertions.assertEquals(expected.size(), actual.size());
expected.forEach((k, v) -> {
Assertions.assertEquals(0, v.compareTo(actual.get(k).getAmount()), "Not matching for " + k);
Assertions.assertEquals(0, v.getAmount().compareTo(actual.get(k).getAmount()), "Not matching for " + k);
});
}
private Map<AllocationType, BigDecimal> allocationMap(double principal, double interest, double fee, double penalty) {
Map<AllocationType, BigDecimal> allocationMap = new HashMap<>();
allocationMap.put(AllocationType.PRINCIPAL, BigDecimal.valueOf(principal));
allocationMap.put(AllocationType.INTEREST, BigDecimal.valueOf(interest));
allocationMap.put(AllocationType.FEE, BigDecimal.valueOf(fee));
allocationMap.put(AllocationType.PENALTY, BigDecimal.valueOf(penalty));
private Map<AllocationType, Money> allocationMap(double principal, double interest, double fee, double penalty,
MonetaryCurrency currency) {
Map<AllocationType, Money> allocationMap = new HashMap<>();
allocationMap.put(AllocationType.PRINCIPAL, Money.of(currency, BigDecimal.valueOf(principal)));
allocationMap.put(AllocationType.INTEREST, Money.of(currency, BigDecimal.valueOf(interest)));
allocationMap.put(AllocationType.FEE, Money.of(currency, BigDecimal.valueOf(fee)));
allocationMap.put(AllocationType.PENALTY, Money.of(currency, BigDecimal.valueOf(penalty)));
return allocationMap;
}
@@ -472,17 +513,17 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionWhenIdProvided() {
// given
LoanTransaction chargebackTransaction = mock(LoanTransaction.class);
Mockito.when(chargebackTransaction.getId()).thenReturn(123L);
when(chargebackTransaction.getId()).thenReturn(123L);
Loan loan = mock(Loan.class);
Mockito.when(chargebackTransaction.getLoan()).thenReturn(loan);
when(chargebackTransaction.getLoan()).thenReturn(loan);
LoanTransaction repayment1 = mock(LoanTransaction.class);
LoanTransaction repayment2 = mock(LoanTransaction.class);
Mockito.when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2));
when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2));
LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
Mockito.when(relation.getToTransaction()).thenReturn(chargebackTransaction);
Mockito.when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
when(relation.getToTransaction()).thenReturn(chargebackTransaction);
when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
TransactionCtx ctx = mock(TransactionCtx.class);
// when
@@ -496,14 +537,14 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testFindOriginalTransactionThrowsRuntimeExceptionWhenIdProvidedAndRelationsAreMissing() {
// given
LoanTransaction chargebackTransaction = mock(LoanTransaction.class);
Mockito.when(chargebackTransaction.getId()).thenReturn(123L);
when(chargebackTransaction.getId()).thenReturn(123L);
Loan loan = mock(Loan.class);
Mockito.when(chargebackTransaction.getLoan()).thenReturn(loan);
when(chargebackTransaction.getLoan()).thenReturn(loan);
LoanTransaction repayment1 = mock(LoanTransaction.class);
LoanTransaction repayment2 = mock(LoanTransaction.class);
Mockito.when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2));
when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2));
Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of());
when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of());
TransactionCtx ctx = mock(TransactionCtx.class);
@@ -517,22 +558,22 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvided() {
// given
LoanTransaction chargebackReplayed = mock(LoanTransaction.class);
Mockito.when(chargebackReplayed.getId()).thenReturn(null);
when(chargebackReplayed.getId()).thenReturn(null);
LoanTransaction repayment1 = mock(LoanTransaction.class);
LoanTransaction repayment2 = mock(LoanTransaction.class);
LoanTransaction originalChargeback = mock(LoanTransaction.class);
Mockito.when(originalChargeback.getId()).thenReturn(123L);
when(originalChargeback.getId()).thenReturn(123L);
LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
Mockito.when(relation.getToTransaction()).thenReturn(originalChargeback);
Mockito.when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
when(relation.getToTransaction()).thenReturn(originalChargeback);
when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
TransactionCtx ctx = mock(TransactionCtx.class);
ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class);
Mockito.when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail);
Mockito.when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L));
Mockito.when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2));
when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail);
when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L));
when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2));
// when
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
@@ -545,25 +586,25 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvidedFallbackToPersistedTransactions() {
// given
LoanTransaction chargebackReplayed = mock(LoanTransaction.class);
Mockito.when(chargebackReplayed.getId()).thenReturn(null);
when(chargebackReplayed.getId()).thenReturn(null);
LoanTransaction repayment1 = mock(LoanTransaction.class);
LoanTransaction repayment2 = mock(LoanTransaction.class);
Loan loan = mock(Loan.class);
Mockito.when(chargebackReplayed.getLoan()).thenReturn(loan);
Mockito.when(loan.getLoanTransactions()).thenReturn(List.of(repayment1, repayment2));
when(chargebackReplayed.getLoan()).thenReturn(loan);
when(loan.getLoanTransactions()).thenReturn(List.of(repayment1, repayment2));
LoanTransaction originalChargeback = mock(LoanTransaction.class);
Mockito.when(originalChargeback.getId()).thenReturn(123L);
when(originalChargeback.getId()).thenReturn(123L);
LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
Mockito.when(relation.getToTransaction()).thenReturn(originalChargeback);
Mockito.when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
when(relation.getToTransaction()).thenReturn(originalChargeback);
when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
TransactionCtx ctx = mock(TransactionCtx.class);
ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class);
Mockito.when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail);
Mockito.when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L));
Mockito.when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of());
when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail);
when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L));
when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of());
// when
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
@@ -476,6 +476,46 @@ public abstract class BaseLoanIntegrationTest {
totalOutstanding));
}
Double outstandingPrincipalExpected = installments[i].outstandingAmounts != null
? installments[i].outstandingAmounts.principalOutstanding
: null;
Double outstandingPrincipal = period.getPrincipalOutstanding();
if (outstandingPrincipalExpected != null) {
Assertions.assertEquals(outstandingPrincipalExpected, outstandingPrincipal,
"%d. installment's outstanding principal is different, expected: %.2f, actual: %.2f".formatted(i,
outstandingPrincipalExpected, outstandingPrincipal));
}
Double outstandingFeeExpected = installments[i].outstandingAmounts != null
? installments[i].outstandingAmounts.feeOutstanding
: null;
Double outstandingFee = period.getFeeChargesOutstanding();
if (outstandingFeeExpected != null) {
Assertions.assertEquals(outstandingFeeExpected, outstandingFee,
"%d. installment's outstanding fee is different, expected: %.2f, actual: %.2f".formatted(i,
outstandingFeeExpected, outstandingFee));
}
Double outstandingPenaltyExpected = installments[i].outstandingAmounts != null
? installments[i].outstandingAmounts.penaltyOutstanding
: null;
Double outstandingPenalty = period.getPenaltyChargesOutstanding();
if (outstandingPenaltyExpected != null) {
Assertions.assertEquals(outstandingPenaltyExpected, outstandingPenalty,
"%d. installment's outstanding penalty is different, expected: %.2f, actual: %.2f".formatted(i,
outstandingPenaltyExpected, outstandingPenalty));
}
Double outstandingTotalExpected = installments[i].outstandingAmounts != null
? installments[i].outstandingAmounts.totalOutstanding
: null;
Double outstandingTotal = period.getTotalOutstandingForPeriod();
if (outstandingTotalExpected != null) {
Assertions.assertEquals(outstandingTotalExpected, outstandingTotal,
"%d. installment's total outstanding is different, expected: %.2f, actual: %.2f".formatted(i,
outstandingTotalExpected, outstandingTotal));
}
}
Assertions.assertEquals(installments[i].completed, period.getComplete());
Assertions.assertEquals(LocalDate.parse(installments[i].dueDate, dateTimeFormatter), period.getDueDate());
@@ -603,22 +643,31 @@ public abstract class BaseLoanIntegrationTest {
}
protected Installment installment(double principalAmount, Boolean completed, String dueDate) {
return new Installment(principalAmount, null, null, null, null, completed, dueDate);
return new Installment(principalAmount, null, null, null, null, completed, dueDate, null);
}
protected Installment installment(double principalAmount, double interestAmount, double totalOutstandingAmount, Boolean completed,
String dueDate) {
return new Installment(principalAmount, interestAmount, null, null, totalOutstandingAmount, completed, dueDate);
return new Installment(principalAmount, interestAmount, null, null, totalOutstandingAmount, completed, dueDate, null);
}
protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double totalOutstandingAmount,
Boolean completed, String dueDate) {
return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate);
return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate, null);
}
protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double penaltyAmount,
double totalOutstandingAmount, Boolean completed, String dueDate) {
return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, totalOutstandingAmount, completed, dueDate);
return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, totalOutstandingAmount, completed, dueDate, null);
}
protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double penaltyAmount,
OutstandingAmounts outstandingAmounts, Boolean completed, String dueDate) {
return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, null, completed, dueDate, outstandingAmounts);
}
protected OutstandingAmounts outstanding(double principal, double fee, double penalty, double total) {
return new OutstandingAmounts(principal, fee, penalty, total);
}
protected BatchRequestBuilder batchRequest() {
@@ -735,6 +784,17 @@ public abstract class BaseLoanIntegrationTest {
Double totalOutstandingAmount;
Boolean completed;
String dueDate;
OutstandingAmounts outstandingAmounts;
}
@AllArgsConstructor
@ToString
public static class OutstandingAmounts {
Double principalOutstanding;
Double feeOutstanding;
Double penaltyOutstanding;
Double totalOutstanding;
}
public static class AmortizationType {
@@ -26,6 +26,8 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.CreditAllocationData;
import org.apache.fineract.client.models.CreditAllocationOrder;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdSummary;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
@@ -38,6 +40,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -46,13 +49,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoanIntegrationTest {
@Test
public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrincipal() {
public void simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipal() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -92,15 +97,13 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 1037.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(343.0, 0, 50, 20, 30.0, false, "01 February 2023"), // TODO: we still need to add the
// fee and the penalty to the
// outstanding
installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
@@ -109,13 +112,145 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
}
@Test
public void createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrincipalOnNPlusOneInstallment() {
public void simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipalOnTheLastDayOfTheInstallment() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
// Add Charges
Long feeId = addCharge(loanId, false, 50, "15 January 2023");
Long penaltyId = addCharge(loanId, true, 20, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Update Business Date
updateBusinessDate("20 January 2023");
// Add Repayment
Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
updateBusinessDate("01 February 2023");
// Add Chargeback
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30
// principal
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "01 February 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0, true, "01 February 2023"), //
installment(343.0, 0, 50, 20, 413.0, false, "01 March 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
});
}
@Test
public void simpleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipalOnTheLastDayOfTheLoan() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
// Add Charges
Long feeId = addCharge(loanId, false, 50, "15 January 2023");
Long penaltyId = addCharge(loanId, true, 20, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Update Business Date
updateBusinessDate("20 January 2023");
// Add Repayment
Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
updateBusinessDate("01 May 2023");
// Add Chargeback
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30
// principal
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "01 May 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(341.0, 0, 50, 20, 411.0, false, "01 May 2023") //
);
});
}
@Test
public void chargebackWithCreditAllocationPenaltyFeeInterestAndPrincipalOnNPlusOneInstallment() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -169,7 +304,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
transaction(313.0, "Repayment", "20 March 2023", 311.0, 313.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(381.0, "Repayment", "20 April 2023", 0.0, 311.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(70.0, "Accrual", "20 April 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "02 May 2023", 100.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
transaction(100.0, "Chargeback", "02 May 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
verifyRepaymentSchedule(loanId, //
@@ -178,20 +313,21 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
installment(311.0, 0, 50, 20, 0.0, true, "01 May 2023"), //
installment(30.0, 0, 0, 0, 30.0, false, "02 May 2023") // TODO: fee and penalty must be added here
// after chargeback
installment(30.0, 0, 50, 20, 100.0, false, "02 May 2023") //
);
});
}
@Test
public void createLoanWithCreditAllocationAndChargebackReverseReplayWithBackdatedPayment() {
public void chargebackWithCreditAllocationAndReverseReplayWithBackdatedPayment() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -232,15 +368,13 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "21 January 2023", 1037.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
transaction(100.0, "Chargeback", "21 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(343.0, 0, 50, 20, 30.0, false, "01 February 2023"), // TODO: we still need to add the
// fee and the penalty to the
// outstanding
installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
@@ -260,13 +394,15 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
}
@Test
public void createLoanWithCreditAllocationAndOnlyTheChargebackReverseReplayedWithBackdatedPayment() {
public void chargebackWithCreditAllocationReverseReplayedWithBackdatedPayment() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -307,15 +443,13 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "22 January 2023", 1037.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
transaction(100.0, "Chargeback", "22 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(343.0, 0, 50, 20, 30.0, false, "01 February 2023"), // TODO: we still need to add the
// fee and the penalty to the
// outstanding
installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
@@ -330,18 +464,24 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(200.0, "Repayment", "21 January 2023", 737.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "22 January 2023", 837.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
transaction(100.0, "Chargeback", "22 January 2023", 767.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
verifyLoanSummaryAmounts(loanId, 30.0, 50.0, 20.0, 837.0);
});
}
@Test
public void createLoanWithCreditAllocationAndChargebackPrincipalInterestFeePenalty() {
public void chargebackWithCreditAllocationPrincipalInterestFeePenalty() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(chargebackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY"));
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -387,16 +527,501 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(413.0, 0, 50, 20, 100.0, false, "01 February 2023"), // TODO: we still need to add the
// fee and the penalty to the
// outstanding
installment(413.0, 0, 50, 20, 100.0, false, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
verifyLoanSummaryAmounts(loanId, 100.0, 0.0, 0.0, 1037);
});
}
@Test
public void chargebackWithCreditAllocationPrincipalInterestFeePenaltyWhenOverpaid() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
// Add Charges
Long feeId = addCharge(loanId, false, 50, "15 January 2023");
Long penaltyId = addCharge(loanId, true, 20, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Update Business Date
updateBusinessDate("20 January 2023");
// Add Repayment
Long repaymentTransaction = addRepaymentForLoan(loanId, 1370.0, "20 January 2023"); // 1250 + 70 = 1320; 50
// overpayment
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") //
);
updateBusinessDate("02 May 2023");
// Add Chargeback
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 principal, 0 interest, 0 fee 0 penalty
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "02 May 2023", 50.0, 100.0, 0.0, 0.0, 0.0, 0.0, 50.0) //
);
// Verify Repayment Schedule
// DEFAULT payment allocation is ..., DUE_PENALTY, DUE_FEE, DUE_PRINCIPAL, ...
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0, true, "01 February 2023"), //
installment(313.0, 0, 0, 0, 0, true, "01 March 2023"), //
installment(313.0, 0, 0, 0, 0, true, "01 April 2023"), //
installment(311.0, 0, 0, 0, 0, true, "01 May 2023"), //
installment(100.0, 0, 0, 0, outstanding(50.0, 0d, 0d, 50.0), false, "02 May 2023") //
);
});
}
@Test
public void chargebackWithCreditAllocationFeePenaltyPrincipalInterestWhenOverpaid() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("FEE", "PENALTY", "PRINCIPAL", "INTEREST")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
// Add Charges
Long feeId = addCharge(loanId, false, 50, "15 January 2023");
Long penaltyId = addCharge(loanId, true, 20, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Update Business Date
updateBusinessDate("20 January 2023");
// Add Repayment
Long repaymentTransaction = addRepaymentForLoan(loanId, 1370.0, "20 January 2023"); // 1250 + 70 = 1320; 50
// overpayment
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") //
);
// Add Chargeback
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 principal, 0 interest, 0 fee 0 penalty
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) //
);
// Verify Repayment Schedule,
// DEFAULT payment allocation is ..., DUE_PENALTY, DUE_FEE, DUE_PRINCIPAL, ...
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(343.0, 0, 100, 40, outstanding(30.0, 20.0, 0.0, 50.0), false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") //
);
verifyLoanSummaryAmounts(loanId, 30.0, 50.0, 20.0, 50.0);
});
}
@Test
public void chargebackWithCreditAllocationFeePenaltyPrincipalInterestWhenOverpaidDefaultPaymentPrincipalFirst() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocationPrincipalFirst(), //
chargebackAllocation("FEE", "PENALTY", "PRINCIPAL", "INTEREST")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
// Add Charges
Long feeId = addCharge(loanId, false, 50, "15 January 2023");
Long penaltyId = addCharge(loanId, true, 20, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Update Business Date
updateBusinessDate("20 January 2023");
// Add Repayment
Long repaymentTransaction = addRepaymentForLoan(loanId, 1370.0, "20 January 2023"); // 1250 + 70 = 1320; 50
// overpayment
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") //
);
// Add Chargeback
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 principal, 0 interest, 0 fee 0 penalty
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 0.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) //
);
// Verify Repayment Schedule,
// DEFAULT payment allocation is ..., DUE_PRINCIPAL, DUE_FEE, DUE_PENALTY ...
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(343.0, 0, 100, 40, outstanding(0.0, 30.0, 20.0, 50.0), false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
installment(311.0, 0, 0, 0, 0.0, true, "01 May 2023") //
);
verifyLoanSummaryAmounts(loanId, 30.0, 50.0, 20.0, 50.0);
});
}
@Test
public void doubleChargebackWithCreditAllocationPenaltyFeeInterestAndPrincipal() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
// Add Charges
Long feeId = addCharge(loanId, false, 50, "15 January 2023");
Long penaltyId = addCharge(loanId, true, 20, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Update Business Date
updateBusinessDate("20 January 2023");
// Add Repayment
Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Add Chargeback
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30
// principal
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
updateBusinessDate("21 January 2023");
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 to principal
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 967, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "21 January 2023", 1067.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(443.0, 0, 100, 40, 200.0, false, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
});
}
@Test
public void doubleChargebackReverseReplayedBothFeeAndPenaltyPayedWithCreditAllocationPenaltyFeeInterestAndPrincipal() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
// Add Charges
Long feeId = addCharge(loanId, false, 50, "15 January 2023");
Long penaltyId = addCharge(loanId, true, 20, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Update Business Date
updateBusinessDate("20 January 2023");
// Add Repayment
Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Add Chargeback
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30
// principal
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
updateBusinessDate("21 January 2023");
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 to principal
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "21 January 2023", 1067.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(443.0, 0, 100, 40, 200.0, false, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Let's add repayment to trigger reverse replay for both chargebacks
addRepaymentForLoan(loanId, 200.0, "19 January 2023");
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(200.0, "Repayment", "19 January 2023", 1120.0, 130.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 737.0, 383.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 837.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "21 January 2023", 937.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) //
);
});
}
@Test
public void doubleChargebackReverseReplayedOnlyPenaltyPayedWithCreditAllocationPenaltyFeeInterestAndPrincipal() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
Long loanProductId = createLoanProduct(//
createDefaultPaymentAllocation(), //
chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")//
);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
// Add Charges
Long feeId = addCharge(loanId, false, 50, "15 January 2023");
Long penaltyId = addCharge(loanId, true, 20, "15 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 383.0, false, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Update Business Date
updateBusinessDate("20 January 2023");
// Add Repayment
Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20 January 2023");
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, 0, 50, 20, 0.0, true, "01 February 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Add Chargeback
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20 penalty + 50 fee + 0 interest + 30
// principal
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(343.0, 0, 100, 40, 100.0, false, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
updateBusinessDate("21 January 2023");
addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100 to principal
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 937.0, 313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 967.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "21 January 2023", 1067.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) //
);
// Verify Repayment Schedule
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(443.0, 0, 100, 40, 200.0, false, "01 February 2023"),
installment(313.0, 0, 0, 0, 313.0, false, "01 March 2023"), //
installment(313.0, 0, 0, 0, 313.0, false, "01 April 2023"), //
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
);
// Let's add repayment to trigger reverse replay for both chargebacks
addRepaymentForLoan(loanId, 20.0, "19 January 2023");
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(20.0, "Repayment", "19 January 2023", 1250.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 917.0, 333.0, 0.0, 50.0, 0.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "20 January 2023", 967.0, 50.0, 0.0, 50.0, 0.0, 0.0, 0.0), //
transaction(100.0, "Chargeback", "21 January 2023", 1067.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) //
);
});
}
private void verifyLoanSummaryAmounts(Long loanId, double creditedPrincipal, double creditedFee, double creditedPenalty,
double totalOutstanding) {
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
GetLoansLoanIdSummary summary = loanResponse.getSummary();
Assertions.assertNotNull(summary);
Assertions.assertEquals(creditedPrincipal, summary.getPrincipalAdjustments());
Assertions.assertEquals(creditedFee, summary.getFeeAdjustments());
Assertions.assertEquals(creditedPenalty, summary.getPenaltyAdjustments());
Assertions.assertEquals(totalOutstanding, summary.getTotalOutstanding());
}
@Nullable
private Long applyAndApproveLoan(Long clientId, Long loanProductId) {
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", 1250.0, 4)//
@@ -415,22 +1040,23 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
return loanId;
}
public Long createLoanProduct(CreditAllocationData... creditAllocationData) {
PostLoanProductsRequest postLoanProductsRequest = loanProductWithAdvancedPaymentAllocationWith4Installments(creditAllocationData);
public Long createLoanProduct(AdvancedPaymentData defaultAllocation, CreditAllocationData creditAllocationData) {
PostLoanProductsRequest postLoanProductsRequest = loanProductWithAdvancedPaymentAllocationWith4Installments(defaultAllocation,
creditAllocationData);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(postLoanProductsRequest);
return loanProductResponse.getResourceId();
}
private PostLoanProductsRequest loanProductWithAdvancedPaymentAllocationWith4Installments(
CreditAllocationData... creditAllocationData) {
private PostLoanProductsRequest loanProductWithAdvancedPaymentAllocationWith4Installments(AdvancedPaymentData defaultAllocation,
CreditAllocationData creditAllocationData) {
return createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().numberOfRepayments(4)//
.repaymentEvery(1)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
.loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) //
.loanScheduleProcessingType(LoanScheduleProcessingType.VERTICAL.toString()) //
.transactionProcessingStrategyCode("advanced-payment-allocation-strategy")
.paymentAllocation(List.of(createDefaultPaymentAllocation(), createRepaymentPaymentAllocation()))
.creditAllocation(Arrays.asList(creditAllocationData));
.paymentAllocation(List.of(defaultAllocation, createRepaymentPaymentAllocation()))
.creditAllocation(List.of(creditAllocationData));
}
private AdvancedPaymentData createDefaultPaymentAllocation() {
@@ -448,6 +1074,21 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
return advancedPaymentData;
}
private AdvancedPaymentData createDefaultPaymentAllocationPrincipalFirst() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
List<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() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("REPAYMENT");