You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2022/04/19 08:04:24 UTC

[fineract] branch develop updated: Credit Balance Refund loan transaction

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

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new d1168833c Credit Balance Refund loan transaction
d1168833c is described below

commit d1168833ccd1be1a5f5e4e75e2d9064300cfd17e
Author: John Woodlock <jo...@gmail.com>
AuthorDate: Thu Apr 14 14:29:36 2022 +0100

    Credit Balance Refund loan transaction
---
 .../AccrualBasedAccountingProcessorForLoan.java    |  24 ++
 .../commands/service/CommandWrapperBuilder.java    |   9 +
 .../common/BusinessEventNotificationConstants.java |   1 +
 .../api/LoanTransactionsApiResource.java           |   5 +
 .../loanaccount/data/LoanTransactionEnumData.java  |   6 +
 .../domain/DefaultLoanLifecycleStateMachine.java   |   2 +
 .../portfolio/loanaccount/domain/Loan.java         |  58 +++-
 .../domain/LoanAccountDomainService.java           |   3 +
 .../domain/LoanAccountDomainServiceJpa.java        |  36 +++
 .../portfolio/loanaccount/domain/LoanEvent.java    |   3 +-
 .../loanaccount/domain/LoanTransaction.java        |  19 ++
 .../loanaccount/domain/LoanTransactionType.java    |   6 +-
 .../handler/CreditBalanceRefundCommandHandler.java |  72 +++++
 .../serialization/LoanEventApiJsonValidator.java   |   3 +
 .../service/LoanReadPlatformService.java           |   2 +
 .../service/LoanReadPlatformServiceImpl.java       |  34 ++-
 .../service/LoanWritePlatformService.java          |   2 +
 .../LoanWritePlatformServiceJpaRepositoryImpl.java |  33 ++-
 .../loanproduct/service/LoanEnumerations.java      |   4 +
 .../db/changelog/tenant/changelog-tenant.xml       |   1 +
 .../0011_add_credit_balance_refund_permission.xml  |  32 +++
 ...ientLoanCreditBalanceRefundIntegrationTest.java | 307 +++++++++++++++++++++
 ...tLoanMultipleDisbursementsIntegrationTest.java} |   4 +-
 .../common/loans/LoanTransactionHelper.java        |  26 ++
 24 files changed, 671 insertions(+), 21 deletions(-)

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 b49baec14..330651ac8 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
@@ -80,6 +80,11 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
                 createJournalEntriesForRefund(loanDTO, loanTransactionDTO, office);
             }
 
+            /** Logic for Credit Balance Refunds **/
+            else if (loanTransactionDTO.getTransactionType().isCreditBalanceRefund()) {
+                createJournalEntriesForCreditBalanceRefund(loanDTO, loanTransactionDTO, office);
+            }
+
             /** Handle Write Offs, waivers and their reversals **/
             else if ((loanTransactionDTO.getTransactionType().isWriteOff() || loanTransactionDTO.getTransactionType().isWaiveInterest()
                     || loanTransactionDTO.getTransactionType().isWaiveCharges())) {
@@ -397,6 +402,25 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
         }
     }
 
