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

aleks 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 4034f4ae1 FINERACT-1675 New Paid loanCharge Refund Transaction
4034f4ae1 is described below

commit 4034f4ae198be98bf1a2b113f6f9dbd7c10e8fde
Author: John Woodlock <[email protected]>
AuthorDate: Sun Aug 7 21:06:16 2022 +0100

    FINERACT-1675 New Paid loanCharge Refund Transaction
---
 .../journalentry/data/LoanTransactionDTO.java      |   2 +
 .../service/AccountingProcessorHelper.java         |  26 +-
 .../AccrualBasedAccountingProcessorForLoan.java    |  13 +
 .../CashBasedAccountingProcessorForLoan.java       |  14 +
 .../commands/service/CommandWrapperBuilder.java    |   9 +
 .../AccountTransfersWritePlatformServiceImpl.java  |  10 +-
 .../transaction/LoanChargeRefundBusinessEvent.java |  28 +
 .../api/LoanTransactionsApiResource.java           |  21 +-
 .../loanaccount/data/LoanTransactionEnumData.java  |   8 +-
 .../portfolio/loanaccount/domain/Loan.java         |  18 +-
 .../domain/LoanAccountDomainService.java           |   7 +-
 .../domain/LoanAccountDomainServiceJpa.java        |  24 +-
 .../loanaccount/domain/LoanTransaction.java        |  31 +-
 .../loanaccount/domain/LoanTransactionType.java    |  12 +-
 .../exception/LoanChargeRefundException.java       |  29 +
 .../handler/LoanChargeRefundCommandHandler.java    |  53 ++
 .../serialization/LoanEventApiJsonValidator.java   |  35 ++
 .../service/LoanReadPlatformServiceImpl.java       |   2 +-
 .../service/LoanWritePlatformService.java          |   2 +
 .../LoanWritePlatformServiceJpaRepositoryImpl.java | 197 ++++++-
 .../loanproduct/service/LoanEnumerations.java      |   4 +
 .../db/changelog/tenant/changelog-tenant.xml       |   2 +
 .../parts/0027_add_charge_refund_permission.xml    |  32 ++
 ...arge_refund_charge_type_to_loan_transaction.xml |  28 +
 .../ClientLoanChargeRefundIntegrationTest.java     | 602 +++++++++++++++++++++
 ...lanceRefundandRepaymentTypeIntegrationTest.java |   4 +-
 .../common/loans/LoanTransactionHelper.java        |  28 +-
 27 files changed, 1202 insertions(+), 39 deletions(-)

diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java
 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java
index 5cfc02b21..9924cfb6e 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java
@@ -56,4 +56,6 @@ public class LoanTransactionDTO {
 
     @Setter
     private boolean isLoanToLoanTransfer;
+
+    private final String chargeRefundChargeType;
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
index 45e3a3a4f..59700c945 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
@@ -124,6 +124,7 @@ public class AccountingProcessorHelper {
             final BigDecimal overPayments = (BigDecimal) 
map.get("overPaymentPortion");
             final boolean reversed = (Boolean) map.get("reversed");
             final Long paymentTypeId = (Long) map.get("paymentTypeId");
+            final String chargeRefundChargeType = (String) 
map.get("chargeRefundChargeType");
 
             final List<ChargePaymentDTO> feePaymentDetails = new ArrayList<>();
             final List<ChargePaymentDTO> penaltyPaymentDetails = new 
ArrayList<>();
@@ -151,7 +152,7 @@ public class AccountingProcessorHelper {
             }
             final LoanTransactionDTO transaction = new 
LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId,
                     transactionDate, transactionType, amount, principal, 
interest, fees, penalties, overPayments, reversed,
-                    penaltyPaymentDetails, feePaymentDetails, 
isAccountTransfer);
+                    penaltyPaymentDetails, feePaymentDetails, 
isAccountTransfer, chargeRefundChargeType);
             Boolean isLoanToLoanTransfer = (Boolean) 
accountingBridgeData.get("isLoanToLoanTransfer");
             if (isLoanToLoanTransfer != null && isLoanToLoanTransfer) {
                 transaction.setLoanToLoanTransfer(true);
@@ -1301,4 +1302,27 @@ public class AccountingProcessorHelper {
     private GLAccount getGLAccountById(final Long accountId) {
         return 
this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
     }
+
+    public Integer getValueForFeeOrPenaltyIncomeAccount(final String 
chargeRefundChargeType) {
+        if (chargeRefundChargeType == null
+                || !(chargeRefundChargeType.equalsIgnoreCase("P") || 
chargeRefundChargeType.equalsIgnoreCase("F"))) {
+            String errorValue;
+            if (chargeRefundChargeType == null) {
+                errorValue = "Null";
+            } else {
+                errorValue = chargeRefundChargeType;
+            }
+            throw new 
PlatformDataIntegrityException("error.msg.chargeRefundChargeType.can.only.be.P.or.F",
+                    "chargeRefundChargeType can only be P (Penalty) or F(Fee) 
- Value is: " + errorValue);
+        }
+        Integer incomeAccount = null;
+        if (chargeRefundChargeType.equalsIgnoreCase("P")) {
+            incomeAccount = 
AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue();
+        } else {
+            incomeAccount = AccrualAccountsForLoan.INCOME_FROM_FEES.getValue();
+
+        }
+        return incomeAccount;
+    }
+
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
index 8b2380297..968916d66 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
@@ -307,6 +307,19 @@ public class AccrualBasedAccountingProcessorForLoan 
implements AccountingProcess
                 }
             }
         }
