This is an automated email from the ASF dual-hosted git repository.

adamsaghy 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 937098c4e FINERACT-1905 Loan Charge Reverse Replay with Advanced 
Payment Allocation
937098c4e is described below

commit 937098c4ee47ff7fad734403f8980ab8349b7e2c
Author: Peter Bagrij <[email protected]>
AuthorDate: Fri Oct 20 13:57:11 2023 +0200

    FINERACT-1905 Loan Charge Reverse Replay with Advanced Payment Allocation
---
 .../portfolio/loanaccount/domain/Loan.java         |   4 +-
 .../domain/LoanRepaymentScheduleInstallment.java   |  19 +
 .../LoanRepaymentScheduleProcessingWrapper.java    | 148 ++----
 ...anChargeRepaymentScheduleProcessingWrapper.java | 216 ++++++++
 ...argeRepaymentScheduleProcessingWrapperTest.java | 154 ++++++
 ...dvancedPaymentScheduleTransactionProcessor.java |  97 ++--
 .../impl/ChargeOrTransaction.java                  |  88 ++++
 .../impl/ChargeOrTransactionTest.java              | 107 ++++
 ...ChargeOffWithAdvancedPaymentAllocationTest.java | 554 +++++++++++----------
 ...eseReplayWithAdvancedPaymentAllocationTest.java | 492 ++++++++++++++++++
 10 files changed, 1492 insertions(+), 387 deletions(-)

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 bc698e2bd..15cf20fee 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
@@ -694,8 +694,8 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom {
 
         // store Id's of existing loan transactions and existing reversed loan
         // transactions
-        final LoanRepaymentScheduleProcessingWrapper wrapper = new 
LoanRepaymentScheduleProcessingWrapper();
-        wrapper.reprocess(getCurrency(), getDisbursementDate(), 
getRepaymentScheduleInstallments(), getActiveCharges());
+        final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new 
SingleLoanChargeRepaymentScheduleProcessingWrapper();
+        wrapper.reprocess(getCurrency(), getDisbursementDate(), 
getRepaymentScheduleInstallments(), loanCharge);
         updateLoanSummaryDerivedFields();
 
         loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, 
this);
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
index 18f305957..89f0e2587 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
@@ -419,6 +419,15 @@ public class LoanRepaymentScheduleInstallment extends 
AbstractAuditableWithUTCDa
         this.penaltyAccrued = null;
     }
 
+    public void resetChargesFields() {
+        this.feeChargesCharged = null;
+        this.feeChargesWaived = null;
+        this.feeChargesWrittenOff = null;
+        this.penaltyCharges = null;
+        this.penaltyChargesWaived = null;
+        this.penaltyChargesWrittenOff = null;
+    }
+
     public Money payPenaltyChargesComponent(final LocalDate transactionDate, 
final Money transactionAmountRemaining) {
 
         final MonetaryCurrency currency = 
transactionAmountRemaining.getCurrency();
@@ -639,6 +648,16 @@ public class LoanRepaymentScheduleInstallment extends 
AbstractAuditableWithUTCDa
         this.penaltyChargesWrittenOff = 
defaultToNullIfZero(penaltyChargesWrittenOff.getAmount());
     }
 