+    private void createJournalEntriesForCreditBalanceRefund(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
+            final Office office) {
+        // loan properties
+        final Long loanProductId = loanDTO.getLoanProductId();
+        final Long loanId = loanDTO.getLoanId();
+        final String currencyCode = loanDTO.getCurrencyCode();
+
+        // transaction properties
+        final String transactionId = loanTransactionDTO.getTransactionId();
+        final Date transactionDate = loanTransactionDTO.getTransactionDate();
+        final BigDecimal refundAmount = loanTransactionDTO.getAmount();
+        final boolean isReversal = loanTransactionDTO.isReversed();
+        final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
+
+        this.helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, currencyCode,
+                AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), AccrualAccountsForLoan.OVERPAYMENT.getValue(), loanProductId,
+                paymentTypeId, loanId, transactionId, transactionDate, refundAmount, isReversal);
+    }
+
     private void createJournalEntriesForRefundForActiveLoan(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) {
         // TODO Auto-generated method stub
         // loan properties
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 b21dfa86b..775cd84e1 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
@@ -878,6 +878,15 @@ public class CommandWrapperBuilder {
         return this;
     }
 
+    public CommandWrapperBuilder creditBalanceRefund(final Long loanId) {
+        this.actionName = "CREDITBALANCEREFUND";
+        this.entityName = "LOAN";
+        this.entityId = null;
+        this.loanId = loanId;
+        this.href = "/loans/" + loanId + "/transactions?command=creditBalanceRefund";
+        return this;
+    }
+
     public CommandWrapperBuilder undoWaiveChargeTransaction(final Long loanId, final Long transactionId) {
         this.actionName = "UNDO";
         this.entityName = "WAIVECHARGE";
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/common/BusinessEventNotificationConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/common/BusinessEventNotificationConstants.java
index f101a56a4..4c4af1cf1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/common/BusinessEventNotificationConstants.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/common/BusinessEventNotificationConstants.java
@@ -55,6 +55,7 @@ public class BusinessEventNotificationConstants {
         LOAN_INTEREST_RECALCULATION("loan_interest_recalculation"), //
         LOAN_REFUND("loan_refund"), //
         LOAN_FORECLOSURE("loan_foreclosure"), //
+        LOAN_CREDIT_BALANCE_REFUND("loan_credit_balance_refund"), //
         LOAN_CREATE("loan_create"), //
         LOAN_PRODUCT_CREATE("loan_product_create"), //
         SAVINGS_ACTIVATE("savings_activated"), //
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 2e536e209..7430fe1a9 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
@@ -164,6 +164,8 @@ public class LoanTransactionsApiResource {
                         DateUtils.getDateTimeZoneOfTenant());
             }
             transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(loanId, transactionDate);
+        } else if (is(commandParam, "creditBalanceRefund")) {
+            transactionData = this.loanReadPlatformService.retrieveCreditBalanceRefundTemplate(loanId);
         } else {
             throw new UnrecognizedQueryParamException("command", commandParam);
         }
@@ -239,6 +241,9 @@ public class LoanTransactionsApiResource {
         } else if (is(commandParam, "foreclosure")) {
             final CommandWrapper commandRequest = builder.loanForeclosure(loanId).build();
             result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
+        } else if (is(commandParam, "creditBalanceRefund")) {
+            final CommandWrapper commandRequest = builder.creditBalanceRefund(loanId).build();
+            result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
         }
 
         if (result == null) {
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 f970eb518..1cf00e9e3 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
@@ -44,6 +44,7 @@ public class LoanTransactionEnumData {
     private final boolean chargePayment;
     private final boolean refund;
     private final boolean refundForActiveLoans;
+    private final boolean creditBalanceRefund;
 
     public LoanTransactionEnumData(final Long id, final String code, final String value) {
         this.id = id;
@@ -65,6 +66,7 @@ public class LoanTransactionEnumData {
         this.refund = Long.valueOf(16).equals(this.id);
         this.chargePayment = Long.valueOf(17).equals(this.id);
         this.refundForActiveLoans = Long.valueOf(18).equals(this.id);
+        this.creditBalanceRefund = Long.valueOf(20).equals(this.id);
     }
 
     public Long id() {
@@ -152,4 +154,8 @@ public class LoanTransactionEnumData {
         return this.refundForActiveLoans;
     }
 
+    public boolean isCreditBalanceRefund() {
+        return this.creditBalanceRefund;
+    }
+
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
index 0189dd4fb..1da59cc4c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
@@ -106,6 +106,8 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
             break;
             case WRITE_OFF_OUTSTANDING_UNDO:
             break;
+            case LOAN_CREDIT_BALANCE_REFUND:
+            break;
             default:
             break;
         }
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 4ac8c2528..dbf7680f2 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
@@ -3615,10 +3615,11 @@ public class Loan extends AbstractPersistableCustom {
         validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
                 transactionForAdjustment.getTransactionDate());
 
-        if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotWaiver()) {
-            final String errorMessage = "Only transactions of type repayment or waiver can be adjusted.";
-            throw new InvalidLoanTransactionTypeException("transaction", "adjustment.is.only.allowed.to.repayment.or.waiver.transaction",
-                    errorMessage);
+        if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotWaiver()
+                && transactionForAdjustment.isNotCreditBalanceRefund()) {
+            final String errorMessage = "Only transactions of type repayment, waiver or credit balance refund can be adjusted.";
+            throw new InvalidLoanTransactionTypeException("transaction",
+                    "adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage);
         }
 
         transactionForAdjustment.reverse();
@@ -3698,7 +3699,8 @@ public class Loan extends AbstractPersistableCustom {
         }
 
         for (final LoanTransaction loanTransaction : this.loanTransactions) {
-            if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) && !loanTransaction.isReversed()) {
+            if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan() || loanTransaction.isCreditBalanceRefund())
+                    && !loanTransaction.isReversed()) {
                 totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
             }
         }
@@ -5157,6 +5159,14 @@ public class Loan extends AbstractPersistableCustom {
                     dataValidationErrors.add(error);
                 }
             break;
+            case LOAN_CREDIT_BALANCE_REFUND:
+                if (!status().isOverpaid()) {
+                    final String defaultUserMessage = "Loan Credit Balance Refund is not allowed. Loan Account is not Overpaid.";
+                    final ApiParameterError error = ApiParameterError
+                            .generalError("error.msg.loan.credit.balance.refund.account.is.not.overpaid", defaultUserMessage);
+                    dataValidationErrors.add(error);
+                }
+            break;
             default:
             break;
         }
@@ -6062,6 +6072,40 @@ public class Loan extends AbstractPersistableCustom {
         return this.guaranteeAmountDerived == null ? BigDecimal.ZERO : this.guaranteeAmountDerived;
     }
 