+
+        /**
+         * Charge Refunds (and their reversals) have an extra refund related 
pair of journal entries in addition to
+         * those related to the repayment above
+         ***/
+        if (!(totalDebitAmount.compareTo(BigDecimal.ZERO) == 0)) {
+            if (loanTransactionDTO.getTransactionType().isChargeRefund()) {
+                Integer incomeAccount = 
this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType());
+                
this.helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, 
currencyCode, incomeAccount,
+                        AccrualAccountsForLoan.FUND_SOURCE.getValue(), 
loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
+                        totalDebitAmount, isReversal);
+            }
+        }
     }
 
     /**
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java
 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java
index 692698abe..1478ef188 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
 import java.util.List;
 import lombok.RequiredArgsConstructor;
 import org.apache.fineract.accounting.closure.domain.GLClosure;
+import 
org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
 import 
org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
 import 
org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
 import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
@@ -256,6 +257,19 @@ public class CashBasedAccountingProcessorForLoan 
implements AccountingProcessorF
                         loanProductId, paymentTypeId, loanId, transactionId, 
transactionDate, totalDebitAmount, isReversal);
             }
         }
+
+        /**
+         * Charge Refunds (and their reversals) have an extra refund related 
pair of journal entries in addition to
+         * those related to the repayment above
+         ***/
+        if (!(totalDebitAmount.compareTo(BigDecimal.ZERO) == 0)) {
+            if (loanTransactionDTO.getTransactionType().isChargeRefund()) {
+                Integer incomeAccount = 
this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType());
+                
this.helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, 
currencyCode, incomeAccount,
+                        AccrualAccountsForLoan.FUND_SOURCE.getValue(), 
loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
+                        totalDebitAmount, isReversal);
+            }
+        }
     }
 
     /**
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
 
b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index 8d750cdbb..81746b1f5 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -824,6 +824,15 @@ public class CommandWrapperBuilder {
         return this;
     }
 
+    public CommandWrapperBuilder refundLoanCharge(final Long loanId) {
+        this.actionName = "CHARGEREFUND";
+        this.entityName = "LOAN";
+        this.entityId = null;
+        this.loanId = loanId;
+        this.href = "/loans/" + loanId + 
"/transactions/template?command=chargerefund";
+        return this;
+    }
+
     public CommandWrapperBuilder loanRecoveryPaymentTransaction(final Long 
loanId) {
         this.actionName = "RECOVERYPAYMENT";
         this.entityName = "LOAN";
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java
index 9502c6265..da811bba6 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java
@@ -177,9 +177,10 @@ public class AccountTransfersWritePlatformServiceImpl 
implements AccountTransfer
             final Boolean isHolidayValidationDone = false;
             final HolidayDetailDTO holidayDetailDto = null;
             final boolean isRecoveryRepayment = false;
+            final String chargeRefundChargeType = null;
             final LoanTransaction loanRepaymentTransaction = 
this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT,
                     toLoanAccount, new CommandProcessingResultBuilder(), 
transactionDate, transactionAmount, paymentDetail, null, null,
-                    isRecoveryRepayment, isAccountTransfer, holidayDetailDto, 
isHolidayValidationDone);
+                    isRecoveryRepayment, chargeRefundChargeType, 
isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
 
             final AccountTransferDetails accountTransferDetails = 
this.accountTransferAssembler.assembleSavingsToLoanTransfer(command,
                     fromSavingsAccount, toLoanAccount, withdrawal, 
loanRepaymentTransaction);
@@ -340,10 +341,11 @@ public class AccountTransfersWritePlatformServiceImpl 
implements AccountTransfer
                 final boolean isRecoveryRepayment = false;
                 final Boolean isHolidayValidationDone = false;
                 final HolidayDetailDTO holidayDetailDto = null;
+                final String chargeRefundChargeType = null;
                 loanTransaction = 
this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, 
toLoanAccount,
                         new CommandProcessingResultBuilder(), 
accountTransferDTO.getTransactionDate(),
                         accountTransferDTO.getTransactionAmount(), 
accountTransferDTO.getPaymentDetail(), null, null, isRecoveryRepayment,
-                        isAccountTransfer, holidayDetailDto, 
isHolidayValidationDone);
+                        chargeRefundChargeType, isAccountTransfer, 
holidayDetailDto, isHolidayValidationDone);
             }
 
             accountTransferDetails = 
this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, 
fromSavingsAccount,
@@ -475,10 +477,10 @@ public class AccountTransfersWritePlatformServiceImpl 
implements AccountTransfer
         LoanTransaction disburseTransaction = 
this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(),
                 accountTransferDTO.getTransactionDate(), 
accountTransferDTO.getTransactionAmount(), 
accountTransferDTO.getPaymentDetail(),
                 accountTransferDTO.getNoteText(), 
accountTransferDTO.getTxnExternalId(), true);
-
+        final String chargeRefundChargeType = null;
         LoanTransaction repayTransaction = 
this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, 
toLoanAccount,
                 new CommandProcessingResultBuilder(), 
accountTransferDTO.getTransactionDate(), 
accountTransferDTO.getTransactionAmount(),
-                accountTransferDTO.getPaymentDetail(), null, null, false, 
isAccountTransfer, null, false, true);
+                accountTransferDTO.getPaymentDetail(), null, null, false, 
chargeRefundChargeType, isAccountTransfer, null, false, true);
 
         AccountTransferDetails accountTransferDetails = 
this.accountTransferAssembler.assembleLoanToLoanTransfer(accountTransferDTO,
                 fromLoanAccount, toLoanAccount, disburseTransaction, 
repayTransaction);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/businessevent/domain/loan/transaction/LoanChargeRefundBusinessEvent.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/businessevent/domain/loan/transaction/LoanChargeRefundBusinessEvent.java
new file mode 100644
index 000000000..390742f28
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/businessevent/domain/loan/transaction/LoanChargeRefundBusinessEvent.java
@@ -0,0 +1,28 @@
+/**
+ * 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.businessevent.domain.loan.transaction;
+
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+
+public class LoanChargeRefundBusinessEvent extends 
LoanTransactionBusinessEvent {
+
+    public LoanChargeRefundBusinessEvent(LoanTransaction value) {
+        super(value);
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
index 77dc0fdf3..bea194e6c 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
@@ -120,7 +120,7 @@ public class LoanTransactionsApiResource {
             + "loans/1/transactions/template?command=recoverypayment" + "\n" + 
"loans/1/transactions/template?command=prepayLoan" + "\n"
             + "loans/1/transactions/template?command=refundbycash" + "\n" + 
"loans/1/transactions/template?command=refundbytransfer" + "\n"
             + "loans/1/transactions/template?command=foreclosure" + "\n"
-            + "loans/1/transactions/template?command=creditBalanceRefund 
(returned 'amount' field will have the overpaid value")
+            + "loans/1/transactions/template?command=creditBalanceRefund 
(returned 'amount' field will have the overpaid value)")
     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class)))
 })
     public String retrieveTransactionTemplate(@PathParam("loanId") 
@Parameter(description = "loanId") final Long loanId,
@@ -228,14 +228,14 @@ public class LoanTransactionsApiResource {
             + "Example Requests:\n" + "\n" + 
"loans/1/transactions?command=repayment" + " | Make a Repayment | \n"
             + "loans/1/transactions?command=merchantIssuedRefund" + " | 
Merchant Issued Refund | \n"
             + "loans/1/transactions?command=payoutRefund" + " | Payout Refund 
| \n" + "loans/1/transactions?command=goodwillCredit"
-            + " | Goodwil Credit | \n" + 
"loans/1/transactions?command=waiveinterest" + " | Waive Interest | \n"
-            + "loans/1/transactions?command=writeoff" + " | Write-off Loan | 
\n" + "loans/1/transactions?command=close-rescheduled"
-            + " | Close Rescheduled Loan | \n" + 
"loans/1/transactions?command=close" + " | Close Loan | \n"
-            + "loans/1/transactions?command=undowriteoff" + " | Undo Loan 
Write-off | \n" + "loans/1/transactions?command=recoverypayment"
-            + " | Make Recovery Payment | \n" + 
"loans/1/transactions?command=refundByCash"
-            + " | Make a Refund of an Active Loan by Cash | \n" + 
"loans/1/transactions?command=foreclosure"
-            + " | Foreclosure of an Active Loan | \n" + 
"loans/1/transactions?command=creditBalanceRefund" + " | Credit Balance Refund"
-            + " |  \n")
+            + " | Goodwil Credit | \n" + 
"loans/1/transactions?command=chargeRefund" + " | Charge Refund | \n"
+            + "loans/1/transactions?command=waiveinterest" + " | Waive 
Interest | \n" + "loans/1/transactions?command=writeoff"
+            + " | Write-off Loan | \n" + 
"loans/1/transactions?command=close-rescheduled" + " | Close Rescheduled Loan | 
\n"
+            + "loans/1/transactions?command=close" + " | Close Loan | \n" + 
"loans/1/transactions?command=undowriteoff"
+            + " | Undo Loan Write-off | \n" + 
"loans/1/transactions?command=recoverypayment" + " | Make Recovery Payment | \n"
+            + "loans/1/transactions?command=refundByCash" + " | Make a Refund 
of an Active Loan by Cash | \n"
+            + "loans/1/transactions?command=foreclosure" + " | Foreclosure of 
an Active Loan | \n"
+            + "loans/1/transactions?command=creditBalanceRefund" + " | Credit 
Balance Refund" + " |  \n")
     @RequestBody(required = true, content = @Content(schema = 
@Schema(implementation = 
LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsRequest.class)))
     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) 
})
@@ -258,6 +258,9 @@ public class LoanTransactionsApiResource {
         } else if (is(commandParam, "goodwillCredit")) {
             final CommandWrapper commandRequest = 
builder.loanGoodwillCreditTransaction(loanId).build();
             result = 
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
+        } else if (is(commandParam, "chargeRefund")) {
+            final CommandWrapper commandRequest = 
builder.refundLoanCharge(loanId).build();
+            result = 
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
         } else if (is(commandParam, "waiveinterest")) {
             final CommandWrapper commandRequest = 
builder.waiveInterestPortionTransaction(loanId).build();
             result = 
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
index 5d8928a75..46633b57d 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
@@ -34,6 +34,7 @@ public class LoanTransactionEnumData {
     private final boolean merchantIssuedRefund;
     private final boolean payoutRefund;
     private final boolean goodwillCredit;
+    private final boolean chargeRefund;
     private final boolean contra;
     private final boolean waiveInterest;
     private final boolean waiveCharges;
@@ -59,6 +60,7 @@ public class LoanTransactionEnumData {
         this.merchantIssuedRefund = Long.valueOf(21).equals(this.id);
         this.payoutRefund = Long.valueOf(22).equals(this.id);
         this.goodwillCredit = Long.valueOf(23).equals(this.id);
+        this.chargeRefund = Long.valueOf(24).equals(this.id);
         this.contra = Long.valueOf(3).equals(this.id);
         this.waiveInterest = Long.valueOf(4).equals(this.id);
         this.waiveCharges = Long.valueOf(9).equals(this.id);
@@ -101,7 +103,7 @@ public class LoanTransactionEnumData {
     }
 
     public boolean isRepaymentType() {
-        if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || 
isGoodwillCredit()) {
+        if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || 
isGoodwillCredit() || isChargeRefund()) {
             return true;
         }
         return false;
@@ -131,6 +133,10 @@ public class LoanTransactionEnumData {
         return this.goodwillCredit;
     }
 
+    public boolean isChargeRefund() {
+        return this.chargeRefund;
+    }
+
     public boolean isWaiveInterest() {
         return this.waiveInterest;
     }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index 4a4d3299c..9a01bf4d9 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -113,6 +113,7 @@ import 
org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTrans
 import 
org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
+import 
org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
@@ -3055,6 +3056,7 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom {
         }
         validateRepaymentTypeAccountStatus(repaymentTransaction, event);
         validateActivityNotBeforeClientOrGroupTransferDate(event, 
repaymentTransaction.getTransactionDate());
+        
validateRepaymentTypeTransactionNotBeforeAChargeRefund(repaymentTransaction, 
"created");
         validateActivityNotBeforeLastTransactionDate(event, 
repaymentTransaction.getTransactionDate());
         if (!isHolidayValidationDone) {
             
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), 
holidayDetailDTO.isAllowTransactionsOnHoliday(),
@@ -3073,7 +3075,7 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom {
 
     private void validateRepaymentTypeAccountStatus(LoanTransaction 
repaymentTransaction, LoanEvent event) {
         if (repaymentTransaction.isGoodwillCredit() || 
repaymentTransaction.isMerchantIssuedRefund()
-                || repaymentTransaction.isPayoutRefund()) {
+                || repaymentTransaction.isPayoutRefund() || 
repaymentTransaction.isChargeRefund()) {
 
             if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) {
                 final List<ApiParameterError> dataValidationErrors = new 
ArrayList<>();
@@ -4917,6 +4919,20 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom {
         }
     }
 
+    public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final 
LoanTransaction repaymentTransaction,
+            final String reversedOrCreated) {
+        if (repaymentTransaction.isRepaymentType() && 
!repaymentTransaction.isChargeRefund()) {
+            for (LoanTransaction txn : this.getLoanTransactions()) {
+                if (txn.isChargeRefund() && 
repaymentTransaction.getTransactionDate().isBefore(txn.getTransactionDate())) {
+                    final String errorMessage = "loan.transaction.cant.be." + 
reversedOrCreated + ".because.later.charge.refund.exists";
+                    final String details = "Loan Transaction: " + this.getId() 
+ " Can't be " + reversedOrCreated
+                            + " because a Later Charge Refund Exists.";
+                    throw new LoanChargeRefundException(errorMessage, details);
+                }
+            }
+        }
+    }
+
     public LocalDate getLastUserTransactionDate() {
         LocalDate currentTransactionDate = getDisbursementDate();
         for (final LoanTransaction previousTransaction : 
this.loanTransactions) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java
index 35cef98b4..6dad41349 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java
@@ -30,7 +30,8 @@ public interface LoanAccountDomainService {
 
     LoanTransaction makeRepayment(LoanTransactionType 
repaymentTransactionType, Loan loan, CommandProcessingResultBuilder 
builderResult,
             LocalDate transactionDate, BigDecimal transactionAmount, 
PaymentDetail paymentDetail, String noteText, String txnExternalId,
-            boolean isRecoveryRepayment, boolean isAccountTransfer, 
HolidayDetailDTO holidatDetailDto, Boolean isHolidayValidationDone);
+            boolean isRecoveryRepayment, String chargeRefundChargeType, 
boolean isAccountTransfer, HolidayDetailDTO holidatDetailDto,
+            Boolean isHolidayValidationDone);
 
     LoanTransaction makeRefund(Long accountId, CommandProcessingResultBuilder 
builderResult, LocalDate transactionDate,
             BigDecimal transactionAmount, PaymentDetail paymentDetail, String 
noteText, String txnExternalId);
@@ -63,8 +64,8 @@ public interface LoanAccountDomainService {
 
     LoanTransaction makeRepayment(LoanTransactionType 
repaymentTransactionType, Loan loan, CommandProcessingResultBuilder 
builderResult,
             LocalDate transactionDate, BigDecimal transactionAmount, 
PaymentDetail paymentDetail, String noteText, String txnExternalId,
-            boolean isRecoveryRepayment, boolean isAccountTransfer, 
HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone,
-            boolean isLoanToLoanTransfer);
+            boolean isRecoveryRepayment, String chargeRefundChargeType, 
boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
+            Boolean isHolidayValidationDone, boolean isLoanToLoanTransfer);
 
     void saveLoanWithDataIntegrityViolationChecks(Loan loan);
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
index 8f727c635..cbc1ba16b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
@@ -129,9 +129,11 @@ public class LoanAccountDomainServiceJpa implements 
LoanAccountDomainService {
     public LoanTransaction makeRepayment(final LoanTransactionType 
repaymentTransactionType, final Loan loan,
             final CommandProcessingResultBuilder builderResult, final 
LocalDate transactionDate, final BigDecimal transactionAmount,
             final PaymentDetail paymentDetail, final String noteText, final 
String txnExternalId, final boolean isRecoveryRepayment,
-            boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, 
Boolean isHolidayValidationDone) {
+            final String chargeRefundChargeType, boolean isAccountTransfer, 
HolidayDetailDTO holidayDetailDto,
+            Boolean isHolidayValidationDone) {
         return makeRepayment(repaymentTransactionType, loan, builderResult, 
transactionDate, transactionAmount, paymentDetail, noteText,
-                txnExternalId, isRecoveryRepayment, isAccountTransfer, 
holidayDetailDto, isHolidayValidationDone, false);
+                txnExternalId, isRecoveryRepayment, chargeRefundChargeType, 
isAccountTransfer, holidayDetailDto, isHolidayValidationDone,
+                false);
     }
 
     @Transactional
@@ -154,8 +156,8 @@ public class LoanAccountDomainServiceJpa implements 
LoanAccountDomainService {
     public LoanTransaction makeRepayment(final LoanTransactionType 
repaymentTransactionType, final Loan loan,
             final CommandProcessingResultBuilder builderResult, final 
LocalDate transactionDate, final BigDecimal transactionAmount,
             final PaymentDetail paymentDetail, final String noteText, final 
String txnExternalId, final boolean isRecoveryRepayment,
-            boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, 
Boolean isHolidayValidationDone,
-            final boolean isLoanToLoanTransfer) {
+            final String chargeRefundChargeType, boolean isAccountTransfer, 
HolidayDetailDTO holidayDetailDto,
+            Boolean isHolidayValidationDone, final boolean 
isLoanToLoanTransfer) {
         AppUser currentUser = getAppUserIfPresent();
         checkClientOrGroupActive(loan);
 
@@ -181,7 +183,7 @@ public class LoanAccountDomainServiceJpa implements 
LoanAccountDomainService {
                     txnExternalId);
         } else {
             newRepaymentTransaction = 
LoanTransaction.repaymentType(repaymentTransactionType, loan.getOffice(), 
repaymentAmount,
-                    paymentDetail, transactionDate, txnExternalId);
+                    paymentDetail, transactionDate, txnExternalId, 
chargeRefundChargeType);
         }
 
         LocalDate recalculateFrom = null;
@@ -223,9 +225,11 @@ public class LoanAccountDomainServiceJpa implements 
LoanAccountDomainService {
 
         recalculateAccruals(loan);
 
-        LoanTransactionBusinessEvent transactionRepaymentEvent = 
getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType,
-                isRecoveryRepayment, newRepaymentTransaction);
-        
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
+        if (!repaymentTransactionType.isChargeRefund()) {
+            LoanTransactionBusinessEvent transactionRepaymentEvent = 
getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType,
+                    isRecoveryRepayment, newRepaymentTransaction);
+            
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
+        }
 
         // disable all active standing orders linked to this loan if status
         // changes to closed
@@ -277,6 +281,8 @@ public class LoanAccountDomainServiceJpa implements 
LoanAccountDomainService {
             repaymentEvent = new 
LoanTransactionPayoutRefundPreBusinessEvent(loan);
         } else if (repaymentTransactionType.isGoodwillCredit()) {
             repaymentEvent = new 
LoanTransactionGoodwillCreditPreBusinessEvent(loan);
+        } else if (repaymentTransactionType.isChargeRefund()) {
+            repaymentEvent = new LoanChargePaymentPreBusinessEvent(loan);
         } else if (isRecoveryRepayment) {
             repaymentEvent = new 
LoanTransactionRecoveryPaymentPreBusinessEvent(loan);
         }
@@ -294,6 +300,8 @@ public class LoanAccountDomainServiceJpa implements 
LoanAccountDomainService {
             repaymentEvent = new 
LoanTransactionPayoutRefundPostBusinessEvent(transaction);
         } else if (repaymentTransactionType.isGoodwillCredit()) {
             repaymentEvent = new 
LoanTransactionGoodwillCreditPostBusinessEvent(transaction);
+        } else if (repaymentTransactionType.isChargeRefund()) {
+            repaymentEvent = new 
LoanChargePaymentPostBusinessEvent(transaction);
         } else if (isRecoveryRepayment) {
             repaymentEvent = new 
LoanTransactionRecoveryPaymentPostBusinessEvent(transaction);
         }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 6aa59b2bf..b1c0f3d8b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -115,6 +115,9 @@ public class LoanTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
     @Column(name = "manually_adjusted_or_reversed", nullable = false)
     private boolean manuallyAdjustedOrReversed;
 
+    @Column(name = "charge_refund_charge_type", length = 1, nullable = true, 
unique = true)
+    private String chargeRefundChargeType;
+
     @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = 
FetchType.LAZY, mappedBy = "loanTransaction")
     private Set<LoanCollateralManagement> loanCollateralManagementSet = new 
HashSet<>();
 
@@ -147,8 +150,9 @@ public class LoanTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
     }
 
     public static LoanTransaction repaymentType(final LoanTransactionType 
repaymentType, final Office office, final Money amount,
-            final PaymentDetail paymentDetail, final LocalDate paymentDate, 
final String externalId) {
-        return new LoanTransaction(null, office, repaymentType, paymentDetail, 
amount.getAmount(), paymentDate, externalId);
+            final PaymentDetail paymentDetail, final LocalDate paymentDate, 
final String externalId, final String chargeRefundChargeType) {
+        return new LoanTransaction(null, office, repaymentType, paymentDetail, 
amount.getAmount(), paymentDate, externalId,
+                chargeRefundChargeType);
     }
 
     public void setLoanTransactionToRepaymentScheduleMappings(final Integer 
installmentId, final BigDecimal chargePerInstallment) {
@@ -344,7 +348,21 @@ public class LoanTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
         this.submittedOnDate = DateUtils.getBusinessLocalDate();
     }
 
+    private LoanTransaction(final Loan loan, final Office office, final 
LoanTransactionType type, final PaymentDetail paymentDetail,
+            final BigDecimal amount, final LocalDate date, final String 
externalId, final String chargeRefundChargeType) {
+        this.loan = loan;
+        this.typeOf = type.getValue();
+        this.paymentDetail = paymentDetail;
+        this.amount = amount;
+        this.dateOf = date;
+        this.externalId = externalId;
+        this.office = office;
+        this.submittedOnDate = DateUtils.getBusinessLocalDate();
+        this.chargeRefundChargeType = chargeRefundChargeType;
+    }
+
     public void reverse() {
+        this.loan.validateRepaymentTypeTransactionNotBeforeAChargeRefund(this, 
"reversed");
         this.reversed = true;
         this.loanTransactionToRepaymentScheduleMappings.clear();
     }
@@ -480,7 +498,7 @@ public class LoanTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
     }
 
     public boolean isRepaymentType() {
-        return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() 
|| isGoodwillCredit();
+        return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() 
|| isGoodwillCredit() || isChargeRefund();
     }
 
     public boolean isRepayment() {
@@ -499,6 +517,10 @@ public class LoanTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
         return LoanTransactionType.GOODWILL_CREDIT.equals(getTypeOf()) && 
isNotReversed();
     }
 
+    public boolean isChargeRefund() {
+        return LoanTransactionType.CHARGE_REFUND.equals(getTypeOf()) && 
isNotReversed();
+    }
+
     public boolean isNotRepaymentType() {
         return !isRepaymentType();
     }
@@ -640,6 +662,9 @@ public class LoanTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
         thisTransactionData.put("feeChargesPortion", this.feeChargesPortion);
         thisTransactionData.put("penaltyChargesPortion", 
this.penaltyChargesPortion);
         thisTransactionData.put("overPaymentPortion", this.overPaymentPortion);
+        if (transactionType.isChargeRefund()) {
+            thisTransactionData.put("chargeRefundChargeType", 
this.chargeRefundChargeType);
+        }
 
         if (this.paymentDetail != null) {
             thisTransactionData.put("paymentTypeId", 
this.paymentDetail.getPaymentType().getId());
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
index e05d97262..7c0e4ab84 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
@@ -55,7 +55,8 @@ public enum LoanTransactionType {
     CREDIT_BALANCE_REFUND(20, "loanTransactionType.creditBalanceRefund"), //
     MERCHANT_ISSUED_REFUND(21, "loanTransactionType.merchantIssuedRefund"), //
     PAYOUT_REFUND(22, "loanTransactionType.payoutRefund"), //
-    GOODWILL_CREDIT(23, "loanTransactionType.goodwillCredit");
+    GOODWILL_CREDIT(23, "loanTransactionType.goodwillCredit"), //
+    CHARGE_REFUND(24, "loanTransactionType.chargeRefund");
 
     private final Integer value;
     private final String code;
@@ -147,6 +148,9 @@ public enum LoanTransactionType {
             case 23:
                 loanTransactionType = LoanTransactionType.GOODWILL_CREDIT;
             break;
+            case 24:
+                loanTransactionType = LoanTransactionType.CHARGE_REFUND;
+            break;
             default:
                 loanTransactionType = LoanTransactionType.INVALID;
             break;
@@ -178,8 +182,12 @@ public enum LoanTransactionType {
         return 
this.value.equals(LoanTransactionType.GOODWILL_CREDIT.getValue());
     }
 
+    public boolean isChargeRefund() {
+        return this.value.equals(LoanTransactionType.CHARGE_REFUND.getValue());
+    }
+
     public boolean isRepaymentType() {
-        return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() 
|| isGoodwillCredit());
+        return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() 
|| isGoodwillCredit() || isChargeRefund());
     }
 
     public boolean isRecoveryRepayment() {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeRefundException.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeRefundException.java
new file mode 100644
index 000000000..54848d422
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeRefundException.java
@@ -0,0 +1,29 @@
+/**
+ * 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.exception;
+
+import 
org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+
+public class LoanChargeRefundException extends 
AbstractPlatformDomainRuleException {
+
+    public LoanChargeRefundException(final String defaultUserMessage, final 
Object... defaultUserMessageArgs) {
+        super("error.msg." + defaultUserMessage, defaultUserMessage, 
defaultUserMessageArgs);
+    }
+
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeRefundCommandHandler.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeRefundCommandHandler.java
new file mode 100644
index 000000000..32b86b2b1
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeRefundCommandHandler.java
@@ -0,0 +1,53 @@
+/**
+ * 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.DataIntegrityErrorHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.orm.jpa.JpaSystemException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "LOAN", action = "CHARGEREFUND")
+public class LoanChargeRefundCommandHandler implements NewCommandSourceHandler 
{
+
+    private final LoanWritePlatformService writePlatformService;
+    private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+
+        try {
+            return 
this.writePlatformService.loanChargeRefund(command.getLoanId(), command);
+        } catch (final JpaSystemException | DataIntegrityViolationException 
dve) {
+            dataIntegrityErrorHandler.handleDataIntegrityIssues(command, 
dve.getMostSpecificCause(), dve, "loancharge.refund",
+                    "Loancharge Refund");
+            return CommandProcessingResult.empty();
+        }
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java
index a1b09c55d..487952f26 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java
@@ -444,6 +444,41 @@ public final class LoanEventApiJsonValidator {
         throwExceptionIfValidationWarningsExist(dataValidationErrors);
     }
 
+    public void validateLoanChargeRefundTransaction(final String json) {
+
+        if (StringUtils.isBlank(json)) {
+            throw new InvalidJsonException();
+        }
+        final String loanChargeIdParam = "loanChargeId";
+        final String installmentNumberParam = "installmentNumber";
+        final String dueDateParam = "dueDate";
+        Set<String> transactionParameters = new HashSet<>(
+                Arrays.asList(loanChargeIdParam, dueDateParam, "locale", 
"dateFormat", installmentNumberParam, //
+                        // remainder below relate to payment part of refund 
and not validated here
+                        "transactionAmount", "externalId", "note", "locale", 
"dateFormat", "paymentTypeId", "accountNumber", "checkNumber",
+                        "routingCode", "receiptNumber", "bankNumber"));
+
+        final Type typeOfMap = new TypeToken<Map<String, Object>>() 
{}.getType();
+        this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, 
transactionParameters);
+
+        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+        final DataValidatorBuilder baseDataValidator = new 
DataValidatorBuilder(dataValidationErrors)
+                .resource("loan.charge.refund.transaction");
+
+        JsonElement element = this.fromApiJsonHelper.parse(json);
+
+        final Long chargeId = 
this.fromApiJsonHelper.extractLongNamed(loanChargeIdParam, element);
+        
baseDataValidator.reset().parameter(loanChargeIdParam).value(chargeId).notNull().integerGreaterThanZero();
+
+        final Integer installmentNumber = 
this.fromApiJsonHelper.extractIntegerWithLocaleNamed(installmentNumberParam, 
element);
+        
baseDataValidator.reset().parameter(installmentNumberParam).value(installmentNumber).ignoreIfNull().integerGreaterThanZero();
+
+        final BigDecimal transactionAmount = 
this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", 
element);
+        
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).ignoreIfNull().positiveAmount();
+
+        throwExceptionIfValidationWarningsExist(dataValidationErrors);
+    }
+
     public void validateInstallmentChargeTransaction(final String json) {
 
         if (StringUtils.isBlank(json)) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index 1c92ad6f5..03e335308 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -1669,7 +1669,7 @@ public class LoanReadPlatformServiceImpl implements 
LoanReadPlatformService {
         LocalDate organisationStartDate = 
this.configurationDomainService.retrieveOrganisationStartDate();
         final StringBuilder sqlBuilder = new StringBuilder(400);
         sqlBuilder.append("select ").append(mapper.schema()).append(
-                " where  (recaldet.is_compounding_to_be_posted_as_transaction 
is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
+                " where (recaldet.is_compounding_to_be_posted_as_transaction 
is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
                 .append(" and (((ls.fee_charges_amount <> 
COALESCE(ls.accrual_fee_charges_derived, 0))")
                 .append(" or (ls.penalty_charges_amount <> 
COALESCE(ls.accrual_penalty_charges_derived, 0))")
                 .append(" or (ls.interest_amount <> 
COALESCE(ls.accrual_interest_derived, 0)))")
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
index 872a8ce3d..d8e8a29c1 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
@@ -68,6 +68,8 @@ public interface LoanWritePlatformService {
 
     CommandProcessingResult undoWaiveLoanCharge(JsonCommand command);
 
+    CommandProcessingResult loanChargeRefund(Long loanId, JsonCommand command);
+
     CommandProcessingResult loanReassignment(Long loanId, JsonCommand command);
 
     CommandProcessingResult bulkLoanReassignment(JsonCommand command);
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 1099164d4..936c37690 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
@@ -105,6 +105,7 @@ import 
org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanDelete
 import 
org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanUpdateChargeBusinessEvent;
 import 
org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanWaiveChargeBusinessEvent;
 import 
org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanWaiveChargeUndoBusinessEvent;
+import 
org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanChargePaymentPostBusinessEvent;
 import 
org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent;
 import 
org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
 import 
org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanWrittenOffPostBusinessEvent;
@@ -180,6 +181,7 @@ import 
org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCount
 import 
org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
+import 
org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException;
 import 
org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
@@ -888,6 +890,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
     @Override
     public CommandProcessingResult makeLoanRepayment(final LoanTransactionType 
repaymentTransactionType, final Long loanId,
             final JsonCommand command, final boolean isRecoveryRepayment) {
+        final String chargeRefundChargeType = null;
+        return 
makeLoanRepaymentWithChargeRefundChargeType(repaymentTransactionType, loanId, 
command, isRecoveryRepayment,
+                chargeRefundChargeType);
+    }
+
+    private CommandProcessingResult 
makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType 
repaymentTransactionType,
+            final Long loanId, final JsonCommand command, final boolean 
isRecoveryRepayment, final String chargeRefundChargeType) {
 
         
this.loanUtilService.validateRepaymentTransactionType(repaymentTransactionType);
         
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
@@ -915,7 +924,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         final CommandProcessingResultBuilder commandProcessingResultBuilder = 
new CommandProcessingResultBuilder();
         LoanTransaction loanTransaction = 
this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan,
                 commandProcessingResultBuilder, transactionDate, 
transactionAmount, paymentDetail, noteText, txnExternalId,
-                isRecoveryRepayment, isAccountTransfer, holidayDetailDto, 
isHolidayValidationDone);
+                isRecoveryRepayment, chargeRefundChargeType, 
isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
 
         // Update loan transaction on repayment.
         if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) {
@@ -984,10 +993,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
                     
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
                 }
                 final CommandProcessingResultBuilder 
commandProcessingResultBuilder = new CommandProcessingResultBuilder();
+                final String chargeRefundChargeType = null;
                 LoanTransaction loanTransaction = 
this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, loan,
                         commandProcessingResultBuilder, 
bulkRepaymentCommand.getTransactionDate(),
                         singleLoanRepaymentCommand.getTransactionAmount(), 
paymentDetail, bulkRepaymentCommand.getNote(), null,
-                        isRecoveryRepayment, isAccountTransfer, 
holidayDetailDTO, isHolidayValidationDone);
+                        isRecoveryRepayment, chargeRefundChargeType, 
isAccountTransfer, holidayDetailDTO, isHolidayValidationDone);
                 transactionIds.add(loanTransaction.getId());
             }
         }
@@ -1646,6 +1656,189 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
                 .build();
     }
 
+    @Transactional
+    @Override
+    public CommandProcessingResult loanChargeRefund(final Long loanId, final 
JsonCommand command) {
+
+        
this.loanEventApiJsonValidator.validateLoanChargeRefundTransaction(command.json());
+
+        final Long loanChargeId = 
command.longValueOfParameterNamed("loanChargeId");
+        final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, 
loanChargeId);
+
+        final Integer installmentNumber = 
command.integerValueOfParameterNamed("installmentNumber");
+        final LocalDate dueDate = 
command.localDateValueOfParameterNamed("dueDate");
+        final BigDecimal transactionAmount = 
command.bigDecimalValueOfParameterNamed("transactionAmount");
+
+        final LoanInstallmentCharge installmentChargeEntry = 
loanChargeRefundEntranceValidation(loanCharge, installmentNumber, dueDate);
+        Integer installmentNumberIdentified = null;
+        if (installmentChargeEntry != null) {
+            installmentNumberIdentified = 
installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber();
+        }
+        final BigDecimal fullRefundAbleAmount = 
loanChargeValidateRefundAmount(loanCharge, installmentChargeEntry, 
transactionAmount);
+
+        JsonCommand repaymentJsonCommand = 
adaptLoanChargeRefundCommandForFutherRepaymentProcessing(command, 
fullRefundAbleAmount);
+
+        boolean isRecoveryRepayment = false;
+        String chargeRefundChargeType = "F";
+        if (loanCharge.isPenaltyCharge()) {
+            chargeRefundChargeType = "P";
+        }
+        // chargeRefundChargeType only included as a parameter for accounting 
reason - in order to identify whether fee
+        // or penalty GL account is relevant
+        CommandProcessingResult result = 
makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType.CHARGE_REFUND,
+                repaymentJsonCommand.getLoanId(), repaymentJsonCommand, 
isRecoveryRepayment, chargeRefundChargeType);
+
+        Long loanChargeRefundTransactionId = result.resourceId();
+        LoanTransaction newChargeRefundTxn = null;
+        for (LoanTransaction chargeRefundTxn : 
loanCharge.getLoan().getLoanTransactions()) {
+            if (chargeRefundTxn.getId().equals(loanChargeRefundTransactionId)) 
{
+                newChargeRefundTxn = chargeRefundTxn;
+                final BigDecimal appliedRefundAmount = 
newChargeRefundTxn.getAmount(loanCharge.getLoan().getCurrency()).getAmount()
+                        .multiply(BigDecimal.valueOf(-1));
+                final LoanChargePaidBy loanChargePaidByForChargeRefund = new 
LoanChargePaidBy(newChargeRefundTxn, loanCharge,
+                        appliedRefundAmount, installmentNumberIdentified);
+                
newChargeRefundTxn.getLoanChargesPaid().add(loanChargePaidByForChargeRefund);
+                
loanCharge.getLoanChargePaidBySet().add(loanChargePaidByForChargeRefund);
+                break;
+            }
+        }
+
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanChargePaymentPostBusinessEvent(newChargeRefundTxn));
+        return result;
+
+    }
+
+    private JsonCommand 
adaptLoanChargeRefundCommandForFutherRepaymentProcessing(JsonCommand command, 
BigDecimal fullRefundAbleAmount) {
+        // creates JsonCommand for onward repayment processing
+        JsonObject jsonObject = (JsonObject) 
this.fromApiJsonHelper.parse(command.json());
+
+        String dateFormat;
+        if (this.fromApiJsonHelper.parameterExists("dateFormat", jsonObject)) {
+            dateFormat = 
this.fromApiJsonHelper.extractStringNamed("dateFormat", jsonObject);
+        } else {
+            dateFormat = "dd MMMM yyyy";
+            jsonObject.addProperty("dateFormat", dateFormat);
+        }
+        DateTimeFormatter dateTimeFormatter = 
DateTimeFormatter.ofPattern(dateFormat);
+        LocalDate transactionDate = DateUtils.getLocalDateOfTenant();
+        String transactionDateString = 
transactionDate.format(dateTimeFormatter);
+        jsonObject.addProperty("transactionDate", transactionDateString);
+        if (!this.fromApiJsonHelper.parameterExists("transactionAmount", 
jsonObject)) {
+            jsonObject.addProperty("transactionAmount", 
fullRefundAbleAmount.toString());
+        }
+        jsonObject.remove("loanChargeId");
+        jsonObject.remove("installmentNumber");
+        jsonObject.remove("dueDate");
+
+        JsonCommand repaymentJsonCommand = 
JsonCommand.fromExistingCommand(command, jsonObject);
+        return repaymentJsonCommand;
+    }
+
+    private BigDecimal loanChargeValidateRefundAmount(LoanCharge loanCharge, 
LoanInstallmentCharge installmentChargeEntry,
+            BigDecimal transactionAmount) {
+        // if transactionAmount not provided return max refundable amount 
(amount paid minus previous refunds)
+        BigDecimal chargeAmountPaid = BigDecimal.ZERO;
+        BigDecimal chargeAmountRefunded = BigDecimal.ZERO;
+        MonetaryCurrency loanCurrency = loanCharge.getLoan().getCurrency();
+        if (loanCharge.isInstalmentFee()) {
+            final Integer installmentNumber = 
installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber();
+            chargeAmountPaid = 
installmentChargeEntry.getAmountPaid(loanCurrency).getAmount();
+            for (LoanChargePaidBy loanChargePaidBy : 
loanCharge.getLoanChargePaidBySet()) {
+                if 
(installmentNumber.equals(loanChargePaidBy.getInstallmentNumber())) {
+                    if (isRefundElementOfChargeRefund(loanChargePaidBy)) {
+                        chargeAmountRefunded = 
chargeAmountRefunded.add(loanChargePaidBy.getAmount());
+                    }
+                }
+            }
+        } else {
+            chargeAmountPaid = 
loanCharge.getAmountPaid(loanCurrency).getAmount();
+            for (LoanChargePaidBy loanChargePaidBy : 
loanCharge.getLoanChargePaidBySet()) {
+                if (isRefundElementOfChargeRefund(loanChargePaidBy)) {
+                    chargeAmountRefunded = 
chargeAmountRefunded.add(loanChargePaidBy.getAmount());
+                }
+            }
+        }
+        chargeAmountRefunded = 
chargeAmountRefunded.multiply(BigDecimal.valueOf(-1));
+
+        if (chargeAmountRefunded.compareTo(chargeAmountPaid) > 0) {
+            final String errorMessage = 
"loan.charge.more.refunded.than.paid.unexpected.system.error";
+            final String details = "Paid: " + chargeAmountPaid.toString() + "  
Refunded: " + chargeAmountPaid.toString();
+            throw new LoanChargeRefundException(errorMessage, details);
+        }
+
+        BigDecimal refundableAmount = 
chargeAmountPaid.subtract(chargeAmountRefunded);
+        if (transactionAmount != null) { // refund amount was provided.
+            if (transactionAmount.compareTo(refundableAmount) > 0) {
+                final String errorMessage = 
"loan.charge.transaction.amount.is.more.than.is.refundable";
+                final String details = "transactionAmount: " + 
transactionAmount.toString() + "  Refundable: "
+                        + refundableAmount.toString();
+                throw new LoanChargeRefundException(errorMessage, details);
+            }
+        }
+
+        return refundableAmount;
+    }
+
+    private boolean isRefundElementOfChargeRefund(LoanChargePaidBy 
loanChargePaidBy) {
+        // The Refund Element is always negative
+        return (loanChargePaidBy.getLoanTransaction().isChargeRefund() && 
loanChargePaidBy.getAmount().compareTo(BigDecimal.ZERO) < 0);
+    }
+
+    private LoanInstallmentCharge 
loanChargeRefundEntranceValidation(LoanCharge loanCharge, Integer 
installmentNumber, LocalDate dueDate) {
+
+        LoanInstallmentCharge installmentChargeEntry = null;
+
+        Loan loan = loanCharge.getLoan();
+        if (!(loan.isOpen() || loan.status().isClosedObligationsMet() || 
loan.status().isOverpaid())) {
+            final String errorMessage = "loan.charge.refund.invalid.status";
+            throw new LoanChargeRefundException(errorMessage, 
loan.status().toString());
+        }
+
+        if (dueDate != null && installmentNumber != null) {
+            
throwLoanChargeRefundException("loan.charge.refund.dueDate.and.installmentNumber.provided.use.only.one",
 installmentNumber,
+                    dueDate);
+        }
+
+        if (loanCharge.isInstalmentFee()) { // identify specific installment
+            if (dueDate == null && installmentNumber == null) {
+                throwLoanChargeRefundException(
+                        
"loan.charge.refund.neither.dueDate.nor.installmentNumber.provided.for.this.installment.charge",
 installmentNumber,
+                        dueDate);
+            }
+
+            if (dueDate != null) {
+                installmentChargeEntry = 
loanCharge.getInstallmentLoanCharge(dueDate);
+            } else if (installmentNumber != null) {
+                installmentChargeEntry = 
loanCharge.getInstallmentLoanCharge(installmentNumber);
+            }
+
+            if (installmentChargeEntry == null) {
+                
throwLoanChargeRefundException("loan.charge.refund.installment.not.found", 
installmentNumber, dueDate);
+            }
+        } else {
+
+            if (dueDate != null || installmentNumber != null) {
+                throwLoanChargeRefundException(
+                        
"loan.charge.refund.dueDate.or.installmentNumber.provided.but.this.is.not.an.installment.charge",
 installmentNumber,
+                        dueDate);
+            }
+        }
+
+        return installmentChargeEntry;
+    }
+
+    private void throwLoanChargeRefundException(String errorMessage, Integer 
installmentNumber, LocalDate dueDate) {
+        String dueDateValue = "";
+        String installmentNumberValue = "";
+        if (dueDate != null) {
+            dueDateValue = dueDate.toString();
+        }
+        if (installmentNumber != null) {
+            installmentNumberValue = installmentNumber.toString();
+        }
+        throw new LoanChargeRefundException(errorMessage, "dueDate: " + 
dueDateValue + "  installmentNumber: " + installmentNumberValue);
+    }
+
     @Transactional
     @Override
     public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand 
command) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
index 0407304a2..fd3328d3b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
@@ -427,6 +427,10 @@ public final class LoanEnumerations {
                 optionData = new 
LoanTransactionEnumData(LoanTransactionType.GOODWILL_CREDIT.getValue().longValue(),
                         LoanTransactionType.GOODWILL_CREDIT.getCode(), 
"Goodwill Credit");
             break;
+            case CHARGE_REFUND:
+                optionData = new 
LoanTransactionEnumData(LoanTransactionType.CHARGE_REFUND.getValue().longValue(),
+                        LoanTransactionType.CHARGE_REFUND.getCode(), "Charge 
Refund");
+            break;
         }
         return optionData;
     }
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index 88f8327e1..48f9e4577 100644
--- 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -46,4 +46,6 @@
     <include file="parts/0024_add_audit_entries.xml" 
relativeToChangelogFile="true"/>
     <include file="parts/0025_add_audit_entries_to_journal_entry.xml" 
relativeToChangelogFile="true"/>
     <include file="parts/0026_reversals_for_reversed_transactions.xml" 
relativeToChangelogFile="true"/>
+    <include file="parts/0027_add_charge_refund_permission.xml" 
relativeToChangelogFile="true"/>
+    <include 
file="parts/0028_add_charge_refund_charge_type_to_loan_transaction.xml" 
relativeToChangelogFile="true"/>
 </databaseChangeLog>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0027_add_charge_refund_permission.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0027_add_charge_refund_permission.xml
new file mode 100644
index 000000000..6738728d3
--- /dev/null
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0027_add_charge_refund_permission.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd";>
+    <changeSet author="fineract" id="1">
+        <insert tableName="m_permission">
+            <column name="grouping" value="transaction_loan" />
+            <column name="code" value="CHARGEREFUND_LOAN" />
+            <column name="entity_name" value="LOAN" />
+            <column name="action_name" value="CHARGEREFUND" />
+            <column name="can_maker_checker" valueBoolean="false" />
+        </insert>
+    </changeSet>
+</databaseChangeLog>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0028_add_charge_refund_charge_type_to_loan_transaction.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0028_add_charge_refund_charge_type_to_loan_transaction.xml
new file mode 100644
index 000000000..5552d4d73
--- /dev/null
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0028_add_charge_refund_charge_type_to_loan_transaction.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd";>
+    <changeSet author="fineract" id="1">
+        <addColumn tableName="m_loan_transaction">
+            <column name="charge_refund_charge_type" type="VARCHAR(1)"/>
+        </addColumn>
+    </changeSet>
+</databaseChangeLog>
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanChargeRefundIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanChargeRefundIntegrationTest.java
new file mode 100644
index 000000000..2d8619216
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanChargeRefundIntegrationTest.java
@@ -0,0 +1,602 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.CommonConstants;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
+import org.apache.fineract.integrationtests.common.accounting.JournalEntry;
+import 
org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
+import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
+import 
org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
+import 
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressWarnings({ "rawtypes", "unchecked" })
+public class ClientLoanChargeRefundIntegrationTest {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ClientLoanChargeRefundIntegrationTest.class);
+
+    private Integer disbursedLoanID;
+    private Account assetAccount;
+    private Account feeIncomeAccount;
+    private Account penaltyIncomeAccount;
+    private Account expenseAccount;
+    private Account overpaymentAccount;
+    private static final String ZERO_INTEREST_RATE = "0";
+    private static final String FOUR_INSTALLMENTS = "4";
+    private static final String NONE = "1";
+    private static final String CASH_BASED = "2";
+    private static final String ACCRUAL_PERIODIC = "3";
+    private static final String MAKE_REPAYMENT_COMMAND = "repayment";
+    private static final String OVERPAID = "overpaid";
+    private static final String CLOSED_OBLIGATION_MET = "closedObligationsMet";
+    private static final String ACTIVE = "active";
+
+    /*
+     * loan disbursed: 4 installments of 3000; zero % interest; a specified 
due date charge of 120 is added and the
+     * amount is allocated to installment 2; allocation strategy is penalty, 
fees, interest, principal
+     */
+    private static final Float oneInstallment = 3000.00f;
+    private static final Float fullLoan = 3000.00f * 4;
+    private static final Float fullChargeRefundAmount = 120.00f;
+    private static final Float oneThirdChargeRefundAmount = 40.00f;
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private LoanTransactionHelper loanTransactionHelper;
+    private LoanTransactionHelper loanTransactionHelperValidationError;
+    private AccountHelper accountHelper;
+    private JournalEntryHelper journalEntryHelper;
+    private Integer createdRepaymentTypeResourceId;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new 
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.loanTransactionHelperValidationError = new 
LoanTransactionHelper(this.requestSpec, new ResponseSpecBuilder().build());
+        this.requestSpec.header("Authorization", "Basic " + 
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new 
ResponseSpecBuilder().expectStatusCode(200).build();
+        this.loanTransactionHelper = new 
LoanTransactionHelper(this.requestSpec, this.responseSpec);
+        this.accountHelper = new AccountHelper(this.requestSpec, 
this.responseSpec);
+        this.assetAccount = this.accountHelper.createAssetAccount();
+        this.feeIncomeAccount = this.accountHelper.createIncomeAccount();
+        this.penaltyIncomeAccount = this.accountHelper.createIncomeAccount();
+        this.expenseAccount = this.accountHelper.createExpenseAccount();
+        this.overpaymentAccount = this.accountHelper.createLiabilityAccount();
+        this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, 
this.responseSpec);
+    }
+
+    @Test
+    public void fullRefundAndReverseOfPaidChargeSucceedsTest_Active_Active() {
+        testRefundAndReverseOfPaidChargeSucceeds(oneInstallment + 
fullChargeRefundAmount, fullChargeRefundAmount, ACTIVE, ACTIVE);
+    }
+
+    @Test
+    public void fullRefundAndReverseOfPaidChargeSucceedsTest_Active_Com() {
+        testRefundAndReverseOfPaidChargeSucceeds(fullLoan, 
fullChargeRefundAmount, ACTIVE, CLOSED_OBLIGATION_MET);
+    }
+
+    @Test
+    public void fullRefundAndReverseOfPaidChargeSucceedsTest_Active_Overpaid() 
{
+        testRefundAndReverseOfPaidChargeSucceeds(fullLoan + 50.00f, 
fullChargeRefundAmount, ACTIVE, OVERPAID);
+    }
+
+    @Test
+    public void fullRefundAndReverseOfPaidChargeSucceedsTest_Com_Overpaid() {
+        testRefundAndReverseOfPaidChargeSucceeds(fullLoan + 
fullChargeRefundAmount, fullChargeRefundAmount, CLOSED_OBLIGATION_MET,
+                OVERPAID);
+    }
+
+    @Test
+    public void 
fullRefundAndReverseOfPaidChargeSucceedsTest_Overpaid_Overpaid() {
+        testRefundAndReverseOfPaidChargeSucceeds(fullLoan + 
fullChargeRefundAmount + 50.00f, fullChargeRefundAmount, OVERPAID, OVERPAID);
+    }
+
+    @Test
+    public void 
partialRefundAndReverseOfPaidChargeSucceedsTest_Active_Active() {
+        testRefundAndReverseOfPaidChargeSucceeds(fullLoan, 
oneThirdChargeRefundAmount, ACTIVE, ACTIVE);
+    }
+
+    @Test
+    public void partialRefundAndReverseOfPaidChargeSucceedsTest_Active_Com() {
+        testRefundAndReverseOfPaidChargeSucceeds(fullLoan + 
(oneThirdChargeRefundAmount * 2), oneThirdChargeRefundAmount, ACTIVE,
+                CLOSED_OBLIGATION_MET);
+    }
+
+    @Test
+    public void 
partialRefundAndReverseOfPaidChargeSucceedsTest_Active_Overpaid() {
+        testRefundAndReverseOfPaidChargeSucceeds(fullLoan + 
(oneThirdChargeRefundAmount * 2) + 1.0f, oneThirdChargeRefundAmount, ACTIVE,
+                OVERPAID);
+    }
+
+    private void testRefundAndReverseOfPaidChargeSucceeds(final Float 
repaymentAmount, final Float refundAmount,
+            final String expectedPostRepaymentStatus, final String 
expectedPostRefundStatus) {
+        // disburse, repay, add charge, charge refund and reverse charge refund
+        Integer loanChargeId = disburseAddChargeAndRepay(repaymentAmount, 
expectedPostRepaymentStatus, NONE, true);
+
+        Float totalOutstandingPreRefund = 
getLoanDetailsSummaryTotalOutstanding(disbursedLoanID);
+        Float overpaidPreRefund = 
getLoanDetailsTotalOverpaidAmount(disbursedLoanID);
+
+        Float expectedTotalOutstandingPostRefund = null;
+        Float expectedOverpaidPostRefund = null;
+        if (totalOutstandingPreRefund.compareTo(refundAmount) >= 0) {
+            expectedTotalOutstandingPostRefund = totalOutstandingPreRefund - 
refundAmount;
+            expectedOverpaidPostRefund = 0.0f;
+        } else {
+            expectedTotalOutstandingPostRefund = 0.0f;
+            if (totalOutstandingPreRefund == 0.0f) {
+                expectedOverpaidPostRefund = overpaidPreRefund + refundAmount;
+            } else {
+                expectedOverpaidPostRefund = refundAmount - 
totalOutstandingPreRefund;
+            }
+        }
+
+        LOG.info("-------------Loancharge Refund -----------");
+        final Integer installmentNumber = null;
+        final String externalId = null;
+        Integer chargeRefundTxnId = (Integer) 
this.loanTransactionHelper.loanChargeRefund(loanChargeId, installmentNumber, 
refundAmount,
+                externalId, this.disbursedLoanID, "resourceId");
+        HashMap loanStatusHashMap = (HashMap) 
this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, 
disbursedLoanID,
+                "status");
+        assertTrue((Boolean) loanStatusHashMap.get(expectedPostRefundStatus), 
"Invalid Post Refund Status");
+
+        Float totalOutstandingPostRefund = 
getLoanDetailsSummaryTotalOutstanding(disbursedLoanID);
+        Float overpaidPostRefund = 
getLoanDetailsTotalOverpaidAmount(disbursedLoanID);
+
+        Assertions.assertEquals(expectedTotalOutstandingPostRefund, 
totalOutstandingPostRefund, "Incorrect totalOutstanding Post Refund");
+        Assertions.assertEquals(expectedOverpaidPostRefund, 
overpaidPostRefund, "Incorrect overpaid Post Refund");
+
+        verifyPaidByEntry(disbursedLoanID, chargeRefundTxnId, refundAmount);
+
+        LOG.info("-------------Reverse Loancharge Refund -----------");
+        final String reverseDate = getTodaysDate();
+        final Float adjustmentAmount = 0.0f;
+        HashMap reverseHashMap = (HashMap) 
this.loanTransactionHelper.adjustLoanTransaction(disbursedLoanID, 
chargeRefundTxnId, reverseDate,
+                adjustmentAmount.toString(), "");
+        loanStatusHashMap = (HashMap) 
this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, 
disbursedLoanID,
+                "status");
+        assertTrue((Boolean) 
loanStatusHashMap.get(expectedPostRepaymentStatus), "Invalid Post Reversed 
Status");
+
+        Float totalOutstandingPostReverse = 
getLoanDetailsSummaryTotalOutstanding(disbursedLoanID);
+        Float overpaidPostReverse = 
getLoanDetailsTotalOverpaidAmount(disbursedLoanID);
+
+        Assertions.assertEquals(totalOutstandingPreRefund, 
totalOutstandingPostReverse, "Incorrect totalOutstanding Post Reverse");
+        Assertions.assertEquals(overpaidPreRefund, overpaidPostReverse, 
"Incorrect overpaid Post Reverse");
+
+    }
+
+    @Test
+    public void refundOfUnpaidChargeFailsTest() {
+
+        final Float repaymentAmount = 3000.00f; // pays installment one but 
none of added charge
+        Integer loanChargeId = disburseAddChargeAndRepay(repaymentAmount, 
ACTIVE, NONE, false);
+
+        LOG.info("-------------Loancharge Refund -----------");
+        final Float refundAmount = 60.00f;
+        final Integer installmentNumber = null;
+        final String externalId = null;
+        ArrayList<HashMap> errors = (ArrayList<HashMap>) 
this.loanTransactionHelperValidationError.loanChargeRefund(loanChargeId,
+                installmentNumber, refundAmount, externalId, 
this.disbursedLoanID, CommonConstants.RESPONSE_ERROR);
+        
assertEquals("error.msg.loan.charge.transaction.amount.is.more.than.is.refundable",
+                
errors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
+    }
+
+    @Test
+    public void refundingMoreThanPaidFailsTest() {
+
+        final Float repaymentAmount = 3090.00f; // pays installment one and 90 
(not all) of added charge
+        Integer loanChargeId = disburseAddChargeAndRepay(repaymentAmount, 
ACTIVE, NONE, false);
+
+        LOG.info("-------------Loancharge Refund -----------");
+        final Float refundAmount = 90.01f; // 0.01 more than paid.
+        final Integer installmentNumber = null;
+        final String externalId = null;
+        ArrayList<HashMap> errors = (ArrayList<HashMap>) 
this.loanTransactionHelperValidationError.loanChargeRefund(loanChargeId,
+                installmentNumber, refundAmount, externalId, 
this.disbursedLoanID, CommonConstants.RESPONSE_ERROR);
+
+        
assertEquals("error.msg.loan.charge.transaction.amount.is.more.than.is.refundable",
+                
errors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
+
+    }
+
+    @Test
+    public void 
onyRefundElementNotRepaymentElementUsedToCalculateRefundableAmountTest() {
+        final Float chargeAmountPaid = 60.00f;
+        final Float repaymentAmount = 3000.00f + chargeAmountPaid;
+        // covers Installment 1 plus half of 120 charge added to installment 2
+        Integer loanChargeId = disburseAddChargeAndRepay(repaymentAmount, 
ACTIVE, NONE, false);
+
+        LOG.info("-------------Loancharge Refund 1 -----------");
+        final Float refundAmount = chargeAmountPaid; // refund charge paid
+        final Integer installmentNumber = null;
+        final String externalId = null;
+        Integer chargeRefundTxnId = (Integer) 
this.loanTransactionHelper.loanChargeRefund(loanChargeId, installmentNumber, 
refundAmount,
+                externalId, this.disbursedLoanID, "resourceId");
+        HashMap loanDetailsHashMap = (HashMap) 
this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec,
+                disbursedLoanID, "");
+        // refund 60 pays off remainder of charge leaving an amount 60 that 
could be refunded
+
+        LOG.info("-------------Loancharge Refund 2 -----------");
+        final Float smallRefund = 0.01f;
+        chargeRefundTxnId = (Integer) 
this.loanTransactionHelper.loanChargeRefund(loanChargeId, installmentNumber, 
smallRefund, externalId,
+                this.disbursedLoanID, "resourceId");
+        loanDetailsHashMap = (HashMap) 
this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, 
disbursedLoanID, "");
+
+    }
+
+    @Test
+    public void refundOfPartiallyPaidChargeCanRepayMoreOfSameChargeTest() {
+        final Float chargeAmountPaid = 80.00f;
+        final Float chargeAmountFull = 120.00f;
+        final Float chargeAmountOutstanding = chargeAmountFull - 
chargeAmountPaid;
+        final Float repaymentAmount = 3000.00f + chargeAmountPaid;
+        // covers Installment 1 plus two thirds of 120 charge added to 
installment 2
+        Integer loanChargeId = disburseAddChargeAndRepay(repaymentAmount, 
ACTIVE, NONE, false);
+        Float feeChargesPaid = 
getLoanDetailsSummaryfeeChargesPaid(disbursedLoanID);
+        Assertions.assertEquals(feeChargesPaid, chargeAmountPaid, "Incorrect 
Partial feeChargesPaid");
+
+        LOG.info("-------------Loancharge Refund -----------");
+        final Float refundAmount = chargeAmountPaid; // refund charge paid
+        final Integer installmentNumber = null;
+        final String externalId = null;
+        Integer chargeRefundTxnId = (Integer) 
this.loanTransactionHelper.loanChargeRefund(loanChargeId, installmentNumber, 
refundAmount,
+                externalId, this.disbursedLoanID, "resourceId");
+        feeChargesPaid = getLoanDetailsSummaryfeeChargesPaid(disbursedLoanID);
+        Assertions.assertEquals(feeChargesPaid, chargeAmountFull, "Incorrect 
Full feeChargesPaid");
+
+        ArrayList<HashMap> loanChargePaidByList = (ArrayList<HashMap>) 
this.loanTransactionHelper.getLoanTransactionDetails(disbursedLoanID,
+                chargeRefundTxnId, "loanChargePaidByList");
+        Assertions.assertNotNull(loanChargePaidByList);
+        Assertions.assertEquals(loanChargePaidByList.size(), 2);
+        // expecting 2 entries 1)-80 refund 2) 40 repayment
+        Float paidByAmount1 = (Float) 
loanChargePaidByList.get(0).get("amount");
+        Assertions.assertNotNull(paidByAmount1);
+        Assertions.assertNotEquals(paidByAmount1, 0.0f);
+        Float paidByAmount2 = (Float) 
loanChargePaidByList.get(1).get("amount");
+        Assertions.assertNotNull(paidByAmount2);
+        Assertions.assertNotEquals(paidByAmount2, 0.0f);
+
+        if (paidByAmount1 < 0.0f) {
+            Assertions.assertEquals(paidByAmount1, chargeAmountPaid * -1, 
"Refund Element Incorrect");
+        } else {
+            Assertions.assertEquals(paidByAmount1, chargeAmountOutstanding, 
"Repayment Element Incorrect");
+        }
+        if (paidByAmount2 < 0.0f) {
+            Assertions.assertEquals(paidByAmount2, chargeAmountPaid * -1, 
"Refund Element Incorrect");
+        } else {
+            Assertions.assertEquals(paidByAmount2, chargeAmountOutstanding, 
"Repayment Element Incorrect");
+        }
+
+    }
+
+    @Test
+    public void 
chargeRefundCreatesCorrectJournalEntriesForPeriodicAccruals_Fee_Test() {
+        chargeRefundCreatesCorrectJournalEntries(ACCRUAL_PERIODIC, false);
+    }
+
+    @Test
+    public void 
chargeRefundCreatesCorrectJournalEntriesForCashAccounting_Fee_Test() {
+        chargeRefundCreatesCorrectJournalEntries(CASH_BASED, false);
+    }
+
+    @Test
+    public void 
chargeRefundCreatesCorrectJournalEntriesForPeriodicAccruals_Penalty_Test() {
+        chargeRefundCreatesCorrectJournalEntries(ACCRUAL_PERIODIC, true);
+    }
+
+    @Test
+    public void 
chargeRefundCreatesCorrectJournalEntriesForCashAccounting_Penalty_Test() {
+        chargeRefundCreatesCorrectJournalEntries(CASH_BASED, true);
+    }
+
+    private void chargeRefundCreatesCorrectJournalEntries(final String 
accountingType, final boolean penalty) {
+
+        final Float repaymentAmount = fullLoan + fullChargeRefundAmount;
+        Integer loanChargeId = disburseAddChargeAndRepay(repaymentAmount, 
CLOSED_OBLIGATION_MET, accountingType, penalty);
+
+        LOG.info("-------------Loancharge Refund -----------");
+        final Integer installmentNumber = null;
+        final String externalId = null;
+        Integer chargeRefundTxnId = (Integer) 
this.loanTransactionHelper.loanChargeRefund(loanChargeId, installmentNumber,
+                oneThirdChargeRefundAmount, externalId, this.disbursedLoanID, 
"resourceId");
+        final String txnDate = getTodaysDate();
+
+        Account incomeAccount = feeIncomeAccount;
+        if (penalty) {
+            incomeAccount = penaltyIncomeAccount;
+        }
+
+        this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, 
txnDate,
+                new JournalEntry(oneThirdChargeRefundAmount, 
JournalEntry.TransactionType.CREDIT));
+        
this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount,
 txnDate,
+                new JournalEntry(oneThirdChargeRefundAmount, 
JournalEntry.TransactionType.CREDIT));
+        
this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, 
txnDate,
+                new JournalEntry(oneThirdChargeRefundAmount, 
JournalEntry.TransactionType.DEBIT));
+
+        LOG.info("-------------Reverse Loancharge Refund -----------");
+        final String reverseDate = getTodaysDate();
+        final Float adjustmentAmount = 0.0f;
+        HashMap reverseHashMap = (HashMap) 
this.loanTransactionHelper.adjustLoanTransaction(disbursedLoanID, 
chargeRefundTxnId, reverseDate,
+                adjustmentAmount.toString(), "");
+
+        this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, 
txnDate,
+                new JournalEntry(oneThirdChargeRefundAmount, 
JournalEntry.TransactionType.DEBIT));
+        
this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount,
 txnDate,
