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

arnold 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 3198b1b99e FINERACT-2208: Penalties are not recalculated after 
backdated transactions
3198b1b99e is described below

commit 3198b1b99e581305b21c7fce4660d2094b9f4ed5
Author: Arnold Galovics <[email protected]>
AuthorDate: Tue Mar 11 16:11:16 2025 +0100

    FINERACT-2208: Penalties are not recalculated after backdated transactions
---
 .../commands/service/CommandWrapperBuilder.java    |   9 +
 .../infrastructure/core/service/DateUtils.java     |   8 +
 .../loanaccount/domain/LoanChargeRepository.java   |   9 +
 .../LoanChargeDeactivationException.java}          |  18 +-
 .../LoanChargeDeactivateOverdueCommandHandler.java |  42 +++
 .../service/LoanChargeWritePlatformService.java    |   2 +
 .../loanaccount/api/LoanChargesApiResource.java    |   5 +
 .../service/LoanAccrualsProcessingServiceImpl.java |   5 +-
 .../LoanChargeWritePlatformServiceImpl.java        |  49 +++
 .../service/LoanJournalEntryPoster.java            |  54 +++
 .../LoanWritePlatformServiceJpaRepositoryImpl.java | 234 ++----------
 .../adjustment/LoanAdjustmentParameter.java        |  25 +-
 .../service/adjustment/LoanAdjustmentService.java  |  19 +-
 .../adjustment/LoanAdjustmentServiceImpl.java      | 303 +++++++++++++++
 .../starter/LoanAccountConfiguration.java          |  11 +-
 ...est.java => LoanAdjustmentServiceImplTest.java} |   9 +-
 .../integrationtests/BaseLoanIntegrationTest.java  |  33 ++
 .../common/charges/ChargesHelper.java              |  53 ++-
 .../LoanPenaltyBackdatedTransactionTest.java       | 410 +++++++++++++++++++++
 19 files changed, 1032 insertions(+), 266 deletions(-)

diff --git 
a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
 
b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index 3823aff799..d50e16a1ec 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -799,6 +799,15 @@ public class CommandWrapperBuilder {
         return this;
     }
 
