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