FINERACT-2042 Handling overpayment of chargeback with credit allocations
This commit is contained in:
committed by
Peter Bagrij
parent
5be1001fd5
commit
4bc6755514
+19
-3
@@ -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;
|
||||
}
|
||||
|
||||
+50
-12
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+13
-1
@@ -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();
|
||||
|
||||
+21
-2
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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()) {
|
||||
|
||||
+110
-69
@@ -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);
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
+43
@@ -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>
|
||||
Reference in New Issue
Block a user