+    public CommandWrapperBuilder deactivateOverdueLoanCharges(final Long 
loanId, final Long loanChargeId) {
+        this.actionName = "DEACTIVATEOVERDUE";
+        this.entityName = "LOANCHARGE";
+        this.entityId = loanChargeId;
+        this.loanId = loanId;
+        this.href = "/loans/" + loanId + "/charges/" + loanChargeId;
+        return this;
+    }
+
     public CommandWrapperBuilder deleteLoanCharge(final Long loanId, final 
Long loanChargeId) {
         this.actionName = "DELETE";
         this.entityName = "LOANCHARGE";
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
index abd567514d..06a5b05857 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
@@ -302,6 +302,10 @@ public final class DateUtils {
         return isAfterBusinessDate(localDate);
     }
 
+    public static boolean isDateInThePast(final LocalDate localDate) {
+        return isBeforeBusinessDate(localDate);
+    }
+
     public static int compare(LocalDate first, LocalDate second) {
         return compare(first, second, true);
     }
@@ -334,6 +338,10 @@ public final class DateUtils {
         return first != null && (second == null || first.isAfter(second));
     }
 
+    public static boolean isAfterInclusive(LocalDate first, LocalDate second) {
+        return isAfter(first, second) || isEqual(first, second);
+    }
+
     public static long getDifference(LocalDate first, LocalDate second, 
@NotNull ChronoUnit unit) {
         if (first == null || second == null) {
             throw new IllegalArgumentException("Dates must not be null to get 
difference");
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
index e3c5ac9ed4..cc7139eede 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
@@ -18,6 +18,8 @@
  */
 package org.apache.fineract.portfolio.loanaccount.domain;
 
+import java.time.LocalDate;
+import java.util.List;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -30,4 +32,11 @@ public interface LoanChargeRepository extends 
JpaRepository<LoanCharge, Long>, J
 
     @Query(FIND_ID_BY_EXTERNAL_ID)
     Long findIdByExternalId(@Param("externalId") ExternalId externalId);
+
+    @Query("""
+            SELECT lc FROM LoanCharge lc
+            WHERE lc.loan.id = :loanId
+            AND lc.dueDate >= :fromDate
+            """)
+    List<LoanCharge> findByLoanIdAndFromDueDate(@Param("loanId") Long loanId, 
@Param("fromDate") LocalDate fromDate);
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeDeactivationException.java
similarity index 52%
copy from 
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
copy to 
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeDeactivationException.java
index e3c5ac9ed4..ccc9b2a056 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeDeactivationException.java
@@ -16,18 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.portfolio.loanaccount.domain;
+package org.apache.fineract.portfolio.loanaccount.exception;
 
-import org.apache.fineract.infrastructure.core.domain.ExternalId;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
+import 
org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
 
-public interface LoanChargeRepository extends JpaRepository<LoanCharge, Long>, 
JpaSpecificationExecutor<LoanCharge> {
+public class LoanChargeDeactivationException extends 
AbstractPlatformDomainRuleException {
 
-    String FIND_ID_BY_EXTERNAL_ID = "SELECT loanCharge.id FROM LoanCharge 
loanCharge WHERE loanCharge.externalId = :externalId";
-
-    @Query(FIND_ID_BY_EXTERNAL_ID)
-    Long findIdByExternalId(@Param("externalId") ExternalId externalId);
+    public LoanChargeDeactivationException(final String errorCode, final 
String defaultUserMessage,
+            final Object... defaultUserMessageArgs) {
+        super("error.msg." + errorCode, defaultUserMessage, 
defaultUserMessageArgs);
+    }
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeDeactivateOverdueCommandHandler.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeDeactivateOverdueCommandHandler.java
new file mode 100644
index 0000000000..d40fe0a114
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeDeactivateOverdueCommandHandler.java
@@ -0,0 +1,42 @@
+/**
+ * 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.handler;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "LOANCHARGE", action = "DEACTIVATEOVERDUE")
+public class LoanChargeDeactivateOverdueCommandHandler implements 
NewCommandSourceHandler {
+
+    private final LoanChargeWritePlatformService writePlatformService;
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        return 
writePlatformService.deactivateOverdueLoanCharge(command.getLoanId(), command);
+    }
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java
index 49ab0e9708..f96a7d60be 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java
@@ -41,5 +41,7 @@ public interface LoanChargeWritePlatformService {
 
     CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long 
loanChargeId, JsonCommand command);
 
+    CommandProcessingResult deactivateOverdueLoanCharge(Long loanId, 
JsonCommand command);
+
     void applyOverdueChargesForLoan(Long loanId, 
Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList);
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java
index 379366f928..e803a5756f 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java
@@ -79,6 +79,7 @@ public class LoanChargesApiResource {
     public static final String COMMAND_PAY = "pay";
     public static final String COMMAND_WAIVE = "waive";
     public static final String COMMAND_ADJUSTMENT = "adjustment";
+    public static final String COMMAND_DEACTIVATE_OVERDUE = 
"deactivateOverdue";
     private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
             Arrays.asList("id", "chargeId", "name", "penalty", 
"chargeTimeType", "dueAsOfDate", "chargeCalculationType", "percentage",
                     "amountPercentageAppliedTo", "currency", "amountWaived", 
"amountWrittenOff", "amountOutstanding", "amountOrPercentage",
@@ -463,6 +464,10 @@ public class LoanChargesApiResource {
             final CommandWrapper commandRequest = new 
CommandWrapperBuilder().payLoanCharge(resolvedLoanId, null)
                     .withJson(apiRequestBodyAsJson).build();
             result = 
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
+        } else if (CommandParameterUtil.is(commandParam, 
COMMAND_DEACTIVATE_OVERDUE)) {
+            final CommandWrapper commandRequest = new 
CommandWrapperBuilder().deactivateOverdueLoanCharges(resolvedLoanId, null)
+                    .withJson(apiRequestBodyAsJson).build();
+            result = 
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
         } else {
             final CommandWrapper commandRequest = new 
CommandWrapperBuilder().createLoanCharge(resolvedLoanId)
                     .withJson(apiRequestBodyAsJson).build();
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
index b3f9161d74..29008d011a 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
@@ -294,6 +294,7 @@ public class LoanAccrualsProcessingServiceImpl implements 
LoanAccrualsProcessing
                 || !loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
             return;
         }
+
         LoanInterestRecalculationDetails recalculationDetails = 
loan.getLoanInterestRecalculationDetails();
         if (recalculationDetails != null && 
recalculationDetails.isCompoundingToBePostedAsTransaction()) {
             return;
@@ -523,7 +524,9 @@ public class LoanAccrualsProcessingServiceImpl implements 
LoanAccrualsProcessing
         loanCharges = loan.getLoanCharges(lc -> !lc.isDueAtDisbursement() && 
(lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate)
                 : isChargeDue(lc, tillDate, chargeOnDueDate, installment, 
period.isFirstPeriod())));
         for (LoanCharge loanCharge : loanCharges) {
-            addChargeAccrual(loanCharge, tillDate, chargeOnDueDate, 
installment, accrualPeriods);
+            if (loanCharge.isActive()) {
+                addChargeAccrual(loanCharge, tillDate, chargeOnDueDate, 
installment, accrualPeriods);
+            }
         }
     }
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
index 93ab1a773a..cfb030d17e 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
@@ -121,6 +121,7 @@ import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
 import 
org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanChargeAdjustmentException;
+import 
org.apache.fineract.portfolio.loanaccount.exception.LoanChargeDeactivationException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
@@ -130,6 +131,8 @@ import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDa
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
+import 
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentParameter;
+import 
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
 import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO;
 import 
org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
 import org.apache.fineract.portfolio.note.domain.Note;
@@ -174,6 +177,7 @@ public class LoanChargeWritePlatformServiceImpl implements 
LoanChargeWritePlatfo
     private final LoanScheduleService loanScheduleService;
     private final ReprocessLoanTransactionsService 
reprocessLoanTransactionsService;
     private final LoanAccountService loanAccountService;
+    private final LoanAdjustmentService loanAdjustmentService;
 
     private static boolean isPartOfThisInstallment(LoanCharge loanCharge, 
LoanRepaymentScheduleInstallment e) {
         return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && 
!DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
@@ -776,6 +780,49 @@ public class LoanChargeWritePlatformServiceImpl implements 
LoanChargeWritePlatfo
                 .build();
     }
 
+    @Transactional
+    @Override
+    public CommandProcessingResult deactivateOverdueLoanCharge(Long loanId, 
JsonCommand command) {
+        LocalDate fromDueDate = command.dateValueOfParameterNamed("dueDate");
+
+        List<LoanCharge> loanCharges = 
loanChargeRepository.findByLoanIdAndFromDueDate(loanId, fromDueDate);
+        loanCharges.forEach(this::inactivateOverdueLoanCharge);
+
+        Loan loan = loanAssembler.assembleFrom(loanId);
+        List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = 
loan
+                .getRepaymentScheduleInstallments(si -> 
DateUtils.isDateInRangeInclusive(fromDueDate, si.getFromDate(), si.getDueDate())
+                        || DateUtils.isAfter(si.getFromDate(), fromDueDate));
+        repaymentScheduleInstallments.forEach(si -> 
si.setPenaltyAccrued(null));
+        List<LoanTransaction> accrualsToReverse = loan.getLoanTransactions(
+                tx -> tx.isNotReversed() && 
DateUtils.isAfterInclusive(tx.getTransactionDate(), fromDueDate) && 
tx.isAccrualRelated());
+        accrualsToReverse.forEach(tx -> 
loanAdjustmentService.adjustLoanTransaction(loan, tx,
+                
LoanAdjustmentParameter.builder().transactionDate(tx.getTransactionDate()).build(),
 null, new HashMap<>()));
+
+        loanRepositoryWrapper.saveAndFlush(loan);
+
+        final CommandProcessingResultBuilder commandProcessingResultBuilder = 
new CommandProcessingResultBuilder();
+        return commandProcessingResultBuilder.withLoanId(loanId) //
+                .withEntityId(loanId) //
+                .withEntityExternalId(loan.getExternalId()) //
+                .build();
+    }
+
+    private void inactivateOverdueLoanCharge(LoanCharge loanCharge) {
+        if (!loanCharge.getChargeTimeType().isOverdueInstallment()) {
+            throw new 
LoanChargeDeactivationException("loan.charge.deactivate.invalid.charge.type",
+                    "Loan charge is not an overdue installment charge");
+        }
+
+        if (!loanCharge.isActive()) {
+            throw new 
LoanChargeDeactivationException("loan.charge.deactivate.invalid.status", "Loan 
charge is not active");
+        }
+
+        loanCharge.setActive(false);
+        loanChargeRepository.saveAndFlush(loanCharge);
+
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanUpdateChargeBusinessEvent(loanCharge));
+    }
+
     @Transactional
     @Override
     public void applyOverdueChargesForLoan(final Long loanId, 
Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList) {
@@ -802,8 +849,10 @@ public class LoanChargeWritePlatformServiceImpl implements 
LoanChargeWritePlatfo
             final JsonElement parsedCommand = 
this.fromApiJsonHelper.parse(overdueInstallment.toString());
             final JsonCommand command = 
JsonCommand.from(overdueInstallment.toString(), parsedCommand, 
this.fromApiJsonHelper, null, null,
                     null, null, null, loanId, null, null, null, null, null, 
null, null, null);
+
             LoanOverdueDTO overdueDTO = 
applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(),
                     overdueInstallment.getPeriodNumber(), command);
+
             loan = overdueDTO.getLoan();
             runInterestRecalculation = runInterestRecalculation || 
overdueDTO.isRunInterestRecalculation();
             if (DateUtils.isAfter(recalculateFrom, 
overdueDTO.getRecalculateFrom())) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java
new file mode 100644
index 0000000000..b1b41291b7
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java
@@ -0,0 +1,54 @@
+/**
+ * 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.service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import 
org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class LoanJournalEntryPoster {
+
+    private final JournalEntryWritePlatformService 
journalEntryWritePlatformService;
+
+    public void postJournalEntries(final Loan loan, final List<Long> 
existingTransactionIds,
+            final List<Long> existingReversedTransactionIds) {
+
+        final MonetaryCurrency currency = loan.getCurrency();
+        boolean isAccountTransfer = false;
+        List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
+        if (loan.isChargedOff()) {
+            accountingBridgeData = 
loan.deriveAccountingBridgeDataForChargeOff(currency.getCode(), 
existingTransactionIds,
+                    existingReversedTransactionIds, isAccountTransfer);
+        } else {
+            
accountingBridgeData.add(loan.deriveAccountingBridgeData(currency.getCode(), 
existingTransactionIds,
+                    existingReversedTransactionIds, isAccountTransfer));
+        }
+        for (Map<String, Object> accountingData : accountingBridgeData) {
+            
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
+        }
+
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index 882578d969..f12731bc93 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -195,7 +195,6 @@ import 
org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbur
 import 
org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService;
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
-import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
 import 
org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
@@ -204,6 +203,8 @@ import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTr
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator;
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
+import 
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentParameter;
+import 
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
 import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
 import 
org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
 import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
@@ -278,6 +279,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
     private final LoanOfficerService loanOfficerService;
     private final ReprocessLoanTransactionsService 
reprocessLoanTransactionsService;
     private final LoanAccountService loanAccountService;
+    private final LoanJournalEntryPoster journalEntryPoster;
+    private final LoanAdjustmentService loanAdjustmentService;
 
     @Transactional
     @Override
@@ -534,7 +537,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
             businessEventNotifierService.notifyPostBusinessEvent(new 
LoanDisbursalTransactionBusinessEvent(disbursalTransaction));
         }
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
 
         return new CommandProcessingResultBuilder() //
@@ -816,7 +819,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
             if (!changes.isEmpty()) {
                 createNote(loan, command, changes);
                 loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
-                postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+                journalEntryPoster.postJournalEntries(loan, 
existingTransactionIds, existingReversedTransactionIds);
                 
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
             }
             final Set<LoanCharge> loanCharges = loan.getActiveCharges();
@@ -1165,7 +1168,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanBalanceChangedBusinessEvent(loan));
         
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
 
         return new 
CommandProcessingResultBuilder().withCommandId(command.commandId()) //
@@ -1332,6 +1335,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
                     "Interest refund transaction: " + transactionId + " cannot 
be reversed or adjusted directly", transactionId);
         }
 
+        Long commandId = command.commandId();
+        final String noteText = command.stringValueOfParameterNamed("note");
         final LocalDate transactionDate = 
command.localDateValueOfParameterNamed("transactionDate");
         final BigDecimal transactionAmount = 
command.bigDecimalValueOfParameterNamed("transactionAmount");
         final ExternalId txnExternalId = 
externalIdFactory.createFromCommand(command, 
LoanApiConstants.externalIdParameterName);
@@ -1354,181 +1359,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         changes.put("dateFormat", command.dateFormat());
         changes.put("paymentTypeId", 
command.longValueOfParameterNamed("paymentTypeId"));
 
-        final List<Long> existingTransactionIds = new ArrayList<>();
-        final List<Long> existingReversedTransactionIds = new ArrayList<>();
-
-        final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), 
transactionAmount);
         final PaymentDetail paymentDetail = 
this.paymentDetailWritePlatformService.createPaymentDetail(command, changes);
-        LoanTransaction newTransactionDetail = 
LoanTransaction.repaymentType(transactionToAdjust.getTypeOf(), loan.getOffice(),
-                transactionAmountAsMoney, paymentDetail, transactionDate, 
txnExternalId, transactionToAdjust.getChargeRefundChargeType());
-        if (transactionToAdjust.isInterestWaiver()) {
-            Money unrecognizedIncome = transactionAmountAsMoney.zero();
-            Money interestComponent = transactionAmountAsMoney;
-            if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
-                Money receivableInterest = 
loan.getReceivableInterest(transactionDate);
-                if 
(transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
-                    interestComponent = receivableInterest;
-                    unrecognizedIncome = 
transactionAmountAsMoney.minus(receivableInterest);
-                }
-            }
-            newTransactionDetail = LoanTransaction.waiver(loan.getOffice(), 
loan, transactionAmountAsMoney, transactionDate,
-                    interestComponent, unrecognizedIncome, txnExternalId);
-        }
-
-        LocalDate recalculateFrom = null;
-
-        if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
-            recalculateFrom = 
DateUtils.isAfter(transactionToAdjust.getTransactionDate(), transactionDate) ? 
transactionDate
-                    : transactionToAdjust.getTransactionDate();
-        }
-
-        ScheduleGeneratorDTO scheduleGeneratorDTO = 
this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
-
-        HolidayDetailDTO holidayDetailDTO = 
scheduleGeneratorDTO.getHolidayDetailDTO();
-        if 
(loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE))
 {
-            // validate cumulative
-            
loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, 
transactionToAdjust.getTransactionDate(),
-                    LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
-        }
-        // common validations
-        
loanTransactionValidator.validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(),
-                holidayDetailDTO.isAllowTransactionsOnHoliday(), 
holidayDetailDTO.getHolidays());
-        
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(),
-                holidayDetailDTO.getWorkingDays(), 
holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
-
-        adjustExistingTransaction(loan, newTransactionDetail, 
loanLifecycleStateMachine, transactionToAdjust, existingTransactionIds,
-                existingReversedTransactionIds, scheduleGeneratorDTO, 
reversalTxnExternalId);
-
-        loanAccrualsProcessingService.reprocessExistingAccruals(loan);
-        if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
-            
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
-        }
-
-        boolean thereIsNewTransaction = 
newTransactionDetail.isGreaterThanZero();
-        if (thereIsNewTransaction) {
-            if (paymentDetail != null) {
-                
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
-            }
-            this.loanTransactionRepository.saveAndFlush(newTransactionDetail);
-        }
-
-        loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
-
-        final String noteText = command.stringValueOfParameterNamed("note");
-        if (StringUtils.isNotBlank(noteText)) {
-            changes.put("note", noteText);
-            Note note;
-            /**
-             * If a new transaction is not created, associate note with the 
transaction to be adjusted
-             **/
-            if (thereIsNewTransaction) {
-                note = Note.loanTransactionNote(loan, newTransactionDetail, 
noteText);
-            } else {
-                note = Note.loanTransactionNote(loan, transactionToAdjust, 
noteText);
-            }
-            this.noteRepository.save(note);
-        }
-
-        Collection<Long> transactionIds = new ArrayList<>();
-        List<LoanTransaction> transactions = loan.getLoanTransactions();
-        for (LoanTransaction transaction : transactions) {
-            if (transaction.isRefund() && transaction.isNotReversed()) {
-                transactionIds.add(transaction.getId());
-            }
-        }
-
-        if (!transactionIds.isEmpty()) {
-            
this.accountTransfersWritePlatformService.reverseTransfersWithFromAccountTransactions(transactionIds,
-                    PortfolioAccountType.LOAN);
-            loan.updateLoanSummaryAndStatus();
-        }
-
-        
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, 
loan.isInterestBearingAndInterestRecalculationEnabled(),
-                false);
-
-        this.loanAccountDomainService.setLoanDelinquencyTag(loan, 
DateUtils.getBusinessLocalDate());
 
-        LoanAdjustTransactionBusinessEvent.Data eventData = new 
LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust);
-        if (newTransactionDetail.isRepaymentLikeType() && 
thereIsNewTransaction) {
-            eventData.setNewTransactionDetail(newTransactionDetail);
-        }
-        Long entityId = transactionToAdjust.getId();
-        ExternalId entityExternalId = transactionToAdjust.getExternalId();
+        LoanAdjustmentParameter parameter = 
LoanAdjustmentParameter.builder().transactionAmount(transactionAmount)
+                
.paymentDetail(paymentDetail).transactionDate(transactionDate).txnExternalId(txnExternalId)
+                
.reversalTxnExternalId(reversalTxnExternalId).noteText(noteText).build();
 
-        if (thereIsNewTransaction) {
-            entityId = newTransactionDetail.getId();
-            entityExternalId = newTransactionDetail.getExternalId();
-        }
-        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanBalanceChangedBusinessEvent(loan));
-        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanAdjustTransactionBusinessEvent(eventData));
-
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
-        
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
-
-        return new CommandProcessingResultBuilder() //
-                .withCommandId(command.commandId()) //
-                .withEntityId(entityId) //
-                .withEntityExternalId(entityExternalId) //
-                .withOfficeId(loan.getOfficeId()) //
-                .withClientId(loan.getClientId()) //
-                .withGroupId(loan.getGroupId()) //
-                .withLoanId(loanId) //
-                .with(changes).build();
-    }
-
-    public void adjustExistingTransaction(final Loan loan, final 
LoanTransaction newTransactionDetail,
-            final LoanLifecycleStateMachine loanLifecycleStateMachine, final 
LoanTransaction transactionForAdjustment,
-            final List<Long> existingTransactionIds, final List<Long> 
existingReversedTransactionIds,
-            final ScheduleGeneratorDTO scheduleGeneratorDTO, final ExternalId 
reversalExternalId) {
-        existingTransactionIds.addAll(loan.findExistingTransactionIds());
-        
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
-
-        
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan,
 LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
-                transactionForAdjustment.getTransactionDate());
-
-        if (transactionForAdjustment.isNotRepaymentLikeType() && 
transactionForAdjustment.isNotWaiver()
-                && transactionForAdjustment.isNotCreditBalanceRefund()) {
-            final String errorMessage = "Only (non-reversed) transactions of 
type repayment, waiver or credit balance refund can be adjusted.";
-            throw new InvalidLoanTransactionTypeException("transaction",
-                    
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions",
 errorMessage);
-        }
-
-        
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transactionForAdjustment.getLoan(),
-                transactionForAdjustment, "reversed");
-        transactionForAdjustment.reverse(reversalExternalId);
-        transactionForAdjustment.manuallyAdjustedOrReversed();
-
-        if 
(transactionForAdjustment.getTypeOf().equals(LoanTransactionType.MERCHANT_ISSUED_REFUND)
-                || 
transactionForAdjustment.getTypeOf().equals(LoanTransactionType.PAYOUT_REFUND)) 
{
-            loan.getLoanTransactions().stream() //
-                    .filter(LoanTransaction::isNotReversed)
-                    .filter(loanTransaction -> 
loanTransaction.getLoanTransactionRelations().stream()
-                            .anyMatch(relation -> 
relation.getRelationType().equals(LoanTransactionRelationTypeEnum.RELATED)
-                                    && 
relation.getToTransaction().getId().equals(transactionForAdjustment.getId())))
-                    .forEach(loanTransaction -> {
-                        
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(),
-                                loanTransaction, "reversed");
-                        loanTransaction.reverse();
-                        loanTransaction.manuallyAdjustedOrReversed();
-                        LoanAdjustTransactionBusinessEvent.Data eventData = 
new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
-                        
businessEventNotifierService.notifyPostBusinessEvent(new 
LoanAdjustTransactionBusinessEvent(eventData));
-                    });
-        }
-
-        if (loan.isClosedWrittenOff()) {
-            // find write off transaction and reverse it
-            final LoanTransaction writeOffTransaction = 
loan.findWriteOffTransaction();
-            
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(writeOffTransaction.getLoan(),
 writeOffTransaction,
-                    "reversed");
-            writeOffTransaction.reverse();
-        }
-
-        loan.updateLoanSummaryAndStatus();
-
-        if (newTransactionDetail.isRepaymentLikeType() || 
newTransactionDetail.isInterestWaiver()) {
-            
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan,
 newTransactionDetail,
-                    loanLifecycleStateMachine, transactionForAdjustment, 
scheduleGeneratorDTO);
-        }
+        return loanAdjustmentService.adjustLoanTransaction(loan, 
transactionToAdjust, parameter, commandId, changes);
     }
 
     @Transactional