+    public void addToChargePortion(final Money feeChargesDue, final Money 
feeChargesWaived, final Money feeChargesWrittenOff,
+            final Money penaltyChargesDue, final Money penaltyChargesWaived, 
final Money penaltyChargesWrittenOff) {
+        this.feeChargesCharged = 
defaultToNullIfZero(feeChargesDue.plus(this.feeChargesCharged).getAmount());
+        this.feeChargesWaived = 
defaultToNullIfZero(feeChargesWaived.plus(this.feeChargesWaived).getAmount());
+        this.feeChargesWrittenOff = 
defaultToNullIfZero(feeChargesWrittenOff.plus(this.feeChargesWrittenOff).getAmount());
+        this.penaltyCharges = 
defaultToNullIfZero(penaltyChargesDue.plus(this.penaltyCharges).getAmount());
+        this.penaltyChargesWaived = 
defaultToNullIfZero(penaltyChargesWaived.plus(this.penaltyChargesWaived).getAmount());
+        this.penaltyChargesWrittenOff = 
defaultToNullIfZero(penaltyChargesWrittenOff.plus(this.penaltyChargesWrittenOff).getAmount());
+    }
+
     public void updateAccrualPortion(final Money interest, final Money 
feeCharges, final Money penalityCharges) {
         this.interestAccrued = defaultToNullIfZero(interest.getAmount());
         this.feeAccrued = defaultToNullIfZero(feeCharges.getAmount());
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java
index 644ae0585..4c032e3cd 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java
@@ -22,9 +22,11 @@ import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Predicate;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
 import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
 import org.apache.fineract.organisation.monetary.domain.Money;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * A wrapper around loan schedule related data exposing needed behaviour by 
loan.
@@ -52,18 +54,20 @@ public class LoanRepaymentScheduleProcessingWrapper {
                 final Money feeChargesDueForRepaymentPeriod = 
cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges,
                         currency, period, totalPrincipal, totalInterest, 
!period.isRecalculatedInterestComponent(),
                         isFirstNonDownPaymentPeriod);
-                final Money feeChargesWaivedForRepaymentPeriod = 
cumulativeFeeChargesWaivedWithin(startDate, period.getDueDate(),
-                        loanCharges, currency, 
!period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod);
-                final Money feeChargesWrittenOffForRepaymentPeriod = 
cumulativeFeeChargesWrittenOffWithin(startDate, period.getDueDate(),
-                        loanCharges, currency, 
!period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod);
+                final Money feeChargesWaivedForRepaymentPeriod = 
cumulativeChargesWaivedWithin(startDate, period.getDueDate(), loanCharges,
+                        currency, !period.isRecalculatedInterestComponent(), 
isFirstNonDownPaymentPeriod, feeCharge());
+                final Money feeChargesWrittenOffForRepaymentPeriod = 
cumulativeChargesWrittenOffWithin(startDate, period.getDueDate(),
+                        loanCharges, currency, 
!period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, 
feeCharge());
 
                 final Money penaltyChargesDueForRepaymentPeriod = 
cumulativePenaltyChargesDueWithin(startDate, period.getDueDate(),
                         loanCharges, currency, period, totalPrincipal, 
totalInterest, !period.isRecalculatedInterestComponent(),
                         isFirstNonDownPaymentPeriod);
-                final Money penaltyChargesWaivedForRepaymentPeriod = 
cumulativePenaltyChargesWaivedWithin(startDate, period.getDueDate(),
-                        loanCharges, currency, 
!period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod);
-                final Money penaltyChargesWrittenOffForRepaymentPeriod = 
cumulativePenaltyChargesWrittenOffWithin(startDate,
-                        period.getDueDate(), loanCharges, currency, 
!period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod);
+                final Money penaltyChargesWaivedForRepaymentPeriod = 
cumulativeChargesWaivedWithin(startDate, period.getDueDate(),
+                        loanCharges, currency, 
!period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod,
+                        LoanCharge::isPenaltyCharge);
+                final Money penaltyChargesWrittenOffForRepaymentPeriod = 
cumulativeChargesWrittenOffWithin(startDate, period.getDueDate(),
+                        loanCharges, currency, 
!period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod,
+                        LoanCharge::isPenaltyCharge);
 
                 period.updateChargePortion(feeChargesDueForRepaymentPeriod, 
feeChargesWaivedForRepaymentPeriod,
                         feeChargesWrittenOffForRepaymentPeriod, 
penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod,
@@ -81,24 +85,9 @@ public class LoanRepaymentScheduleProcessingWrapper {
         Money cumulative = Money.zero(monetaryCurrency);
         for (final LoanCharge loanCharge : loanCharges) {
             if (loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement()) 
{
-                boolean isDue = isFirstPeriod ? 
loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, 
periodEnd)
-                        : 
loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
+                boolean isDue = loanChargeIsDue(periodStart, periodEnd, 
isFirstPeriod, loanCharge);
                 if (loanCharge.isInstalmentFee() && 
isInstallmentChargeApplicable) {
-                    if (loanCharge.getChargeCalculation().isPercentageBased()) 
{
-                        BigDecimal amount = BigDecimal.ZERO;
-                        if 
(loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
-                            amount = 
amount.add(period.getPrincipal(monetaryCurrency).getAmount())
-                                    
.add(period.getInterestCharged(monetaryCurrency).getAmount());
-                        } else if 
(loanCharge.getChargeCalculation().isPercentageOfInterest()) {
-                            amount = 
amount.add(period.getInterestCharged(monetaryCurrency).getAmount());
-                        } else {
-                            amount = 
amount.add(period.getPrincipal(monetaryCurrency).getAmount());
-                        }
-                        BigDecimal loanChargeAmt = 
amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100));
-                        cumulative = cumulative.plus(loanChargeAmt);
-                    } else {
-                        cumulative = 
cumulative.plus(loanCharge.amountOrPercentage());
-                    }
+                    cumulative = 
cumulative.plus(getInstallmentFee(monetaryCurrency, period, loanCharge));
                 } else if (loanCharge.isOverdueInstallmentCharge() && isDue && 
loanCharge.getChargeCalculation().isPercentageBased()) {
                     cumulative = cumulative.plus(loanCharge.chargeAmount());
                 } else if (isDue && 
loanCharge.getChargeCalculation().isPercentageBased()) {
@@ -134,16 +123,15 @@ public class LoanRepaymentScheduleProcessingWrapper {
         return cumulative;
     }
 
-    private Money cumulativeFeeChargesWaivedWithin(final LocalDate 
periodStart, final LocalDate periodEnd,
-            final Set<LoanCharge> loanCharges, final MonetaryCurrency 
currency, boolean isInstallmentChargeApplicable,
-            boolean isFirstPeriod) {
+    private Money cumulativeChargesWaivedWithin(final LocalDate periodStart, 
final LocalDate periodEnd, final Set<LoanCharge> loanCharges,
+            final MonetaryCurrency currency, boolean 
isInstallmentChargeApplicable, boolean isFirstPeriod,
+            Predicate<LoanCharge> predicate) {
 
         Money cumulative = Money.zero(currency);
 
         for (final LoanCharge loanCharge : loanCharges) {
-            if (loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement()) 
{
-                boolean isDue = isFirstPeriod ? 
loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, 
periodEnd)
-                        : 
loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
+            if (predicate.test(loanCharge)) {
+                boolean isDue = loanChargeIsDue(periodStart, periodEnd, 
isFirstPeriod, loanCharge);
                 if (loanCharge.isInstalmentFee() && 
isInstallmentChargeApplicable) {
                     LoanInstallmentCharge loanChargePerInstallment = 
loanCharge.getInstallmentLoanCharge(periodEnd);
                     if (loanChargePerInstallment != null) {
@@ -158,16 +146,15 @@ public class LoanRepaymentScheduleProcessingWrapper {
         return cumulative;
     }
 
-    private Money cumulativeFeeChargesWrittenOffWithin(final LocalDate 
periodStart, final LocalDate periodEnd,
+    private Money cumulativeChargesWrittenOffWithin(final LocalDate 
periodStart, final LocalDate periodEnd,
             final Set<LoanCharge> loanCharges, final MonetaryCurrency 
currency, boolean isInstallmentChargeApplicable,
-            boolean isFirstPeriod) {
+            boolean isFirstPeriod, Predicate<LoanCharge> chargePredicate) {
 
         Money cumulative = Money.zero(currency);
 
         for (final LoanCharge loanCharge : loanCharges) {
-            if (loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement()) 
{
-                boolean isDue = isFirstPeriod ? 
loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, 
periodEnd)
-                        : 
loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
+            if (chargePredicate.test(loanCharge)) {
+                boolean isDue = loanChargeIsDue(periodStart, periodEnd, 
isFirstPeriod, loanCharge);
                 if (loanCharge.isInstalmentFee() && 
isInstallmentChargeApplicable) {
                     LoanInstallmentCharge loanChargePerInstallment = 
loanCharge.getInstallmentLoanCharge(periodEnd);
                     if (loanChargePerInstallment != null) {
@@ -182,6 +169,15 @@ public class LoanRepaymentScheduleProcessingWrapper {
         return cumulative;
     }
 
+    private Predicate<LoanCharge> feeCharge() {
+        return loanCharge -> loanCharge.isFeeCharge() && 
!loanCharge.isDueAtDisbursement();
+    }
+
+    private boolean loanChargeIsDue(LocalDate periodStart, LocalDate 
periodEnd, boolean isFirstPeriod, LoanCharge loanCharge) {
+        return isFirstPeriod ? 
loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, 
periodEnd)
+                : 
loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
+    }
+
     private Money cumulativePenaltyChargesDueWithin(final LocalDate 
periodStart, final LocalDate periodEnd,
             final Set<LoanCharge> loanCharges, final MonetaryCurrency 
currency, LoanRepaymentScheduleInstallment period,
             final Money totalPrincipal, final Money totalInterest, boolean 
isInstallmentChargeApplicable, boolean isFirstPeriod) {
@@ -190,24 +186,9 @@ public class LoanRepaymentScheduleProcessingWrapper {
 
         for (final LoanCharge loanCharge : loanCharges) {
             if (loanCharge.isPenaltyCharge()) {
-                boolean isDue = isFirstPeriod ? 
loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, 
periodEnd)
-                        : 
loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
+                boolean isDue = loanChargeIsDue(periodStart, periodEnd, 
isFirstPeriod, loanCharge);
                 if (loanCharge.isInstalmentFee() && 
isInstallmentChargeApplicable) {
-                    if (loanCharge.getChargeCalculation().isPercentageBased()) 
{
-                        BigDecimal amount = BigDecimal.ZERO;
-                        if 
(loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
-                            amount = 
amount.add(period.getPrincipal(currency).getAmount())
-                                    
.add(period.getInterestCharged(currency).getAmount());
-                        } else if 
(loanCharge.getChargeCalculation().isPercentageOfInterest()) {
-                            amount = 
amount.add(period.getInterestCharged(currency).getAmount());
-                        } else {
-                            amount = 
amount.add(period.getPrincipal(currency).getAmount());
-                        }
-                        BigDecimal loanChargeAmt = 
amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100));
-                        cumulative = cumulative.plus(loanChargeAmt);
-                    } else {
-                        cumulative = 
cumulative.plus(loanCharge.amountOrPercentage());
-                    }
+                    cumulative = cumulative.plus(getInstallmentFee(currency, 
period, loanCharge));
                 } else if (loanCharge.isOverdueInstallmentCharge() && isDue && 
loanCharge.getChargeCalculation().isPercentageBased()) {
                     cumulative = cumulative.plus(loanCharge.chargeAmount());
                 } else if (isDue && 
loanCharge.getChargeCalculation().isPercentageBased()) {
@@ -230,51 +211,28 @@ public class LoanRepaymentScheduleProcessingWrapper {
         return cumulative;
     }
 
-    private Money cumulativePenaltyChargesWaivedWithin(final LocalDate 
periodStart, final LocalDate periodEnd,
-            final Set<LoanCharge> loanCharges, final MonetaryCurrency 
currency, boolean isInstallmentChargeApplicable,
-            boolean isFirstPeriod) {
-
-        Money cumulative = Money.zero(currency);
-
-        for (final LoanCharge loanCharge : loanCharges) {
-            if (loanCharge.isPenaltyCharge()) {
-                boolean isDue = isFirstPeriod ? 
loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, 
periodEnd)
-                        : 
loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
-                if (loanCharge.isInstalmentFee() && 
isInstallmentChargeApplicable) {
-                    LoanInstallmentCharge loanChargePerInstallment = 
loanCharge.getInstallmentLoanCharge(periodEnd);
-                    if (loanChargePerInstallment != null) {
-                        cumulative = 
cumulative.plus(loanChargePerInstallment.getAmountWaived(currency));
-                    }
-                } else if (isDue) {
-                    cumulative = 
cumulative.plus(loanCharge.getAmountWaived(currency));
-                }
-            }
+    private BigDecimal getInstallmentFee(MonetaryCurrency currency, 
LoanRepaymentScheduleInstallment period, LoanCharge loanCharge) {
+        if (loanCharge.getChargeCalculation().isPercentageBased()) {
+            BigDecimal amount = BigDecimal.ZERO;
+            amount = getBaseAmount(currency, period, loanCharge, amount);
+            return 
amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100));
+        } else {
+            return loanCharge.amountOrPercentage();
         }
-
-        return cumulative;
     }
 
-    private Money cumulativePenaltyChargesWrittenOffWithin(final LocalDate 
periodStart, final LocalDate periodEnd,
-            final Set<LoanCharge> loanCharges, final MonetaryCurrency 
currency, boolean isInstallmentChargeApplicable,
-            final boolean isFirstPeriod) {
-
-        Money cumulative = Money.zero(currency);
-
-        for (final LoanCharge loanCharge : loanCharges) {
-            if (loanCharge.isPenaltyCharge()) {
-                boolean isDue = isFirstPeriod ? 
loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, 
periodEnd)
-                        : 
loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
-                if (loanCharge.isInstalmentFee() && 
isInstallmentChargeApplicable) {
-                    LoanInstallmentCharge loanChargePerInstallment = 
loanCharge.getInstallmentLoanCharge(periodEnd);
-                    if (loanChargePerInstallment != null) {
-                        cumulative = 
cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency));
-                    }
-                } else if (isDue) {
-                    cumulative = 
cumulative.plus(loanCharge.getAmountWrittenOff(currency));
-                }
-            }
+    @NotNull
+    private BigDecimal getBaseAmount(MonetaryCurrency monetaryCurrency, 
LoanRepaymentScheduleInstallment period, LoanCharge loanCharge,
+            BigDecimal amount) {
+        if 
(loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
+            amount = 
amount.add(period.getPrincipal(monetaryCurrency).getAmount())
+                    
.add(period.getInterestCharged(monetaryCurrency).getAmount());
+        } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) 
{
+            amount = 
amount.add(period.getInterestCharged(monetaryCurrency).getAmount());
+        } else {
+            amount = 
amount.add(period.getPrincipal(monetaryCurrency).getAmount());
         }
-
-        return cumulative;
+        return amount;
     }
+
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java
new file mode 100644
index 000000000..c119dfb49
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java
@@ -0,0 +1,216 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.domain;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.function.Predicate;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A wrapper around loan schedule related data exposing needed behaviour by 
loan.
+ */
+public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
+
+    public void reprocess(final MonetaryCurrency currency, final LocalDate 
disbursementDate,
+            final List<LoanRepaymentScheduleInstallment> repaymentPeriods, 
LoanCharge loanCharge) {
+
+        Money totalInterest = Money.zero(currency);
+        Money totalPrincipal = Money.zero(currency);
+        for (final LoanRepaymentScheduleInstallment installment : 
repaymentPeriods) {
+            totalInterest = 
totalInterest.plus(installment.getInterestCharged(currency));
+            totalPrincipal = 
totalPrincipal.plus(installment.getPrincipal(currency));
+        }
+        LocalDate startDate = disbursementDate;
+        for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) 
{
+
+            if (!period.isDownPayment()) {
+
+                boolean isFirstNonDownPaymentPeriod = repaymentPeriods.stream()
+                        .filter(repaymentPeriod -> 
repaymentPeriod.getInstallmentNumber() < period.getInstallmentNumber())
+                        
.allMatch(LoanRepaymentScheduleInstallment::isDownPayment);
+
+                final Money feeChargesDueForRepaymentPeriod = 
feeChargesDueWithin(startDate, period.getDueDate(), loanCharge, currency,
+                        period, totalPrincipal, totalInterest, 
!period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod);
+                final Money feeChargesWaivedForRepaymentPeriod = 
chargesWaivedWithin(startDate, period.getDueDate(), loanCharge, currency,
+                        !period.isRecalculatedInterestComponent(), 
isFirstNonDownPaymentPeriod, feeCharge());
+                final Money feeChargesWrittenOffForRepaymentPeriod = 
loanChargesWrittenOffWithin(startDate, period.getDueDate(), loanCharge,
+                        currency, !period.isRecalculatedInterestComponent(), 
isFirstNonDownPaymentPeriod, feeCharge());
+
+                final Money penaltyChargesDueForRepaymentPeriod = 
penaltyChargesDueWithin(startDate, period.getDueDate(), loanCharge,
+                        currency, period, totalPrincipal, totalInterest, 
!period.isRecalculatedInterestComponent(),
+                        isFirstNonDownPaymentPeriod);
+                final Money penaltyChargesWaivedForRepaymentPeriod = 
chargesWaivedWithin(startDate, period.getDueDate(), loanCharge,
+                        currency, !period.isRecalculatedInterestComponent(), 
isFirstNonDownPaymentPeriod, LoanCharge::isPenaltyCharge);
+                final Money penaltyChargesWrittenOffForRepaymentPeriod = 
loanChargesWrittenOffWithin(startDate, period.getDueDate(),
+                        loanCharge, currency, 
!period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod,
+                        LoanCharge::isPenaltyCharge);
+
+                period.addToChargePortion(feeChargesDueForRepaymentPeriod, 
feeChargesWaivedForRepaymentPeriod,
+                        feeChargesWrittenOffForRepaymentPeriod, 
penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod,
+                        penaltyChargesWrittenOffForRepaymentPeriod);
+
+                startDate = period.getDueDate();
+            }
+        }
+    }
+
+    private Money feeChargesDueWithin(final LocalDate periodStart, final 
LocalDate periodEnd, final LoanCharge loanCharge,
+            final MonetaryCurrency monetaryCurrency, 
LoanRepaymentScheduleInstallment period, final Money totalPrincipal,
+            final Money totalInterest, boolean isInstallmentChargeApplicable, 
boolean isFirstPeriod) {
+
+        if (loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement()) {
+            boolean isDue = loanChargeIsDue(periodStart, periodEnd, 
isFirstPeriod, loanCharge);
+            if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) 
{
+                return Money.of(monetaryCurrency, 
getInstallmentFee(monetaryCurrency, period, loanCharge));
+            } else if (loanCharge.isOverdueInstallmentCharge() && isDue && 
loanCharge.getChargeCalculation().isPercentageBased()) {
+                return Money.of(monetaryCurrency, loanCharge.chargeAmount());
+            } else if (isDue && 
loanCharge.getChargeCalculation().isPercentageBased()) {
+                BigDecimal amount = BigDecimal.ZERO;
+                if 
(loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
+                    amount = 
amount.add(totalPrincipal.getAmount()).add(totalInterest.getAmount());
+                } else if 
(loanCharge.getChargeCalculation().isPercentageOfInterest()) {
+                    amount = amount.add(totalInterest.getAmount());
+                } else {
+                    // If charge type is specified due date and loan is
+                    // multi disburment loan.
+                    // Then we need to get as of this loan charge due date
+                    // how much amount disbursed.
+                    if (loanCharge.getLoan() != null && 
loanCharge.isSpecifiedDueDate() && 
loanCharge.getLoan().isMultiDisburmentLoan()) {
+                        for (final LoanDisbursementDetails 
loanDisbursementDetails : loanCharge.getLoan().getDisbursementDetails()) {
+                            if 
(!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), 
loanCharge.getDueDate())) {
+                                amount = 
amount.add(loanDisbursementDetails.principal());
+                            }
+                        }
+                    } else {
+                        amount = amount.add(totalPrincipal.getAmount());
+                    }
+                }
+                BigDecimal loanChargeAmt = 
amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100));
+                return Money.of(monetaryCurrency, loanChargeAmt);
+            } else if (isDue) {
+                return Money.of(monetaryCurrency, loanCharge.amount());
+            }
+        }
+        return Money.zero(monetaryCurrency);
+    }
+
+    private Money chargesWaivedWithin(final LocalDate periodStart, final 
LocalDate periodEnd, final LoanCharge loanCharge,
+            final MonetaryCurrency currency, boolean 
isInstallmentChargeApplicable, boolean isFirstPeriod,
+            Predicate<LoanCharge> predicate) {
+
+        if (predicate.test(loanCharge)) {
+            boolean isDue = loanChargeIsDue(periodStart, periodEnd, 
isFirstPeriod, loanCharge);
+            if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) 
{
+                LoanInstallmentCharge loanChargePerInstallment = 
loanCharge.getInstallmentLoanCharge(periodEnd);
+                if (loanChargePerInstallment != null) {
+                    return loanChargePerInstallment.getAmountWaived(currency);
+                }
+            } else if (isDue) {
+                return loanCharge.getAmountWaived(currency);
+            }
+        }
+
+        return Money.zero(currency);
+    }
+
+    private Money loanChargesWrittenOffWithin(final LocalDate periodStart, 
final LocalDate periodEnd, final LoanCharge loanCharge,
+            final MonetaryCurrency currency, boolean 
isInstallmentChargeApplicable, boolean isFirstPeriod,
+            Predicate<LoanCharge> chargePredicate) {
+        if (chargePredicate.test(loanCharge)) {
+            boolean isDue = loanChargeIsDue(periodStart, periodEnd, 
isFirstPeriod, loanCharge);
+            if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) 
{
+                LoanInstallmentCharge loanChargePerInstallment = 
loanCharge.getInstallmentLoanCharge(periodEnd);
+                if (loanChargePerInstallment != null) {
+                    return 
loanChargePerInstallment.getAmountWrittenOff(currency);
+                }
+            } else if (isDue) {
+                return loanCharge.getAmountWrittenOff(currency);
+            }
+        }
+        return Money.zero(currency);
+    }
+
+    private Predicate<LoanCharge> feeCharge() {
+        return loanCharge -> loanCharge.isFeeCharge() && 
!loanCharge.isDueAtDisbursement();
+    }
+
+    private boolean loanChargeIsDue(LocalDate periodStart, LocalDate 
periodEnd, boolean isFirstPeriod, LoanCharge loanCharge) {
+        return isFirstPeriod ? 
loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, 
periodEnd)
+                : 
loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
+    }
+
+    private Money penaltyChargesDueWithin(final LocalDate periodStart, final 
LocalDate periodEnd, final LoanCharge loanCharge,
+            final MonetaryCurrency currency, LoanRepaymentScheduleInstallment 
period, final Money totalPrincipal, final Money totalInterest,
+            boolean isInstallmentChargeApplicable, boolean isFirstPeriod) {
+
+        if (loanCharge.isPenaltyCharge()) {
+            boolean isDue = loanChargeIsDue(periodStart, periodEnd, 
isFirstPeriod, loanCharge);
+            if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) 
{
+                return Money.of(currency, getInstallmentFee(currency, period, 
loanCharge));
+            } else if (loanCharge.isOverdueInstallmentCharge() && isDue && 
loanCharge.getChargeCalculation().isPercentageBased()) {
+                return Money.of(currency, loanCharge.chargeAmount());
+            } else if (isDue && 
loanCharge.getChargeCalculation().isPercentageBased()) {
+                BigDecimal amount = BigDecimal.ZERO;
+                if 
(loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
+                    amount = 
amount.add(totalPrincipal.getAmount()).add(totalInterest.getAmount());
+                } else if 
(loanCharge.getChargeCalculation().isPercentageOfInterest()) {
+                    amount = amount.add(totalInterest.getAmount());
+                } else {
+                    amount = amount.add(totalPrincipal.getAmount());
+                }
+                BigDecimal loanChargeAmt = 
amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100));
+                return Money.of(currency, loanChargeAmt);
+            } else if (isDue) {
+                return Money.of(currency, loanCharge.amount());
+            }
+        }
+
+        return Money.zero(currency);
+    }
+
+    private BigDecimal getInstallmentFee(MonetaryCurrency currency, 
LoanRepaymentScheduleInstallment period, LoanCharge loanCharge) {
+        if (loanCharge.getChargeCalculation().isPercentageBased()) {
+            BigDecimal amount = BigDecimal.ZERO;
+            amount = getBaseAmount(currency, period, loanCharge, amount);
+            return 
amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100));
+        } else {
+            return loanCharge.amountOrPercentage();
+        }
+    }
+
+    @NotNull
+    private BigDecimal getBaseAmount(MonetaryCurrency monetaryCurrency, 
LoanRepaymentScheduleInstallment period, LoanCharge loanCharge,
+            BigDecimal amount) {
+        if 
(loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
+            amount = 
amount.add(period.getPrincipal(monetaryCurrency).getAmount())
+                    
.add(period.getInterestCharged(monetaryCurrency).getAmount());
+        } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) 
{
+            amount = 
amount.add(period.getInterestCharged(monetaryCurrency).getAmount());
+        } else {
+            amount = 
amount.add(period.getPrincipal(monetaryCurrency).getAmount());
+        }
+        return amount;
+    }
+
+}
diff --git 
a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java
 