+                new JournalEntry(oneThirdChargeRefundAmount, 
JournalEntry.TransactionType.DEBIT));
+        
this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, 
txnDate,
+                new JournalEntry(oneThirdChargeRefundAmount, 
JournalEntry.TransactionType.CREDIT));
+
+    }
+
+    @Test
+    public void repaymentReversalDisallowedIfLaterChargeRefundTest() {
+
+        // repayment covers 2 installments plus charge
+        final Float repaymentAmount = oneInstallment + oneInstallment + 
fullChargeRefundAmount;
+        final Integer loanChargeId = 
disburseAddChargeAndRepay(repaymentAmount, ACTIVE, ACCRUAL_PERIODIC, false);
+        final Integer repayment1Id = createdRepaymentTypeResourceId;
+
+        final String repayment2Date = "20 January 2022";
+        final Float repayment2Amount = oneInstallment; // installment 3
+        makeRepaymentType(MAKE_REPAYMENT_COMMAND, repayment2Date, 
repayment2Amount);
+        Integer repayment2Id = createdRepaymentTypeResourceId;
+
+        LOG.info("-------------Loancharge Refund -----------");
+        final Integer installmentNumber = null;
+        final String externalId = null;
+        final Integer chargeRefundTxnId = (Integer) 
this.loanTransactionHelper.loanChargeRefund(loanChargeId, installmentNumber,
+                oneThirdChargeRefundAmount, externalId, this.disbursedLoanID, 
"resourceId");
+
+        final String reverseDate = getTodaysDate();
+        final Float adjustmentAmount = 0.0f;
+        LOG.info("-------------Reverse Repayment 2  -----------");
+        ArrayList<HashMap> errors = (ArrayList<HashMap>) 
this.loanTransactionHelperValidationError.adjustLoanTransaction(disbursedLoanID,
+                repayment2Id, reverseDate, adjustmentAmount.toString(), 
CommonConstants.RESPONSE_ERROR);
+
+        
assertEquals("error.msg.loan.transaction.cant.be.reversed.because.later.charge.refund.exists",
+                
errors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
+    }
+
+    @Test
+    public void repaymentNotAllowedIfLaterChargeRefundTest() {
+
+        // repayment covers 2 installments plus charge
+        final Float repaymentAmount = oneInstallment + oneInstallment + 
fullChargeRefundAmount;
+        final Integer loanChargeId = 
disburseAddChargeAndRepay(repaymentAmount, ACTIVE, ACCRUAL_PERIODIC, false);
+
+        LOG.info("-------------Loancharge Refund -----------");
+        final Integer installmentNumber = null;
+        final String externalId = null;
+        final Integer chargeRefundTxnId = (Integer) 
this.loanTransactionHelper.loanChargeRefund(loanChargeId, installmentNumber,
+                oneThirdChargeRefundAmount, externalId, this.disbursedLoanID, 
"resourceId");
+
+        final String repayment2Date = "20 January 2022";
+        final Float repayment2Amount = oneInstallment; // installment 3
+        ArrayList<HashMap> errors = (ArrayList<HashMap>) 
this.loanTransactionHelperValidationError.makeRepaymentTypePayment(
+                MAKE_REPAYMENT_COMMAND, repayment2Date, repayment2Amount, 
this.disbursedLoanID, CommonConstants.RESPONSE_ERROR);
+
+        
assertEquals("error.msg.loan.transaction.cant.be.created.because.later.charge.refund.exists",
+                
errors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
+
+    }
+
+    private void disburseLoanOfAccountingRule(final String accountingType, 
final String loanAmount, final String loanDate,
+            final boolean penalty) {
+        this.disbursedLoanID = fromStartToDisburseLoan(loanDate, loanAmount, 
penalty, accountingType, assetAccount, feeIncomeAccount,
+                expenseAccount, overpaymentAccount);
+    }
+
+    private Integer fromStartToDisburseLoan(String submitApproveDisburseDate, 
String principal, final boolean penalty,
+            final String accountingRule, final Account... accounts) {
+
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, 
this.responseSpec);
+        ClientHelper.verifyClientCreatedOnServer(this.requestSpec, 
this.responseSpec, clientID);
+
+        boolean allowMultipleDisbursals = false;
+        final Integer loanProductID = createLoanProduct(principal, 
allowMultipleDisbursals, accountingRule, accounts);
+        Assertions.assertNotNull(loanProductID);
+        // if (penalty) {
+        LOG.info("-----------------------------------Setting Specific Penalty 
Income Account-----------------------------------------");
+        final String accountId = 
penaltyIncomeAccount.getAccountID().toString();
+        final String putURL = "/fineract-provider/api/v1/loanproducts/" + 
loanProductID + "?" + Utils.TENANT_IDENTIFIER;
+
+        final HashMap<String, Object> map = new HashMap<>();
+        map.put("incomeFromPenaltyAccountId", accountId);
+        final String jsonBodyToSend = new Gson().toJson(map);
+        Utils.performServerPut(requestSpec, responseSpec, putURL, 
jsonBodyToSend);
+        // }
+
+        final List<HashMap> charges = null;
+        final Integer loanID = applyForLoanApplication(clientID, 
loanProductID, charges, principal, submitApproveDisburseDate);
+        Assertions.assertNotNull(loanID);
+        HashMap loanStatusHashMap = 
LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
+        LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+        LOG.info("-----------------------------------APPROVE 
LOAN-----------------------------------------");
+        loanStatusHashMap = 
this.loanTransactionHelper.approveLoan(submitApproveDisburseDate, loanID);
+        LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+        LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
+
+        LOG.info("-------------------------------DISBURSE LOAN 
-------------------------------------------"); //
+        // String loanDetails = 
this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, 
loanID);
+        loanStatusHashMap = 
this.loanTransactionHelper.disburseLoan(submitApproveDisburseDate, loanID, 
principal);
+        LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+        return loanID;
+    }
+
+    private HashMap makeRepaymentType(final String repaymentTypeCommand, final 
String repaymentDate, final Float repayment) {
+        LOG.info("-------------Make repayment Type -----------");
+        createdRepaymentTypeResourceId = (Integer) 
this.loanTransactionHelper.makeRepaymentTypePayment(repaymentTypeCommand, 
repaymentDate,
+                repayment, this.disbursedLoanID, "resourceId");
+        HashMap loanStatusHashMap = (HashMap) 
this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, 
disbursedLoanID,
+                "status");
+        return loanStatusHashMap;
+    }
+
+    private static String getLoanChargeAsJSON(final String chargeId, final 
String dueDate, final String amount, final String externalId) {
+        final HashMap<String, String> map = new HashMap<>();
+        map.put("locale", "en_GB");
+        map.put("dateFormat", "dd MMMM yyyy");
+        map.put("amount", amount);
+        map.put("dueDate", dueDate);
+        map.put("chargeId", chargeId);
+        map.put("externalId", externalId);
+        String json = new Gson().toJson(map);
+        LOG.info("{}", json);
+        return json;
+    }
+
+    private Integer createLoanProduct(final String principal, final boolean 
multiDisburseLoan, final String accountingRule,
+            final Account... accounts) {
+        LOG.info("------------------------------CREATING NEW LOAN PRODUCT 
---------------------------------------");
+        LoanProductTestBuilder builder = new LoanProductTestBuilder() //
+                .withPrincipal(principal) //
+                .withNumberOfRepayments("4") //
+                .withRepaymentAfterEvery("1") //
+                .withRepaymentTypeAsMonth() //
+                .withinterestRatePerPeriod("0") //
+                .withInterestRateFrequencyTypeAsMonths() //
+                .withAmortizationTypeAsEqualInstallments() //
+                .withInterestTypeAsDecliningBalance() //
+                .withTranches(multiDisburseLoan) //
+                .withAccounting(accountingRule, accounts);
+        if (multiDisburseLoan) {
+            builder = 
builder.withInterestCalculationPeriodTypeAsRepaymentPeriod(true);
+        }
+        final String loanProductJSON = builder.build(null);
+        return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
+    }
+
+    private Integer applyForLoanApplication(final Integer clientID, final 
Integer loanProductID, List<HashMap> charges, String principal,
+            String loanDate) {
+        LOG.info("--------------------------------APPLYING FOR LOAN 
APPLICATION--------------------------------");
+        final String savingsId = null;
+        final String loanApplicationJSON = new LoanApplicationTestBuilder() //
+                .withPrincipal(principal) //
+                .withLoanTermFrequency("4") //
+                .withLoanTermFrequencyAsMonths() //
+                .withNumberOfRepayments(FOUR_INSTALLMENTS) //
+                .withRepaymentEveryAfter("1") //
+                .withRepaymentFrequencyTypeAsMonths() //
+                .withInterestRatePerPeriod(ZERO_INTEREST_RATE) //
+                .withAmortizationTypeAsEqualInstallments() //
+                .withInterestTypeAsDecliningBalance() //
+                .withInterestCalculationPeriodTypeSameAsRepaymentPeriod() //
+                .withExpectedDisbursementDate(loanDate) //
+                .withSubmittedOnDate(loanDate) //
+                .withCharges(charges).build(clientID.toString(), 
loanProductID.toString(), savingsId);
+        return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+    }
+
+    private Integer disburseAddChargeAndRepay(final Float repaymentAmount, 
final String expectedPostRepaymentStatus,
+            final String accountingType, final boolean penalty) {
+        final String loanDate = "01 January 2022";
+        final String loanAmount = "12,000.00";
+        disburseLoanOfAccountingRule(accountingType, loanAmount, loanDate, 
penalty);
+
+        final Integer charge = ChargesHelper.createCharges(requestSpec, 
responseSpec, ChargesHelper
+                
.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_PERCENTAGE_AMOUNT_AND_INTEREST,
 "1", penalty));
+
+        final String externalId = null;
+        final float amount = 1.0f;
+        final String chargeDueDate = "15 February 2022"; // will be added to 
the 2nd installment (March)
+        final Integer loanChargeId = 
this.loanTransactionHelper.addChargesForLoan(this.disbursedLoanID,
+                getLoanChargeAsJSON(String.valueOf(charge), chargeDueDate, 
String.valueOf(amount), externalId));
+        Assertions.assertNotNull(loanChargeId);
+
+        final String repaymentDate = "10 January 2022";
+        HashMap loanStatusHashMap = makeRepaymentType(MAKE_REPAYMENT_COMMAND, 
repaymentDate, repaymentAmount);
+        assertTrue((Boolean) 
loanStatusHashMap.get(expectedPostRepaymentStatus));
+        return loanChargeId;
+    }
+
+    private Float getLoanDetailsSummaryTotalOutstanding(final Integer loanId) {
+
+        HashMap loanDetailsHashMap = (HashMap) 
this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, 
loanId,
+                "summary");
+        Float amount = (Float) loanDetailsHashMap.get("totalOutstanding");
+        if (amount == null) {
+            amount = 0.0f;
+        }
+        return amount;
+    }
+
+    private Float getLoanDetailsSummaryfeeChargesPaid(final Integer loanId) {
+
+        HashMap loanDetailsHashMap = (HashMap) 
this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, 
loanId,
+                "summary");
+        Float amount = (Float) loanDetailsHashMap.get("feeChargesPaid");
+        if (amount == null) {
+            amount = 0.0f;
+        }
+        return amount;
+    }
+
+    private Float getLoanDetailsTotalOverpaidAmount(final Integer loanId) {
+
+        Float amount = (Float) 
this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, 
loanId, "totalOverpaid");
+        if (amount == null) {
+            amount = 0.0f;
+        }
+        return amount;
+    }
+
+    private void verifyPaidByEntry(final Integer loanId, final Integer 
chargeRefundTxnId, final Float refundAmount) {
+        ArrayList<HashMap> loanChargePaidByList = (ArrayList<HashMap>) 
this.loanTransactionHelper.getLoanTransactionDetails(loanId,
+                chargeRefundTxnId, "loanChargePaidByList");
+        Assertions.assertNotNull(loanChargePaidByList);
+        Assertions.assertEquals(loanChargePaidByList.size(), 1);
+
+        Float paidByAmount = (Float) loanChargePaidByList.get(0).get("amount") 
* -1;
+        Assertions.assertEquals(refundAmount, paidByAmount, "Incorrect Paid By 
Amount");
+    }
+
+    private String getTodaysDate() {
+        DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy", 
Locale.US);
+        dateFormat.setTimeZone(Utils.getTimeZoneOfTenant());
+        Calendar todaysDate = 
Calendar.getInstance(Utils.getTimeZoneOfTenant());
+        return dateFormat.format(todaysDate.getTime());
+    }
+
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java
index 7e5a9748e..a31a568ec 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java
@@ -263,7 +263,7 @@ public class 
ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest {
                 disbursedLoanID, "resourceId");
         Assertions.assertNotNull(resourceId);
 