@@ -1611,7 +1448,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
             this.noteRepository.save(note);
         }
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         this.loanAccountDomainService.setLoanDelinquencyTag(loan, 
transactionDate);
         
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanChargebackTransactionBusinessEvent(newTransaction));
@@ -1714,7 +1551,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanBalanceChangedBusinessEvent(loan));
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanWaiveInterestBusinessEvent(waiveInterestTransaction));
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
         return new CommandProcessingResultBuilder() //
                 .withCommandId(command.commandId()) //
@@ -1808,7 +1645,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
             businessEventNotifierService.notifyPostBusinessEvent(new 
LoanBalanceChangedBusinessEvent(loan));
             businessEventNotifierService.notifyPostBusinessEvent(new 
LoanWrittenOffPostBusinessEvent(loanTransaction));
 
-            postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+            journalEntryPoster.postJournalEntries(loan, 
existingTransactionIds, existingReversedTransactionIds);
             
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
 
             
builder.withEntityId(loanTransaction.getId()).withEntityExternalId(loanTransaction.getExternalId());
@@ -1885,7 +1722,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         // disable all active standing instructions linked to the loan
         
this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
 
         CommandProcessingResult result;
@@ -2021,7 +1858,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         this.loanTransactionRepository.saveAndFlush(newTransferTransaction);
         saveLoanWithDataIntegrityViolationChecks(loan);
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanInitiateTransferBusinessEvent(loan));
         return newTransferTransaction;
     }