b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java
new file mode 100644
index 000000000..31c2ca10f
--- /dev/null
+++ 
b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java
@@ -0,0 +1,154 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.domain;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.organisation.monetary.data.CurrencyData;
+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.charge.domain.Charge;
+import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
+import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
+import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
+
+    private final SingleLoanChargeRepaymentScheduleProcessingWrapper underTest 
= new SingleLoanChargeRepaymentScheduleProcessingWrapper();
+    private static final MockedStatic<MoneyHelper> MONEY_HELPER = 
Mockito.mockStatic(MoneyHelper.class);
+
+    private MonetaryCurrency currency = MonetaryCurrency.fromCurrencyData(new 
CurrencyData("USD"));
+
+    private ArgumentCaptor<Money> feeChargesDue = 
ArgumentCaptor.forClass(Money.class);
+    private ArgumentCaptor<Money> feeChargesWaived = 
ArgumentCaptor.forClass(Money.class);
+    private ArgumentCaptor<Money> feeChargesWrittenOff = 
ArgumentCaptor.forClass(Money.class);
+    private ArgumentCaptor<Money> penaltyChargesDue = 
ArgumentCaptor.forClass(Money.class);
+    private ArgumentCaptor<Money> penaltyChargesWaived = 
ArgumentCaptor.forClass(Money.class);
+    private ArgumentCaptor<Money> penaltyChargesWrittenOff = 
ArgumentCaptor.forClass(Money.class);
+
+    @BeforeAll
+    public static void init() {
+        
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
+    }
+
+    @Test
+    public void testOnePeriodWithFeeCharge() {
+        LocalDate disbursementDate = LocalDate.of(2023, 01, 1);
+        ThreadLocalContextUtil.setBusinessDates(new 
HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, disbursementDate)));
+
+        LoanRepaymentScheduleInstallment period = createPeriod(1, 
LocalDate.of(2023, 01, 1), LocalDate.of(2023, 01, 30));
+        LoanCharge loanCharge = createCharge(false);
+
+        underTest.reprocess(currency, disbursementDate, List.of(period), 
loanCharge);
+
+        verify(period, "10.0", "0.0", "0.0", "0.0", "0.0", "0.0");
+    }
+
+    @Test
+    public void testOnePeriodWithPenaltyCharge() {
+        LocalDate disbursementDate = LocalDate.of(2023, 01, 1);
+        ThreadLocalContextUtil.setBusinessDates(new 
HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, disbursementDate)));
+
+        LoanRepaymentScheduleInstallment period = createPeriod(1, 
LocalDate.of(2023, 01, 1), LocalDate.of(2023, 01, 30));
+        LoanCharge loanCharge = createCharge(true);
+
+        underTest.reprocess(currency, disbursementDate, List.of(period), 
loanCharge);
+
+        verify(period, "0.0", "0.0", "0.0", "10.0", "0.0", "0.0");
+    }
+
+    @Test
+    public void testTwoPeriodsWithPenaltyCharge() {
+        LocalDate disbursementDate = LocalDate.of(2023, 01, 1);
+        ThreadLocalContextUtil.setBusinessDates(new 
HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, disbursementDate)));
+
+        LoanRepaymentScheduleInstallment period1 = createPeriod(1, 
LocalDate.of(2023, 01, 1), LocalDate.of(2023, 01, 31));
+        LoanRepaymentScheduleInstallment period2 = createPeriod(1, 
LocalDate.of(2023, 02, 1), LocalDate.of(2023, 02, 28));
+
+        LoanCharge loanCharge = createCharge(true);
+
+        underTest.reprocess(currency, disbursementDate, List.of(period1, 
period2), loanCharge);
+
+        verify(period1, "0.0", "0.0", "0.0", "10.0", "0.0", "0.0");
+        verify(period2, "0.0", "0.0", "0.0", "0.0", "0.0", "0.0");
+    }
+
+    private void verify(LoanRepaymentScheduleInstallment period, String 
expectedFeeChargesDue, String expectedFeeChargesWaived,
+            String expectedFeeChargesWrittenOff, String 
expectedPenaltyChargesDue, String expectedPenaltyChargesWaived,
+            String expectedPenaltyChargesWrittenOff) {
+
+        Mockito.verify(period, 
times(1)).addToChargePortion(feeChargesDue.capture(), 
feeChargesWaived.capture(),
+                feeChargesWrittenOff.capture(), penaltyChargesDue.capture(), 
penaltyChargesWaived.capture(),
+                penaltyChargesWrittenOff.capture());
+
+        Assertions.assertTrue(new 
BigDecimal(expectedFeeChargesDue).compareTo(feeChargesDue.getValue().getAmount())
 == 0);
+        Assertions.assertTrue(new 
BigDecimal(expectedFeeChargesWaived).compareTo(feeChargesWaived.getValue().getAmount())
 == 0);
+        Assertions.assertTrue(new 
BigDecimal(expectedFeeChargesWrittenOff).compareTo(feeChargesWrittenOff.getValue().getAmount())
 == 0);
+        Assertions.assertTrue(new 
BigDecimal(expectedPenaltyChargesDue).compareTo(penaltyChargesDue.getValue().getAmount())
 == 0);
+        Assertions.assertTrue(new 
BigDecimal(expectedPenaltyChargesWaived).compareTo(penaltyChargesWaived.getValue().getAmount())
 == 0);
+        Assertions.assertTrue(
+                new 
BigDecimal(expectedPenaltyChargesWrittenOff).compareTo(penaltyChargesWrittenOff.getValue().getAmount())
 == 0);
+    }
+
+    @NotNull
+    private static LoanCharge createCharge(boolean penalty) {
+        Charge charge = mock(Charge.class);
+        when(charge.getId()).thenReturn(1L);
+        when(charge.getName()).thenReturn("charge a");
+        when(charge.getCurrencyCode()).thenReturn("UDS");
+        when(charge.isPenalty()).thenReturn(penalty);
+        LoanCharge loanCharge = new LoanCharge(null, charge, new 
BigDecimal(1000), new BigDecimal(10), ChargeTimeType.SPECIFIED_DUE_DATE,
+                ChargeCalculationType.FLAT, LocalDate.of(2023, 01, 15), 
ChargePaymentMode.REGULAR, 1, null, null);
+        return loanCharge;
+    }
+
+    @NotNull
+    private LoanRepaymentScheduleInstallment createPeriod(int periodId, 
LocalDate start, LocalDate end) {
+        LoanRepaymentScheduleInstallment period = 
Mockito.mock(LoanRepaymentScheduleInstallment.class);
+        Mockito.when(period.getInstallmentNumber()).thenReturn(periodId);
+        Mockito.when(period.getFromDate()).thenReturn(start);
+        Mockito.when(period.getDueDate()).thenReturn(end);
+        Money principal = Money.of(currency, new BigDecimal("1000.0"));
+        Money interest = Money.of(currency, BigDecimal.ZERO);
+
+        Mockito.when(period.getPrincipal(eq(currency))).thenReturn(principal);
+        
Mockito.when(period.getInterestCharged(eq(currency))).thenReturn(interest);
+        return period;
+    }
+
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index 76bfb0ca6..0f71ee189 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -21,6 +21,7 @@ package 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
@@ -37,13 +38,14 @@ import 
org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail
 import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
-import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
 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;
 import 
org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
 import 
org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
 import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
+import org.jetbrains.annotations.NotNull;
 import org.springframework.context.annotation.Profile;
 
 //TODO: remove `test` profile when it is finished
@@ -53,6 +55,8 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
 
     public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = 
"advanced-payment-allocation-strategy";
 
+    private final SingleLoanChargeRepaymentScheduleProcessingWrapper 
loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
+
     @Override
     public String getCode() {
         return ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
@@ -91,10 +95,44 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
         throw new NotImplementedException();
     }
 
+    private void processSingleTransaction(LoanTransaction loanTransaction, 
MonetaryCurrency currency,
+            List<LoanRepaymentScheduleInstallment> installments, 
Set<LoanCharge> charges,
+            ChangedTransactionDetail changedTransactionDetail) {
+        if (loanTransaction.getId() == null) {
+            processLatestTransaction(loanTransaction, currency, installments, 
charges, Money.zero(currency));
+            loanTransaction.adjustInterestComponent(currency);
+        } else {
+            /*
+             * For existing transactions, check if the re-payment breakup 
(principal, interest, fees, penalties) has
+             * changed.<br>
+             */
+            final LoanTransaction newLoanTransaction = 
LoanTransaction.copyTransactionProperties(loanTransaction);
+
+            // Reset derived component of new loan transaction and
+            // re-process transaction
+            processLatestTransaction(newLoanTransaction, currency, 
installments, charges, Money.zero(currency));
+            newLoanTransaction.adjustInterestComponent(currency);
+            /*
+             * Check if the transaction amounts have changed. If so, reverse 
the original transaction and update
+             * changedTransactionDetail accordingly
+             */
+            if (LoanTransaction.transactionAmountsMatch(currency, 
loanTransaction, newLoanTransaction)) {
+                
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
+                        
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
+            } else {
+                createNewTransaction(loanTransaction, newLoanTransaction, 
changedTransactionDetail);
+            }
+        }
+    }
+
+    private void processSingleCharge(LoanCharge loanCharge, MonetaryCurrency 
currency, List<LoanRepaymentScheduleInstallment> installments,
+            LocalDate disbursementDate) {
+        loanChargeProcessor.reprocess(currency, disbursementDate, 
installments, loanCharge);
+    }
+
     @Override
