This is an automated email from the ASF dual-hosted git repository.
bagrijp pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 10c314e5d FINERACT-2042 Reverse Replay of Credit Allocation
10c314e5d is described below
commit 10c314e5d96c01cfd6239fb4b42affda8b41e971
Author: Peter Bagrij <[email protected]>
AuthorDate: Mon Feb 12 11:53:56 2024 +0100
FINERACT-2042 Reverse Replay of Credit Allocation
---
.../domain/ChangedTransactionDetail.java | 6 +-
.../portfolio/loanaccount/domain/Loan.java | 25 ++--
...tLoanRepaymentScheduleTransactionProcessor.java | 26 ++--
.../LoanRepaymentScheduleTransactionProcessor.java | 23 ++-
...dvancedPaymentScheduleTransactionProcessor.java | 125 +++++++++-------
.../LoanChargeWritePlatformServiceImpl.java | 6 +-
...cedPaymentScheduleTransactionProcessorTest.java | 163 ++++++++++++++++++---
...ebackWithCreditAllocationsIntegrationTests.java | 93 ++++++++++--
8 files changed, 353 insertions(+), 114 deletions(-)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
index de2c149d7..79e6e74d5 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ChangedTransactionDetail.java
@@ -20,16 +20,16 @@ package org.apache.fineract.portfolio.loanaccount.domain;
import java.util.LinkedHashMap;
import java.util.Map;
+import lombok.Getter;
/**
* Stores details of {@link LoanTransaction}'s that were reversed or newly
created
*/
+@Getter
public class ChangedTransactionDetail {
private final Map<Long, LoanTransaction> newTransactionMappings = new
LinkedHashMap<>();
- public Map<Long, LoanTransaction> getNewTransactionMappings() {
- return this.newTransactionMappings;
- }
+ private final Map<LoanTransaction, Long> currentTransactionToOldId = new
LinkedHashMap<>();
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index 400c769ee..9d2dca1a6 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -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.ScheduleGeneratorDTO;
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.exception.ExceedingTrancheCountException;
import
org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
@@ -800,8 +801,8 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
}
final Set<LoanCharge> loanCharges = new HashSet<>(1);
loanCharges.add(charge);
-
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargesPayment,
getCurrency(), chargePaymentInstallments,
- loanCharges, new MoneyHolder(getTotalOverpaidAsMoney()));
+
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargesPayment,
+ new TransactionCtx(getCurrency(), chargePaymentInstallments,
loanCharges, new MoneyHolder(getTotalOverpaidAsMoney())));
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(chargesPayment.getTransactionDate(),
loanLifecycleStateMachine);
@@ -3324,8 +3325,8 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
if (isTransactionChronologicallyLatest && adjustedTransaction == null
&& (!reprocess ||
!this.repaymentScheduleDetail().isInterestRecalculationEnabled()) &&
!isForeclosure()) {
-
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
getCurrency(),
- getRepaymentScheduleInstallments(), getActiveCharges(),
new MoneyHolder(getTotalOverpaidAsMoney()));
+
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
new TransactionCtx(getCurrency(),
+ getRepaymentScheduleInstallments(), getActiveCharges(),
new MoneyHolder(getTotalOverpaidAsMoney())));
reprocess = false;
if
(this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (currentInstallment == null ||
currentInstallment.isNotFullyPaidOff()) {
@@ -3917,8 +3918,8 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
}
addLoanTransaction(loanTransaction);
-
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
loanCurrency(),
- getRepaymentScheduleInstallments(), getActiveCharges(),
new MoneyHolder(getTotalOverpaidAsMoney()));
+
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
new TransactionCtx(loanCurrency(),
+ getRepaymentScheduleInstallments(), getActiveCharges(),
new MoneyHolder(getTotalOverpaidAsMoney())));
updateLoanSummaryDerivedFields();
}
@@ -4022,8 +4023,8 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
}
addLoanTransaction(loanTransaction);
-
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
loanCurrency(),
- getRepaymentScheduleInstallments(),
getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
+
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
new TransactionCtx(loanCurrency(),
+ getRepaymentScheduleInstallments(),
getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
updateLoanSummaryDerivedFields();
} else if (totalOutstanding.isGreaterThanZero()) {
@@ -6377,8 +6378,8 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
// If is a refund
if (adjustedTransaction == null) {
-
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
getCurrency(),
- getRepaymentScheduleInstallments(), getActiveCharges(),
new MoneyHolder(getTotalOverpaidAsMoney()));
+
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
new TransactionCtx(getCurrency(),
+ getRepaymentScheduleInstallments(), getActiveCharges(),
new MoneyHolder(getTotalOverpaidAsMoney())));
} else {
final List<LoanTransaction>
allNonContraTransactionsPostDisbursement =
retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail =
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
@@ -6408,8 +6409,8 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
.determineProcessor(this.transactionProcessingStrategyCode);
addLoanTransaction(chargebackTransaction);
-
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction,
getCurrency(),
- getRepaymentScheduleInstallments(), getActiveCharges(), new
MoneyHolder(getTotalOverpaidAsMoney()));
+
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction,
new TransactionCtx(getCurrency(),
+ getRepaymentScheduleInstallments(), getActiveCharges(), new
MoneyHolder(getTotalOverpaidAsMoney())));
updateLoanSummaryDerivedFields();
if
(!doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(),
loanLifecycleStateMachine)) {
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
index 583dc130b..596513e7e 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
@@ -164,7 +164,7 @@ public abstract class
AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (loanTransaction.isRepaymentLikeType() ||
loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
// pass through for new transactions
if (loanTransaction.getId() == null) {
- processLatestTransaction(loanTransaction, currency,
installments, charges, overpaymentHolder);
+ processLatestTransaction(loanTransaction, new
TransactionCtx(currency, installments, charges, overpaymentHolder));
loanTransaction.adjustInterestComponent(currency);
} else {
/**
@@ -175,7 +175,7 @@ public abstract class
AbstractLoanRepaymentScheduleTransactionProcessor implemen
// Reset derived component of new loan transaction and
// re-process transaction
- processLatestTransaction(newLoanTransaction, currency,
installments, charges, overpaymentHolder);
+ processLatestTransaction(newLoanTransaction, new
TransactionCtx(currency, installments, charges, overpaymentHolder));
newLoanTransaction.adjustInterestComponent(currency);
/**
* 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
- public void processLatestTransaction(final LoanTransaction
loanTransaction, final MonetaryCurrency currency,
- final List<LoanRepaymentScheduleInstallment> installments, final
Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
+ public void processLatestTransaction(final LoanTransaction
loanTransaction, final TransactionCtx ctx) {
switch (loanTransaction.getTypeOf()) {
- case WRITEOFF -> handleWriteOff(loanTransaction, currency,
installments);
- case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction,
currency, installments, charges);
- case CHARGEBACK -> handleChargeback(loanTransaction, currency,
installments, overpaymentHolder);
+ case WRITEOFF -> handleWriteOff(loanTransaction,
ctx.getCurrency(), ctx.getInstallments());
+ case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction,
ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
+ case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
default -> {
- Money transactionAmountUnprocessed =
handleTransactionAndCharges(loanTransaction, currency, installments, charges,
null,
- false);
+ Money transactionAmountUnprocessed =
handleTransactionAndCharges(loanTransaction, ctx.getCurrency(),
ctx.getInstallments(),
+ ctx.getCharges(), null, false);
if (transactionAmountUnprocessed.isGreaterThanZero()) {
if (loanTransaction.isWaiver()) {
loanTransaction.updateComponentsAndTotal(transactionAmountUnprocessed.zero(),
transactionAmountUnprocessed.zero(),
@@ -228,9 +227,9 @@ public abstract class
AbstractLoanRepaymentScheduleTransactionProcessor implemen
onLoanOverpayment(loanTransaction,
transactionAmountUnprocessed);
loanTransaction.setOverPayments(transactionAmountUnprocessed);
}
-
overpaymentHolder.setMoneyObject(transactionAmountUnprocessed);
+
ctx.getOverpaymentHolder().setMoneyObject(transactionAmountUnprocessed);
} 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);
}
- protected void handleChargeback(LoanTransaction loanTransaction,
MonetaryCurrency currency,
- List<LoanRepaymentScheduleInstallment> installments, MoneyHolder
overpaidAmountHolder) {
- processCreditTransaction(loanTransaction, overpaidAmountHolder,
currency, installments);
+ protected void handleChargeback(LoanTransaction loanTransaction,
TransactionCtx ctx) {
+ processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(),
ctx.getCurrency(), ctx.getInstallments());
}
protected void handleCreditBalanceRefund(LoanTransaction loanTransaction,
MonetaryCurrency currency,
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
index ebf16d6a7..7e862ebe1 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
@@ -21,6 +21,8 @@ package
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor;
import java.time.LocalDate;
import java.util.List;
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.Money;
import
org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
@@ -30,6 +32,22 @@ import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
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 getName();
@@ -37,11 +55,10 @@ public interface LoanRepaymentScheduleTransactionProcessor {
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.
*/
- void processLatestTransaction(LoanTransaction loanTransaction,
MonetaryCurrency currency,
- List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, MoneyHolder overpaymentHolder);
+ void processLatestTransaction(LoanTransaction loanTransaction,
TransactionCtx ctx);
/**
* Provides support for passing all {@link LoanTransaction}'s so it will
completely re-process the entire loan
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index f9c223ca5..56e9dc90c 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -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.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.INTEREST;
import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
@@ -30,6 +31,7 @@ import java.math.MathContext;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
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.LoanTransaction;
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.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
@@ -164,19 +167,20 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
}
@Override
- public void processLatestTransaction(LoanTransaction loanTransaction,
MonetaryCurrency currency,
- List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
+ public void processLatestTransaction(LoanTransaction loanTransaction,
TransactionCtx ctx) {
switch (loanTransaction.getTypeOf()) {
- case DISBURSEMENT -> handleDisbursement(loanTransaction, currency,
installments, overpaymentHolder);
- case WRITEOFF -> handleWriteOff(loanTransaction, currency,
installments);
- case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction,
currency, installments, charges);
- case CHARGEBACK -> handleChargeback(loanTransaction, currency,
installments, overpaymentHolder);
- case CREDIT_BALANCE_REFUND ->
handleCreditBalanceRefund(loanTransaction, currency, installments,
overpaymentHolder);
+ case DISBURSEMENT -> handleDisbursement(loanTransaction,
ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
+ case WRITEOFF -> handleWriteOff(loanTransaction,
ctx.getCurrency(), ctx.getInstallments());
+ case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction,
ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
+ case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
+ 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,
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
- handleRepayment(loanTransaction, currency, installments,
charges, overpaymentHolder);
- case CHARGE_OFF -> handleChargeOff(loanTransaction, currency,
installments);
- case CHARGE_PAYMENT -> handleChargePayment(loanTransaction,
currency, installments, charges, overpaymentHolder);
+ handleRepayment(loanTransaction, ctx.getCurrency(),
ctx.getInstallments(), ctx.getCharges(), ctx.getOverpaymentHolder());
+ case CHARGE_OFF -> handleChargeOff(loanTransaction,
ctx.getCurrency(), ctx.getInstallments());
+ 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.");
// TODO: Cover rest of the transaction types
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) {
return (loanTransaction.getLoan().getCreditAllocationRules() == null
|| !loanTransaction.getLoan().getCreditAllocationRules()
.stream().anyMatch(e ->
e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
}
- @Override
- protected void processCreditTransaction(LoanTransaction loanTransaction,
MoneyHolder overpaymentHolder, MonetaryCurrency currency,
- List<LoanRepaymentScheduleInstallment> installments) {
+ protected LoanTransaction findOriginalTransaction(LoanTransaction
loanTransaction, TransactionCtx ctx) {
+ if (loanTransaction.getId() != null) { // this the normal case without
reverse-replay
+ 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)) {
- super.processCreditTransaction(loanTransaction, overpaymentHolder,
currency, installments);
+ 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);
- installments.sort(byDate);
- final Money zeroMoney = Money.zero(currency);
- Money transactionAmount = loanTransaction.getAmount(currency);
+ ctx.getInstallments().sort(byDate);
+ final Money zeroMoney = Money.zero(ctx.getCurrency());
+ Money transactionAmount =
loanTransaction.getAmount(ctx.getCurrency());
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));
loanTransaction.setOverPayments(repaidAmount);
-
overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(repaidAmount));
+
ctx.getOverpaymentHolder().setMoneyObject(ctx.getOverpaymentHolder().getMoneyObject().minus(repaidAmount));
if (amountToDistribute.isGreaterThanZero()) {
if (loanTransaction.isChargeback()) {
- Optional<LoanTransaction> originalTransaction =
loanTransaction.getLoan().getLoanTransactions(
- tr ->
tr.getLoanTransactionRelations().stream().anyMatch(this.hasMatchingToLoanTransaction(loanTransaction)))
- .stream().findFirst();
- if (originalTransaction.isEmpty()) {
- throw new RuntimeException("Chargeback transaction
must have an original transaction");
- }
-
- Map<AllocationType, BigDecimal> originalAllocation =
getOriginalAllocation(originalTransaction.get());
+ LoanTransaction originalTransaction =
findOriginalTransaction(loanTransaction, ctx);
+ Map<AllocationType, BigDecimal> originalAllocation =
getOriginalAllocation(originalTransaction);
LoanCreditAllocationRule chargeBackAllocationRule =
getChargebackAllocationRules(loanTransaction);
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),
chargebackAllocation.get(FEE),
chargebackAllocation.get(PENALTY));
@@ -230,13 +263,13 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
final LocalDate transactionDate =
loanTransaction.getTransactionDate();
boolean loanTransactionMapped = false;
LocalDate pastDueDate = null;
- for (final LoanRepaymentScheduleInstallment
currentInstallment : installments) {
+ 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(currency);
+ Money originalInterest =
currentInstallment.getInterestCharged(ctx.getCurrency());
currentInstallment.updateInterestCharged(
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
@@ -256,7 +289,7 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
}
currentInstallment.addToCredits(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate,
chargebackAllocation.get(PRINCIPAL));
- Money originalInterest =
currentInstallment.getInterestCharged(currency);
+ Money originalInterest =
currentInstallment.getInterestCharged(ctx.getCurrency());
currentInstallment.updateInterestCharged(
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
if (repaidAmount.isGreaterThanZero()) {
@@ -272,10 +305,11 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
// New installment will be added (N+1 scenario)
if (!loanTransactionMapped) {
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.addToPrincipal(transactionDate,
chargebackAllocation.get(PRINCIPAL));
- Money originalInterest =
currentInstallment.getInterestCharged(currency);
+ Money originalInterest =
currentInstallment.getInterestCharged(ctx.getCurrency());
currentInstallment.updateInterestCharged(
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
if (repaidAmount.isGreaterThanZero()) {
@@ -286,12 +320,12 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
} else {
Loan loan = loanTransaction.getLoan();
LoanRepaymentScheduleInstallment installment = new
LoanRepaymentScheduleInstallment(loan,
- (installments.size() + 1), pastDueDate,
transactionDate, zeroMoney.getAmount(), zeroMoney.getAmount(),
- zeroMoney.getAmount(),
zeroMoney.getAmount(), false, null);
+ (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(currency);
+ Money originalInterest =
installment.getInterestCharged(ctx.getCurrency());
installment.updateInterestCharged(
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
loan.addLoanRepaymentScheduleInstallment(installment);
@@ -348,17 +382,8 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
return result;
}
- private Predicate<LoanTransactionRelation>
hasMatchingToLoanTransaction(LoanTransaction loanTransaction) {
- return relation -> {
- 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;
- }
- };
+ private Predicate<LoanTransactionRelation>
hasMatchingToLoanTransaction(Long id, LoanTransactionRelationTypeEnum typeEnum)
{
+ return relation -> relation.getRelationType().equals(typeEnum) &&
Objects.equals(relation.getToTransaction().getId(), id);
}
@Override
@@ -419,8 +444,9 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
private void processSingleTransaction(LoanTransaction loanTransaction,
MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, ChangedTransactionDetail changedTransactionDetail,
MoneyHolder overpaymentHolder) {
+ TransactionCtx ctx = new TransactionCtx(currency, installments,
charges, overpaymentHolder, changedTransactionDetail);
if (loanTransaction.getId() == null) {
- processLatestTransaction(loanTransaction, currency, installments,
charges, overpaymentHolder);
+ processLatestTransaction(loanTransaction, ctx);
if (loanTransaction.isInterestWaiver()) {
loanTransaction.adjustInterestComponent(currency);
}
@@ -430,10 +456,11 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
* changed.<br>
*/
final LoanTransaction newLoanTransaction =
LoanTransaction.copyTransactionProperties(loanTransaction);
+
ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().put(newLoanTransaction,
loanTransaction.getId());
// Reset derived component of new loan transaction and
// re-process transaction
- processLatestTransaction(newLoanTransaction, currency,
installments, charges, overpaymentHolder);
+ processLatestTransaction(newLoanTransaction, ctx);
if (loanTransaction.isInterestWaiver()) {
newLoanTransaction.adjustInterestComponent(currency);
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
index 4cead1e12..8fce7d71e 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
@@ -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.LoanTransactionType;
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.impl.AdvancedPaymentScheduleTransactionProcessor;
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);
final LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor =
loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(loan.transactionProcessingStrategy());
-
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction,
loan.getCurrency(),
- loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()));
+
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction,
+ new TransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
+ new MoneyHolder(loan.getTotalOverpaidAsMoney())));
loan.addLoanTransaction(loanChargeAdjustmentTransaction);
loan.updateLoanSummaryAndStatus();
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
index 6facec110..fb79dc290 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
@@ -18,14 +18,12 @@
*/
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.loanproduct.domain.AllocationType.FEE;
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.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.lenient;
@@ -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.Money;
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.LoanCharge;
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.LoanRepaymentScheduleInstallment;
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.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
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(loanTransaction.isPenaltyPayment()).thenReturn(false);
- underTest.processLatestTransaction(loanTransaction, currency,
List.of(installment), Set.of(charge),
- new MoneyHolder(overpaidAmount));
+ underTest.processLatestTransaction(loanTransaction,
+ new TransactionCtx(currency, List.of(installment),
Set.of(charge), new MoneyHolder(overpaidAmount)));
Mockito.verify(installment,
Mockito.times(1)).payFeeChargesComponent(eq(transactionDate),
eq(chargeAmountMoney));
Mockito.verify(loanTransaction,
Mockito.times(1)).updateComponents(refEq(zero), refEq(zero),
refEq(chargeAmountMoney), refEq(zero));
@@ -185,8 +187,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.when(charge.updatePaidAmountBy(refEq(transactionAmountMoney),
eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false);
- underTest.processLatestTransaction(loanTransaction, currency,
List.of(installment), Set.of(charge),
- new MoneyHolder(overpaidAmount));
+ underTest.processLatestTransaction(loanTransaction,
+ new TransactionCtx(currency, List.of(installment),
Set.of(charge), new MoneyHolder(overpaidAmount)));
Mockito.verify(installment,
Mockito.times(1)).payFeeChargesComponent(eq(transactionDate),
eq(transactionAmountMoney));
Mockito.verify(loanTransaction,
Mockito.times(1)).updateComponents(refEq(zero), refEq(zero),
refEq(transactionAmountMoney),
@@ -239,8 +241,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
Mockito.when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true);
- underTest.processLatestTransaction(loanTransaction, currency,
List.of(installment), Set.of(charge),
- new MoneyHolder(overpaidAmount));
+ underTest.processLatestTransaction(loanTransaction,
+ new TransactionCtx(currency, List.of(installment),
Set.of(charge), new MoneyHolder(overpaidAmount)));
Mockito.verify(installment,
Mockito.times(1)).payFeeChargesComponent(eq(transactionDate),
eq(chargeAmountMoney));
Mockito.verify(loanTransaction,
Mockito.times(1)).updateComponents(refEq(zero), refEq(zero),
refEq(chargeAmountMoney), refEq(zero));
@@ -255,19 +257,22 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void
testProcessCreditTransactionWithAllocationRuleInterestAndPrincipal() {
// given
Loan loan = mock(Loan.class);
+ LoanTransaction chargeBackTransaction =
createChargebackTransaction(loan);
+
LoanCreditAllocationRule mockCreditAllocationRule =
createMockCreditAllocationRule(INTEREST, PRINCIPAL, PENALTY, FEE);
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
- LoanTransaction repayment = createRepayment(loan);
-
lenient().when(loan.getLoanTransactions(any())).thenReturn(List.of(repayment));
+ LoanTransaction repayment = createRepayment(loan,
chargeBackTransaction);
+
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
- LoanTransaction chargeBackTransaction =
createChargebackTransaction(loan);
MoneyHolder overpaymentHolder = new
MoneyHolder(Money.zero(MONETARY_CURRENCY));
List<LoanRepaymentScheduleInstallment> installments = new
ArrayList<>();
LoanRepaymentScheduleInstallment installment =
createMockInstallment(LocalDate.of(2023, 1, 31), false);
installments.add(installment);
// when
- underTest.processCreditTransaction(chargeBackTransaction,
overpaymentHolder, MONETARY_CURRENCY, installments);
+
+ TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY,
installments, null, overpaymentHolder);
+ underTest.processCreditTransaction(chargeBackTransaction, ctx);
// then
Mockito.verify(installment, Mockito.times(1)).addToCredits(new
BigDecimal("25.00"));
@@ -294,19 +299,21 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void
testProcessCreditTransactionWithAllocationRulePrincipalAndInterest() {
// given
Loan loan = mock(Loan.class);
+ LoanTransaction chargeBackTransaction =
createChargebackTransaction(loan);
+
LoanCreditAllocationRule mockCreditAllocationRule =
createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE);
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
- LoanTransaction repayment = createRepayment(loan);
-
lenient().when(loan.getLoanTransactions(any())).thenReturn(List.of(repayment));
+ LoanTransaction repayment = createRepayment(loan,
chargeBackTransaction);
+
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
- LoanTransaction chargeBackTransaction =
createChargebackTransaction(loan);
MoneyHolder overpaymentHolder = new
MoneyHolder(Money.zero(MONETARY_CURRENCY));
List<LoanRepaymentScheduleInstallment> installments = new
ArrayList<>();
LoanRepaymentScheduleInstallment installment =
createMockInstallment(LocalDate.of(2023, 1, 31), false);
installments.add(installment);
// when
- underTest.processCreditTransaction(chargeBackTransaction,
overpaymentHolder, MONETARY_CURRENCY, installments);
+ TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY,
installments, null, overpaymentHolder);
+ underTest.processCreditTransaction(chargeBackTransaction, ctx);
// then
Mockito.verify(installment, Mockito.times(1)).addToCredits(new
BigDecimal("25.00"));
@@ -333,12 +340,13 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void
testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment()
{
// given
Loan loan = mock(Loan.class);
+ LoanTransaction chargeBackTransaction =
createChargebackTransaction(loan);
+
LoanCreditAllocationRule mockCreditAllocationRule =
createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE);
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
- LoanTransaction repayment = createRepayment(loan);
-
lenient().when(loan.getLoanTransactions(any())).thenReturn(List.of(repayment));
+ LoanTransaction repayment = createRepayment(loan,
chargeBackTransaction);
+
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
- LoanTransaction chargeBackTransaction =
createChargebackTransaction(loan);
MoneyHolder overpaymentHolder = new
MoneyHolder(Money.zero(MONETARY_CURRENCY));
List<LoanRepaymentScheduleInstallment> installments = new
ArrayList<>();
LoanRepaymentScheduleInstallment installment1 =
createMockInstallment(LocalDate.of(2022, 12, 20), false);
@@ -347,7 +355,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
installments.add(installment2);
// when
- underTest.processCreditTransaction(chargeBackTransaction,
overpaymentHolder, MONETARY_CURRENCY, installments);
+ TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY,
installments, null, overpaymentHolder);
+ underTest.processCreditTransaction(chargeBackTransaction, ctx);
// then
Mockito.verify(installment2, Mockito.times(1)).addToCredits(new
BigDecimal("25.00"));
@@ -387,7 +396,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
return mockCreditAllocationRule;
}
- private LoanTransaction createRepayment(Loan loan) {
+ private LoanTransaction createRepayment(Loan loan, LoanTransaction
toTransaction) {
LoanTransaction repayment = mock(LoanTransaction.class);
lenient().when(repayment.getLoan()).thenReturn(loan);
lenient().when(repayment.isRepayment()).thenReturn(true);
@@ -396,13 +405,19 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(20));
lenient().when(repayment.getFeeChargesPortion()).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;
}
private LoanTransaction createChargebackTransaction(Loan loan) {
LoanTransaction chargeback = mock(LoanTransaction.class);
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.getAmount()).thenReturn(BigDecimal.valueOf(25));
Money amount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(25));
@@ -453,4 +468,108 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
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);
+ }
+
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java
index 620a1bb0d..1f761043f 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java
@@ -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.loanproduct.domain.PaymentAllocationType;
import org.jetbrains.annotations.Nullable;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -52,7 +51,7 @@ public class
LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
// Create Client
Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
- Long loanProductId =
createLoanProduct(charbackAllocation("PENALTY", "FEE", "INTEREST",
"PRINCIPAL"));
+ Long loanProductId =
createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST",
"PRINCIPAL"));
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -115,7 +114,7 @@ public class
LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
// Create Client
Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
- Long loanProductId =
createLoanProduct(charbackAllocation("PENALTY", "FEE", "INTEREST",
"PRINCIPAL"));
+ Long loanProductId =
createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST",
"PRINCIPAL"));
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -186,13 +185,12 @@ public class
LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
}
@Test
- @Disabled
public void
createLoanWithCreditAllocationAndChargebackReverseReplayWithBackdatedPayment() {
runAt("01 January 2023", () -> {
// Create Client
Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
- Long loanProductId =
createLoanProduct(charbackAllocation("PENALTY", "FEE", "INTEREST",
"PRINCIPAL"));
+ Long loanProductId =
createLoanProduct(chargebackAllocation("PENALTY", "FEE", "INTEREST",
"PRINCIPAL"));
// Apply and Approve Loan
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") //
);
- // 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");
verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023",
1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(200.0, "Repayment", "19 January 2023", 1120.0,
130.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
transaction(383.0, "Repayment", "20 January 2023", 737.0,
383.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
- transaction(100.0, "Chargeback", "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
Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
- Long loanProductId =
createLoanProduct(charbackAllocation("PRINCIPAL", "INTEREST", "FEE",
"PENALTY"));
+ Long loanProductId =
createLoanProduct(chargebackAllocation("PRINCIPAL", "INTEREST", "FEE",
"PENALTY"));
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductId);
@@ -388,7 +463,7 @@ public class
LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
return advancedPaymentData;
}
- private CreditAllocationData charbackAllocation(String... allocationRules)
{
+ private CreditAllocationData chargebackAllocation(String...
allocationRules) {
CreditAllocationData creditAllocationData = new CreditAllocationData();
creditAllocationData.setTransactionType("CHARGEBACK");
creditAllocationData.setCreditAllocationOrder(createCreditAllocationOrders(allocationRules));