@@ -2051,7 +1888,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
         saveLoanWithDataIntegrityViolationChecks(loan);
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanAcceptTransferBusinessEvent(loan));
 
         return newTransferAcceptanceTransaction;
@@ -2077,7 +1914,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
         saveLoanWithDataIntegrityViolationChecks(loan);
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanWithdrawTransferBusinessEvent(loan));
 
         return newTransferAcceptanceTransaction;
@@ -2209,25 +2046,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
                 .build();
     }
 
-    private void postJournalEntries(final Loan loan, final List<Long> 
existingTransactionIds,
-            final List<Long> existingReversedTransactionIds) {
-
-        final MonetaryCurrency currency = loan.getCurrency();
-        boolean isAccountTransfer = false;
-        List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
-        if (loan.isChargedOff()) {
-            accountingBridgeData = 
loan.deriveAccountingBridgeDataForChargeOff(currency.getCode(), 
existingTransactionIds,
-                    existingReversedTransactionIds, isAccountTransfer);
-        } else {
-            
accountingBridgeData.add(loan.deriveAccountingBridgeData(currency.getCode(), 
existingTransactionIds,
-                    existingReversedTransactionIds, isAccountTransfer));
-        }
-        for (Map<String, Object> accountingData : accountingBridgeData) {
-            
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
-        }
-
-    }
-
     @Transactional
     @Override
     public void applyMeetingDateChanges(final Calendar calendar, final 
Collection<CalendarInstance> loanCalendarInstances) {
@@ -2503,7 +2321,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
 
         this.loanAccountDomainService.setLoanDelinquencyTag(loan, 
DateUtils.getBusinessLocalDate());
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
 
         return new CommandProcessingResultBuilder() //
@@ -2647,7 +2465,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
                 false);
         this.loanAccountDomainService.setLoanDelinquencyTag(loan, 
DateUtils.getBusinessLocalDate());
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
 
         return new CommandProcessingResultBuilder() //
@@ -2709,7 +2527,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
                 false);
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanInterestRecalculationBusinessEvent(loan));
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
         return loan;
     }
@@ -3129,7 +2947,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
                     businessEventNotifierService.notifyPostBusinessEvent(new 
LoanAdjustTransactionBusinessEvent(data));
                 });
 
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanChargeOffPostBusinessEvent(chargeOffTransaction));
         return new CommandProcessingResultBuilder() //
                 .withCommandId(command.commandId()) //
@@ -3192,7 +3010,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         reprocessLoanTransactionsService.reprocessTransactions(loan);
 
         saveLoanWithDataIntegrityViolationChecks(loan);
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanUndoChargeOffBusinessEvent(chargedOffTransaction));
 
         return new CommandProcessingResultBuilder() //
@@ -3267,7 +3085,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanBalanceChangedBusinessEvent(loan));
 
         // Create journal entries for the new transaction(s)
-        postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
 
         Long entityId = refundTransaction.getId();
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentParameter.java
similarity index 57%
copy from 
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentParameter.java
index e3c5ac9ed4..b6dca479ee 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentParameter.java
@@ -16,18 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.portfolio.loanaccount.domain;
+package org.apache.fineract.portfolio.loanaccount.service.adjustment;
 
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import lombok.Builder;
+import lombok.Data;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
+import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
 