-    public ChangedTransactionDetail reprocessLoanTransactions(LocalDate 
disbursementDate,
-            List<LoanTransaction> transactionsPostDisbursement, 
MonetaryCurrency currency,
-            List<LoanRepaymentScheduleInstallment> installments, 
Set<LoanCharge> charges) {
+    public ChangedTransactionDetail reprocessLoanTransactions(LocalDate 
disbursementDate, List<LoanTransaction> loanTransactions,
+            MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> 
installments, Set<LoanCharge> charges) {
 
         // TODO: rewrite this whole logic step by step
         if (charges != null) {
@@ -107,44 +145,37 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
 
         for (final LoanRepaymentScheduleInstallment currentInstallment : 
installments) {
             currentInstallment.resetDerivedComponents();
+            currentInstallment.resetChargesFields();
             currentInstallment.updateDerivedFields(currency, disbursementDate);
         }
 
-        // TODO: Remove this reprocess and add the charges to the installment 
in chronological order
-        final LoanRepaymentScheduleProcessingWrapper wrapper = new 
LoanRepaymentScheduleProcessingWrapper();
-        wrapper.reprocess(currency, disbursementDate, installments, charges);
+        List<ChargeOrTransaction> chargeOrTransactions = 
createSortedChargesAndTransactionsList(loanTransactions, charges);
+
         final ChangedTransactionDetail changedTransactionDetail = new 
ChangedTransactionDetail();
-        for (final LoanTransaction loanTransaction : 
transactionsPostDisbursement) {
-            if (loanTransaction.getId() == null) {
-                processLatestTransaction(loanTransaction, currency, 
installments, charges, Money.zero(currency));
-                loanTransaction.adjustInterestComponent(currency);
-            } else {
-                /**
-                 * For existing transactions, check if the re-payment breakup 
(principal, interest, fees, penalties) has
-                 * changed.<br>
-                 **/
-                final LoanTransaction newLoanTransaction = 
LoanTransaction.copyTransactionProperties(loanTransaction);
-
-                // Reset derived component of new loan transaction and
-                // re-process transaction
-                processLatestTransaction(newLoanTransaction, currency, 
installments, charges, Money.zero(currency));
-                newLoanTransaction.adjustInterestComponent(currency);
-                /**
-                 * Check if the transaction amounts have changed. If so, 
reverse the original transaction and update
-                 * changedTransactionDetail accordingly
-                 **/
-                if (LoanTransaction.transactionAmountsMatch(currency, 
loanTransaction, newLoanTransaction)) {
-                    
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
-                            
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
-                } else {
-                    createNewTransaction(loanTransaction, newLoanTransaction, 
changedTransactionDetail);
-                }
-            }
+        for (final ChargeOrTransaction chargeOrTransaction : 
chargeOrTransactions) {
+            chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction 
-> processSingleTransaction(loanTransaction, currency,
+                    installments, charges, changedTransactionDetail));
+            chargeOrTransaction.getLoanCharge()
+                    .ifPresent(loanCharge -> processSingleCharge(loanCharge, 
currency, installments, disbursementDate));
         }
         reprocessInstallments(installments, currency);
         return changedTransactionDetail;
     }
 
+    @NotNull
+    private List<ChargeOrTransaction> 
createSortedChargesAndTransactionsList(List<LoanTransaction> loanTransactions,
+            Set<LoanCharge> charges) {
+        List<ChargeOrTransaction> chargeOrTransactions = new ArrayList<>();
+        if (charges != null) {
+            
chargeOrTransactions.addAll(charges.stream().map(ChargeOrTransaction::new).toList());
+        }
+        if (loanTransactions != null) {
+            
chargeOrTransactions.addAll(loanTransactions.stream().map(ChargeOrTransaction::new).toList());
+        }
+        Collections.sort(chargeOrTransactions);
+        return chargeOrTransactions;
+    }
+
     @Override
     public void processLatestTransaction(LoanTransaction loanTransaction, 
MonetaryCurrency currency,
             List<LoanRepaymentScheduleInstallment> installments, 
Set<LoanCharge> charges, Money overpaidAmount) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java
new file mode 100644
index 000000000..34f677e69
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
+
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.util.Optional;
+import lombok.Getter;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import org.jetbrains.annotations.NotNull;
+
+@Getter
+public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
+
+    private final Optional<LoanCharge> loanCharge;
+    private final Optional<LoanTransaction> loanTransaction;
+
+    public ChargeOrTransaction(LoanCharge loanCharge) {
+        this.loanCharge = Optional.of(loanCharge);
+        this.loanTransaction = Optional.empty();
+    }
+
+    public ChargeOrTransaction(LoanTransaction loanTransaction) {
+        this.loanTransaction = Optional.of(loanTransaction);
+        this.loanCharge = Optional.empty();
+    }
+
+    private LocalDate getEffectiveDate() {
+        if (loanCharge.isPresent()) {
+            return loanCharge.get().getDueDate();
+        } else if (loanTransaction.isPresent()) {
+            return loanTransaction.get().getTransactionDate();
+        } else {
+            throw new RuntimeException("Either charge or transaction should be 
present");
+        }
+    }
+
+    private LocalDate getSubmittedOnDate() {
+        if (loanCharge.isPresent()) {
+            return loanCharge.get().getSubmittedOnDate();
+        } else if (loanTransaction.isPresent()) {
+            return loanTransaction.get().getSubmittedOnDate();
+        } else {
+            throw new RuntimeException("Either charge or transaction should be 
present");
+        }
+    }
+
+    private OffsetDateTime getCreatedDateTime() {
+        if (loanCharge.isPresent() && 
loanCharge.get().getCreatedDate().isPresent()) {
+            return loanCharge.get().getCreatedDate().get();
+        } else if (loanTransaction.isPresent()) {
+            return loanTransaction.get().getCreatedDateTime();
+        } else {
+            throw new RuntimeException("Either charge with createdDate or 
transaction created datetime should be present");
+        }
+    }
+
+    @Override
+    public int compareTo(@NotNull ChargeOrTransaction o) {
+        int datePortion = 
this.getEffectiveDate().compareTo(o.getEffectiveDate());
+        if (datePortion == 0) {
+            int submittedDate = 
this.getSubmittedOnDate().compareTo(o.getSubmittedOnDate());
+            if (submittedDate == 0) {
+                return 
this.getCreatedDateTime().compareTo(o.getCreatedDateTime());
+            }
+            return submittedDate;
+        }
+        return datePortion;
+    }
+
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java
new file mode 100644
index 000000000..892fc6547
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java
@@ -0,0 +1,107 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
+
+import com.google.common.collect.Collections2;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class ChargeOrTransactionTest {
+
+    @Test
+    public void testCompareToEqual() {
+        ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", 
"2023-10-17T10:15:30+01:00");
+        ChargeOrTransaction transaction = createTransaction("2023-10-17", 
"2023-10-17", "2023-10-17T10:15:30+01:00");
+        Assertions.assertTrue(charge.compareTo(transaction) == 0);
+        Assertions.assertTrue(transaction.compareTo(charge) == 0);
+    }
+
+    @Test
+    public void testCompareToCreatedDateTime() {
+        ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", 
"2023-10-17T10:15:31+01:00");
+        ChargeOrTransaction transaction = createTransaction("2023-10-17", 
"2023-10-17", "2023-10-17T10:15:30+01:00");
+        Assertions.assertTrue(charge.compareTo(transaction) > 0);
+        Assertions.assertTrue(transaction.compareTo(charge) < 0);
+    }
+
+    @Test
+    public void testCompareToSubmittedOnDate() {
+        ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", 
"2023-10-17T10:15:30+01:00");
+        ChargeOrTransaction transaction = createTransaction("2023-10-17", 
"2023-10-16", "2023-10-17T10:15:30+01:00");
+        Assertions.assertTrue(charge.compareTo(transaction) > 0);
+        Assertions.assertTrue(transaction.compareTo(charge) < 0);
+    }
+
+    @Test
+    public void testComparatorEffectiveDate() {
+        ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", 
"2023-10-17T10:15:30+01:00");
+        ChargeOrTransaction transaction = createTransaction("2023-10-16", 
"2023-10-17", "2023-10-17T10:15:30+01:00");
+        Assertions.assertTrue(charge.compareTo(transaction) > 0);
+        Assertions.assertTrue(transaction.compareTo(charge) < 0);
+    }
+
+    @Test
+    public void testComparatorOnDifferentSubmittedDay() {
+        ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-17", 
"2023-10-17T10:15:30+01:00");
+        ChargeOrTransaction cot2 = createTransaction("2023-10-17", 
"2023-10-19", "2023-10-19T10:16:30+01:00");
+        ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-18", 
"2023-10-18T10:14:30+01:00");
+        Collection<List<ChargeOrTransaction>> permutations = 
Collections2.permutations(List.of(cot1, cot2, cot3));
+        List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
+        for (List<ChargeOrTransaction> permutation : permutations) {
+            Assertions.assertEquals(expected, 
permutation.stream().sorted().toList());
+        }
+    }
+
+    @Test
+    public void testComparatorOnSameDay() {
+        ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-19", 
"2023-10-19T10:15:31+01:00");
+        ChargeOrTransaction cot2 = createTransaction("2023-10-17", 
"2023-10-19", "2023-10-19T10:15:33+01:00");
+        ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-19", 
"2023-10-19T10:15:32+01:00");
+        Collection<List<ChargeOrTransaction>> permutations = 
Collections2.permutations(List.of(cot1, cot2, cot3));
+        List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
+        for (List<ChargeOrTransaction> permutation : permutations) {
+            Assertions.assertEquals(expected, 
permutation.stream().sorted().toList());
+        }
+    }
+
+    private ChargeOrTransaction createCharge(String effectiveDate, String 
submittedDate, String creationDateTime) {
+        LoanCharge charge = Mockito.mock(LoanCharge.class);
+        
Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate));
+        
Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
+        
Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime)));
+        return new ChargeOrTransaction(charge);
+    }
+
+    private ChargeOrTransaction createTransaction(String transactionDate, 
String submittedDate, String creationDateTime) {
+        LoanTransaction transaction = Mockito.mock(LoanTransaction.class);
+        
Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
+        
Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate));
+        
Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime));
+        return new ChargeOrTransaction(transaction);
+    }
+
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java
index e6d32ab67..2523a58b6 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java
@@ -18,6 +18,9 @@
  */
 package org.apache.fineract.integrationtests;
 
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import static 
org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -39,6 +42,7 @@ import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.fineract.client.models.AdvancedPaymentData;
 import org.apache.fineract.client.models.AllowAttributeOverrides;
+import org.apache.fineract.client.models.BusinessDateRequest;
 import org.apache.fineract.client.models.ChargeData;
 import org.apache.fineract.client.models.ChargeToGLAccountMapper;
 import 
org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
@@ -55,7 +59,9 @@ import 
org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
 import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
 import org.apache.fineract.client.models.PostPaymentTypesRequest;
 import org.apache.fineract.client.models.PostPaymentTypesResponse;
+import org.apache.fineract.integrationtests.common.BusinessDateHelper;
 import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
 import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
 import org.apache.fineract.integrationtests.common.Utils;
 import org.apache.fineract.integrationtests.common.accounting.Account;
@@ -88,6 +94,8 @@ public class 
LoanAccountChargeOffWithAdvancedPaymentAllocationTest {
     private AccountHelper accountHelper;
     private LoanProductHelper loanProductHelper;
     private PaymentTypeHelper paymentTypeHelper;
+    private final BusinessDateHelper businessDateHelper = new 
BusinessDateHelper();
+    private static final String DATETIME_PATTERN = "dd MMMM yyyy";
     // asset
     private Account loansReceivable;
     private Account interestFeeReceivable;
@@ -212,72 +220,76 @@ public class 
LoanAccountChargeOffWithAdvancedPaymentAllocationTest {
     // Reverse Replay of Charge-Off
     @Test
     public void loanChargeOffReverseReplayWithAdvancedPaymentStrategyTest() {
-        String loanExternalIdStr = UUID.randomUUID().toString();
-        final Integer loanProductID = 
createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
-        final Integer clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
-        final Integer loanId = createLoanAccount(clientId, loanProductID, 
loanExternalIdStr);
-
-        // apply charges
-        Integer feeCharge = ChargesHelper.createCharges(requestSpec, 
responseSpec,
-                
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", false));
-
-        LocalDate targetDate = LocalDate.of(2022, 9, 5);
-        final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate);
-        Integer feeLoanChargeId = 
loanTransactionHelper.addChargesForLoan(loanId,
-                
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge),
 feeCharge1AddedDate, "10"));
-
-        // apply penalty
-        Integer penalty = ChargesHelper.createCharges(requestSpec, 
responseSpec,
-                
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", true));
-
-        final String penaltyCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
-
-        Integer penalty1LoanChargeId = 
this.loanTransactionHelper.addChargesForLoan(loanId,
-                
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty),
 penaltyCharge1AddedDate, "10"));