-        HashMap creditBalanceRefundMap = 
this.loanTransactionHelper.getLoanTransactionDetails(disbursedLoanID, 
resourceId);
+        HashMap creditBalanceRefundMap = (HashMap) 
this.loanTransactionHelper.getLoanTransactionDetails(disbursedLoanID, 
resourceId, "");
         Assertions.assertNotNull(creditBalanceRefundMap.get("externalId"));
         Assertions.assertEquals(creditBalanceRefundMap.get("externalId"), 
externalId, "Incorrect External Id Saved");
 
@@ -347,7 +347,7 @@ public class 
ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest {
         HashMap loanStatusHashMap = (HashMap) 
this.loanTransactionHelper.makeRepaymentTypePayment(repaymentTransactionType,
                 "06 January 2022", 200.00f, this.disbursedLoanID, "");
         Integer newTransactionId = (Integer) 
loanStatusHashMap.get("resourceId");
-        loanStatusHashMap = 
this.loanTransactionHelper.getLoanTransactionDetails(this.disbursedLoanID, 
newTransactionId);
+        loanStatusHashMap = (HashMap) 
this.loanTransactionHelper.getLoanTransactionDetails(this.disbursedLoanID, 
newTransactionId, "");
 
         HashMap typeMap = (HashMap) loanStatusHashMap.get("type");
         Boolean isTypeCorrect = (Boolean) 
typeMap.get(repaymentTransactionType);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index 27692b027..f568cba5a 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -63,6 +63,7 @@ public class LoanTransactionHelper {
     private static final String WAIVE_INTEREST_COMMAND = "waiveinterest";
     private static final String MAKE_REPAYMENT_COMMAND = "repayment";
     private static final String UNDO = "undo";
+    private static final String LOANCHARGE_REFUND_REPAYMENT_COMMAND = 
"chargeRefund";
     private static final String CREDIT_BALANCE_REFUND_COMMAND = 
"creditBalanceRefund";
     private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = 
"withdrawnByApplicant";
     private static final String RECOVER_FROM_GUARANTORS_COMMAND = 
"recoverGuarantees";
@@ -349,6 +350,12 @@ public class LoanTransactionHelper {
                 getCreditBalanceRefundBodyAsJSON(date, amountToBePaid, 
externalId), jsonAttributeToGetback);
     }
 
+    public Object loanChargeRefund(final Integer loanChargeId, final Integer 
installmentNumber, final Float amountToBePaid,
+            final String externalId, final Integer loanID, String 
jsonAttributeToGetback) {
+        return 
performLoanTransaction(createLoanTransactionURL(LOANCHARGE_REFUND_REPAYMENT_COMMAND,
 loanID),
+                getLoanChargeRefundBodyAsJSON(loanChargeId, installmentNumber, 
amountToBePaid, externalId), jsonAttributeToGetback);
+    }
+
     public Object makeRepaymentTypePayment(final String repaymentTypeCommand, 
final String date, final Float amountToBePaid,
             final Integer loanID, String jsonAttributeToGetback) {
         return 
performLoanTransaction(createLoanTransactionURL(repaymentTypeCommand, loanID), 
getRepaymentBodyAsJSON(date, amountToBePaid),
@@ -452,10 +459,10 @@ public class LoanTransactionHelper {
         return Utils.performServerGet(requestSpec, responseSpec, 
GET_LOAN_CHARGES_URL, "");
     }
 
-    public HashMap getLoanTransactionDetails(final Integer loanId, final 
Integer txnId) {
+    public Object getLoanTransactionDetails(final Integer loanId, final 
Integer txnId, final String param) {
         final String GET_LOAN_CHARGES_URL = "/fineract-provider/api/v1/loans/" 
+ loanId + "/transactions/" + txnId + "?"
                 + Utils.TENANT_IDENTIFIER;
-        return Utils.performServerGet(requestSpec, responseSpec, 
GET_LOAN_CHARGES_URL, "");
+        return Utils.performServerGet(requestSpec, responseSpec, 
GET_LOAN_CHARGES_URL, param);
     }
 
     public HashMap getPostDatedCheck(final Integer loanId, final Integer 
installmentId) {
@@ -535,6 +542,23 @@ public class LoanTransactionHelper {
         return new Gson().toJson(map);
     }
 
+    private String getLoanChargeRefundBodyAsJSON(final Integer loanChargeId, 
final Integer installmentNumber, final Float transactionAmount,
+            final String externalId) {
+        final HashMap<String, String> map = new HashMap<>();
+        map.put("locale", "en");
+        map.put("dateFormat", "dd MMMM yyyy");
+        map.put("loanChargeId", loanChargeId.toString());
+        map.put("transactionAmount", transactionAmount.toString());
+        map.put("note", "Loancharge Refund Made!!!");
+        if (externalId != null) {
+            map.put("externalId", externalId);
+        }
+        if (installmentNumber != null) {
+            map.put("installmentNumber", installmentNumber.toString());
+        }
+        return new Gson().toJson(map);
+    }
+
     private String getCreditBalanceRefundBodyAsJSON(final String 
transactionDate, final Float transactionAmount, final String externalId) {
         final HashMap<String, String> map = new HashMap<>();
         map.put("locale", "en");

Reply via email to