-public interface LoanChargeRepository extends JpaRepository<LoanCharge, Long>, 
JpaSpecificationExecutor<LoanCharge> {
+@Data
+@Builder
+public class LoanAdjustmentParameter {
 
-    String FIND_ID_BY_EXTERNAL_ID = "SELECT loanCharge.id FROM LoanCharge 
loanCharge WHERE loanCharge.externalId = :externalId";
-
-    @Query(FIND_ID_BY_EXTERNAL_ID)
-    Long findIdByExternalId(@Param("externalId") ExternalId externalId);
+    private BigDecimal transactionAmount;
+    private PaymentDetail paymentDetail;
+    private LocalDate transactionDate;
+    private ExternalId txnExternalId;
+    private ExternalId reversalTxnExternalId;
+    private String noteText;
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentService.java
similarity index 52%
copy from 
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentService.java
index e3c5ac9ed4..884b7a3016 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentService.java
@@ -16,18 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.portfolio.loanaccount.domain;
+package org.apache.fineract.portfolio.loanaccount.service.adjustment;
 
-import org.apache.fineract.infrastructure.core.domain.ExternalId;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
+import java.util.Map;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
 
-public interface LoanChargeRepository extends JpaRepository<LoanCharge, Long>, 
JpaSpecificationExecutor<LoanCharge> {
+public interface LoanAdjustmentService {
 
-    String FIND_ID_BY_EXTERNAL_ID = "SELECT loanCharge.id FROM LoanCharge 
loanCharge WHERE loanCharge.externalId = :externalId";
-
-    @Query(FIND_ID_BY_EXTERNAL_ID)
-    Long findIdByExternalId(@Param("externalId") ExternalId externalId);
+    CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction 
transactionToAdjust, LoanAdjustmentParameter parameter,
+            Long commandId, Map<String, Object> changes);
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
new file mode 100644
index 0000000000..a57a3954c2
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
@@ -0,0 +1,303 @@
+/**
+ * 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.service.adjustment;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.core.data.ApiParameterError;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import 
org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.portfolio.account.PortfolioAccountType;
+import 
org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
+import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
+import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
+import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
+import 
org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
+import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
+import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
+import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
+import org.apache.fineract.portfolio.note.domain.Note;
+import org.apache.fineract.portfolio.note.domain.NoteRepository;
+import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
+import 
org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.orm.jpa.JpaSystemException;
+import org.springframework.stereotype.Component;
+
+@RequiredArgsConstructor
+@Component
+public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
+
+    private final LoanTransactionValidator loanTransactionValidator;
+    private final LoanRepositoryWrapper loanRepositoryWrapper;
+    private final LoanAccountDomainService loanAccountDomainService;
+    private final NoteRepository noteRepository;
+    private final LoanTransactionRepository loanTransactionRepository;
+    private final PaymentDetailWritePlatformService 
paymentDetailWritePlatformService;
+    private final AccountTransfersWritePlatformService 
accountTransfersWritePlatformService;
+    private final BusinessEventNotifierService businessEventNotifierService;
+    private final LoanUtilService loanUtilService;
+    private final LoanRepaymentScheduleInstallmentRepository 
loanRepaymentScheduleInstallmentRepository;
+    private final LoanLifecycleStateMachine loanLifecycleStateMachine;
+    private final LoanAccrualTransactionBusinessEventService 
loanAccrualTransactionBusinessEventService;
+    private final LoanDownPaymentHandlerService loanDownPaymentHandlerService;
+    private final LoanAccrualsProcessingService loanAccrualsProcessingService;
+    private final LoanChargeValidator loanChargeValidator;
+    private final LoanJournalEntryPoster journalEntryPoster;
+
+    @Override
+    public CommandProcessingResult adjustLoanTransaction(Loan loan, 
LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
+            Long commandId, Map<String, Object> changes) {
+        LocalDate transactionDate = parameter.getTransactionDate();
+        BigDecimal transactionAmount = parameter.getTransactionAmount();
+        PaymentDetail paymentDetail = parameter.getPaymentDetail();
+        ExternalId txnExternalId = parameter.getTxnExternalId();
+        ExternalId reversalTxnExternalId = 
parameter.getReversalTxnExternalId();
+        String noteText = parameter.getNoteText();
+
+        final List<Long> existingTransactionIds = new ArrayList<>();
+        final List<Long> existingReversedTransactionIds = new ArrayList<>();
+
+        final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), 
transactionAmount);
+        LoanTransaction newTransactionDetail = 
LoanTransaction.repaymentType(transactionToAdjust.getTypeOf(), loan.getOffice(),
+                transactionAmountAsMoney, paymentDetail, transactionDate, 
txnExternalId, transactionToAdjust.getChargeRefundChargeType());
+        if (transactionToAdjust.isInterestWaiver()) {
+            Money unrecognizedIncome = transactionAmountAsMoney.zero();
+            Money interestComponent = transactionAmountAsMoney;
+            if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
+                Money receivableInterest = 
loan.getReceivableInterest(transactionDate);
+                if 
(transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
+                    interestComponent = receivableInterest;
+                    unrecognizedIncome = 
transactionAmountAsMoney.minus(receivableInterest);
+                }
+            }
+            newTransactionDetail = LoanTransaction.waiver(loan.getOffice(), 
loan, transactionAmountAsMoney, transactionDate,
+                    interestComponent, unrecognizedIncome, txnExternalId);
+        }
+
+        LocalDate recalculateFrom = null;
+
+        if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
+            recalculateFrom = 
DateUtils.isAfter(transactionToAdjust.getTransactionDate(), transactionDate) ? 
transactionDate
+                    : transactionToAdjust.getTransactionDate();
+        }
+
+        ScheduleGeneratorDTO scheduleGeneratorDTO = 
this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
+
+        HolidayDetailDTO holidayDetailDTO = 
scheduleGeneratorDTO.getHolidayDetailDTO();
+        if 
(loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE))
 {
+            // validate cumulative
+            
loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, 
transactionToAdjust.getTransactionDate(),
+                    LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
+        }
+        // common validations
+        
loanTransactionValidator.validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(),
+                holidayDetailDTO.isAllowTransactionsOnHoliday(), 
holidayDetailDTO.getHolidays());
+        
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(),
+                holidayDetailDTO.getWorkingDays(), 
holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
+
+        adjustExistingTransaction(loan, newTransactionDetail, 
loanLifecycleStateMachine, transactionToAdjust, existingTransactionIds,
+                existingReversedTransactionIds, scheduleGeneratorDTO, 
reversalTxnExternalId);
+
+        loanAccrualsProcessingService.reprocessExistingAccruals(loan);
+        if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
+            
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
+        }
+
+        boolean thereIsNewTransaction = 
newTransactionDetail.isGreaterThanZero();
+        if (thereIsNewTransaction) {
+            if (paymentDetail != null) {
+                
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
+            }
+            this.loanTransactionRepository.saveAndFlush(newTransactionDetail);
+        }
+
+        loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
+
+        if (StringUtils.isNotBlank(noteText)) {
+            changes.put("note", noteText);
+            Note note;
+            /**
+             * If a new transaction is not created, associate note with the 
transaction to be adjusted
+             **/
+            if (thereIsNewTransaction) {
+                note = Note.loanTransactionNote(loan, newTransactionDetail, 
noteText);
+            } else {
+                note = Note.loanTransactionNote(loan, transactionToAdjust, 
noteText);
+            }
+            this.noteRepository.save(note);
+        }
+
+        Collection<Long> transactionIds = new ArrayList<>();
+        List<LoanTransaction> transactions = loan.getLoanTransactions();
+        for (LoanTransaction transaction : transactions) {
+            if (transaction.isRefund() && transaction.isNotReversed()) {
+                transactionIds.add(transaction.getId());
+            }
+        }
+
+        if (!transactionIds.isEmpty()) {
+            
this.accountTransfersWritePlatformService.reverseTransfersWithFromAccountTransactions(transactionIds,
+                    PortfolioAccountType.LOAN);
+            loan.updateLoanSummaryAndStatus();
+        }
+
+        
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, 
loan.isInterestBearingAndInterestRecalculationEnabled(),
+                false);
+
+        this.loanAccountDomainService.setLoanDelinquencyTag(loan, 
DateUtils.getBusinessLocalDate());
+
+        LoanAdjustTransactionBusinessEvent.Data eventData = new 
LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust);
+        if (newTransactionDetail.isRepaymentLikeType() && 
thereIsNewTransaction) {
+            eventData.setNewTransactionDetail(newTransactionDetail);
+        }
+        Long entityId = transactionToAdjust.getId();
+        ExternalId entityExternalId = transactionToAdjust.getExternalId();
+
+        if (thereIsNewTransaction) {
+            entityId = newTransactionDetail.getId();
+            entityExternalId = newTransactionDetail.getExternalId();
+        }
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanBalanceChangedBusinessEvent(loan));
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanAdjustTransactionBusinessEvent(eventData));
+
+        journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
+        
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
+
+        return new CommandProcessingResultBuilder() //
+                .withCommandId(commandId) //
+                .withEntityId(entityId) //
+                .withEntityExternalId(entityExternalId) //
+                .withOfficeId(loan.getOfficeId()) //
+                .withClientId(loan.getClientId()) //
+                .withGroupId(loan.getGroupId()) //
+                .withLoanId(loan.getId()) //
+                .with(changes).build();
+    }
+
+    public void adjustExistingTransaction(final Loan loan, final 
LoanTransaction newTransactionDetail,
+            final LoanLifecycleStateMachine loanLifecycleStateMachine, final 
LoanTransaction transactionForAdjustment,
+            final List<Long> existingTransactionIds, final List<Long> 
existingReversedTransactionIds,
+            final ScheduleGeneratorDTO scheduleGeneratorDTO, final ExternalId 
reversalExternalId) {
+        existingTransactionIds.addAll(loan.findExistingTransactionIds());
+        
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
+
+        
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan,
 LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
+                transactionForAdjustment.getTransactionDate());
+
+        if (!transactionForAdjustment.isAccrualRelated() && 
transactionForAdjustment.isNotRepaymentLikeType()
+                && transactionForAdjustment.isNotWaiver() && 
transactionForAdjustment.isNotCreditBalanceRefund()) {
+            final String errorMessage = "Only (non-reversed) transactions of 
type repayment, waiver, accrual or credit balance refund can be adjusted.";
+            throw new InvalidLoanTransactionTypeException("transaction",
+                    
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions",
 errorMessage);
+        }
+
+        
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transactionForAdjustment.getLoan(),
+                transactionForAdjustment, "reversed");
+        transactionForAdjustment.reverse(reversalExternalId);
+        transactionForAdjustment.manuallyAdjustedOrReversed();
+
+        if 
(transactionForAdjustment.getTypeOf().equals(LoanTransactionType.MERCHANT_ISSUED_REFUND)
+                || 
transactionForAdjustment.getTypeOf().equals(LoanTransactionType.PAYOUT_REFUND)) 
{
+            loan.getLoanTransactions().stream() //
+                    .filter(LoanTransaction::isNotReversed)
+                    .filter(loanTransaction -> 
loanTransaction.getLoanTransactionRelations().stream()
+                            .anyMatch(relation -> 
relation.getRelationType().equals(LoanTransactionRelationTypeEnum.RELATED)
+                                    && 
relation.getToTransaction().getId().equals(transactionForAdjustment.getId())))
+                    .forEach(loanTransaction -> {
+                        
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(),
+                                loanTransaction, "reversed");
+                        loanTransaction.reverse();
+                        loanTransaction.manuallyAdjustedOrReversed();
+                        LoanAdjustTransactionBusinessEvent.Data eventData = 
new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
+                        
businessEventNotifierService.notifyPostBusinessEvent(new 
LoanAdjustTransactionBusinessEvent(eventData));
+                    });
+        }
+
+        if (loan.isClosedWrittenOff()) {
+            // find write off transaction and reverse it
+            final LoanTransaction writeOffTransaction = 
loan.findWriteOffTransaction();
+            
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(writeOffTransaction.getLoan(),
 writeOffTransaction,
+                    "reversed");
+            writeOffTransaction.reverse();
+        }
+
+        loan.updateLoanSummaryAndStatus();
+
+        if (newTransactionDetail.isRepaymentLikeType() || 
newTransactionDetail.isInterestWaiver()) {
+            
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan,
 newTransactionDetail,
+                    loanLifecycleStateMachine, transactionForAdjustment, 
scheduleGeneratorDTO);
+        }
+    }
+
+    private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan 
loan) {
+        /*
+         * Due to the "saveAndFlushLoanWithDataIntegrityViolationChecks" 
method the loan is saved and flushed in the
+         * middle of the transaction. EclipseLink is in some situations are 
saving inconsistently the newly created
+         * associations, like the newly created repayment schedule 
installments. The save and flush cannot be removed
+         * safely till any native queries are used as part of this transaction 
either. See:
+         * this.loanAccountDomainService.recalculateAccruals(loan);
+         */
+        try {
+            
loanRepaymentScheduleInstallmentRepository.saveAll(loan.getRepaymentScheduleInstallments());
+            return this.loanRepositoryWrapper.saveAndFlush(loan);
+        } catch (final JpaSystemException | DataIntegrityViolationException e) 
{
+            final Throwable realCause = e.getCause();
+            final List<ApiParameterError> dataValidationErrors = new 
ArrayList<>();
+            final DataValidatorBuilder baseDataValidator = new 
DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
+            if 
(realCause.getMessage().toLowerCase().contains("external_id_unique")) {
+                
baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).failWithCode("value.must.be.unique");
+            }
+            if (!dataValidationErrors.isEmpty()) {
+                throw new 
PlatformApiDataValidationException("validation.msg.validation.errors.exist", 
"Validation errors exist.",
+                        dataValidationErrors, e);
+            }
+            throw e;
+        }
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
index dcf9cdb646..b4aa6029ab 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
@@ -127,6 +127,7 @@ import 
org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetails
 import 
