FINERACT-2042 Reverse Replay of Credit Allocation
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 de2c149..79e6e74 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 @@
 
 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 400c769..9d2dca1 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.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 @@
         }
         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 @@
 
         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 @@
             }
 
             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 @@
                 }
 
                 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 @@
 
         // 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 @@
                 .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 583dc13..596513e 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 @@
             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 @@
 
                     // 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 @@
     }
 
     @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 @@
                         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 @@
         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 ebf16d6..7e862eb 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 @@
 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 @@
 
 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 @@
     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 f9c223c..56e9dc9 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 @@
 
 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.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.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 @@
     }
 
     @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 @@
         }
     }
 
+    @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 @@
                     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 @@
                             }
                             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 @@
                     // 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 @@
                         } 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 @@
         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 @@
     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 @@
              * 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 4cead1e..8fce7d7 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.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 @@
         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 6facec1..fb79dc2 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.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.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 @@
         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 @@
         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 @@
         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 @@
     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 @@
     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 @@
     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 @@
         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 @@
         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 @@
         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 @@
         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 620a1bb..1f76104 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.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 @@
             // 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 @@
             // 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 @@
     }
 
     @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 @@
                     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 @@
             // 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 @@
         return advancedPaymentData;
     }
 
-    private CreditAllocationData charbackAllocation(String... allocationRules) {
+    private CreditAllocationData chargebackAllocation(String... allocationRules) {
         CreditAllocationData creditAllocationData = new CreditAllocationData();
         creditAllocationData.setTransactionType("CHARGEBACK");
         creditAllocationData.setCreditAllocationOrder(createCreditAllocationOrders(allocationRules));