-
-        // make Repayment
-        final PostLoansLoanIdTransactionsResponse repaymentTransaction = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
-                new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM 
yyyy").transactionDate("9 September 2022").locale("en")
-                        .transactionAmount(10.0));
-
-        GetLoansLoanIdResponse loanDetails = 
this.loanTransactionHelper.getLoanDetails((long) loanId);
-        assertTrue(loanDetails.getStatus().getActive());
-
-        // set loan as chargeoff
-        String randomText = Utils.randomStringGenerator("en", 5) + 
Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
-        Integer chargeOffReasonId = 
CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
-        String transactionExternalId = UUID.randomUUID().toString();
-        PostLoansLoanIdTransactionsResponse chargeOffTransaction = 
this.loanTransactionHelper.chargeOffLoan((long) loanId,
-                new PostLoansLoanIdTransactionsRequest().transactionDate("10 
September 2022").locale("en").dateFormat("dd MMMM yyyy")
-                        
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
-
-        loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId);
-        assertTrue(loanDetails.getStatus().getActive());
-        assertTrue(loanDetails.getChargedOff());
-
-        // verify amounts for charge-off transaction
-        verifyTransaction(LocalDate.of(2022, 9, 10), 1010.0f, 1000.0f, 0.0f, 
10.0f, 0.0f, loanId, "chargeoff");
-
-        Long reversedAndReplayedTransactionId = 
chargeOffTransaction.getResourceId();
-
-        // reverse Repayment
-        loanTransactionHelper.reverseRepayment(loanId, 
repaymentTransaction.getResourceId().intValue(), "11 September 2022");
-
-        // verify chargeOffTransaction gets reverse replayed
-
-        GetLoansLoanIdTransactionsTransactionIdResponse 
getLoansTransactionResponse = loanTransactionHelper
-                .getLoanTransactionDetails((long) loanId, 
transactionExternalId);
-        assertNotNull(getLoansTransactionResponse);
-        assertNotNull(getLoansTransactionResponse.getTransactionRelations());
-
-        // test replayed relationship
-        GetLoanTransactionRelation transactionRelation = 
getLoansTransactionResponse.getTransactionRelations().iterator().next();
-        assertEquals(reversedAndReplayedTransactionId, 
transactionRelation.getToLoanTransaction());
-        assertEquals("REPLAYED", transactionRelation.getRelationType());
-
-        // verify amounts for charge-off transaction
-        verifyTransaction(LocalDate.of(2022, 9, 10), 1020.0f, 1000.0f, 0.0f, 
10.0f, 10.0f, loanId, "chargeoff");
-
+        runAt("9 September 2022", () -> {
+            String loanExternalIdStr = UUID.randomUUID().toString();
+            final Integer loanProductID = 
createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
+            final Integer clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
+            final Integer loanId = createLoanAccount(clientId, loanProductID, 
loanExternalIdStr);
+
+            // apply charges
+            Integer feeCharge = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", false));
+
+            LocalDate targetDate = LocalDate.of(2022, 9, 5);
+            final String feeCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+            Integer feeLoanChargeId = 
loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge),
 feeCharge1AddedDate, "10"));
+
+            // apply penalty
+            Integer penalty = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", true));
+
+            final String penaltyCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+
+            Integer penalty1LoanChargeId = 
this.loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty),
 penaltyCharge1AddedDate, "10"));
+
+            // make Repayment
+            final PostLoansLoanIdTransactionsResponse repaymentTransaction = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
+                    new PostLoansLoanIdTransactionsRequest().dateFormat("dd 
MMMM yyyy").transactionDate("9 September 2022").locale("en")
+                            .transactionAmount(10.0));
+
+            GetLoansLoanIdResponse loanDetails = 
this.loanTransactionHelper.getLoanDetails((long) loanId);
+            assertTrue(loanDetails.getStatus().getActive());
+
+            // set loan as chargeoff
+            updateBusinessDate("10 September 2022");
+            String randomText = Utils.randomStringGenerator("en", 5) + 
Utils.randomNumberGenerator(6)
+                    + Utils.randomStringGenerator("is", 5);
+            Integer chargeOffReasonId = 
CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
+            String transactionExternalId = UUID.randomUUID().toString();
+            PostLoansLoanIdTransactionsResponse chargeOffTransaction = 
this.loanTransactionHelper.chargeOffLoan((long) loanId,
+                    new 
PostLoansLoanIdTransactionsRequest().transactionDate("10 September 
2022").locale("en").dateFormat("dd MMMM yyyy")
+                            
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
+
+            loanDetails = this.loanTransactionHelper.getLoanDetails((long) 
loanId);
+            assertTrue(loanDetails.getStatus().getActive());
+            assertTrue(loanDetails.getChargedOff());
+
+            // verify amounts for charge-off transaction
+            verifyTransaction(LocalDate.of(2022, 9, 10), 1010.0f, 1000.0f, 
0.0f, 10.0f, 0.0f, loanId, "chargeoff");
+
+            Long reversedAndReplayedTransactionId = 
chargeOffTransaction.getResourceId();
+
+            // reverse Repayment
+            updateBusinessDate("11 September 2022");
+            loanTransactionHelper.reverseRepayment(loanId, 
repaymentTransaction.getResourceId().intValue(), "11 September 2022");
+
+            // verify chargeOffTransaction gets reverse replayed
+
+            GetLoansLoanIdTransactionsTransactionIdResponse 
getLoansTransactionResponse = loanTransactionHelper
+                    .getLoanTransactionDetails((long) loanId, 
transactionExternalId);
+            assertNotNull(getLoansTransactionResponse);
+            
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
+
+            // test replayed relationship
+            GetLoanTransactionRelation transactionRelation = 
getLoansTransactionResponse.getTransactionRelations().iterator().next();
+            assertEquals(reversedAndReplayedTransactionId, 
transactionRelation.getToLoanTransaction());
+            assertEquals("REPLAYED", transactionRelation.getRelationType());
+
+            // verify amounts for charge-off transaction
+            verifyTransaction(LocalDate.of(2022, 9, 10), 1020.0f, 1000.0f, 
0.0f, 10.0f, 10.0f, loanId, "chargeoff");
+        });
     }
 
     // undo Charge-Off
@@ -329,202 +341,211 @@ public class 
LoanAccountChargeOffWithAdvancedPaymentAllocationTest {
     // Backdated repayment transaction, Reverse replay of charge off
     @Test
     public void postChargeOffAddBackdatedTransactionAndReverseReplayTest() {
-        String loanExternalIdStr = UUID.randomUUID().toString();
-        final Integer loanProductID = 
createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
-        final Integer clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
-        final Integer loanId = createLoanAccount(clientId, loanProductID, 
loanExternalIdStr);
-
-        // apply charges
-        Integer feeCharge = ChargesHelper.createCharges(requestSpec, 
responseSpec,
-                
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", false));
-
-        LocalDate targetDate = LocalDate.of(2022, 9, 5);
-        final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate);
-        Integer feeLoanChargeId = 
loanTransactionHelper.addChargesForLoan(loanId,
-                
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge),
 feeCharge1AddedDate, "10"));
-
-        // set loan as chargeoff
-        String randomText = Utils.randomStringGenerator("en", 5) + 
Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
-        Integer chargeOffReasonId = 
CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
-        String transactionExternalId = UUID.randomUUID().toString();
-        PostLoansLoanIdTransactionsResponse chargeOffTransaction = 
loanTransactionHelper.chargeOffLoan((long) loanId,
-                new PostLoansLoanIdTransactionsRequest().transactionDate("14 
September 2022").locale("en").dateFormat("dd MMMM yyyy")
-                        
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
-
-        GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails((long) loanId);
-        assertTrue(loanDetails.getStatus().getActive());
-        assertTrue(loanDetails.getChargedOff());
-
-        Long reversedAndReplayedTransactionId = 
chargeOffTransaction.getResourceId();
-
-        // verify Journal Entries For ChargeOff Transaction
-        GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = 
journalEntryHelper
-                .getJournalEntries("L" + 
chargeOffTransaction.getResourceId().toString());
-
-        assertNotNull(journalEntriesForChargeOff);
-        List<JournalEntryTransactionItem> journalEntries = 
journalEntriesForChargeOff.getPageItems();
-        assertEquals(4, journalEntries.size());
-
-        verifyJournalEntry(journalEntries.get(3), 1000.0, LocalDate.of(2022, 
9, 14), loansReceivable, "CREDIT");
-        verifyJournalEntry(journalEntries.get(2), 10.0, LocalDate.of(2022, 9, 
14), interestFeeReceivable, "CREDIT");
-        verifyJournalEntry(journalEntries.get(1), 1000.0, LocalDate.of(2022, 
9, 14), creditLossBadDebt, "DEBIT");
-        verifyJournalEntry(journalEntries.get(0), 10.0, LocalDate.of(2022, 9, 
14), feeChargeOff, "DEBIT");
-
-        // make Repayment before chargeoff date
-        final PostLoansLoanIdTransactionsResponse repaymentTransaction = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
-                new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM 
yyyy").transactionDate("7 September 2022").locale("en")
-                        .transactionAmount(100.0));
-
-        loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
-        assertTrue(loanDetails.getStatus().getActive());
-        assertTrue(loanDetails.getChargedOff());
-
-        // verify Journal Entries for Repayment transaction
-
-        GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = 
journalEntryHelper
-                .getJournalEntries("L" + 
repaymentTransaction.getResourceId().toString());
-        assertNotNull(journalEntriesForRepayment);
-
-        journalEntries = journalEntriesForRepayment.getPageItems();
-        assertEquals(3, journalEntries.size());
-
-        verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 
7), loansReceivable, "CREDIT");
-        verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 
7), interestFeeReceivable, "CREDIT");
-        verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 
7), suspenseClearingAccount, "DEBIT");
-
-        // verify reverse replay of Charge-Off
-
-        GetLoansLoanIdTransactionsTransactionIdResponse 
getLoansTransactionResponse = loanTransactionHelper
-                .getLoanTransactionDetails((long) loanId, 
transactionExternalId);
-        assertNotNull(getLoansTransactionResponse);
-        assertNotNull(getLoansTransactionResponse.getTransactionRelations());
-
-        // test replayed relationship
-        GetLoanTransactionRelation transactionRelation = 
getLoansTransactionResponse.getTransactionRelations().iterator().next();
-        assertEquals(reversedAndReplayedTransactionId, 
transactionRelation.getToLoanTransaction());
-        assertEquals("REPLAYED", transactionRelation.getRelationType());
-
-        // verify amounts for charge-off transaction
-        verifyTransaction(LocalDate.of(2022, 9, 14), 910.0f, 910.0f, 0.0f, 
0.0f, 0.0f, loanId, "chargeoff");
-
-        // make Repayment after chargeoff date
-        final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
-                new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM 
yyyy").transactionDate("15 September 2022").locale("en")
-                        .transactionAmount(100.0));
-
-        loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
-        assertTrue(loanDetails.getStatus().getActive());
-        assertTrue(loanDetails.getChargedOff());
-
-        // verify Journal Entries for Repayment transaction
-        journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" 
+ repaymentTransaction_1.getResourceId().toString());
-
-        assertNotNull(journalEntriesForRepayment);
-
-        journalEntries = journalEntriesForRepayment.getPageItems();
-        assertEquals(2, journalEntries.size());
-
-        verifyJournalEntry(journalEntries.get(1), 100.0, LocalDate.of(2022, 9, 
15), recoveries, "CREDIT");
-        verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 
15), suspenseClearingAccount, "DEBIT");
+        runAt("3 September 2022", () -> {
+            String loanExternalIdStr = UUID.randomUUID().toString();
+            final Integer loanProductID = 
createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
+            final Integer clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
+            final Integer loanId = createLoanAccount(clientId, loanProductID, 
loanExternalIdStr);
+
+            // apply charges
+            updateBusinessDate("5 September 2022");
+            Integer feeCharge = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", false));
+
+            LocalDate targetDate = LocalDate.of(2022, 9, 5);
+            final String feeCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+            Integer feeLoanChargeId = 
loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge),
 feeCharge1AddedDate, "10"));