+    public void creditBalanceRefund(LoanTransaction newCreditBalanceRefundTransaction,
+            LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, List<Long> existingTransactionIds,
+            List<Long> existingReversedTransactionIds) {
+        validateAccountStatus(LoanEvent.LOAN_CREDIT_BALANCE_REFUND);
+
+        validateRefundDateIsAfterLastRepayment(newCreditBalanceRefundTransaction.getTransactionDate());
+
+        if (!newCreditBalanceRefundTransaction.isGreaterThanZeroAndLessThanOrEqualTo(this.totalOverpaid)) {
+            final String errorMessage = "Transaction Amount ("
+                    + newCreditBalanceRefundTransaction.getAmount(getCurrency()).getAmount().toString()
+                    + ") must be > zero and <= Overpaid amount (" + this.totalOverpaid.toString() + ").";
+            final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+            final ApiParameterError error = ApiParameterError.parameterError(
+                    "error.msg.transactionAmount.invalid.must.be.>zero.and<=overpaidamount", errorMessage, "transactionAmount",
+                    newCreditBalanceRefundTransaction.getAmount(getCurrency()));
+            dataValidationErrors.add(error);
+
+            throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
+                    dataValidationErrors);
+        }
+
+        existingTransactionIds.addAll(findExistingTransactionIds());
+        existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
+
+        this.loanTransactions.add(newCreditBalanceRefundTransaction);
+
+        updateLoanSummaryDerivedFields();
+
+        if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
+            this.loanStatus = LoanStatus.CLOSED_OBLIGATIONS_MET.getValue();
+        }
+
+    }
+
     public ChangedTransactionDetail makeRefundForActiveLoan(final LoanTransaction loanTransaction,
             final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
             final List<Long> existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List<Holiday> holidays,
@@ -6179,7 +6223,8 @@ public class Loan extends AbstractPersistableCustom {
 
         LocalDate lastTransactionDate = null;
         for (final LoanTransaction transaction : this.loanTransactions) {
-            if ((transaction.isRepayment() || transaction.isRefundForActiveLoan()) && transaction.isNonZero()) {
+            if ((transaction.isRepayment() || transaction.isRefundForActiveLoan() || transaction.isCreditBalanceRefund())
+                    && transaction.isNonZero() && transaction.isNotReversed()) {
                 lastTransactionDate = transaction.getTransactionDate();
             }
         }
@@ -6841,4 +6886,5 @@ public class Loan extends AbstractPersistableCustom {
     public void adjustNetDisbursalAmount(BigDecimal adjustedAmount) {
         this.netDisbursalAmount = adjustedAmount.subtract(this.deriveSumTotalOfChargesDueAtDisbursement());
     }
+
 }
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 71db423b9..29819a3e2 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
@@ -78,4 +78,7 @@ public interface LoanAccountDomainService {
     void disableStandingInstructionsLinkedToClosedLoan(Loan loan);
 
     void recalculateAccruals(Loan loan, boolean isInterestCalcualtionHappened);
+
+    CommandProcessingResultBuilder creditBalanceRefund(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount,
+            String noteText, String externalId);
 }
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 2a2aa6f68..cf6c62775 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
@@ -623,6 +623,41 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
         return user;
     }
 
+    @Override
+    public CommandProcessingResultBuilder creditBalanceRefund(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount,
+            String noteText, String externalId) {
+        final Loan loan = this.loanAccountAssembler.assembleFrom(loanId);
+        this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BusinessEvents.LOAN_CREDIT_BALANCE_REFUND,
+                constructEntityMap(BusinessEntity.LOAN, loan));
+        final List<Long> existingTransactionIds = new ArrayList<>();
+        final List<Long> existingReversedTransactionIds = new ArrayList<>();
+        AppUser currentUser = getAppUserIfPresent();
+
+        final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount);
+        final LoanTransaction newCreditBalanceRefundTransaction = LoanTransaction.creditBalanceRefund(loan, loan.getOffice(), refundAmount,
+                transactionDate, externalId, DateUtils.getLocalDateTimeOfTenant(), currentUser);
+
+        loan.creditBalanceRefund(newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds,
+                existingReversedTransactionIds);
+
+        this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction);
+
+        if (StringUtils.isNotBlank(noteText)) {
+            final Note note = Note.loanTransactionNote(loan, newCreditBalanceRefundTransaction, noteText);
+            this.noteRepository.save(note);
+        }
+
+        postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
+        recalculateAccruals(loan);
+        this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_CREDIT_BALANCE_REFUND,
+                constructEntityMap(BusinessEntity.LOAN_TRANSACTION, newCreditBalanceRefundTransaction));
+
+        return new CommandProcessingResultBuilder().withEntityId(newCreditBalanceRefundTransaction.getId()) //
+                .withOfficeId(loan.getOfficeId()) //
+                .withClientId(loan.getClientId()) //
+                .withGroupId(loan.getGroupId());
+    }
+
     @Override
     public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
             BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId) {
@@ -794,4 +829,5 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
             }
         }
     }
