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 <[email protected]>
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");

Reply via email to