+
+            // set loan as chargeoff
+            updateBusinessDate("14 September 2022");
+            String randomText = Utils.randomStringGenerator("en", 5) + 
Utils.randomNumberGenerator(6)
+                    + Utils.randomStringGenerator("is", 5);
+            Integer chargeOffReasonId = 
CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
+            String transactionExternalId = UUID.randomUUID().toString();
+            PostLoansLoanIdTransactionsResponse chargeOffTransaction = 
loanTransactionHelper.chargeOffLoan((long) loanId,
+                    new 
PostLoansLoanIdTransactionsRequest().transactionDate("14 September 
2022").locale("en").dateFormat("dd MMMM yyyy")
+                            
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails((long) loanId);
+            assertTrue(loanDetails.getStatus().getActive());
+            assertTrue(loanDetails.getChargedOff());
+
+            Long reversedAndReplayedTransactionId = 
chargeOffTransaction.getResourceId();
+
+            // verify Journal Entries For ChargeOff Transaction
+            GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff 
= journalEntryHelper
+                    .getJournalEntries("L" + 
chargeOffTransaction.getResourceId().toString());
+
+            assertNotNull(journalEntriesForChargeOff);
+            List<JournalEntryTransactionItem> journalEntries = 
journalEntriesForChargeOff.getPageItems();
+            assertEquals(4, journalEntries.size());
+
+            verifyJournalEntry(journalEntries.get(3), 1000.0, 
LocalDate.of(2022, 9, 14), loansReceivable, "CREDIT");
+            verifyJournalEntry(journalEntries.get(2), 10.0, LocalDate.of(2022, 
9, 14), interestFeeReceivable, "CREDIT");
+            verifyJournalEntry(journalEntries.get(1), 1000.0, 
LocalDate.of(2022, 9, 14), creditLossBadDebt, "DEBIT");
+            verifyJournalEntry(journalEntries.get(0), 10.0, LocalDate.of(2022, 
9, 14), feeChargeOff, "DEBIT");
+
+            // make Repayment before chargeoff date - business date is still 
on 14 September 2022
+            final PostLoansLoanIdTransactionsResponse repaymentTransaction = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
+                    new PostLoansLoanIdTransactionsRequest().dateFormat("dd 
MMMM yyyy").transactionDate("7 September 2022").locale("en")
+                            .transactionAmount(100.0));
+
+            loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
+            assertTrue(loanDetails.getStatus().getActive());
+            assertTrue(loanDetails.getChargedOff());
+
+            // verify Journal Entries for Repayment transaction
+
+            GetJournalEntriesTransactionIdResponse journalEntriesForRepayment 
= journalEntryHelper
+                    .getJournalEntries("L" + 
repaymentTransaction.getResourceId().toString());
+            assertNotNull(journalEntriesForRepayment);
+
+            journalEntries = journalEntriesForRepayment.getPageItems();
+            assertEquals(3, journalEntries.size());
+
+            verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 
9, 7), loansReceivable, "CREDIT");
+            verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 
9, 7), interestFeeReceivable, "CREDIT");
+            verifyJournalEntry(journalEntries.get(0), 100.0, 
LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT");
+
+            // verify reverse replay of Charge-Off
+
+            GetLoansLoanIdTransactionsTransactionIdResponse 
getLoansTransactionResponse = loanTransactionHelper
+                    .getLoanTransactionDetails((long) loanId, 
transactionExternalId);
+            assertNotNull(getLoansTransactionResponse);
+            
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
+
+            // test replayed relationship
+            GetLoanTransactionRelation transactionRelation = 
getLoansTransactionResponse.getTransactionRelations().iterator().next();
+            assertEquals(reversedAndReplayedTransactionId, 
transactionRelation.getToLoanTransaction());
+            assertEquals("REPLAYED", transactionRelation.getRelationType());
+
+            // verify amounts for charge-off transaction
+            verifyTransaction(LocalDate.of(2022, 9, 14), 910.0f, 910.0f, 0.0f, 
0.0f, 0.0f, loanId, "chargeoff");
+
+            // make Repayment after chargeoff date
+            updateBusinessDate("15 September 2022");
+            final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
+                    new PostLoansLoanIdTransactionsRequest().dateFormat("dd 
MMMM yyyy").transactionDate("15 September 2022").locale("en")
+                            .transactionAmount(100.0));
+
+            loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
+            assertTrue(loanDetails.getStatus().getActive());
+            assertTrue(loanDetails.getChargedOff());
+
+            // verify Journal Entries for Repayment transaction
+            journalEntriesForRepayment = 
journalEntryHelper.getJournalEntries("L" + 
repaymentTransaction_1.getResourceId().toString());
+
+            assertNotNull(journalEntriesForRepayment);
+
+            journalEntries = journalEntriesForRepayment.getPageItems();
+            assertEquals(2, journalEntries.size());
+
+            verifyJournalEntry(journalEntries.get(1), 100.0, 
LocalDate.of(2022, 9, 15), recoveries, "CREDIT");
+            verifyJournalEntry(journalEntries.get(0), 100.0, 
LocalDate.of(2022, 9, 15), suspenseClearingAccount, "DEBIT");
+        });
     }
 
     // Repayment before charge off on charge off date, reverse replay of 
charge off
     @Test
     public void transactionOnChargeOffDateReverseTest() {
-        String loanExternalIdStr = UUID.randomUUID().toString();
-        final Integer loanProductID = 
createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
-        final Integer clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
-        final Integer loanId = createLoanAccount(clientId, loanProductID, 
loanExternalIdStr);
-
-        // apply charges
-        Integer feeCharge = ChargesHelper.createCharges(requestSpec, 
responseSpec,
-                
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", false));
-
-        LocalDate targetDate = LocalDate.of(2022, 9, 5);
-        final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate);
-        Integer feeLoanChargeId = 
loanTransactionHelper.addChargesForLoan(loanId,
-                
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge),
 feeCharge1AddedDate, "10"));
-
-        // make Repayment before charge-off on charge off date
-        final PostLoansLoanIdTransactionsResponse repaymentTransaction = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
-                new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM 
yyyy").transactionDate("7 September 2022").locale("en")
-                        .transactionAmount(100.0));
-
-        GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails((long) loanId);
-        assertTrue(loanDetails.getStatus().getActive());
-
-        // verify Journal Entries for Repayment transaction
-        GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = 
journalEntryHelper
-                .getJournalEntries("L" + 
repaymentTransaction.getResourceId().toString());
-
-        assertNotNull(journalEntriesForRepayment);
-
-        List<JournalEntryTransactionItem> journalEntries = 
journalEntriesForRepayment.getPageItems();
-        assertEquals(3, journalEntries.size());
-
-        verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 
7), loansReceivable, "CREDIT");
-        verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 
7), interestFeeReceivable, "CREDIT");
-        verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 
7), suspenseClearingAccount, "DEBIT");
-
-        // set loan as chargeoff
-        String randomText = Utils.randomStringGenerator("en", 5) + 
Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
-        Integer chargeOffReasonId = 
CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
-        String transactionExternalId = UUID.randomUUID().toString();
-        PostLoansLoanIdTransactionsResponse chargeOffTransaction = 
loanTransactionHelper.chargeOffLoan((long) loanId,
-                new PostLoansLoanIdTransactionsRequest().transactionDate("7 
September 2022").locale("en").dateFormat("dd MMMM yyyy")
-                        
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
-
-        loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
-        assertTrue(loanDetails.getStatus().getActive());
-        assertTrue(loanDetails.getChargedOff());
-
-        Long reversedAndReplayedTransactionId = 
chargeOffTransaction.getResourceId();
-
-        // verify Journal Entries For ChargeOff Transaction
-        GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = 
journalEntryHelper
-                .getJournalEntries("L" + 
chargeOffTransaction.getResourceId().toString());
-
-        assertNotNull(journalEntriesForChargeOff);
-        journalEntries = journalEntriesForChargeOff.getPageItems();
-        assertEquals(2, journalEntries.size());
-
-        verifyJournalEntry(journalEntries.get(1), 910.0, LocalDate.of(2022, 9, 
7), loansReceivable, "CREDIT");
-        verifyJournalEntry(journalEntries.get(0), 910.0, LocalDate.of(2022, 9, 
7), creditLossBadDebt, "DEBIT");
-
-        // reverse Repayment
-        loanTransactionHelper.reverseRepayment(loanId, 
repaymentTransaction.getResourceId().intValue(), "7 September 2022");
-        loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
-        assertTrue(loanDetails.getStatus().getActive());
-        assertTrue(loanDetails.getChargedOff());
-
-        // verify Journal Entries for Reversed Repayment transaction
-        journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" 
+ repaymentTransaction.getResourceId().toString());
-        assertNotNull(journalEntriesForRepayment);
-
-        journalEntries = journalEntriesForRepayment.getPageItems();
-        assertEquals(6, journalEntries.size());
-
-        verifyJournalEntry(journalEntries.get(5), 90.0, LocalDate.of(2022, 9, 
7), loansReceivable, "CREDIT");
-        verifyJournalEntry(journalEntries.get(4), 10.0, LocalDate.of(2022, 9, 
7), interestFeeReceivable, "CREDIT");
-        verifyJournalEntry(journalEntries.get(3), 100.0, LocalDate.of(2022, 9, 
7), suspenseClearingAccount, "DEBIT");
-        verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 
7), loansReceivable, "DEBIT");
-        verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 
7), interestFeeReceivable, "DEBIT");
-        verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 
7), suspenseClearingAccount, "CREDIT");
-
-        // verify reverse replay of Charge-Off
-
-        GetLoansLoanIdTransactionsTransactionIdResponse 
getLoansTransactionResponse = loanTransactionHelper
-                .getLoanTransactionDetails((long) loanId, 
transactionExternalId);
-        assertNotNull(getLoansTransactionResponse);
-        assertNotNull(getLoansTransactionResponse.getTransactionRelations());
-
-        // test replayed relationship
-        GetLoanTransactionRelation transactionRelation = 
getLoansTransactionResponse.getTransactionRelations().iterator().next();
-        assertEquals(reversedAndReplayedTransactionId, 
transactionRelation.getToLoanTransaction());
-        assertEquals("REPLAYED", transactionRelation.getRelationType());
-
-        // verify amounts for charge-off transaction
-        verifyTransaction(LocalDate.of(2022, 9, 7), 1010.0f, 1000.0f, 0.0f, 
10.0f, 0.0f, loanId, "chargeoff");
+        runAt("7 September 2022", () -> {
+            String loanExternalIdStr = UUID.randomUUID().toString();
+            final Integer loanProductID = 
createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
+            final Integer clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
+            final Integer loanId = createLoanAccount(clientId, loanProductID, 
loanExternalIdStr);
+
+            // apply charges
+            Integer feeCharge = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", false));
+
+            LocalDate targetDate = LocalDate.of(2022, 9, 5);
+            final String feeCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+            Integer feeLoanChargeId = 
loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge),
 feeCharge1AddedDate, "10"));