+
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java
index fe4601759..7db08e3b1 100755
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java
@@ -43,5 +43,6 @@ public enum LoanEvent {
     LOAN_CLOSED, //
     LOAN_EDIT_MULTI_DISBURSE_DATE, //
     LOAN_REFUND, //
-    LOAN_FORECLOSURE;
+    LOAN_FORECLOSURE, //
+    LOAN_CREDIT_BALANCE_REFUND;
 }
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 bc1970fac..351566a2f 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
@@ -297,6 +297,13 @@ public class LoanTransaction extends AbstractPersistableCustom {
         return applyCharge;
     }
 
+    public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate,
+            final String externalId, final LocalDateTime createdDate, final AppUser appUser) {
+        final PaymentDetail paymentDetail = null;
+        return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND, paymentDetail, amount.getAmount(), paymentDate,
+                externalId, createdDate, appUser);
+    }
+
     public static LoanTransaction refundForActiveLoan(final Office office, final Money amount, final PaymentDetail paymentDetail,
             final LocalDate paymentDate, final String externalId, final LocalDateTime createdDate, final AppUser appUser) {
         return new LoanTransaction(null, office, LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, paymentDetail, amount.getAmount(), paymentDate,
@@ -573,6 +580,10 @@ public class LoanTransaction extends AbstractPersistableCustom {
         return !isInterestWaiver() && !isChargesWaiver();
     }
 
+    public boolean isNotCreditBalanceRefund() {
+        return !isCreditBalanceRefund();
+    }
+
     public boolean isChargePayment() {
         return getTypeOf().isChargePayment() && isNotReversed();
     }
@@ -616,6 +627,10 @@ public class LoanTransaction extends AbstractPersistableCustom {
         return getAmount(currency).isGreaterThanZero();
     }
 
+    public boolean isGreaterThanZeroAndLessThanOrEqualTo(BigDecimal totalOverpaid) {
+        return isNonZero() && this.amount.compareTo(totalOverpaid) <= 0;
+    }
+
     public boolean isNotZero(final MonetaryCurrency currency) {
         return !getAmount(currency).isZero();
     }
@@ -700,6 +715,10 @@ public class LoanTransaction extends AbstractPersistableCustom {
         return LoanTransactionType.REFUND.equals(getTypeOf()) && isNotReversed();
     }
 
+    public boolean isCreditBalanceRefund() {
+        return LoanTransactionType.CREDIT_BALANCE_REFUND.equals(getTypeOf()) && isNotReversed();
+    }
+
     public void updateExternalId(final String externalId) {
         this.externalId = externalId;
     }
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 b053327ab..c38bda6be 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
@@ -51,7 +51,8 @@ public enum LoanTransactionType {
     REFUND(16, "loanTransactionType.refund"), //
     CHARGE_PAYMENT(17, "loanTransactionType.chargePayment"), //
     REFUND_FOR_ACTIVE_LOAN(18, "loanTransactionType.refund"), //
-    INCOME_POSTING(19, "loanTransactionType.incomePosting");
+    INCOME_POSTING(19, "loanTransactionType.incomePosting"), //
+    CREDIT_BALANCE_REFUND(20, "loanTransactionType.creditBalanceRefund");
 
     private final Integer value;
     private final String code;
@@ -131,6 +132,9 @@ public enum LoanTransactionType {
             case 19:
                 loanTransactionType = LoanTransactionType.INCOME_POSTING;
             break;
+            case 20:
+                loanTransactionType = LoanTransactionType.CREDIT_BALANCE_REFUND;
+            break;
             default:
                 loanTransactionType = LoanTransactionType.INVALID;
             break;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/CreditBalanceRefundCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/CreditBalanceRefundCommandHandler.java
new file mode 100644
index 000000000..798290c51
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/CreditBalanceRefundCommandHandler.java
@@ -0,0 +1,72 @@
+/**
+ * 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 lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
+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;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+@CommandType(entity = "LOAN", action = "CREDITBALANCEREFUND")
+public class CreditBalanceRefundCommandHandler implements NewCommandSourceHandler {
+
+    private final LoanWritePlatformService writePlatformService;
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+
+        try {
+            return this.writePlatformService.creditBalanceRefund(command.getLoanId(), command);
+        } catch (final JpaSystemException | DataIntegrityViolationException dve) {
+            handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.creditBalanceRefund", "Credit Balance Refund");
+            return CommandProcessingResult.empty();
+        }
+    }
+
+    private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve, final String msgType,
+            final String msgDescription) {
+
+        if (realCause.getMessage().contains("external_id")) {
+
+            final String externalId = command.stringValueOfParameterNamed("externalId");
+            throw new PlatformDataIntegrityException("error.msg." + msgType + ".duplicate.externalId",
+                    msgDescription + " with externalId `" + externalId + "` already exists", "externalId", externalId);
+        }
+
+        logAsErrorUnexpectedDataIntegrityException(dve);
+        throw new PlatformDataIntegrityException("error.msg.loan.charge.unknown.data.integrity.issue",
+                "Unknown data integrity issue with resource.");
+    }
+
+    private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) {
+        log.error("Error occured.", dve);
+    }
+}
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 a7186060b..a1b09c55d 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
@@ -529,6 +529,9 @@ public final class LoanEventApiJsonValidator {
         final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
         baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
 
+        final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element);
+        baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100);
+
         validatePaymentDetails(baseDataValidator, element);
         throwExceptionIfValidationWarningsExist(dataValidationErrors);
     }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
index 151793467..8618c33f0 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
@@ -128,6 +128,8 @@ public interface LoanReadPlatformService {
 
     LoanTransactionData retrieveRefundByCashTemplate(Long loanId);
 
+    LoanTransactionData retrieveCreditBalanceRefundTemplate(Long loanId);
+
     Collection<InterestRatePeriodData> retrieveLoanInterestRatePeriodData(LoanAccountData loan);
 
     Collection<Long> retrieveLoanIdsWithPendingIncomePostingTransactions();
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 0d8e30566..eca9a5b81 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
@@ -2118,24 +2118,38 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
 
     @Override
     public LoanTransactionData retrieveRefundByCashTemplate(Long loanId) {
-        // TODO Auto-generated method stub
         this.context.authenticatedUser();
 
-        // TODO - KW - OPTIMIZE - write simple sql query to fetch back date of
-        // possible next transaction date.
+        final Collection<PaymentTypeData> paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
         final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
-        final MonetaryCurrency currency = loan.getCurrency();
+        return retrieveRefundTemplate(loanId, LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, paymentOptions, loan.getCurrency(),
+                retrieveTotalPaidInAdvance(loan.getId()).getPaidInAdvance(), loan.getNetDisbursalAmount());
+    }
+
+    @Override
+    public LoanTransactionData retrieveCreditBalanceRefundTemplate(Long loanId) {
+        this.context.authenticatedUser();
+
+        final Collection<PaymentTypeData> paymentOptions = null;
+        final BigDecimal netDisbursal = null;
+        final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
+        return retrieveRefundTemplate(loanId, LoanTransactionType.CREDIT_BALANCE_REFUND, paymentOptions, loan.getCurrency(),
+                loan.getTotalOverpaid(), netDisbursal);
+
+    }
+
+    private LoanTransactionData retrieveRefundTemplate(Long loanId, LoanTransactionType loanTransactionType,
+            Collection<PaymentTypeData> paymentOptions, MonetaryCurrency currency, BigDecimal transactionAmount, BigDecimal netDisbursal) {
+
         final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
 
         final CurrencyData currencyData = applicationCurrency.toData();
 
-        final LocalDate earliestUnpaidInstallmentDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
+        final LocalDate currentDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
 
-        final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN);
-        final Collection<PaymentTypeData> paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
-        return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate,
-                retrieveTotalPaidInAdvance(loan.getId()).getPaidInAdvance(), null, loan.getNetDisbursalAmount(), null, null, null, null,
-                null, paymentOptions, null, null, null, null, false);
+        final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(loanTransactionType);
+        return new LoanTransactionData(null, null, null, transactionType, null, currencyData, currentDate, transactionAmount, null,
+                netDisbursal, null, null, null, null, null, paymentOptions, null, null, null, null, false);
     }
 
     @Override
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 9be2fdbe8..9c49ade84 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
@@ -116,4 +116,6 @@ public interface LoanWritePlatformService {
 
     CommandProcessingResult makeGLIMLoanRepayment(Long loanId, JsonCommand command);
 
+    CommandProcessingResult creditBalanceRefund(Long loanId, 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 83ccfadfe..512242dee 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
@@ -3213,10 +3213,40 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
         return installments;
     }
 
+    @Override
+    public CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command) {
+        this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
+
+        final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
+        final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
+        final String noteText = command.stringValueOfParameterNamedAllowingNull("note");
+        final String externalId = command.stringValueOfParameterNamedAllowingNull("externalId");
+
+        final Map<String, Object> changes = new LinkedHashMap<>();
+        changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
+        changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
+        changes.put("locale", command.locale());
+        changes.put("dateFormat", command.dateFormat());
+
+        if (StringUtils.isNotBlank(noteText)) {
+            changes.put("note", noteText);
+        }
+        if (StringUtils.isNotBlank(externalId)) {
+            changes.put("externalId", externalId);
+        }
+
+        final CommandProcessingResultBuilder commandProcessingResultBuilder = this.loanAccountDomainService.creditBalanceRefund(loanId,
+                transactionDate, transactionAmount, noteText, externalId);
+
+        return commandProcessingResultBuilder //
+                .withCommandId(command.commandId()).with(changes) //
+                .build();
+
+    }
+
     @Override
     @Transactional
     public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) {
-        // TODO Auto-generated method stub
 
         this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
 
@@ -3400,4 +3430,5 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
         }
 
     }
+
 }
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 e2f8e525d..56faedcba 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
@@ -411,6 +411,10 @@ public final class LoanEnumerations {
                 optionData = new LoanTransactionEnumData(LoanTransactionType.INCOME_POSTING.getValue().longValue(),
                         LoanTransactionType.INCOME_POSTING.getCode(), "Income Posting");
             break;
+            case CREDIT_BALANCE_REFUND:
+                optionData = new LoanTransactionEnumData(LoanTransactionType.CREDIT_BALANCE_REFUND.getValue().longValue(),
+                        LoanTransactionType.CREDIT_BALANCE_REFUND.getCode(), "Credit Balance 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 7dc81891f..e871a7f94 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
@@ -30,4 +30,5 @@
     <include file="parts/0008_loan_charge_add_external_id.xml" relativeToChangelogFile="true"/>
     <include file="parts/0009_hold_reason_savings_account.xml" relativeToChangelogFile="true"/>
     <include file="parts/0010_lien_allowed_on_savings_account_products.xml" relativeToChangelogFile="true"/>
+    <include file="parts/0011_add_credit_balance_refund_permission.xml" relativeToChangelogFile="true"/>
 </databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0011_add_credit_balance_refund_permission.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0011_add_credit_balance_refund_permission.xml
new file mode 100644
index 000000000..dd626b8d4
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0011_add_credit_balance_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="CREDITBALANCEREFUND_LOAN" />
+            <column name="entity_name" value="LOAN" />
+            <column name="action_name" value="CREDITBALANCEREFUND" />
+            <column name="can_maker_checker" valueBoolean="false" />
+        </insert>
+    </changeSet>
+</databaseChangeLog>
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundIntegrationTest.java
new file mode 100644
index 000000000..91959773e
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundIntegrationTest.java
@@ -0,0 +1,307 @@
+/**
+ * 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 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.util.ArrayList;
+import java.util.HashMap;
+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.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 ClientLoanCreditBalanceRefundIntegrationTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientLoanCreditBalanceRefundIntegrationTest.class);
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private LoanTransactionHelper loanTransactionHelper;
+    private LoanTransactionHelper loanTransactionHelperValidationError;
+    private JournalEntryHelper journalEntryHelper;
+    private AccountHelper accountHelper;
+    private Integer disbursedLoanID;
+    private static final String ACCRUAL_PERIODIC = "3";
+    private Account assetAccount;
+    private Account incomeAccount;
+    private Account expenseAccount;
+    private Account overpaymentAccount;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
+        this.loanTransactionHelperValidationError = new LoanTransactionHelper(this.requestSpec, new ResponseSpecBuilder().build());
+        this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec);
+        this.assetAccount = this.accountHelper.createAssetAccount();
+        this.incomeAccount = this.accountHelper.createIncomeAccount();
+        this.expenseAccount = this.accountHelper.createExpenseAccount();
+        this.overpaymentAccount = this.accountHelper.createLiabilityAccount();
+        this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec);
+
+        final String principal = "12000.00";
+        final String submitApproveDisburseDate = "01 January 2022";
+        this.disbursedLoanID = fromStartToDisburseLoan(submitApproveDisburseDate, principal, ACCRUAL_PERIODIC, assetAccount, incomeAccount,
+                expenseAccount, overpaymentAccount);
+
+    }
+
+    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("1") //
+                .withInterestRateFrequencyTypeAsMonths() //
+                .withAmortizationTypeAsEqualInstallments() //
+                .withInterestTypeAsDecliningBalance() //
+                .withAccounting(accountingRule, accounts) //
+                .withTranches(multiDisburseLoan);
+        if (multiDisburseLoan) {
+            builder = builder.withInterestCalculationPeriodTypeAsRepaymentPeriod(true);
+            builder = builder.withMaxTrancheCount("30");
+        }
+        final String loanProductJSON = builder.build(null);
+        return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
+    }
+
+    private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, String principal, String submitDate) {
+        LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
+        final String loanApplicationJSON = new LoanApplicationTestBuilder() //
+                .withPrincipal(principal) //
+                .withLoanTermFrequency("4") //
+                .withLoanTermFrequencyAsMonths() //
+                .withNumberOfRepayments("4") //
+                .withRepaymentEveryAfter("1") //
+                .withRepaymentFrequencyTypeAsMonths() //
+                .withInterestRatePerPeriod("2") //
+                .withAmortizationTypeAsEqualInstallments() //
+                .withInterestTypeAsDecliningBalance() //
+                .withInterestCalculationPeriodTypeSameAsRepaymentPeriod() //
+                .withExpectedDisbursementDate(submitDate) //
+                .withSubmittedOnDate(submitDate) //
+                .build(clientID.toString(), loanProductID.toString(), null);
+        return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+    }
+
+    private Integer fromStartToDisburseLoan(String submitApproveDisburseDate, String principal, 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);
+
+        final Integer loanID = applyForLoanApplication(clientID, loanProductID, 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 makeRepayment(final String repaymentDate, final Float repayment) {
+        LOG.info("-------------Make repayment -----------");
+        this.loanTransactionHelper.makeRepayment(repaymentDate, repayment, disbursedLoanID);
+        HashMap loanStatusHashMap = (HashMap) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
+                "status");
+        return loanStatusHashMap;
+    }
+
+    @Test
+    public void creditBalanceRefundCanOnlyBeAppliedWhereLoanStatusIsOverpaidTest() {
+        HashMap loanStatusHashMap = makeRepayment("06 January 2022", 2000.00f); // not full payment
+        LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
+        final String creditBalanceRefundDate = "09 January 2022";
+        final Float refund = 1000.00f;
+        final String externalId = null;
+        ArrayList<HashMap> cbrErrors = (ArrayList<HashMap>) loanTransactionHelperValidationError
+                .creditBalanceRefund(creditBalanceRefundDate, refund, externalId, disbursedLoanID, CommonConstants.RESPONSE_ERROR);
+
+        assertEquals("error.msg.loan.credit.balance.refund.account.is.not.overpaid",
+                cbrErrors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
+
+        // ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec,
+        // this.responseSpec, loanID);
+        // final int loanScheduleLineCount = loanSchedule.size();
+
+    }
+
+    @Test
+    public void cantRefundMoreThanOverpaidTest() {
+        HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
+        LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
+
+        final String creditBalanceRefundDate = "09 January 2022";
+        Float refund = 10000.00f;
+        final String externalId = null;
+        ArrayList<HashMap> cbrErrors = (ArrayList<HashMap>) loanTransactionHelperValidationError
+                .creditBalanceRefund(creditBalanceRefundDate, refund, externalId, disbursedLoanID, CommonConstants.RESPONSE_ERROR);
+
+        assertEquals("error.msg.transactionAmount.invalid.must.be.>zero.and<=overpaidamount",
+                cbrErrors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
+
+        refund = (float) -1.00;
+        cbrErrors = (ArrayList<HashMap>) loanTransactionHelperValidationError.creditBalanceRefund(creditBalanceRefundDate, refund,
+                externalId, disbursedLoanID, CommonConstants.RESPONSE_ERROR);
+        assertEquals("validation.msg.loan.transaction.transactionAmount.not.greater.than.zero",
+                cbrErrors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
+
+    }
+
+    @Test
+    public void fullRefundChangesStatusToClosedObligationMetTest() {
+        HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
+        LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
+
+        final Float totalOverpaid = (Float) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
+                "totalOverpaid");
+
+        final String creditBalanceRefundDate = "09 January 2022";
+        final String externalId = null;
+        loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, totalOverpaid, externalId, disbursedLoanID, null);
+        loanStatusHashMap = (HashMap) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
+                "status");
+        LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap);
+
+        final Float floatZero = 0.0f;
+        Float totalOverpaidAtEnd = (Float) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
+                "totalOverpaid");
+        if (totalOverpaidAtEnd == null) {
+            totalOverpaidAtEnd = floatZero;
+        }
+        assertEquals(totalOverpaidAtEnd, floatZero);
+
+    }
+
+    @Test
+    public void partialRefundKeepsOverpaidStatusTest() {
+        HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
+        LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
+
+        final Float refund = 5000.00f; // partial refund
+
+        final String creditBalanceRefundDate = "09 January 2022";
+        final String externalId = null;
+        loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, refund, externalId, disbursedLoanID, null);
+        loanStatusHashMap = (HashMap) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
+                "status");
+        LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
+
+    }
+
+    @Test
+    public void newCreditBalanceRefundSavesExternalIdTest() {
+
+        HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
+        LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
+
+        final Float refund = 1000.00f; // partial refund
+        final String creditBalanceRefundDate = "09 January 2022";
+        final String externalId = "cbrextID" + disbursedLoanID.toString();
+        Integer resourceId = (Integer) loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, refund, externalId,
+                disbursedLoanID, "resourceId");
+        Assertions.assertNotNull(resourceId);
+
+        HashMap creditBalanceRefundMap = this.loanTransactionHelper.getLoanTransactionDetails(disbursedLoanID, resourceId);
+        Assertions.assertNotNull(creditBalanceRefundMap.get("externalId"));
+        Assertions.assertEquals(creditBalanceRefundMap.get("externalId"), externalId, "Incorrect External Id Saved");
+
+    }
+
+    @Test
+    public void newCreditBalanceRefundFindsDuplicateExternalIdTest() {
+
+        HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
+        LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
+
+        final Float refund = 1000.00f; // partial refund
+        final String creditBalanceRefundDate = "09 January 2022";
+        final String externalId = "cbrextID" + disbursedLoanID.toString();
+        final Integer resourceId = (Integer) loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, refund, externalId,
+                disbursedLoanID, "resourceId");
+        Assertions.assertNotNull(resourceId);
+
+        final Float refund2 = 10.00f; // partial refund
+        final String creditBalanceRefundDate2 = "10 January 2022";
+        ArrayList<HashMap> cbrErrors = (ArrayList<HashMap>) loanTransactionHelperValidationError
+                .creditBalanceRefund(creditBalanceRefundDate2, refund2, externalId, disbursedLoanID, CommonConstants.RESPONSE_ERROR);
+        assertEquals("error.msg.loan.creditBalanceRefund.duplicate.externalId",
+                cbrErrors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
+
+    }
+
+    @Test
+    public void newCreditBalanceRefundCreatesCorrectJournalEntriesForPeriodicAccrualsTest() {
+
+        HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
+        LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
+
+        final Float refund = 1000.00f; // partial refund
+        final String creditBalanceRefundDate = "09 January 2022";
+        final String externalId = null;
+        final Integer resourceId = (Integer) loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, refund, externalId,
+                disbursedLoanID, "resourceId");
+        Assertions.assertNotNull(resourceId);
+
+        this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, creditBalanceRefundDate,
+                new JournalEntry(refund, JournalEntry.TransactionType.DEBIT));
+        this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount, creditBalanceRefundDate,
+                new JournalEntry(refund, JournalEntry.TransactionType.CREDIT));
+
+    }
+
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationMultipleDisbursementsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanMultipleDisbursementsIntegrationTest.java
similarity index 99%
rename from integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationMultipleDisbursementsTest.java
rename to integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanMultipleDisbursementsIntegrationTest.java
index 90283f695..aa40781a4 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationMultipleDisbursementsTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanMultipleDisbursementsIntegrationTest.java
@@ -46,9 +46,9 @@ import org.slf4j.LoggerFactory;
  * repayments and verifying accounting transactions
  */
 @SuppressWarnings({ "rawtypes", "unchecked" })
-public class ClientLoanIntegrationMultipleDisbursementsTest {
+public class ClientLoanMultipleDisbursementsIntegrationTest {
 
-    private static final Logger LOG = LoggerFactory.getLogger(ClientLoanIntegrationMultipleDisbursementsTest.class);
+    private static final Logger LOG = LoggerFactory.getLogger(ClientLoanMultipleDisbursementsIntegrationTest.class);
 
     public static final String MINIMUM_OPENING_BALANCE = "1000.0";
     public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
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 f45ddc6d7..50942ac27 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 WRITE_OFF_LOAN_COMMAND = "writeoff";
     private static final String WAIVE_INTEREST_COMMAND = "waiveinterest";
     private static final String MAKE_REPAYMENT_COMMAND = "repayment";
+    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";
     private static final String MAKE_REFUND_BY_CASH_COMMAND = "refundByCash";
@@ -332,6 +333,12 @@ public class LoanTransactionHelper {
         return resourceId;
     }
 
+    public Object creditBalanceRefund(final String date, final Float amountToBePaid, final String externalId, final Integer loanID,
+            String jsonAttributeToGetback) {
+        return performLoanTransaction(createLoanTransactionURL(CREDIT_BALANCE_REFUND_COMMAND, loanID),
+                getCreditBalanceRefundBodyAsJSON(date, amountToBePaid, externalId), jsonAttributeToGetback);
+    }
+
     public HashMap makeRepayment(final String date, final Float amountToBePaid, final Integer loanID) {
         return (HashMap) performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID),
                 getRepaymentBodyAsJSON(date, amountToBePaid), "");
@@ -425,6 +432,12 @@ public class LoanTransactionHelper {
         return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL, "");
     }
 
+    public HashMap getLoanTransactionDetails(final Integer loanId, final Integer txnId) {
+        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, "");
+    }
+
     public HashMap getPostDatedCheck(final Integer loanId, final Integer installmentId) {
         final String GET_POST_DATED_TRANS_URL = "/fineract-provider/api/v1/loans/" + loanId + "/postdatedchecks/" + installmentId + "?"
                 + Utils.TENANT_IDENTIFIER;
@@ -500,6 +513,19 @@ public class LoanTransactionHelper {
         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");
+        map.put("dateFormat", "dd MMMM yyyy");
+        map.put("transactionDate", transactionDate);
+        map.put("transactionAmount", transactionAmount.toString());
+        map.put("note", "Credit Balance Refund Made!!!");
+        if (externalId != null) {
+            map.put("externalId", externalId);
+        }
+        return new Gson().toJson(map);
+    }
+
     private String getRepaymentBodyAsJSON(final String transactionDate, final Float transactionAmount) {
         final HashMap<String, String> map = new HashMap<>();
         map.put("locale", "en");