org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementService;
 import 
org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
 import 
org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerServiceImpl;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
 import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
 import 
org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
 import 
org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
@@ -142,6 +143,7 @@ import 
org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServic
 import 
org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
 import 
org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl;
 import 
org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
+import 
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
 import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
 import 
org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
 import 
org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
@@ -296,7 +298,7 @@ public class LoanAccountConfiguration {
             LoanAccrualsProcessingService loanAccrualsProcessingService,
             LoanDownPaymentTransactionValidator 
loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator,
             LoanScheduleService loanScheduleService, 
ReprocessLoanTransactionsService reprocessLoanTransactionsService,
-            LoanAccountService loanAccountService) {
+            LoanAccountService loanAccountService, LoanAdjustmentService 
loanAdjustmentService) {
         return new 
LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, 
chargeRepository,
                 businessEventNotifierService, loanTransactionRepository, 
accountTransfersWritePlatformService, loanRepositoryWrapper,
                 journalEntryWritePlatformService, loanAccountDomainService, 
loanChargeRepository, loanWritePlatformService, loanUtilService,
@@ -304,7 +306,7 @@ public class LoanAccountConfiguration {
                 configurationDomainService, 
loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
                 accountTransferDetailRepository, loanChargeAssembler, 
paymentDetailWritePlatformService, noteRepository,
                 loanAccrualTransactionBusinessEventService, 
loanAccrualsProcessingService, loanDownPaymentTransactionValidator,
-                loanChargeValidator, loanScheduleService, 
reprocessLoanTransactionsService, loanAccountService);
+                loanChargeValidator, loanScheduleService, 
reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService);
     }
 
     @Bean
@@ -392,7 +394,8 @@ public class LoanAccountConfiguration {
             LoanOfficerValidator loanOfficerValidator, 
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator,
             LoanDisbursementService loanDisbursementService, 
LoanScheduleService loanScheduleService,
             LoanChargeValidator loanChargeValidator, LoanOfficerService 
loanOfficerService,
-            ReprocessLoanTransactionsService reprocessLoanTransactionsService, 
LoanAccountService loanAccountService) {
+            ReprocessLoanTransactionsService reprocessLoanTransactionsService, 
LoanAccountService loanAccountService,
+            LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService 
loanAdjustmentService) {
         return new LoanWritePlatformServiceJpaRepositoryImpl(context, 
loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
                 loanRepositoryWrapper, loanAccountDomainService, 
noteRepository, loanTransactionRepository,
                 loanTransactionRelationRepository, loanAssembler, 
journalEntryWritePlatformService, calendarInstanceRepository,
@@ -406,7 +409,7 @@ public class LoanAccountConfiguration {
                 loanAccountLockService, externalIdFactory, 
loanAccrualTransactionBusinessEventService, errorHandler,
                 loanDownPaymentHandlerService, loanTransactionAssembler, 
loanAccrualsProcessingService, loanOfficerValidator,
                 loanDownPaymentTransactionValidator, loanDisbursementService, 
loanScheduleService, loanChargeValidator, loanOfficerService,
-                reprocessLoanTransactionsService, loanAccountService);
+                reprocessLoanTransactionsService, loanAccountService, 
journalEntryPoster, loanAdjustmentService);
     }
 
     @Bean
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java
similarity index 97%
rename from 
fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java
rename to 
fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java
index a4fc70a01c..16588e705c 100644
--- 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java
@@ -78,6 +78,7 @@ import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationVa
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
 import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
+import 
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentServiceImpl;
 import org.apache.fineract.portfolio.note.domain.NoteRepository;
 import 
org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
 import 