+
+            // make Repayment before charge-off on charge off date
+            final PostLoansLoanIdTransactionsResponse repaymentTransaction = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
+                    new PostLoansLoanIdTransactionsRequest().dateFormat("dd 
MMMM yyyy").transactionDate("7 September 2022").locale("en")
+                            .transactionAmount(100.0));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails((long) loanId);
+            assertTrue(loanDetails.getStatus().getActive());
+
+            // verify Journal Entries for Repayment transaction
+            GetJournalEntriesTransactionIdResponse journalEntriesForRepayment 
= journalEntryHelper
+                    .getJournalEntries("L" + 
repaymentTransaction.getResourceId().toString());
+
+            assertNotNull(journalEntriesForRepayment);
+
+            List<JournalEntryTransactionItem> journalEntries = 
journalEntriesForRepayment.getPageItems();
+            assertEquals(3, journalEntries.size());
+
+            verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 
9, 7), loansReceivable, "CREDIT");
+            verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 
9, 7), interestFeeReceivable, "CREDIT");
+            verifyJournalEntry(journalEntries.get(0), 100.0, 
LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT");
+
+            // set loan as chargeoff
+            String randomText = Utils.randomStringGenerator("en", 5) + 
Utils.randomNumberGenerator(6)
+                    + Utils.randomStringGenerator("is", 5);
+            Integer chargeOffReasonId = 
CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
+            String transactionExternalId = UUID.randomUUID().toString();
+            PostLoansLoanIdTransactionsResponse chargeOffTransaction = 
loanTransactionHelper.chargeOffLoan((long) loanId,
+                    new 
PostLoansLoanIdTransactionsRequest().transactionDate("7 September 
2022").locale("en").dateFormat("dd MMMM yyyy")
+                            
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
+
+            loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
+            assertTrue(loanDetails.getStatus().getActive());
+            assertTrue(loanDetails.getChargedOff());
+
+            Long reversedAndReplayedTransactionId = 
chargeOffTransaction.getResourceId();
+
+            // verify Journal Entries For ChargeOff Transaction
+            GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff 
= journalEntryHelper
+                    .getJournalEntries("L" + 
chargeOffTransaction.getResourceId().toString());
+
+            assertNotNull(journalEntriesForChargeOff);
+            journalEntries = journalEntriesForChargeOff.getPageItems();
+            assertEquals(2, journalEntries.size());
+
+            verifyJournalEntry(journalEntries.get(1), 910.0, 
LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT");
+            verifyJournalEntry(journalEntries.get(0), 910.0, 
LocalDate.of(2022, 9, 7), creditLossBadDebt, "DEBIT");
+
+            // reverse Repayment
+            loanTransactionHelper.reverseRepayment(loanId, 
repaymentTransaction.getResourceId().intValue(), "7 September 2022");
+            loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
+            assertTrue(loanDetails.getStatus().getActive());
+            assertTrue(loanDetails.getChargedOff());
+
+            // verify Journal Entries for Reversed Repayment transaction
+            journalEntriesForRepayment = 
journalEntryHelper.getJournalEntries("L" + 
repaymentTransaction.getResourceId().toString());
+            assertNotNull(journalEntriesForRepayment);
+
+            journalEntries = journalEntriesForRepayment.getPageItems();
+            assertEquals(6, journalEntries.size());
+
+            verifyJournalEntry(journalEntries.get(5), 90.0, LocalDate.of(2022, 
9, 7), loansReceivable, "CREDIT");
+            verifyJournalEntry(journalEntries.get(4), 10.0, LocalDate.of(2022, 
9, 7), interestFeeReceivable, "CREDIT");
+            verifyJournalEntry(journalEntries.get(3), 100.0, 
LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT");
+            verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 
9, 7), loansReceivable, "DEBIT");
+            verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 
9, 7), interestFeeReceivable, "DEBIT");
+            verifyJournalEntry(journalEntries.get(0), 100.0, 
LocalDate.of(2022, 9, 7), suspenseClearingAccount, "CREDIT");
+
+            // verify reverse replay of Charge-Off
+
+            GetLoansLoanIdTransactionsTransactionIdResponse 
getLoansTransactionResponse = loanTransactionHelper
+                    .getLoanTransactionDetails((long) loanId, 
transactionExternalId);
+            assertNotNull(getLoansTransactionResponse);
+            
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
+
+            // test replayed relationship
+            GetLoanTransactionRelation transactionRelation = 
getLoansTransactionResponse.getTransactionRelations().iterator().next();
+            assertEquals(reversedAndReplayedTransactionId, 
transactionRelation.getToLoanTransaction());
+            assertEquals("REPLAYED", transactionRelation.getRelationType());
+
+            // verify amounts for charge-off transaction
+            verifyTransaction(LocalDate.of(2022, 9, 7), 1010.0f, 1000.0f, 
0.0f, 10.0f, 0.0f, loanId, "chargeoff");
+        });
 
     }
 
@@ -756,4 +777,23 @@ public class 
LoanAccountChargeOffWithAdvancedPaymentAllocationTest {
             return paymentAllocationOrder;
         }).toList();
     }
+
+    private void runAt(String date, Runnable runnable) {
+        try {
+            
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, 
responseSpec, 42, true);
+            GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, 
responseSpec, TRUE);
+            businessDateHelper.updateBusinessDate(
+                    new 
BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
+            runnable.run();
+        } finally {
+            GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, 
responseSpec, FALSE);
+            
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, 
responseSpec, 42, false);
+        }
+    }
+
+    private void updateBusinessDate(String date) {
+        businessDateHelper.updateBusinessDate(
+                new 
BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
+    }
+
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeReveseReplayWithAdvancedPaymentAllocationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeReveseReplayWithAdvancedPaymentAllocationTest.java
new file mode 100644
index 000000000..0334570cb
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeReveseReplayWithAdvancedPaymentAllocationTest.java
@@ -0,0 +1,492 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import static 
org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.fineract.client.models.AdvancedPaymentData;
+import org.apache.fineract.client.models.AllowAttributeOverrides;
+import org.apache.fineract.client.models.BusinessDateRequest;
+import org.apache.fineract.client.models.ChargeData;
+import org.apache.fineract.client.models.ChargeToGLAccountMapper;
+import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings;
+import 
org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.PaymentAllocationOrder;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
+import org.apache.fineract.client.models.PostPaymentTypesRequest;
+import org.apache.fineract.client.models.PostPaymentTypesResponse;
+import org.apache.fineract.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
+import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
+import 
org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
+import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
+import org.apache.fineract.integrationtests.common.funds.FundsHelper;
+import org.apache.fineract.integrationtests.common.funds.FundsResourceHandler;
+import 
org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanProductHelper;
+import 
org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import 
org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
+import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(LoanTestLifecycleExtension.class)
+public class LoanAccountChargeReveseReplayWithAdvancedPaymentAllocationTest {
+
+    private static final DateTimeFormatter DATE_FORMATTER = new 
DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter();
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private ClientHelper clientHelper;
+    private LoanTransactionHelper loanTransactionHelper;
+    private JournalEntryHelper journalEntryHelper;
+    private AccountHelper accountHelper;
+    private LoanProductHelper loanProductHelper;
+    private PaymentTypeHelper paymentTypeHelper;
+    private final BusinessDateHelper businessDateHelper = new 
BusinessDateHelper();
+    private static final String DATETIME_PATTERN = "dd MMMM yyyy";
+    // asset
+    private Account loansReceivable;
+    private Account interestFeeReceivable;
+    private Account suspenseAccount;
+    private Account fundReceivables;
+    // liability
+    private Account suspenseClearingAccount;
+    private Account overpaymentAccount;
+    // income
+    private Account interestIncome;
+    private Account feeIncome;
+    private Account feeChargeOff;
+    private Account recoveries;
+    private Account interestIncomeChargeOff;
+    // expense
+    private Account creditLossBadDebt;
+    private Account creditLossBadDebtFraud;
+    private Account writtenOff;
+    private Account goodwillExpenseAccount;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new 
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + 
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new 
ResponseSpecBuilder().expectStatusCode(200).build();
+        this.loanTransactionHelper = new 
LoanTransactionHelper(this.requestSpec, this.responseSpec);
+        this.accountHelper = new AccountHelper(this.requestSpec, 
this.responseSpec);
+        this.loanProductHelper = new LoanProductHelper();
+        this.paymentTypeHelper = new PaymentTypeHelper();
+
+        // Asset
+        this.loansReceivable = this.accountHelper.createAssetAccount();
+        this.interestFeeReceivable = this.accountHelper.createAssetAccount();
+        this.suspenseAccount = this.accountHelper.createAssetAccount();
+        this.fundReceivables = this.accountHelper.createAssetAccount();
+
+        // Liability
+        this.suspenseClearingAccount = 
this.accountHelper.createLiabilityAccount();
+        this.overpaymentAccount = this.accountHelper.createLiabilityAccount();
+
+        // income
+        this.interestIncome = this.accountHelper.createIncomeAccount();
+        this.feeIncome = this.accountHelper.createIncomeAccount();
+        this.feeChargeOff = this.accountHelper.createIncomeAccount();
+        this.recoveries = this.accountHelper.createIncomeAccount();
+        this.interestIncomeChargeOff = 
this.accountHelper.createIncomeAccount();
+
+        // expense
+        this.creditLossBadDebt = this.accountHelper.createExpenseAccount();
+        this.creditLossBadDebtFraud = 
this.accountHelper.createExpenseAccount();
+        this.writtenOff = this.accountHelper.createExpenseAccount();
+        this.goodwillExpenseAccount = 
this.accountHelper.createExpenseAccount();
+
+        this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, 
this.responseSpec);
+        this.clientHelper = new ClientHelper(this.requestSpec, 
this.responseSpec);
+    }
+
+    @Test
+    public void testLoanChargeReverseReplayWithAdvancedPaymentStrategy() {
+        runAt("10 September 2022", () -> {
+            String loanExternalIdStr = UUID.randomUUID().toString();
+            final Integer loanProductID = 
createLoanProductWithPeriodicAccrualAccounting(true);
+            final Integer clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
+            final Integer loanId = createLoanAccount(clientId, loanProductID, 
loanExternalIdStr, true, "02 September 2022",
+                    "03 September 2022");
+
+            // make an in advance repayment
+            final PostLoansLoanIdTransactionsResponse repaymentTransaction = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
+                    new PostLoansLoanIdTransactionsRequest().dateFormat("dd 
MMMM yyyy").transactionDate("8 September 2022").locale("en")
+                            .transactionAmount(100.0));
+
+            // apply charges
+            Integer feeCharge = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", false));
+
+            LocalDate targetDate = LocalDate.of(2022, 9, 9);
+            final String feeCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+            Integer feeLoanChargeId = 
loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge),
 feeCharge1AddedDate, "10"));
+
+            // apply penalty
+            Integer penalty = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "20", true));
+
+            final String penaltyCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+
+            Integer penalty1LoanChargeId = 
this.loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty),
 penaltyCharge1AddedDate, "20"));
+
+            GetLoansLoanIdResponse loanDetails = 
this.loanTransactionHelper.getLoanDetails((long) loanId);
+            assertNotNull(loanDetails.getRepaymentSchedule());
+            assertNotNull(loanDetails.getRepaymentSchedule().getPeriods());
+            assertEquals(2, 
loanDetails.getRepaymentSchedule().getPeriods().size());
+            assertEquals(20.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding());
+            assertEquals(10.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding());
+            assertEquals(900.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding());
+            assertEquals(930.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalOutstandingForPeriod());
+        });
+    }
+
+    @Test
+    public void testLoanChargeReverseReplayWithStandardPaymentStrategy() {
+        runAt("10 September 2022", () -> {
+            String loanExternalIdStr = UUID.randomUUID().toString();
+            final Integer loanProductID = 
createLoanProductWithPeriodicAccrualAccounting(false);
+            final Integer clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
+            final Integer loanId = createLoanAccount(clientId, loanProductID, 
loanExternalIdStr, false, "02 September 2022",
+                    "03 September 2022");
+
+            // make an in advance repayment
+            final PostLoansLoanIdTransactionsResponse repaymentTransaction = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
+                    new PostLoansLoanIdTransactionsRequest().dateFormat("dd 
MMMM yyyy").transactionDate("8 September 2022").locale("en")
+                            .transactionAmount(100.0));
+
+            // apply charges
+            Integer feeCharge = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", false));
+
+            LocalDate targetDate = LocalDate.of(2022, 9, 9);
+            final String feeCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+            Integer feeLoanChargeId = 
loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge),
 feeCharge1AddedDate, "10"));
+
+            // apply penalty
+            Integer penalty = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "20", true));
+
+            final String penaltyCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+
+            Integer penalty1LoanChargeId = 
this.loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty),
 penaltyCharge1AddedDate, "20"));
+
+            GetLoansLoanIdResponse loanDetails = 
this.loanTransactionHelper.getLoanDetails((long) loanId);
+            assertNotNull(loanDetails.getRepaymentSchedule());
+            assertNotNull(loanDetails.getRepaymentSchedule().getPeriods());
+            assertEquals(2, 
loanDetails.getRepaymentSchedule().getPeriods().size());
+            assertEquals(0.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding());
+            assertEquals(0.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding());
+            assertEquals(930.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding());
+            assertEquals(930.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalOutstandingForPeriod());
+        });
+    }
+
+    @Test
+    public void 
testRepaymentReverseReplayedOnBackdatedChargeWithAdvancedPaymentStrategy() {
+        runAt("1 September 2022", () -> {
+            String loanExternalIdStr = UUID.randomUUID().toString();
+            final Integer loanProductID = 
createLoanProductWithPeriodicAccrualAccounting(true);
+            final Integer clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
+            final Integer loanId = createLoanAccount(clientId, loanProductID, 
loanExternalIdStr, true, "1 September 2022",
+                    "1 September 2022");
+
+            // make a repayment on 3rd od Sept
+            updateBusinessDate("3 September 2022");
+            final PostLoansLoanIdTransactionsResponse repaymentTransaction = 
loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
+                    new PostLoansLoanIdTransactionsRequest().dateFormat("dd 
MMMM yyyy").transactionDate("3 September 2022").locale("en")
+                            .transactionAmount(100.0));
+
+            // apply charges on 4th of Sept backdated to 2nd of Sept 2022
+            updateBusinessDate("4 September 2022");
+            Integer feeCharge = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "10", false));
+
+            LocalDate targetDate = LocalDate.of(2022, 9, 2);
+            final String feeCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+            Integer feeLoanChargeId = 
loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge),
 feeCharge1AddedDate, "10"));
