FINERACT-2042 Reverse Replay of Credit Allocation
This commit is contained in:
committed by
Peter Bagrij
parent
6c953dbc87
commit
10c314e5d9
+3
-3
@@ -20,16 +20,16 @@ package org.apache.fineract.portfolio.loanaccount.domain;
|
|||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores details of {@link LoanTransaction}'s that were reversed or newly created
|
* Stores details of {@link LoanTransaction}'s that were reversed or newly created
|
||||||
*/
|
*/
|
||||||
|
@Getter
|
||||||
public class ChangedTransactionDetail {
|
public class ChangedTransactionDetail {
|
||||||
|
|
||||||
private final Map<Long, LoanTransaction> newTransactionMappings = new LinkedHashMap<>();
|
private final Map<Long, LoanTransaction> newTransactionMappings = new LinkedHashMap<>();
|
||||||
|
|
||||||
public Map<Long, LoanTransaction> getNewTransactionMappings() {
|
private final Map<LoanTransaction, Long> currentTransactionToOldId = new LinkedHashMap<>();
|
||||||
return this.newTransactionMappings;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-12
@@ -111,6 +111,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementDa
|
|||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
|
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
||||||
@@ -800,8 +801,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
final Set<LoanCharge> loanCharges = new HashSet<>(1);
|
final Set<LoanCharge> loanCharges = new HashSet<>(1);
|
||||||
loanCharges.add(charge);
|
loanCharges.add(charge);
|
||||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargesPayment, getCurrency(), chargePaymentInstallments,
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargesPayment,
|
||||||
loanCharges, new MoneyHolder(getTotalOverpaidAsMoney()));
|
new TransactionCtx(getCurrency(), chargePaymentInstallments, loanCharges, new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||||
|
|
||||||
updateLoanSummaryDerivedFields();
|
updateLoanSummaryDerivedFields();
|
||||||
doPostLoanTransactionChecks(chargesPayment.getTransactionDate(), loanLifecycleStateMachine);
|
doPostLoanTransactionChecks(chargesPayment.getTransactionDate(), loanLifecycleStateMachine);
|
||||||
@@ -3324,8 +3325,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
|
|
||||||
if (isTransactionChronologicallyLatest && adjustedTransaction == null
|
if (isTransactionChronologicallyLatest && adjustedTransaction == null
|
||||||
&& (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !isForeclosure()) {
|
&& (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !isForeclosure()) {
|
||||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, getCurrency(),
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
|
||||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
|
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||||
reprocess = false;
|
reprocess = false;
|
||||||
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||||
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
|
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
|
||||||
@@ -3917,8 +3918,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addLoanTransaction(loanTransaction);
|
addLoanTransaction(loanTransaction);
|
||||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, loanCurrency(),
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(),
|
||||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
|
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||||
|
|
||||||
updateLoanSummaryDerivedFields();
|
updateLoanSummaryDerivedFields();
|
||||||
}
|
}
|
||||||
@@ -4022,8 +4023,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addLoanTransaction(loanTransaction);
|
addLoanTransaction(loanTransaction);
|
||||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, loanCurrency(),
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(),
|
||||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
|
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||||
|
|
||||||
updateLoanSummaryDerivedFields();
|
updateLoanSummaryDerivedFields();
|
||||||
} else if (totalOutstanding.isGreaterThanZero()) {
|
} else if (totalOutstanding.isGreaterThanZero()) {
|
||||||
@@ -6377,8 +6378,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
|
|
||||||
// If is a refund
|
// If is a refund
|
||||||
if (adjustedTransaction == null) {
|
if (adjustedTransaction == null) {
|
||||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, getCurrency(),
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
|
||||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
|
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||||
} else {
|
} else {
|
||||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
|
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
|
||||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||||
@@ -6408,8 +6409,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
.determineProcessor(this.transactionProcessingStrategyCode);
|
.determineProcessor(this.transactionProcessingStrategyCode);
|
||||||
|
|
||||||
addLoanTransaction(chargebackTransaction);
|
addLoanTransaction(chargebackTransaction);
|
||||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, getCurrency(),
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, new TransactionCtx(getCurrency(),
|
||||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
|
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||||
|
|
||||||
updateLoanSummaryDerivedFields();
|
updateLoanSummaryDerivedFields();
|
||||||
if (!doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) {
|
if (!doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) {
|
||||||
|
|||||||
+12
-14
@@ -164,7 +164,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
|
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
|
||||||
// pass through for new transactions
|
// pass through for new transactions
|
||||||
if (loanTransaction.getId() == null) {
|
if (loanTransaction.getId() == null) {
|
||||||
processLatestTransaction(loanTransaction, currency, installments, charges, overpaymentHolder);
|
processLatestTransaction(loanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder));
|
||||||
loanTransaction.adjustInterestComponent(currency);
|
loanTransaction.adjustInterestComponent(currency);
|
||||||
} else {
|
} else {
|
||||||
/**
|
/**
|
||||||
@@ -175,7 +175,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
|
|
||||||
// Reset derived component of new loan transaction and
|
// Reset derived component of new loan transaction and
|
||||||
// re-process transaction
|
// re-process transaction
|
||||||
processLatestTransaction(newLoanTransaction, currency, installments, charges, overpaymentHolder);
|
processLatestTransaction(newLoanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder));
|
||||||
newLoanTransaction.adjustInterestComponent(currency);
|
newLoanTransaction.adjustInterestComponent(currency);
|
||||||
/**
|
/**
|
||||||
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
|
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
|
||||||
@@ -211,15 +211,14 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processLatestTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
|
public void processLatestTransaction(final LoanTransaction loanTransaction, final TransactionCtx ctx) {
|
||||||
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
|
|
||||||
switch (loanTransaction.getTypeOf()) {
|
switch (loanTransaction.getTypeOf()) {
|
||||||
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
|
case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||||
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
|
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
|
||||||
case CHARGEBACK -> handleChargeback(loanTransaction, currency, installments, overpaymentHolder);
|
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
|
||||||
default -> {
|
default -> {
|
||||||
Money transactionAmountUnprocessed = handleTransactionAndCharges(loanTransaction, currency, installments, charges, null,
|
Money transactionAmountUnprocessed = handleTransactionAndCharges(loanTransaction, ctx.getCurrency(), ctx.getInstallments(),
|
||||||
false);
|
ctx.getCharges(), null, false);
|
||||||
if (transactionAmountUnprocessed.isGreaterThanZero()) {
|
if (transactionAmountUnprocessed.isGreaterThanZero()) {
|
||||||
if (loanTransaction.isWaiver()) {
|
if (loanTransaction.isWaiver()) {
|
||||||
loanTransaction.updateComponentsAndTotal(transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero(),
|
loanTransaction.updateComponentsAndTotal(transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero(),
|
||||||
@@ -228,9 +227,9 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
|
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
|
||||||
loanTransaction.setOverPayments(transactionAmountUnprocessed);
|
loanTransaction.setOverPayments(transactionAmountUnprocessed);
|
||||||
}
|
}
|
||||||
overpaymentHolder.setMoneyObject(transactionAmountUnprocessed);
|
ctx.getOverpaymentHolder().setMoneyObject(transactionAmountUnprocessed);
|
||||||
} else {
|
} else {
|
||||||
overpaymentHolder.setMoneyObject(Money.zero(currency));
|
ctx.getOverpaymentHolder().setMoneyObject(Money.zero(ctx.getCurrency()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -742,9 +741,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
|
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
protected void handleChargeback(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaidAmountHolder) {
|
processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
|
||||||
processCreditTransaction(loanTransaction, overpaidAmountHolder, currency, installments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handleCreditBalanceRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
protected void handleCreditBalanceRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||||
|
|||||||
+20
-3
@@ -21,6 +21,8 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||||
@@ -30,6 +32,22 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
|||||||
|
|
||||||
public interface LoanRepaymentScheduleTransactionProcessor {
|
public interface LoanRepaymentScheduleTransactionProcessor {
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
class TransactionCtx {
|
||||||
|
|
||||||
|
private final MonetaryCurrency currency;
|
||||||
|
private final List<LoanRepaymentScheduleInstallment> installments;
|
||||||
|
private final Set<LoanCharge> charges;
|
||||||
|
private final MoneyHolder overpaymentHolder;
|
||||||
|
private final ChangedTransactionDetail changedTransactionDetail;
|
||||||
|
|
||||||
|
public TransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges,
|
||||||
|
MoneyHolder overpaymentHolder) {
|
||||||
|
this(currency, installments, charges, overpaymentHolder, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String getCode();
|
String getCode();
|
||||||
|
|
||||||
String getName();
|
String getName();
|
||||||
@@ -37,11 +55,10 @@ public interface LoanRepaymentScheduleTransactionProcessor {
|
|||||||
boolean accept(String s);
|
boolean accept(String s);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides support for processing the latest transaction (which should be latest transaction) against the loan
|
* Provides support for processing the latest transaction (which should be the latest transaction) against the loan
|
||||||
* schedule.
|
* schedule.
|
||||||
*/
|
*/
|
||||||
void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx);
|
||||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides support for passing all {@link LoanTransaction}'s so it will completely re-process the entire loan
|
* Provides support for passing all {@link LoanTransaction}'s so it will completely re-process the entire loan
|
||||||
|
|||||||
+76
-49
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
|
|||||||
|
|
||||||
import static java.util.stream.Collectors.mapping;
|
import static java.util.stream.Collectors.mapping;
|
||||||
import static java.util.stream.Collectors.toList;
|
import static java.util.stream.Collectors.toList;
|
||||||
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
||||||
@@ -30,6 +31,7 @@ import java.math.MathContext;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -63,6 +65,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleIns
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
|
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||||
@@ -164,19 +167,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
|
|
||||||
switch (loanTransaction.getTypeOf()) {
|
switch (loanTransaction.getTypeOf()) {
|
||||||
case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments, overpaymentHolder);
|
case DISBURSEMENT -> handleDisbursement(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
|
||||||
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
|
case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||||
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
|
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
|
||||||
case CHARGEBACK -> handleChargeback(loanTransaction, currency, installments, overpaymentHolder);
|
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
|
||||||
case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, currency, installments, overpaymentHolder);
|
case CREDIT_BALANCE_REFUND ->
|
||||||
|
handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
|
||||||
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
||||||
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
|
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
|
||||||
handleRepayment(loanTransaction, currency, installments, charges, overpaymentHolder);
|
handleRepayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(), ctx.getOverpaymentHolder());
|
||||||
case CHARGE_OFF -> handleChargeOff(loanTransaction, currency, installments);
|
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||||
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, currency, installments, charges, overpaymentHolder);
|
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(),
|
||||||
|
ctx.getOverpaymentHolder());
|
||||||
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
|
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
|
||||||
// TODO: Cover rest of the transaction types
|
// TODO: Cover rest of the transaction types
|
||||||
default -> {
|
default -> {
|
||||||
@@ -185,44 +189,73 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void handleChargeback(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
|
processCreditTransaction(loanTransaction, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasNoCustomCreditAllocationRule(LoanTransaction loanTransaction) {
|
private boolean hasNoCustomCreditAllocationRule(LoanTransaction loanTransaction) {
|
||||||
return (loanTransaction.getLoan().getCreditAllocationRules() == null || !loanTransaction.getLoan().getCreditAllocationRules()
|
return (loanTransaction.getLoan().getCreditAllocationRules() == null || !loanTransaction.getLoan().getCreditAllocationRules()
|
||||||
.stream().anyMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
|
.stream().anyMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected LoanTransaction findOriginalTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency,
|
if (loanTransaction.getId() != null) { // this the normal case without reverse-replay
|
||||||
List<LoanRepaymentScheduleInstallment> installments) {
|
Optional<LoanTransaction> originalTransaction = loanTransaction.getLoan().getLoanTransactions().stream()
|
||||||
|
.filter(tr -> tr.getLoanTransactionRelations().stream()
|
||||||
|
.anyMatch(this.hasMatchingToLoanTransaction(loanTransaction.getId(), CHARGEBACK)))
|
||||||
|
.findFirst();
|
||||||
|
if (originalTransaction.isEmpty()) {
|
||||||
|
throw new RuntimeException("Chargeback transaction must have an original transaction");
|
||||||
|
}
|
||||||
|
return originalTransaction.get();
|
||||||
|
} else { // when there is no id, then it might be that the original transaction is changed, so we need to look
|
||||||
|
// it up from the Ctx.
|
||||||
|
Long originalChargebackTransactionId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction);
|
||||||
|
Collection<LoanTransaction> updatedTransactions = ctx.getChangedTransactionDetail().getNewTransactionMappings().values();
|
||||||
|
Optional<LoanTransaction> updatedTransaction = updatedTransactions.stream().filter(tr -> tr.getLoanTransactionRelations()
|
||||||
|
.stream().anyMatch(this.hasMatchingToLoanTransaction(originalChargebackTransactionId, CHARGEBACK))).findFirst();
|
||||||
|
|
||||||
|
if (updatedTransaction.isPresent()) {
|
||||||
|
return updatedTransaction.get();
|
||||||
|
} else { // if it is not there, then it simply means that this has not changed during reverse replay
|
||||||
|
Optional<LoanTransaction> originalTransaction = loanTransaction.getLoan().getLoanTransactions().stream()
|
||||||
|
.filter(tr -> tr.getLoanTransactionRelations().stream()
|
||||||
|
.anyMatch(this.hasMatchingToLoanTransaction(originalChargebackTransactionId, CHARGEBACK)))
|
||||||
|
.findFirst();
|
||||||
|
if (originalTransaction.isEmpty()) {
|
||||||
|
throw new RuntimeException("Chargeback transaction must have an original transaction");
|
||||||
|
}
|
||||||
|
return originalTransaction.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
if (hasNoCustomCreditAllocationRule(loanTransaction)) {
|
if (hasNoCustomCreditAllocationRule(loanTransaction)) {
|
||||||
super.processCreditTransaction(loanTransaction, overpaymentHolder, currency, installments);
|
super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
|
||||||
} else {
|
} else {
|
||||||
log.debug("Processing credit transaction with custom credit allocation rules");
|
log.debug("Processing credit transaction with custom credit allocation rules");
|
||||||
|
|
||||||
loanTransaction.resetDerivedComponents();
|
loanTransaction.resetDerivedComponents();
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||||
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
|
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
|
||||||
installments.sort(byDate);
|
ctx.getInstallments().sort(byDate);
|
||||||
final Money zeroMoney = Money.zero(currency);
|
final Money zeroMoney = Money.zero(ctx.getCurrency());
|
||||||
Money transactionAmount = loanTransaction.getAmount(currency);
|
Money transactionAmount = loanTransaction.getAmount(ctx.getCurrency());
|
||||||
Money amountToDistribute = MathUtil
|
Money amountToDistribute = MathUtil
|
||||||
.negativeToZero(loanTransaction.getAmount(currency).minus(overpaymentHolder.getMoneyObject()));
|
.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(ctx.getOverpaymentHolder().getMoneyObject()));
|
||||||
Money repaidAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute));
|
Money repaidAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute));
|
||||||
loanTransaction.setOverPayments(repaidAmount);
|
loanTransaction.setOverPayments(repaidAmount);
|
||||||
overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(repaidAmount));
|
ctx.getOverpaymentHolder().setMoneyObject(ctx.getOverpaymentHolder().getMoneyObject().minus(repaidAmount));
|
||||||
|
|
||||||
if (amountToDistribute.isGreaterThanZero()) {
|
if (amountToDistribute.isGreaterThanZero()) {
|
||||||
if (loanTransaction.isChargeback()) {
|
if (loanTransaction.isChargeback()) {
|
||||||
Optional<LoanTransaction> originalTransaction = loanTransaction.getLoan().getLoanTransactions(
|
LoanTransaction originalTransaction = findOriginalTransaction(loanTransaction, ctx);
|
||||||
tr -> tr.getLoanTransactionRelations().stream().anyMatch(this.hasMatchingToLoanTransaction(loanTransaction)))
|
Map<AllocationType, BigDecimal> originalAllocation = getOriginalAllocation(originalTransaction);
|
||||||
.stream().findFirst();
|
|
||||||
if (originalTransaction.isEmpty()) {
|
|
||||||
throw new RuntimeException("Chargeback transaction must have an original transaction");
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<AllocationType, BigDecimal> originalAllocation = getOriginalAllocation(originalTransaction.get());
|
|
||||||
LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction);
|
LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction);
|
||||||
Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation,
|
Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation,
|
||||||
amountToDistribute.getAmount(), chargeBackAllocationRule.getAllocationTypes(), currency);
|
amountToDistribute.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency());
|
||||||
|
|
||||||
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
|
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
|
||||||
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
|
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
|
||||||
@@ -230,13 +263,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||||
boolean loanTransactionMapped = false;
|
boolean loanTransactionMapped = false;
|
||||||
LocalDate pastDueDate = null;
|
LocalDate pastDueDate = null;
|
||||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) {
|
||||||
pastDueDate = currentInstallment.getDueDate();
|
pastDueDate = currentInstallment.getDueDate();
|
||||||
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
|
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
|
||||||
|
|
||||||
currentInstallment.addToCredits(transactionAmount.getAmount());
|
currentInstallment.addToCredits(transactionAmount.getAmount());
|
||||||
currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
|
currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
|
||||||
Money originalInterest = currentInstallment.getInterestCharged(currency);
|
Money originalInterest = currentInstallment.getInterestCharged(ctx.getCurrency());
|
||||||
currentInstallment.updateInterestCharged(
|
currentInstallment.updateInterestCharged(
|
||||||
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
|
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
|
||||||
|
|
||||||
@@ -256,7 +289,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
currentInstallment.addToCredits(transactionAmount.getAmount());
|
currentInstallment.addToCredits(transactionAmount.getAmount());
|
||||||
currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
|
currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
|
||||||
Money originalInterest = currentInstallment.getInterestCharged(currency);
|
Money originalInterest = currentInstallment.getInterestCharged(ctx.getCurrency());
|
||||||
currentInstallment.updateInterestCharged(
|
currentInstallment.updateInterestCharged(
|
||||||
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
|
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
|
||||||
if (repaidAmount.isGreaterThanZero()) {
|
if (repaidAmount.isGreaterThanZero()) {
|
||||||
@@ -272,10 +305,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
// New installment will be added (N+1 scenario)
|
// New installment will be added (N+1 scenario)
|
||||||
if (!loanTransactionMapped) {
|
if (!loanTransactionMapped) {
|
||||||
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
|
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
|
||||||
LoanRepaymentScheduleInstallment currentInstallment = installments.get(installments.size() - 1);
|
LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments()
|
||||||
|
.get(ctx.getInstallments().size() - 1);
|
||||||
currentInstallment.addToCredits(transactionAmount.getAmount());
|
currentInstallment.addToCredits(transactionAmount.getAmount());
|
||||||
currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
|
currentInstallment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
|
||||||
Money originalInterest = currentInstallment.getInterestCharged(currency);
|
Money originalInterest = currentInstallment.getInterestCharged(ctx.getCurrency());
|
||||||
currentInstallment.updateInterestCharged(
|
currentInstallment.updateInterestCharged(
|
||||||
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
|
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
|
||||||
if (repaidAmount.isGreaterThanZero()) {
|
if (repaidAmount.isGreaterThanZero()) {
|
||||||
@@ -286,12 +320,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
} else {
|
} else {
|
||||||
Loan loan = loanTransaction.getLoan();
|
Loan loan = loanTransaction.getLoan();
|
||||||
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
|
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
|
||||||
(installments.size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(), zeroMoney.getAmount(),
|
(ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(),
|
||||||
zeroMoney.getAmount(), zeroMoney.getAmount(), false, null);
|
zeroMoney.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), false, null);
|
||||||
installment.markAsAdditional();
|
installment.markAsAdditional();
|
||||||
installment.addToCredits(transactionAmount.getAmount());
|
installment.addToCredits(transactionAmount.getAmount());
|
||||||
installment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
|
installment.addToPrincipal(transactionDate, chargebackAllocation.get(PRINCIPAL));
|
||||||
Money originalInterest = installment.getInterestCharged(currency);
|
Money originalInterest = installment.getInterestCharged(ctx.getCurrency());
|
||||||
installment.updateInterestCharged(
|
installment.updateInterestCharged(
|
||||||
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
|
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
|
||||||
loan.addLoanRepaymentScheduleInstallment(installment);
|
loan.addLoanRepaymentScheduleInstallment(installment);
|
||||||
@@ -348,17 +382,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate<LoanTransactionRelation> hasMatchingToLoanTransaction(LoanTransaction loanTransaction) {
|
private Predicate<LoanTransactionRelation> hasMatchingToLoanTransaction(Long id, LoanTransactionRelationTypeEnum typeEnum) {
|
||||||
return relation -> {
|
return relation -> relation.getRelationType().equals(typeEnum) && Objects.equals(relation.getToTransaction().getId(), id);
|
||||||
if (loanTransaction.getId() != null && relation.getToTransaction().getId() != null) {
|
|
||||||
return Objects.equals(relation.getToTransaction().getId(), loanTransaction.getId());
|
|
||||||
} else {
|
|
||||||
return relation.getToTransaction().getTypeOf().equals(loanTransaction.getTypeOf())
|
|
||||||
&& relation.getToTransaction().getAmount().compareTo(loanTransaction.getAmount()) == 0
|
|
||||||
&& relation.getToTransaction().isReversed() == loanTransaction.isReversed()
|
|
||||||
&& relation.getToTransaction().getTransactionDate().compareTo(loanTransaction.getTransactionDate()) == 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -419,8 +444,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, ChangedTransactionDetail changedTransactionDetail,
|
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, ChangedTransactionDetail changedTransactionDetail,
|
||||||
MoneyHolder overpaymentHolder) {
|
MoneyHolder overpaymentHolder) {
|
||||||
|
TransactionCtx ctx = new TransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail);
|
||||||
if (loanTransaction.getId() == null) {
|
if (loanTransaction.getId() == null) {
|
||||||
processLatestTransaction(loanTransaction, currency, installments, charges, overpaymentHolder);
|
processLatestTransaction(loanTransaction, ctx);
|
||||||
if (loanTransaction.isInterestWaiver()) {
|
if (loanTransaction.isInterestWaiver()) {
|
||||||
loanTransaction.adjustInterestComponent(currency);
|
loanTransaction.adjustInterestComponent(currency);
|
||||||
}
|
}
|
||||||
@@ -430,10 +456,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
* changed.<br>
|
* changed.<br>
|
||||||
*/
|
*/
|
||||||
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
|
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
|
||||||
|
ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().put(newLoanTransaction, loanTransaction.getId());
|
||||||
|
|
||||||
// Reset derived component of new loan transaction and
|
// Reset derived component of new loan transaction and
|
||||||
// re-process transaction
|
// re-process transaction
|
||||||
processLatestTransaction(newLoanTransaction, currency, installments, charges, overpaymentHolder);
|
processLatestTransaction(newLoanTransaction, ctx);
|
||||||
if (loanTransaction.isInterestWaiver()) {
|
if (loanTransaction.isInterestWaiver()) {
|
||||||
newLoanTransaction.adjustInterestComponent(currency);
|
newLoanTransaction.adjustInterestComponent(currency);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -115,6 +115,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationT
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
|
import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
|
||||||
@@ -832,8 +833,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
|||||||
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
|
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
|
||||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||||
.determineProcessor(loan.transactionProcessingStrategy());
|
.determineProcessor(loan.transactionProcessingStrategy());
|
||||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction, loan.getCurrency(),
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction,
|
||||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()));
|
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||||
|
new MoneyHolder(loan.getTotalOverpaidAsMoney())));
|
||||||
|
|
||||||
loan.addLoanTransaction(loanChargeAdjustmentTransaction);
|
loan.addLoanTransaction(loanChargeAdjustmentTransaction);
|
||||||
loan.updateLoanSummaryAndStatus();
|
loan.updateLoanSummaryAndStatus();
|
||||||
|
|||||||
+141
-22
@@ -18,14 +18,12 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGEBACK;
|
|
||||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REPAYMENT;
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REPAYMENT;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.ArgumentMatchers.refEq;
|
import static org.mockito.ArgumentMatchers.refEq;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
@@ -48,6 +46,7 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
|||||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||||
@@ -55,7 +54,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||||
@@ -140,8 +142,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
|
Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
|
||||||
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
|
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
|
||||||
|
|
||||||
underTest.processLatestTransaction(loanTransaction, currency, List.of(installment), Set.of(charge),
|
underTest.processLatestTransaction(loanTransaction,
|
||||||
new MoneyHolder(overpaidAmount));
|
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
|
||||||
|
|
||||||
Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
|
Mockito.verify(installment, 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(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
|
||||||
@@ -185,8 +187,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
Mockito.when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
|
Mockito.when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
|
||||||
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
|
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
|
||||||
|
|
||||||
underTest.processLatestTransaction(loanTransaction, currency, List.of(installment), Set.of(charge),
|
underTest.processLatestTransaction(loanTransaction,
|
||||||
new MoneyHolder(overpaidAmount));
|
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
|
||||||
|
|
||||||
Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
|
Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
|
||||||
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney),
|
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney),
|
||||||
@@ -239,8 +241,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
Mockito.when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
|
Mockito.when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
|
||||||
Mockito.when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true);
|
Mockito.when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true);
|
||||||
|
|
||||||
underTest.processLatestTransaction(loanTransaction, currency, List.of(installment), Set.of(charge),
|
underTest.processLatestTransaction(loanTransaction,
|
||||||
new MoneyHolder(overpaidAmount));
|
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
|
||||||
|
|
||||||
Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
|
Mockito.verify(installment, 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(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
|
||||||
@@ -255,19 +257,22 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
public void testProcessCreditTransactionWithAllocationRuleInterestAndPrincipal() {
|
public void testProcessCreditTransactionWithAllocationRuleInterestAndPrincipal() {
|
||||||
// given
|
// given
|
||||||
Loan loan = mock(Loan.class);
|
Loan loan = mock(Loan.class);
|
||||||
|
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan);
|
||||||
|
|
||||||
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(INTEREST, PRINCIPAL, PENALTY, FEE);
|
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(INTEREST, PRINCIPAL, PENALTY, FEE);
|
||||||
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
||||||
LoanTransaction repayment = createRepayment(loan);
|
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction);
|
||||||
lenient().when(loan.getLoanTransactions(any())).thenReturn(List.of(repayment));
|
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
|
||||||
|
|
||||||
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan);
|
|
||||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
||||||
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
|
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
|
||||||
LoanRepaymentScheduleInstallment installment = createMockInstallment(LocalDate.of(2023, 1, 31), false);
|
LoanRepaymentScheduleInstallment installment = createMockInstallment(LocalDate.of(2023, 1, 31), false);
|
||||||
installments.add(installment);
|
installments.add(installment);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
underTest.processCreditTransaction(chargeBackTransaction, overpaymentHolder, MONETARY_CURRENCY, installments);
|
|
||||||
|
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
|
||||||
|
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
Mockito.verify(installment, Mockito.times(1)).addToCredits(new BigDecimal("25.00"));
|
Mockito.verify(installment, Mockito.times(1)).addToCredits(new BigDecimal("25.00"));
|
||||||
@@ -294,19 +299,21 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterest() {
|
public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterest() {
|
||||||
// given
|
// given
|
||||||
Loan loan = mock(Loan.class);
|
Loan loan = mock(Loan.class);
|
||||||
|
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan);
|
||||||
|
|
||||||
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE);
|
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE);
|
||||||
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
||||||
LoanTransaction repayment = createRepayment(loan);
|
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction);
|
||||||
lenient().when(loan.getLoanTransactions(any())).thenReturn(List.of(repayment));
|
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
|
||||||
|
|
||||||
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan);
|
|
||||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
||||||
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
|
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
|
||||||
LoanRepaymentScheduleInstallment installment = createMockInstallment(LocalDate.of(2023, 1, 31), false);
|
LoanRepaymentScheduleInstallment installment = createMockInstallment(LocalDate.of(2023, 1, 31), false);
|
||||||
installments.add(installment);
|
installments.add(installment);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
underTest.processCreditTransaction(chargeBackTransaction, overpaymentHolder, MONETARY_CURRENCY, installments);
|
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
|
||||||
|
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
Mockito.verify(installment, Mockito.times(1)).addToCredits(new BigDecimal("25.00"));
|
Mockito.verify(installment, Mockito.times(1)).addToCredits(new BigDecimal("25.00"));
|
||||||
@@ -333,12 +340,13 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment() {
|
public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment() {
|
||||||
// given
|
// given
|
||||||
Loan loan = mock(Loan.class);
|
Loan loan = mock(Loan.class);
|
||||||
|
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan);
|
||||||
|
|
||||||
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE);
|
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE);
|
||||||
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
||||||
LoanTransaction repayment = createRepayment(loan);
|
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction);
|
||||||
lenient().when(loan.getLoanTransactions(any())).thenReturn(List.of(repayment));
|
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
|
||||||
|
|
||||||
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan);
|
|
||||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
||||||
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
|
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
|
||||||
LoanRepaymentScheduleInstallment installment1 = createMockInstallment(LocalDate.of(2022, 12, 20), false);
|
LoanRepaymentScheduleInstallment installment1 = createMockInstallment(LocalDate.of(2022, 12, 20), false);
|
||||||
@@ -347,7 +355,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
installments.add(installment2);
|
installments.add(installment2);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
underTest.processCreditTransaction(chargeBackTransaction, overpaymentHolder, MONETARY_CURRENCY, installments);
|
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
|
||||||
|
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
Mockito.verify(installment2, Mockito.times(1)).addToCredits(new BigDecimal("25.00"));
|
Mockito.verify(installment2, Mockito.times(1)).addToCredits(new BigDecimal("25.00"));
|
||||||
@@ -387,7 +396,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
return mockCreditAllocationRule;
|
return mockCreditAllocationRule;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoanTransaction createRepayment(Loan loan) {
|
private LoanTransaction createRepayment(Loan loan, LoanTransaction toTransaction) {
|
||||||
LoanTransaction repayment = mock(LoanTransaction.class);
|
LoanTransaction repayment = mock(LoanTransaction.class);
|
||||||
lenient().when(repayment.getLoan()).thenReturn(loan);
|
lenient().when(repayment.getLoan()).thenReturn(loan);
|
||||||
lenient().when(repayment.isRepayment()).thenReturn(true);
|
lenient().when(repayment.isRepayment()).thenReturn(true);
|
||||||
@@ -396,13 +405,19 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(20));
|
lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(20));
|
||||||
lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.ZERO);
|
lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.ZERO);
|
||||||
lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.ZERO);
|
lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
|
||||||
|
lenient().when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
|
||||||
|
lenient().when(relation.getToTransaction()).thenReturn(toTransaction);
|
||||||
|
|
||||||
|
lenient().when(repayment.getLoanTransactionRelations()).thenReturn(Set.of(relation));
|
||||||
return repayment;
|
return repayment;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoanTransaction createChargebackTransaction(Loan loan) {
|
private LoanTransaction createChargebackTransaction(Loan loan) {
|
||||||
LoanTransaction chargeback = mock(LoanTransaction.class);
|
LoanTransaction chargeback = mock(LoanTransaction.class);
|
||||||
lenient().when(chargeback.isChargeback()).thenReturn(true);
|
lenient().when(chargeback.isChargeback()).thenReturn(true);
|
||||||
lenient().when(chargeback.getTypeOf()).thenReturn(CHARGEBACK);
|
lenient().when(chargeback.getTypeOf()).thenReturn(LoanTransactionType.CHARGEBACK);
|
||||||
lenient().when(chargeback.getLoan()).thenReturn(loan);
|
lenient().when(chargeback.getLoan()).thenReturn(loan);
|
||||||
lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(25));
|
lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(25));
|
||||||
Money amount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(25));
|
Money amount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(25));
|
||||||
@@ -453,4 +468,108 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
return allocationMap;
|
return allocationMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionWhenIdProvided() {
|
||||||
|
// given
|
||||||
|
LoanTransaction chargebackTransaction = mock(LoanTransaction.class);
|
||||||
|
Mockito.when(chargebackTransaction.getId()).thenReturn(123L);
|
||||||
|
Loan loan = mock(Loan.class);
|
||||||
|
Mockito.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));
|
||||||
|
|
||||||
|
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));
|
||||||
|
TransactionCtx ctx = mock(TransactionCtx.class);
|
||||||
|
|
||||||
|
// when
|
||||||
|
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackTransaction, ctx);
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertEquals(originalTransaction, repayment2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindOriginalTransactionThrowsRuntimeExceptionWhenIdProvidedAndRelationsAreMissing() {
|
||||||
|
// given
|
||||||
|
LoanTransaction chargebackTransaction = mock(LoanTransaction.class);
|
||||||
|
Mockito.when(chargebackTransaction.getId()).thenReturn(123L);
|
||||||
|
Loan loan = mock(Loan.class);
|
||||||
|
Mockito.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));
|
||||||
|
|
||||||
|
Mockito.when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of());
|
||||||
|
|
||||||
|
TransactionCtx ctx = mock(TransactionCtx.class);
|
||||||
|
|
||||||
|
// when + then
|
||||||
|
RuntimeException runtimeException = Assertions.assertThrows(RuntimeException.class,
|
||||||
|
() -> underTest.findOriginalTransaction(chargebackTransaction, ctx));
|
||||||
|
Assertions.assertEquals("Chargeback transaction must have an original transaction", runtimeException.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvided() {
|
||||||
|
// given
|
||||||
|
LoanTransaction chargebackReplayed = mock(LoanTransaction.class);
|
||||||
|
Mockito.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);
|
||||||
|
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));
|
||||||
|
|
||||||
|
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
|
||||||
|
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertEquals(originalTransaction, repayment2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvidedFallbackToPersistedTransactions() {
|
||||||
|
// given
|
||||||
|
LoanTransaction chargebackReplayed = mock(LoanTransaction.class);
|
||||||
|
Mockito.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));
|
||||||
|
|
||||||
|
LoanTransaction originalChargeback = mock(LoanTransaction.class);
|
||||||
|
Mockito.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));
|
||||||
|
|
||||||
|
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
|
||||||
|
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertEquals(originalTransaction, repayment2);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-9
@@ -38,7 +38,6 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
|||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.junit.jupiter.api.Disabled;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
// Create Client
|
// Create Client
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
// Create Loan Product
|
// Create Loan Product
|
||||||
Long loanProductId = createLoanProduct(charbackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
|
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
|
||||||
|
|
||||||
// Apply and Approve Loan
|
// Apply and Approve Loan
|
||||||
Long loanId = applyAndApproveLoan(clientId, loanProductId);
|
Long loanId = applyAndApproveLoan(clientId, loanProductId);
|
||||||
@@ -115,7 +114,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
// Create Client
|
// Create Client
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
// Create Loan Product
|
// Create Loan Product
|
||||||
Long loanProductId = createLoanProduct(charbackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
|
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
|
||||||
|
|
||||||
// Apply and Approve Loan
|
// Apply and Approve Loan
|
||||||
Long loanId = applyAndApproveLoan(clientId, loanProductId);
|
Long loanId = applyAndApproveLoan(clientId, loanProductId);
|
||||||
@@ -186,13 +185,12 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
|
||||||
public void createLoanWithCreditAllocationAndChargebackReverseReplayWithBackdatedPayment() {
|
public void createLoanWithCreditAllocationAndChargebackReverseReplayWithBackdatedPayment() {
|
||||||
runAt("01 January 2023", () -> {
|
runAt("01 January 2023", () -> {
|
||||||
// Create Client
|
// Create Client
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
// Create Loan Product
|
// Create Loan Product
|
||||||
Long loanProductId = createLoanProduct(charbackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
|
Long loanProductId = createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL"));
|
||||||
|
|
||||||
// Apply and Approve Loan
|
// Apply and Approve Loan
|
||||||
Long loanId = applyAndApproveLoan(clientId, loanProductId);
|
Long loanId = applyAndApproveLoan(clientId, loanProductId);
|
||||||
@@ -248,14 +246,91 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
|
installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
|
||||||
);
|
);
|
||||||
|
|
||||||
// let's add a backdated repayment on 19th of January reverse replaying the chargeback
|
// let's add a backdated repayment on 19th of January to trigger reverse replaying the chargeback, that will
|
||||||
|
// pay both the charges earlier.
|
||||||
addRepaymentForLoan(loanId, 200.0, "19 January 2023");
|
addRepaymentForLoan(loanId, 200.0, "19 January 2023");
|
||||||
|
|
||||||
verifyTransactions(loanId, //
|
verifyTransactions(loanId, //
|
||||||
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||||
transaction(200.0, "Repayment", "19 January 2023", 1120.0, 130.0, 0.0, 50.0, 20.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(383.0, "Repayment", "20 January 2023", 737.0, 383.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) //
|
transaction(100.0, "Chargeback", "21 January 2023", 837.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createLoanWithCreditAllocationAndOnlyTheChargebackReverseReplayedWithBackdatedPayment() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
// Create Client
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
// Create Loan Product
|
||||||
|
Long loanProductId = createLoanProduct(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") //
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("22 January 2023");
|
||||||
|
|
||||||
|
// Add Chargeback20 penalty + 50 fee + 0 interest + 30 principal
|
||||||
|
addChargebackForLoan(loanId, repaymentTransaction, 100.0);
|
||||||
|
|
||||||
|
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) //
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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(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 a backdated repayment on 21th of January that will reverse replay the chargeback transaction
|
||||||
|
// but will leave the
|
||||||
|
// original repayment from 20th of January unchanged.
|
||||||
|
addRepaymentForLoan(loanId, 200.0, "21 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(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) //
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -266,7 +341,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
// Create Client
|
// Create Client
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
// Create Loan Product
|
// Create Loan Product
|
||||||
Long loanProductId = createLoanProduct(charbackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY"));
|
Long loanProductId = createLoanProduct(chargebackAllocation("PRINCIPAL", "INTEREST", "FEE", "PENALTY"));
|
||||||
|
|
||||||
// Apply and Approve Loan
|
// Apply and Approve Loan
|
||||||
Long loanId = applyAndApproveLoan(clientId, loanProductId);
|
Long loanId = applyAndApproveLoan(clientId, loanProductId);
|
||||||
@@ -388,7 +463,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
return advancedPaymentData;
|
return advancedPaymentData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CreditAllocationData charbackAllocation(String... allocationRules) {
|
private CreditAllocationData chargebackAllocation(String... allocationRules) {
|
||||||
CreditAllocationData creditAllocationData = new CreditAllocationData();
|
CreditAllocationData creditAllocationData = new CreditAllocationData();
|
||||||
creditAllocationData.setTransactionType("CHARGEBACK");
|
creditAllocationData.setTransactionType("CHARGEBACK");
|
||||||
creditAllocationData.setCreditAllocationOrder(createCreditAllocationOrders(allocationRules));
|
creditAllocationData.setCreditAllocationOrder(createCreditAllocationOrders(allocationRules));
|
||||||
|
|||||||
Reference in New Issue
Block a user