org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository;
@@ -90,10 +91,10 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-class LoanWritePlatformServiceJpaRepositoryImplTest {
+class LoanAdjustmentServiceImplTest {
 
     @InjectMocks
-    private LoanWritePlatformServiceJpaRepositoryImpl loanWritePlatformService;
+    private LoanAdjustmentServiceImpl underTest;
 
     @Mock
     private LoanRepaymentScheduleTransactionProcessorFactory 
transactionProcessorFactory;
@@ -240,7 +241,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
         when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
 
         // Act
-        loanWritePlatformService.adjustExistingTransaction(loan, 
newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
+        underTest.adjustExistingTransaction(loan, newTransactionDetail, 
loanLifecycleStateMachine, transactionForAdjustment,
                 existingTransactionIds, existingReversedTransactionIds, 
scheduleGeneratorDTO, reversalExternalId);
 
         // Assert
@@ -285,7 +286,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
         when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
 
         // Act
-        loanWritePlatformService.adjustExistingTransaction(loan, 
newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
+        underTest.adjustExistingTransaction(loan, newTransactionDetail, 
loanLifecycleStateMachine, transactionForAdjustment,
                 existingTransactionIds, existingReversedTransactionIds, 
scheduleGeneratorDTO, reversalExternalId);
 
         // Assert
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index 414180cc4e..372607070e 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -61,6 +61,7 @@ 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.GetJournalEntriesTransactionIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdChargesChargeIdResponse;
 import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
 import org.apache.fineract.client.models.GetLoansLoanIdResponse;
 import org.apache.fineract.client.models.GetLoansLoanIdStatus;
@@ -71,6 +72,7 @@ import org.apache.fineract.client.models.LoanPointInTimeData;
 import org.apache.fineract.client.models.PaymentAllocationOrder;
 import org.apache.fineract.client.models.PostChargesResponse;
 import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest;
 import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse;
 import org.apache.fineract.client.models.PostLoansLoanIdRequest;
 import org.apache.fineract.client.models.PostLoansLoanIdResponse;
@@ -796,6 +798,14 @@ public abstract class BaseLoanIntegrationTest extends 
IntegrationTest {
         return chargeId.longValue();
     }
 
+    protected Long createOverduePenaltyPercentageCharge(double 
percentageAmount, Integer feeFrequency, int feeInterval) {
+        Integer chargeId = ChargesHelper.createCharges(requestSpec, 
responseSpec,
+                
ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentageWithFeeInterval(String.valueOf(percentageAmount),
+                        feeFrequency, feeInterval));
+        assertNotNull(chargeId);
+        return chargeId.longValue();
+    }
+
     protected void verifyRepaymentSchedule(GetLoansLoanIdResponse 
savedLoanResponse, GetLoansLoanIdResponse actualLoanResponse,
             int totalPeriods, int identicalPeriods) {
         List<GetLoansLoanIdRepaymentPeriod> savedPeriods = 
savedLoanResponse.getRepaymentSchedule().getPeriods();
@@ -934,6 +944,18 @@ public abstract class BaseLoanIntegrationTest extends 
IntegrationTest {
         }
     }
 
+    protected void runFromToInclusive(String fromDate, String toDate, Runnable 
runnable) {
+        DateTimeFormatter format = 
DateTimeFormatter.ofPattern(DATETIME_PATTERN);
+        LocalDate startDate = LocalDate.parse(fromDate, format);
+        LocalDate endDate = LocalDate.parse(toDate, format);
+
+        LocalDate currentDate = startDate;
+        while (currentDate.isBefore(endDate) || currentDate.isEqual(endDate)) {
+            runAt(format.format(currentDate), runnable);
+            currentDate = currentDate.plusDays(1);
+        }
+    }
+
     protected void runAt(String date, Runnable runnable) {
         try {
             
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
@@ -1092,6 +1114,17 @@ public abstract class BaseLoanIntegrationTest extends 
IntegrationTest {
         return loanTransactionHelper.addChargeForLoan(loanId.intValue(), 
payload, responseSpec);
     }
 
+    protected List<GetLoansLoanIdChargesChargeIdResponse> 
getOverdueInstallmentLoanCharges(Long loanId) {
+        return 
ok(fineractClient().loanCharges.retrieveAllLoanCharges(loanId)).stream() //
+                .filter(ch -> ch.getChargeTimeType().getId().intValue() == 
ChargesHelper.CHARGE_OVERDUE_INSTALLMENT_FEE) //
+                .toList(); //
+    }
+
+    protected void deactivateOverdueLoanCharges(Long loanId, String 
fromDueDate) {
+        ok(fineractClient().loanCharges.executeLoanCharge(loanId,
+                new 
PostLoansLoanIdChargesRequest().dueDate(fromDueDate).dateFormat(DATETIME_PATTERN).locale("en"),
 "deactivateOverdue"));
+    }
+
     protected void waiveLoanCharge(Long loanId, Long chargeId, Integer 
installmentNumber) {
         String payload = 
LoanTransactionHelper.getWaiveChargeJSON(installmentNumber.toString());
         loanTransactionHelper.waiveChargesForLoan(loanId.intValue(), 
chargeId.intValue(), payload);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java
index 36707f9091..d27171b306 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java
@@ -50,19 +50,19 @@ public final class ChargesHelper {
     private static final Integer CHARGE_APPLIES_TO_CLIENT = 3;
     private static final Integer CHARGE_APPLIES_TO_SHARES = 4;
 
-    private static final Integer CHARGE_DISBURSEMENT_FEE = 1;
-    private static final Integer CHARGE_SPECIFIED_DUE_DATE = 2;
-    private static final Integer CHARGE_SAVINGS_ACTIVATION_FEE = 3;
-    private static final Integer CHARGE_WITHDRAWAL_FEE = 5;
-    private static final Integer CHARGE_ANNUAL_FEE = 6;
-    private static final Integer CHARGE_MONTHLY_FEE = 7;
-    private static final Integer CHARGE_INSTALLMENT_FEE = 8;
-    private static final Integer CHARGE_OVERDUE_INSTALLMENT_FEE = 9;
-    private static final Integer CHARGE_OVERDRAFT_FEE = 10;
-    private static final Integer WEEKLY_FEE = 11;
-    private static final Integer SHAREACCOUNT_ACTIVATION = 13;
-    private static final Integer SHARE_PURCHASE = 14;
-    private static final Integer SHARE_REDEEM = 15;
+    public static final Integer CHARGE_DISBURSEMENT_FEE = 1;
+    public static final Integer CHARGE_SPECIFIED_DUE_DATE = 2;
+    public static final Integer CHARGE_SAVINGS_ACTIVATION_FEE = 3;
+    public static final Integer CHARGE_WITHDRAWAL_FEE = 5;
+    public static final Integer CHARGE_ANNUAL_FEE = 6;
+    public static final Integer CHARGE_MONTHLY_FEE = 7;
+    public static final Integer CHARGE_INSTALLMENT_FEE = 8;
+    public static final Integer CHARGE_OVERDUE_INSTALLMENT_FEE = 9;
+    public static final Integer CHARGE_OVERDRAFT_FEE = 10;
+    public static final Integer WEEKLY_FEE = 11;
+    public static final Integer SHAREACCOUNT_ACTIVATION = 13;
+    public static final Integer SHARE_PURCHASE = 14;
+    public static final Integer SHARE_REDEEM = 15;
 
     private static final Integer CHARGE_SAVINGS_NO_ACTIVITY_FEE = 16;
 
@@ -76,10 +76,10 @@ public final class ChargesHelper {
     private static final Integer CHARGE_PAYMENT_MODE_REGULAR = 0;
     private static final Integer CHARGE_PAYMENT_MODE_ACCOUNT_TRANSFER = 1;
 
-    private static final Integer CHARGE_FEE_FREQUENCY_DAYS = 0;
-    private static final Integer CHARGE_FEE_FREQUENCY_WEEKS = 1;
-    private static final Integer CHARGE_FEE_FREQUENCY_MONTHS = 2;
-    private static final Integer CHARGE_FEE_FREQUENCY_YEARS = 3;
+    public static final Integer CHARGE_FEE_FREQUENCY_DAYS = 0;
+    public static final Integer CHARGE_FEE_FREQUENCY_WEEKS = 1;
+    public static final Integer CHARGE_FEE_FREQUENCY_MONTHS = 2;
+    public static final Integer CHARGE_FEE_FREQUENCY_YEARS = 3;
 
     private static final boolean ACTIVE = true;
     private static final boolean PENALTY = true;
@@ -491,6 +491,25 @@ public final class ChargesHelper {
         return chargesCreateJson;
     }
 
+    // TODO: Rewrite to use fineract-client instead!
+    // Example: 
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
+    // org.apache.fineract.client.models.PostLoansLoanIdRequest)
+    @Deprecated(forRemoval = true)
+    public static String 
getLoanOverdueFeeJSONWithCalculationTypePercentageWithFeeInterval(String 
penaltyPercentageAmount,
+            Integer feeFrequency, int feeInterval) {
+        final HashMap<String, Object> map = populateDefaultsForLoan();
+        map.put("penalty", ChargesHelper.PENALTY);
+        map.put("amount", penaltyPercentageAmount);
+        map.put("chargePaymentMode", 
ChargesHelper.CHARGE_PAYMENT_MODE_REGULAR);
+        map.put("chargeTimeType", CHARGE_OVERDUE_INSTALLMENT_FEE);
+        map.put("chargeCalculationType", 
ChargesHelper.CHARGE_CALCULATION_TYPE_PERCENTAGE_AMOUNT_AND_INTEREST);
+        map.put("feeFrequency", feeFrequency);
+        map.put("feeInterval", feeInterval);
+        String chargesCreateJson = new Gson().toJson(map);
+        LOG.info("{}", chargesCreateJson);
+        return chargesCreateJson;
+    }
+
     // TODO: Rewrite to use fineract-client instead!
     // Example: 
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
     // org.apache.fineract.client.models.PostLoansLoanIdRequest)
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/penalty/LoanPenaltyBackdatedTransactionTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/penalty/LoanPenaltyBackdatedTransactionTest.java
new file mode 100644
index 0000000000..6c9cd69752
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/penalty/LoanPenaltyBackdatedTransactionTest.java
@@ -0,0 +1,410 @@
+/**
+ * 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.loan.penalty;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.fineract.client.models.ChargeData;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
+import 
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
+import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class LoanPenaltyBackdatedTransactionTest extends 
BaseLoanIntegrationTest {
+
+    @BeforeEach
+    public void before() {
+        PutGlobalConfigurationsRequest request = new 
PutGlobalConfigurationsRequest().value(0L).enabled(true);
+        
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.PENALTY_WAIT_PERIOD,
 request);
+    }
+
+    @AfterEach
+    public void after() {
+        // go back to defaults
+        PutGlobalConfigurationsRequest request = new 
PutGlobalConfigurationsRequest().value(2L).enabled(true);
+        
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.PENALTY_WAIT_PERIOD,
 request);
+    }
+
+    @Test
+    public void 
test_PenaltyRecalculationWorksForBackdatedTx_WhenCumulative_1() {
+        AtomicReference<Long> aLoanId = new AtomicReference<>();
+
+        runAt("01 January 2023", () -> {
+            // Create Client
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 3;
+            int repaymentEvery = 4;
+
+            // Create charges
+            double chargeAmount = 1.0;
+            Long chargeId = createOverduePenaltyPercentageCharge(chargeAmount, 
ChargesHelper.CHARGE_FEE_FREQUENCY_DAYS, 1);
+
+            // Create Loan Product
+            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
+                    
.graceOnArrearsAgeing(0).numberOfRepayments(numberOfRepayments) //
+                    .repaymentEvery(repaymentEvery) //
+                    .installmentAmountInMultiplesOf(null) //
+                    
.repaymentFrequencyType(RepaymentFrequencyType.DAYS.longValue()) //
+                    .interestType(InterestType.DECLINING_BALANCE)//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
+                    
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
+                    
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
+                    .isInterestRecalculationEnabled(true)//
+                    .recalculationRestFrequencyInterval(1)//
+                    
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
+                    
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
+                    .allowPartialPeriodInterestCalcualtion(false)//
+                    .disallowExpectedDisbursements(false)//
+                    .allowApprovedDisbursedAmountsOverApplied(false)//
+                    .overAppliedNumber(null)//
+                    .overAppliedCalculationType(null)//
+                    .multiDisburseLoan(null)//
+                    .charges(List.of(new ChargeData().id(chargeId)));//
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            // Apply and Approve Loan
+            double amount = 5000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)//
+                    .repaymentEvery(repaymentEvery)//
+                    .loanTermFrequency(numberOfRepayments * repaymentEvery)//
+                    .repaymentFrequencyType(RepaymentFrequencyType.DAYS)//
+                    .loanTermFrequencyType(RepaymentFrequencyType.DAYS)//
+                    .interestType(InterestType.DECLINING_BALANCE)//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            aLoanId.getAndSet(approvedLoanResult.getLoanId());
+            Long loanId = aLoanId.get();
+
+            // disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 
2023");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023") //
+            );
+        });
+
+        runFromToInclusive("01 January 2023", "09 January 2023", () -> {
+            // run accrual posting
+            schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue 
loans");
+            schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
+        });
+
+        runAt("09 January 2023", () -> {
+            Long loanId = aLoanId.get();
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0) //
+            );
+
+            // repay 1k
+            addRepaymentForLoan(loanId, 1000.0, "07 January 2023");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(1000.0, "Repayment", "07 January 2023", 
4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0) //
+            );
+
+            // reverse accruals
+            deactivateOverdueLoanCharges(loanId, "07 January 2023");
+
+            // run accrual posting
+            schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue 
loans");
+            schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(1000.0, "Repayment", "07 January 2023", 
4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0, true), //
+                    transaction(30.33, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 30.33, 0.0, 0.0) //
+            );
+        });
+    }
+
+    @Test
+    public void 
test_PenaltyRecalculationWorksForBackdatedTx_WhenCumulative_2() {
+        AtomicReference<Long> aLoanId = new AtomicReference<>();
+
+        runAt("01 January 2023", () -> {
+            // Create Client
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 3;
+            int repaymentEvery = 4;
+
+            // Create charges
+            double chargeAmount = 1.0;
+            Long chargeId = createOverduePenaltyPercentageCharge(chargeAmount, 
ChargesHelper.CHARGE_FEE_FREQUENCY_DAYS, 1);
+
+            // Create Loan Product
+            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
+                    
.graceOnArrearsAgeing(0).numberOfRepayments(numberOfRepayments) //
+                    .repaymentEvery(repaymentEvery) //
+                    .installmentAmountInMultiplesOf(null) //
+                    
.repaymentFrequencyType(RepaymentFrequencyType.DAYS.longValue()) //
+                    .interestType(InterestType.DECLINING_BALANCE)//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
+                    
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
+                    
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
+                    .isInterestRecalculationEnabled(true)//
+                    .recalculationRestFrequencyInterval(1)//
+                    
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
+                    
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
+                    .allowPartialPeriodInterestCalcualtion(false)//
+                    .disallowExpectedDisbursements(false)//
+                    .allowApprovedDisbursedAmountsOverApplied(false)//
+                    .overAppliedNumber(null)//
+                    .overAppliedCalculationType(null)//
+                    .multiDisburseLoan(null)//
+                    .charges(List.of(new ChargeData().id(chargeId)));//
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            // Apply and Approve Loan
+            double amount = 5000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)//
+                    .repaymentEvery(repaymentEvery)//
+                    .loanTermFrequency(numberOfRepayments * repaymentEvery)//
+                    .repaymentFrequencyType(RepaymentFrequencyType.DAYS)//
+                    .loanTermFrequencyType(RepaymentFrequencyType.DAYS)//
+                    .interestType(InterestType.DECLINING_BALANCE)//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            aLoanId.getAndSet(approvedLoanResult.getLoanId());
+            Long loanId = aLoanId.get();
+
+            // disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 
2023");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023") //
+            );
+        });
+
+        runFromToInclusive("01 January 2023", "09 January 2023", () -> {
+            // run accrual posting
+            schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue 
loans");
+            schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
+        });
+
+        runAt("09 January 2023", () -> {
+            Long loanId = aLoanId.get();
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0) //
+            );
+
+            // repay 1k
+            addRepaymentForLoan(loanId, 1000.0, "07 January 2023");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(1000.0, "Repayment", "07 January 2023", 
4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0) //
+            );
+
+            // reverse accruals
+            deactivateOverdueLoanCharges(loanId, "05 January 2023");
+
+            // run accrual posting
+            schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue 
loans");
+            schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0, true), //
+                    transaction(6.83, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 6.83, 0.0, 0.0), //
+                    transaction(1000.0, "Repayment", "07 January 2023", 
4006.83, 993.17, 0.0, 0.0, 6.83, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0, true), //
+                    transaction(20.49, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 20.49, 0.0, 0.0) //
+            );
+        });
+    }
+
+    @Test
+    public void 
test_PenaltyRecalculationWorksForBackdatedTx_WhenCumulative_3() {
+        AtomicReference<Long> aLoanId = new AtomicReference<>();
+
+        runAt("01 January 2023", () -> {
+            // Create Client
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 3;
+            int repaymentEvery = 4;
+
+            // Create charges
+            double chargeAmount = 1.0;
+            Long chargeId = createOverduePenaltyPercentageCharge(chargeAmount, 
ChargesHelper.CHARGE_FEE_FREQUENCY_DAYS, 1);
+
+            // Create Loan Product
+            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
+                    
.graceOnArrearsAgeing(0).numberOfRepayments(numberOfRepayments) //
+                    .repaymentEvery(repaymentEvery) //
+                    .installmentAmountInMultiplesOf(null) //
+                    
.repaymentFrequencyType(RepaymentFrequencyType.DAYS.longValue()) //
+                    .interestType(InterestType.DECLINING_BALANCE)//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
+                    
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
+                    
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
+                    .isInterestRecalculationEnabled(true)//
+                    .recalculationRestFrequencyInterval(1)//
+                    
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
+                    
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
+                    .allowPartialPeriodInterestCalcualtion(false)//
+                    .disallowExpectedDisbursements(false)//
+                    .allowApprovedDisbursedAmountsOverApplied(false)//
+                    .overAppliedNumber(null)//
+                    .overAppliedCalculationType(null)//
+                    .multiDisburseLoan(null)//
+                    .charges(List.of(new ChargeData().id(chargeId)));//
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            // Apply and Approve Loan
+            double amount = 5000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)//
+                    .repaymentEvery(repaymentEvery)//
+                    .loanTermFrequency(numberOfRepayments * repaymentEvery)//
+                    .repaymentFrequencyType(RepaymentFrequencyType.DAYS)//
+                    .loanTermFrequencyType(RepaymentFrequencyType.DAYS)//
+                    .interestType(InterestType.DECLINING_BALANCE)//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            aLoanId.getAndSet(approvedLoanResult.getLoanId());
+            Long loanId = aLoanId.get();
+
+            // disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 
2023");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023") //
+            );
+        });
+
+        runFromToInclusive("01 January 2023", "09 January 2023", () -> {
+            // run accrual posting
+            schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue 
loans");
+            schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
+        });
+
+        runAt("09 January 2023", () -> {
+            Long loanId = aLoanId.get();
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0) //
+            );
+
+            // repay 1k
+            addRepaymentForLoan(loanId, 1000.0, "07 January 2023");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(1000.0, "Repayment", "07 January 2023", 
4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0) //
+            );
+
+            // reverse accruals
+            deactivateOverdueLoanCharges(loanId, "07 January 2023");
+
+            // run accrual posting
+            schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue 
loans");
+            schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(1000.0, "Repayment", "07 January 2023", 
4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0, true), //
+                    transaction(30.33, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 30.33, 0.0, 0.0) //
+            );
+        });
+
+        runAt("10 January 2023", () -> {
+            Long loanId = aLoanId.get();
+
+            // repay 1k
+            addRepaymentForLoan(loanId, 1000.0, "10 January 2023");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023", 
5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+                    transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 
0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(1000.0, "Repayment", "07 January 2023", 
4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
+                    transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 50.01, 0.0, 0.0, true), //
+                    transaction(30.33, "Accrual", "09 January 2023", 0.0, 0.0, 
0.0, 0.0, 30.33, 0.0, 0.0), //
+                    transaction(1000.0, "Repayment", "10 January 2023", 
3047.0, 969.67, 0.0, 0.0, 30.33, 0.0, 0.0) //
+            );
+        });
+    }
+}

Reply via email to