+
+            // apply penalty
+            Integer penalty = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                    
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
 "20", true));
+
+            final String penaltyCharge1AddedDate = 
DATE_FORMATTER.format(targetDate);
+
+            Integer penalty1LoanChargeId = 
this.loanTransactionHelper.addChargesForLoan(loanId,
+                    
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty),
 penaltyCharge1AddedDate, "20"));
+
+            GetLoansLoanIdResponse loanDetails = 
this.loanTransactionHelper.getLoanDetails((long) loanId);
+            assertNotNull(loanDetails.getRepaymentSchedule());
+            assertNotNull(loanDetails.getRepaymentSchedule().getPeriods());
+            assertEquals(2, 
loanDetails.getRepaymentSchedule().getPeriods().size());
+            assertEquals(0.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding());
+            assertEquals(0.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding());
+            assertEquals(930.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding());
+            assertEquals(930.0, 
loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalOutstandingForPeriod());
+        });
+    }
+
+    private Integer createLoanAccount(final Integer clientID, final Integer 
loanProductID, final String externalId,
+            final boolean advancedPaymentStrategy, String approveDate, String 
disbursementDate) {
+
+        String loanApplicationJSON = new 
LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("30")
+                
.withLoanTermFrequencyAsDays().withNumberOfRepayments("1").withRepaymentEveryAfter("30").withRepaymentFrequencyTypeAsDays()
+                
.withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments()
+                
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate("03
 September 2022")
+                .withSubmittedOnDate("01 September 
2022").withLoanType("individual").withExternalId(externalId)
+                .withRepaymentStrategy(advancedPaymentStrategy ? 
"advanced-payment-allocation-strategy" : "mifos-standard-strategy")
+                .build(clientID.toString(), loanProductID.toString(), null);
+
+        final Integer loanId = 
loanTransactionHelper.getLoanId(loanApplicationJSON);
+        loanTransactionHelper.approveLoan(approveDate, "1000", loanId, null);
+        
loanTransactionHelper.disburseLoanWithTransactionAmount(disbursementDate, 
loanId, "1000");
+        return loanId;
+    }
+
+    private Integer createLoanProductWithPeriodicAccrualAccounting(boolean 
advancedPaymentStrategy) {
+
+        String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6);
+        String shortName = Utils.uniqueRandomStringGenerator("", 4);
+
+        List<Integer> principalVariationsForBorrowerCycle = new ArrayList<>();
+        List<Integer> numberOfRepaymentVariationsForBorrowerCycle = new 
ArrayList<>();
+        List<Integer> interestRateVariationsForBorrowerCycle = new 
ArrayList<>();
+        List<ChargeData> charges = new ArrayList<>();
+        List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings = new 
ArrayList<>();
+        List<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings = 
new ArrayList<>();
+
+        String paymentTypeName = PaymentTypeHelper.randomNameGenerator("P_T", 
5);
+        String description = PaymentTypeHelper.randomNameGenerator("PT_Desc", 
15);
+        Boolean isCashPayment = false;
+        Integer position = 1;
+
+        PostPaymentTypesResponse paymentTypesResponse = 
paymentTypeHelper.createPaymentType(new PostPaymentTypesRequest()
+                
.name(paymentTypeName).description(description).isCashPayment(isCashPayment).position(position));
+        Long paymentTypeIdOne = paymentTypesResponse.getResourceId();
+        Assertions.assertNotNull(paymentTypeIdOne);
+
+        List<GetLoanPaymentChannelToFundSourceMappings> 
paymentChannelToFundSourceMappings = new ArrayList<>();
+        GetLoanPaymentChannelToFundSourceMappings 
loanPaymentChannelToFundSourceMappings = new 
GetLoanPaymentChannelToFundSourceMappings();
+        
loanPaymentChannelToFundSourceMappings.fundSourceAccountId(fundReceivables.getAccountID().longValue());
+        
loanPaymentChannelToFundSourceMappings.paymentTypeId(paymentTypeIdOne.longValue());
+        
paymentChannelToFundSourceMappings.add(loanPaymentChannelToFundSourceMappings);
+
+        // fund
+        FundsHelper fh = 
FundsHelper.create(Utils.uniqueRandomStringGenerator("", 
10)).externalId(UUID.randomUUID().toString()).build();
+        String jsonData = fh.toJSON();
+
+        final Long fundID = createFund(jsonData, this.requestSpec, 
this.responseSpec);
+        Assertions.assertNotNull(fundID);
+
+        // Delinquency Bucket
+        final Integer delinquencyBucketId = 
DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
+
+        String futureInstallmentAllocationRule = "NEXT_INSTALLMENT";
+
+        PostLoanProductsRequest loanProductsRequest = new 
PostLoanProductsRequest().name(name)//
+                .shortName(shortName)//
+                .description("Loan Product Description")//
+                .fundId(fundID)//
+                .startDate(null)//
+                .closeDate(null)//
+                .includeInBorrowerCycle(false)//
+                .currencyCode("USD")//
+                .digitsAfterDecimal(2)//
+                .inMultiplesOf(0)//
+                .installmentAmountInMultiplesOf(1)//
+                .useBorrowerCycle(false)//
+                .minPrincipal(100.0)//
+                .principal(1000.0)//
+                .maxPrincipal(10000.0)//
+                .minNumberOfRepayments(1)//
+                .numberOfRepayments(1)//
+                .maxNumberOfRepayments(30)//
+                .isLinkedToFloatingInterestRates(false)//
+                .minInterestRatePerPeriod((double) 0)//
+                .interestRatePerPeriod((double) 0)//
+                .maxInterestRatePerPeriod((double) 0)//
+                .interestRateFrequencyType(2)//
+                .repaymentEvery(30)//
+                .repaymentFrequencyType(0)//
+                
.principalVariationsForBorrowerCycle(principalVariationsForBorrowerCycle)//
+                
.numberOfRepaymentVariationsForBorrowerCycle(numberOfRepaymentVariationsForBorrowerCycle)//
+                
.interestRateVariationsForBorrowerCycle(interestRateVariationsForBorrowerCycle)//
+                .amortizationType(1)//
+                .interestType(0)//
+                .isEqualAmortization(false)//
+                .interestCalculationPeriodType(1)//
+                
.transactionProcessingStrategyCode("mifos-standard-strategy").daysInYearType(1)//
+                .daysInMonthType(1)//
+                .canDefineInstallmentAmount(true)//
+                .graceOnArrearsAgeing(3)//
+                .overdueDaysForNPA(179)//
+                .accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
+                .principalThresholdForLastInstallment(50)//
+                .allowVariableInstallments(false)//
+                .canUseForTopup(false)//
+                .isInterestRecalculationEnabled(false)//
+                .holdGuaranteeFunds(false)//
+                .multiDisburseLoan(true)//
+                .allowAttributeOverrides(new AllowAttributeOverrides()//
+                        .amortizationType(true)//
+                        .interestType(true)//
+                        .transactionProcessingStrategyCode(true)//
+                        .interestCalculationPeriodType(true)//
+                        .inArrearsTolerance(true)//
+                        .repaymentEvery(true)//
+                        .graceOnPrincipalAndInterestPayment(true)//
+                        .graceOnArrearsAgeing(true))//
+                .allowPartialPeriodInterestCalcualtion(true)//
+                .maxTrancheCount(10)//
+                .outstandingLoanBalance(10000.0)//
+                .charges(charges)//
+                .accountingRule(3)//
+                
.fundSourceAccountId(suspenseClearingAccount.getAccountID().longValue())//
+                
.loanPortfolioAccountId(loansReceivable.getAccountID().longValue())//
+                
.transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())//
+                
.interestOnLoanAccountId(interestIncome.getAccountID().longValue())//
+                .incomeFromFeeAccountId(feeIncome.getAccountID().longValue())//
+                
.incomeFromPenaltyAccountId(feeIncome.getAccountID().longValue())//
+                
.incomeFromRecoveryAccountId(recoveries.getAccountID().longValue())//
+                .writeOffAccountId(writtenOff.getAccountID().longValue())//
+                
.overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())//
+                
.receivableInterestAccountId(interestFeeReceivable.getAccountID().longValue())//
+                
.receivableFeeAccountId(interestFeeReceivable.getAccountID().longValue())//
+                
.receivablePenaltyAccountId(interestFeeReceivable.getAccountID().longValue())//
+                .dateFormat("dd MMMM yyyy")//
+                .locale("en_GB")//
+                .disallowExpectedDisbursements(true)//
+                .allowApprovedDisbursedAmountsOverApplied(true)//
+                .overAppliedCalculationType("percentage")//
+                .overAppliedNumber(50)//
+                .delinquencyBucketId(delinquencyBucketId.longValue())//
+                
.goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())//
+                
.incomeFromGoodwillCreditInterestAccountId(interestIncomeChargeOff.getAccountID().longValue())//
+                
.incomeFromGoodwillCreditFeesAccountId(feeChargeOff.getAccountID().longValue())//
+                
.incomeFromGoodwillCreditPenaltyAccountId(feeChargeOff.getAccountID().longValue())//
+                
.paymentChannelToFundSourceMappings(paymentChannelToFundSourceMappings)//
+                
.penaltyToIncomeAccountMappings(penaltyToIncomeAccountMappings)//
+                .feeToIncomeAccountMappings(feeToIncomeAccountMappings)//
+                
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOff.getAccountID().longValue())//
+                
.incomeFromChargeOffFeesAccountId(feeChargeOff.getAccountID().longValue())//
+                
.chargeOffExpenseAccountId(creditLossBadDebt.getAccountID().longValue())//
+                
.chargeOffFraudExpenseAccountId(creditLossBadDebtFraud.getAccountID().longValue())//
+                
.incomeFromChargeOffPenaltyAccountId(feeChargeOff.getAccountID().longValue());//
+
+        if (advancedPaymentStrategy) {
+            AdvancedPaymentData defaultAllocation = 
createDefaultPaymentAllocation(futureInstallmentAllocationRule);
+
+            loanProductsRequest //
+                    
.transactionProcessingStrategyCode("advanced-payment-allocation-strategy")//
+                    .addPaymentAllocationItem(defaultAllocation);
+        }
+
+        PostLoanProductsResponse loanProductCreateResponse = 
loanProductHelper.createLoanProduct(loanProductsRequest);
+        return loanProductCreateResponse.getResourceId().intValue();
+    }
+
+    private Long createFund(final String fundJSON, final RequestSpecification 
requestSpec, final ResponseSpecification responseSpec) {
+        String fundId = 
String.valueOf(FundsResourceHandler.createFund(fundJSON, requestSpec, 
responseSpec));
+        if (fundId.equals("null")) {
+            // Invalid JSON data parameters
+            return null;
+        }
+
+        return Long.valueOf(fundId);
+    }
+
+    private AdvancedPaymentData createDefaultPaymentAllocation(String 
futureInstallmentAllocationRule) {
+        AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
+        advancedPaymentData.setTransactionType("DEFAULT");
+        
advancedPaymentData.setFutureInstallmentAllocationRule(futureInstallmentAllocationRule);
+
+        List<PaymentAllocationOrder> paymentAllocationOrders = 
getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
+                PaymentAllocationType.PAST_DUE_FEE, 
PaymentAllocationType.PAST_DUE_PRINCIPAL, 
PaymentAllocationType.PAST_DUE_INTEREST,
+                PaymentAllocationType.DUE_PENALTY, 
PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
+                PaymentAllocationType.DUE_INTEREST, 
PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
+                PaymentAllocationType.IN_ADVANCE_PRINCIPAL, 
PaymentAllocationType.IN_ADVANCE_INTEREST);
+
+        advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
+        return advancedPaymentData;
+    }
+
+    private List<PaymentAllocationOrder> 
getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
+        AtomicInteger integer = new AtomicInteger(1);
+        return Arrays.stream(paymentAllocationTypes).map(pat -> {
+            PaymentAllocationOrder paymentAllocationOrder = new 
PaymentAllocationOrder();
+            paymentAllocationOrder.setPaymentAllocationRule(pat.name());
+            paymentAllocationOrder.setOrder(integer.getAndIncrement());
+            return paymentAllocationOrder;
+        }).toList();
+    }
+
+    private void runAt(String date, Runnable runnable) {
+        try {
+            
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, 
responseSpec, 42, true);
+            GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, 
responseSpec, TRUE);
+            businessDateHelper.updateBusinessDate(
+                    new 
BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
+            runnable.run();
+        } finally {
+            GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, 
responseSpec, FALSE);
+            
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, 
responseSpec, 42, false);
+        }
+    }
+
+    private void updateBusinessDate(String date) {
+        businessDateHelper.updateBusinessDate(
+                new 
BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
+    }
+
+}

Reply via email to