FINERACT-1675 New Paid loanCharge Refund Transaction
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
6f39c8ffed
commit
4034f4ae19
+2
@@ -56,4 +56,6 @@ public class LoanTransactionDTO {
|
|||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
private boolean isLoanToLoanTransfer;
|
private boolean isLoanToLoanTransfer;
|
||||||
|
|
||||||
|
private final String chargeRefundChargeType;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-1
@@ -124,6 +124,7 @@ public class AccountingProcessorHelper {
|
|||||||
final BigDecimal overPayments = (BigDecimal) map.get("overPaymentPortion");
|
final BigDecimal overPayments = (BigDecimal) map.get("overPaymentPortion");
|
||||||
final boolean reversed = (Boolean) map.get("reversed");
|
final boolean reversed = (Boolean) map.get("reversed");
|
||||||
final Long paymentTypeId = (Long) map.get("paymentTypeId");
|
final Long paymentTypeId = (Long) map.get("paymentTypeId");
|
||||||
|
final String chargeRefundChargeType = (String) map.get("chargeRefundChargeType");
|
||||||
|
|
||||||
final List<ChargePaymentDTO> feePaymentDetails = new ArrayList<>();
|
final List<ChargePaymentDTO> feePaymentDetails = new ArrayList<>();
|
||||||
final List<ChargePaymentDTO> penaltyPaymentDetails = new ArrayList<>();
|
final List<ChargePaymentDTO> penaltyPaymentDetails = new ArrayList<>();
|
||||||
@@ -151,7 +152,7 @@ public class AccountingProcessorHelper {
|
|||||||
}
|
}
|
||||||
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId,
|
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId,
|
||||||
transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed,
|
transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed,
|
||||||
penaltyPaymentDetails, feePaymentDetails, isAccountTransfer);
|
penaltyPaymentDetails, feePaymentDetails, isAccountTransfer, chargeRefundChargeType);
|
||||||
Boolean isLoanToLoanTransfer = (Boolean) accountingBridgeData.get("isLoanToLoanTransfer");
|
Boolean isLoanToLoanTransfer = (Boolean) accountingBridgeData.get("isLoanToLoanTransfer");
|
||||||
if (isLoanToLoanTransfer != null && isLoanToLoanTransfer) {
|
if (isLoanToLoanTransfer != null && isLoanToLoanTransfer) {
|
||||||
transaction.setLoanToLoanTransfer(true);
|
transaction.setLoanToLoanTransfer(true);
|
||||||
@@ -1301,4 +1302,27 @@ public class AccountingProcessorHelper {
|
|||||||
private GLAccount getGLAccountById(final Long accountId) {
|
private GLAccount getGLAccountById(final Long accountId) {
|
||||||
return this.accountRepositoryWrapper.findOneWithNotFoundDetection(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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+14
@@ -24,6 +24,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
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.CashAccountsForLoan;
|
||||||
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
|
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
|
||||||
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
|
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
|
||||||
@@ -256,6 +257,19 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
|
|||||||
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+9
@@ -824,6 +824,15 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
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) {
|
public CommandWrapperBuilder loanRecoveryPaymentTransaction(final Long loanId) {
|
||||||
this.actionName = "RECOVERYPAYMENT";
|
this.actionName = "RECOVERYPAYMENT";
|
||||||
this.entityName = "LOAN";
|
this.entityName = "LOAN";
|
||||||
|
|||||||
+6
-4
@@ -177,9 +177,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
final Boolean isHolidayValidationDone = false;
|
final Boolean isHolidayValidationDone = false;
|
||||||
final HolidayDetailDTO holidayDetailDto = null;
|
final HolidayDetailDTO holidayDetailDto = null;
|
||||||
final boolean isRecoveryRepayment = false;
|
final boolean isRecoveryRepayment = false;
|
||||||
|
final String chargeRefundChargeType = null;
|
||||||
final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT,
|
final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT,
|
||||||
toLoanAccount, new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null,
|
toLoanAccount, new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null,
|
||||||
isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||||
|
|
||||||
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(command,
|
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(command,
|
||||||
fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction);
|
fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction);
|
||||||
@@ -340,10 +341,11 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
final boolean isRecoveryRepayment = false;
|
final boolean isRecoveryRepayment = false;
|
||||||
final Boolean isHolidayValidationDone = false;
|
final Boolean isHolidayValidationDone = false;
|
||||||
final HolidayDetailDTO holidayDetailDto = null;
|
final HolidayDetailDTO holidayDetailDto = null;
|
||||||
|
final String chargeRefundChargeType = null;
|
||||||
loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount,
|
loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount,
|
||||||
new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(),
|
new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(),
|
||||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), null, null, isRecoveryRepayment,
|
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), null, null, isRecoveryRepayment,
|
||||||
isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||||
}
|
}
|
||||||
|
|
||||||
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount,
|
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount,
|
||||||
@@ -475,10 +477,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
LoanTransaction disburseTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(),
|
LoanTransaction disburseTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(),
|
||||||
accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
||||||
accountTransferDTO.getNoteText(), accountTransferDTO.getTxnExternalId(), true);
|
accountTransferDTO.getNoteText(), accountTransferDTO.getTxnExternalId(), true);
|
||||||
|
final String chargeRefundChargeType = null;
|
||||||
LoanTransaction repayTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount,
|
LoanTransaction repayTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount,
|
||||||
new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
|
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,
|
AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToLoanTransfer(accountTransferDTO,
|
||||||
fromLoanAccount, toLoanAccount, disburseTransaction, repayTransaction);
|
fromLoanAccount, toLoanAccount, disburseTransaction, repayTransaction);
|
||||||
|
|||||||
+28
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-9
@@ -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=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=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
|
||||||
+ "loans/1/transactions/template?command=foreclosure" + "\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({
|
@ApiResponses({
|
||||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
|
||||||
public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId,
|
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"
|
+ "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=merchantIssuedRefund" + " | Merchant Issued Refund | \n"
|
||||||
+ "loans/1/transactions?command=payoutRefund" + " | Payout Refund | \n" + "loans/1/transactions?command=goodwillCredit"
|
+ "loans/1/transactions?command=payoutRefund" + " | Payout Refund | \n" + "loans/1/transactions?command=goodwillCredit"
|
||||||
+ " | Goodwil Credit | \n" + "loans/1/transactions?command=waiveinterest" + " | Waive Interest | \n"
|
+ " | Goodwil Credit | \n" + "loans/1/transactions?command=chargeRefund" + " | Charge Refund | \n"
|
||||||
+ "loans/1/transactions?command=writeoff" + " | Write-off Loan | \n" + "loans/1/transactions?command=close-rescheduled"
|
+ "loans/1/transactions?command=waiveinterest" + " | Waive Interest | \n" + "loans/1/transactions?command=writeoff"
|
||||||
+ " | Close Rescheduled Loan | \n" + "loans/1/transactions?command=close" + " | Close Loan | \n"
|
+ " | Write-off Loan | \n" + "loans/1/transactions?command=close-rescheduled" + " | Close Rescheduled Loan | \n"
|
||||||
+ "loans/1/transactions?command=undowriteoff" + " | Undo Loan Write-off | \n" + "loans/1/transactions?command=recoverypayment"
|
+ "loans/1/transactions?command=close" + " | Close Loan | \n" + "loans/1/transactions?command=undowriteoff"
|
||||||
+ " | Make Recovery Payment | \n" + "loans/1/transactions?command=refundByCash"
|
+ " | Undo Loan Write-off | \n" + "loans/1/transactions?command=recoverypayment" + " | Make Recovery Payment | \n"
|
||||||
+ " | Make a Refund of an Active Loan by Cash | \n" + "loans/1/transactions?command=foreclosure"
|
+ "loans/1/transactions?command=refundByCash" + " | Make a Refund of an Active Loan by Cash | \n"
|
||||||
+ " | Foreclosure of an Active Loan | \n" + "loans/1/transactions?command=creditBalanceRefund" + " | Credit Balance Refund"
|
+ "loans/1/transactions?command=foreclosure" + " | Foreclosure of an Active Loan | \n"
|
||||||
+ " | \n")
|
+ "loans/1/transactions?command=creditBalanceRefund" + " | Credit Balance Refund" + " | \n")
|
||||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsRequest.class)))
|
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsRequest.class)))
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) })
|
@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")) {
|
} else if (is(commandParam, "goodwillCredit")) {
|
||||||
final CommandWrapper commandRequest = builder.loanGoodwillCreditTransaction(loanId).build();
|
final CommandWrapper commandRequest = builder.loanGoodwillCreditTransaction(loanId).build();
|
||||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
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")) {
|
} else if (is(commandParam, "waiveinterest")) {
|
||||||
final CommandWrapper commandRequest = builder.waiveInterestPortionTransaction(loanId).build();
|
final CommandWrapper commandRequest = builder.waiveInterestPortionTransaction(loanId).build();
|
||||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
|
|||||||
+7
-1
@@ -34,6 +34,7 @@ public class LoanTransactionEnumData {
|
|||||||
private final boolean merchantIssuedRefund;
|
private final boolean merchantIssuedRefund;
|
||||||
private final boolean payoutRefund;
|
private final boolean payoutRefund;
|
||||||
private final boolean goodwillCredit;
|
private final boolean goodwillCredit;
|
||||||
|
private final boolean chargeRefund;
|
||||||
private final boolean contra;
|
private final boolean contra;
|
||||||
private final boolean waiveInterest;
|
private final boolean waiveInterest;
|
||||||
private final boolean waiveCharges;
|
private final boolean waiveCharges;
|
||||||
@@ -59,6 +60,7 @@ public class LoanTransactionEnumData {
|
|||||||
this.merchantIssuedRefund = Long.valueOf(21).equals(this.id);
|
this.merchantIssuedRefund = Long.valueOf(21).equals(this.id);
|
||||||
this.payoutRefund = Long.valueOf(22).equals(this.id);
|
this.payoutRefund = Long.valueOf(22).equals(this.id);
|
||||||
this.goodwillCredit = Long.valueOf(23).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.contra = Long.valueOf(3).equals(this.id);
|
||||||
this.waiveInterest = Long.valueOf(4).equals(this.id);
|
this.waiveInterest = Long.valueOf(4).equals(this.id);
|
||||||
this.waiveCharges = Long.valueOf(9).equals(this.id);
|
this.waiveCharges = Long.valueOf(9).equals(this.id);
|
||||||
@@ -101,7 +103,7 @@ public class LoanTransactionEnumData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRepaymentType() {
|
public boolean isRepaymentType() {
|
||||||
if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit()) {
|
if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -131,6 +133,10 @@ public class LoanTransactionEnumData {
|
|||||||
return this.goodwillCredit;
|
return this.goodwillCredit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isChargeRefund() {
|
||||||
|
return this.chargeRefund;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isWaiveInterest() {
|
public boolean isWaiveInterest() {
|
||||||
return this.waiveInterest;
|
return this.waiveInterest;
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -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.InvalidLoanTransactionTypeException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException;
|
import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
|
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.LoanDisbursalException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
|
||||||
@@ -3055,6 +3056,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
validateRepaymentTypeAccountStatus(repaymentTransaction, event);
|
validateRepaymentTypeAccountStatus(repaymentTransaction, event);
|
||||||
validateActivityNotBeforeClientOrGroupTransferDate(event, repaymentTransaction.getTransactionDate());
|
validateActivityNotBeforeClientOrGroupTransferDate(event, repaymentTransaction.getTransactionDate());
|
||||||
|
validateRepaymentTypeTransactionNotBeforeAChargeRefund(repaymentTransaction, "created");
|
||||||
validateActivityNotBeforeLastTransactionDate(event, repaymentTransaction.getTransactionDate());
|
validateActivityNotBeforeLastTransactionDate(event, repaymentTransaction.getTransactionDate());
|
||||||
if (!isHolidayValidationDone) {
|
if (!isHolidayValidationDone) {
|
||||||
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(),
|
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(),
|
||||||
@@ -3073,7 +3075,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
|
|
||||||
private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) {
|
private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) {
|
||||||
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund()
|
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund()
|
||||||
|| repaymentTransaction.isPayoutRefund()) {
|
|| repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund()) {
|
||||||
|
|
||||||
if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) {
|
if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) {
|
||||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
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() {
|
public LocalDate getLastUserTransactionDate() {
|
||||||
LocalDate currentTransactionDate = getDisbursementDate();
|
LocalDate currentTransactionDate = getDisbursementDate();
|
||||||
for (final LoanTransaction previousTransaction : this.loanTransactions) {
|
for (final LoanTransaction previousTransaction : this.loanTransactions) {
|
||||||
|
|||||||
+4
-3
@@ -30,7 +30,8 @@ public interface LoanAccountDomainService {
|
|||||||
|
|
||||||
LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
|
LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
|
||||||
LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
|
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,
|
LoanTransaction makeRefund(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
|
||||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId);
|
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId);
|
||||||
@@ -63,8 +64,8 @@ public interface LoanAccountDomainService {
|
|||||||
|
|
||||||
LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
|
LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
|
||||||
LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
|
LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
|
||||||
boolean isRecoveryRepayment, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone,
|
boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
|
||||||
boolean isLoanToLoanTransfer);
|
Boolean isHolidayValidationDone, boolean isLoanToLoanTransfer);
|
||||||
|
|
||||||
void saveLoanWithDataIntegrityViolationChecks(Loan loan);
|
void saveLoanWithDataIntegrityViolationChecks(Loan loan);
|
||||||
|
|
||||||
|
|||||||
+16
-8
@@ -129,9 +129,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
|||||||
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan,
|
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan,
|
||||||
final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount,
|
final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount,
|
||||||
final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment,
|
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,
|
return makeRepayment(repaymentTransactionType, loan, builderResult, transactionDate, transactionAmount, paymentDetail, noteText,
|
||||||
txnExternalId, isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone, false);
|
txnExternalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone,
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -154,8 +156,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
|||||||
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan,
|
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan,
|
||||||
final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount,
|
final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount,
|
||||||
final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment,
|
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,
|
||||||
final boolean isLoanToLoanTransfer) {
|
Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) {
|
||||||
AppUser currentUser = getAppUserIfPresent();
|
AppUser currentUser = getAppUserIfPresent();
|
||||||
checkClientOrGroupActive(loan);
|
checkClientOrGroupActive(loan);
|
||||||
|
|
||||||
@@ -181,7 +183,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
|||||||
txnExternalId);
|
txnExternalId);
|
||||||
} else {
|
} else {
|
||||||
newRepaymentTransaction = LoanTransaction.repaymentType(repaymentTransactionType, loan.getOffice(), repaymentAmount,
|
newRepaymentTransaction = LoanTransaction.repaymentType(repaymentTransactionType, loan.getOffice(), repaymentAmount,
|
||||||
paymentDetail, transactionDate, txnExternalId);
|
paymentDetail, transactionDate, txnExternalId, chargeRefundChargeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDate recalculateFrom = null;
|
LocalDate recalculateFrom = null;
|
||||||
@@ -223,9 +225,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
|||||||
|
|
||||||
recalculateAccruals(loan);
|
recalculateAccruals(loan);
|
||||||
|
|
||||||
LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType,
|
if (!repaymentTransactionType.isChargeRefund()) {
|
||||||
isRecoveryRepayment, newRepaymentTransaction);
|
LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType,
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
isRecoveryRepayment, newRepaymentTransaction);
|
||||||
|
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||||
|
}
|
||||||
|
|
||||||
// disable all active standing orders linked to this loan if status
|
// disable all active standing orders linked to this loan if status
|
||||||
// changes to closed
|
// changes to closed
|
||||||
@@ -277,6 +281,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
|||||||
repaymentEvent = new LoanTransactionPayoutRefundPreBusinessEvent(loan);
|
repaymentEvent = new LoanTransactionPayoutRefundPreBusinessEvent(loan);
|
||||||
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
||||||
repaymentEvent = new LoanTransactionGoodwillCreditPreBusinessEvent(loan);
|
repaymentEvent = new LoanTransactionGoodwillCreditPreBusinessEvent(loan);
|
||||||
|
} else if (repaymentTransactionType.isChargeRefund()) {
|
||||||
|
repaymentEvent = new LoanChargePaymentPreBusinessEvent(loan);
|
||||||
} else if (isRecoveryRepayment) {
|
} else if (isRecoveryRepayment) {
|
||||||
repaymentEvent = new LoanTransactionRecoveryPaymentPreBusinessEvent(loan);
|
repaymentEvent = new LoanTransactionRecoveryPaymentPreBusinessEvent(loan);
|
||||||
}
|
}
|
||||||
@@ -294,6 +300,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
|||||||
repaymentEvent = new LoanTransactionPayoutRefundPostBusinessEvent(transaction);
|
repaymentEvent = new LoanTransactionPayoutRefundPostBusinessEvent(transaction);
|
||||||
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
||||||
repaymentEvent = new LoanTransactionGoodwillCreditPostBusinessEvent(transaction);
|
repaymentEvent = new LoanTransactionGoodwillCreditPostBusinessEvent(transaction);
|
||||||
|
} else if (repaymentTransactionType.isChargeRefund()) {
|
||||||
|
repaymentEvent = new LoanChargePaymentPostBusinessEvent(transaction);
|
||||||
} else if (isRecoveryRepayment) {
|
} else if (isRecoveryRepayment) {
|
||||||
repaymentEvent = new LoanTransactionRecoveryPaymentPostBusinessEvent(transaction);
|
repaymentEvent = new LoanTransactionRecoveryPaymentPostBusinessEvent(transaction);
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-3
@@ -115,6 +115,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
@Column(name = "manually_adjusted_or_reversed", nullable = false)
|
@Column(name = "manually_adjusted_or_reversed", nullable = false)
|
||||||
private boolean manuallyAdjustedOrReversed;
|
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")
|
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction")
|
||||||
private Set<LoanCollateralManagement> loanCollateralManagementSet = new HashSet<>();
|
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,
|
public static LoanTransaction repaymentType(final LoanTransactionType repaymentType, final Office office, final Money amount,
|
||||||
final PaymentDetail paymentDetail, final LocalDate paymentDate, final String externalId) {
|
final PaymentDetail paymentDetail, final LocalDate paymentDate, final String externalId, final String chargeRefundChargeType) {
|
||||||
return new LoanTransaction(null, office, repaymentType, paymentDetail, amount.getAmount(), paymentDate, externalId);
|
return new LoanTransaction(null, office, repaymentType, paymentDetail, amount.getAmount(), paymentDate, externalId,
|
||||||
|
chargeRefundChargeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) {
|
public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) {
|
||||||
@@ -344,7 +348,21 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
this.submittedOnDate = DateUtils.getBusinessLocalDate();
|
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() {
|
public void reverse() {
|
||||||
|
this.loan.validateRepaymentTypeTransactionNotBeforeAChargeRefund(this, "reversed");
|
||||||
this.reversed = true;
|
this.reversed = true;
|
||||||
this.loanTransactionToRepaymentScheduleMappings.clear();
|
this.loanTransactionToRepaymentScheduleMappings.clear();
|
||||||
}
|
}
|
||||||
@@ -480,7 +498,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRepaymentType() {
|
public boolean isRepaymentType() {
|
||||||
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit();
|
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRepayment() {
|
public boolean isRepayment() {
|
||||||
@@ -499,6 +517,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
return LoanTransactionType.GOODWILL_CREDIT.equals(getTypeOf()) && isNotReversed();
|
return LoanTransactionType.GOODWILL_CREDIT.equals(getTypeOf()) && isNotReversed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isChargeRefund() {
|
||||||
|
return LoanTransactionType.CHARGE_REFUND.equals(getTypeOf()) && isNotReversed();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isNotRepaymentType() {
|
public boolean isNotRepaymentType() {
|
||||||
return !isRepaymentType();
|
return !isRepaymentType();
|
||||||
}
|
}
|
||||||
@@ -640,6 +662,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
thisTransactionData.put("feeChargesPortion", this.feeChargesPortion);
|
thisTransactionData.put("feeChargesPortion", this.feeChargesPortion);
|
||||||
thisTransactionData.put("penaltyChargesPortion", this.penaltyChargesPortion);
|
thisTransactionData.put("penaltyChargesPortion", this.penaltyChargesPortion);
|
||||||
thisTransactionData.put("overPaymentPortion", this.overPaymentPortion);
|
thisTransactionData.put("overPaymentPortion", this.overPaymentPortion);
|
||||||
|
if (transactionType.isChargeRefund()) {
|
||||||
|
thisTransactionData.put("chargeRefundChargeType", this.chargeRefundChargeType);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.paymentDetail != null) {
|
if (this.paymentDetail != null) {
|
||||||
thisTransactionData.put("paymentTypeId", this.paymentDetail.getPaymentType().getId());
|
thisTransactionData.put("paymentTypeId", this.paymentDetail.getPaymentType().getId());
|
||||||
|
|||||||
+10
-2
@@ -55,7 +55,8 @@ public enum LoanTransactionType {
|
|||||||
CREDIT_BALANCE_REFUND(20, "loanTransactionType.creditBalanceRefund"), //
|
CREDIT_BALANCE_REFUND(20, "loanTransactionType.creditBalanceRefund"), //
|
||||||
MERCHANT_ISSUED_REFUND(21, "loanTransactionType.merchantIssuedRefund"), //
|
MERCHANT_ISSUED_REFUND(21, "loanTransactionType.merchantIssuedRefund"), //
|
||||||
PAYOUT_REFUND(22, "loanTransactionType.payoutRefund"), //
|
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 Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
@@ -147,6 +148,9 @@ public enum LoanTransactionType {
|
|||||||
case 23:
|
case 23:
|
||||||
loanTransactionType = LoanTransactionType.GOODWILL_CREDIT;
|
loanTransactionType = LoanTransactionType.GOODWILL_CREDIT;
|
||||||
break;
|
break;
|
||||||
|
case 24:
|
||||||
|
loanTransactionType = LoanTransactionType.CHARGE_REFUND;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
loanTransactionType = LoanTransactionType.INVALID;
|
loanTransactionType = LoanTransactionType.INVALID;
|
||||||
break;
|
break;
|
||||||
@@ -178,8 +182,12 @@ public enum LoanTransactionType {
|
|||||||
return this.value.equals(LoanTransactionType.GOODWILL_CREDIT.getValue());
|
return this.value.equals(LoanTransactionType.GOODWILL_CREDIT.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isChargeRefund() {
|
||||||
|
return this.value.equals(LoanTransactionType.CHARGE_REFUND.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isRepaymentType() {
|
public boolean isRepaymentType() {
|
||||||
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit());
|
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRecoveryRepayment() {
|
public boolean isRecoveryRepayment() {
|
||||||
|
|||||||
+29
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+53
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -444,6 +444,41 @@ public final class LoanEventApiJsonValidator {
|
|||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
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) {
|
public void validateInstallmentChargeTransaction(final String json) {
|
||||||
|
|
||||||
if (StringUtils.isBlank(json)) {
|
if (StringUtils.isBlank(json)) {
|
||||||
|
|||||||
+1
-1
@@ -1669,7 +1669,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
|||||||
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
|
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
|
||||||
final StringBuilder sqlBuilder = new StringBuilder(400);
|
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||||
sqlBuilder.append("select ").append(mapper.schema()).append(
|
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(" 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.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
|
||||||
.append(" or (ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
|
.append(" or (ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
|
||||||
|
|||||||
+2
@@ -68,6 +68,8 @@ public interface LoanWritePlatformService {
|
|||||||
|
|
||||||
CommandProcessingResult undoWaiveLoanCharge(JsonCommand command);
|
CommandProcessingResult undoWaiveLoanCharge(JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult loanChargeRefund(Long loanId, JsonCommand command);
|
||||||
|
|
||||||
CommandProcessingResult loanReassignment(Long loanId, JsonCommand command);
|
CommandProcessingResult loanReassignment(Long loanId, JsonCommand command);
|
||||||
|
|
||||||
CommandProcessingResult bulkLoanReassignment(JsonCommand command);
|
CommandProcessingResult bulkLoanReassignment(JsonCommand command);
|
||||||
|
|||||||
+195
-2
@@ -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.LoanUpdateChargeBusinessEvent;
|
||||||
import org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanWaiveChargeBusinessEvent;
|
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.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.LoanUndoWrittenOffBusinessEvent;
|
||||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
|
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
|
||||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanWrittenOffPostBusinessEvent;
|
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.InstallmentNotFoundException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
|
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.LoanForeclosureException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
|
||||||
@@ -888,6 +890,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult makeLoanRepayment(final LoanTransactionType repaymentTransactionType, final Long loanId,
|
public CommandProcessingResult makeLoanRepayment(final LoanTransactionType repaymentTransactionType, final Long loanId,
|
||||||
final JsonCommand command, final boolean isRecoveryRepayment) {
|
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.loanUtilService.validateRepaymentTransactionType(repaymentTransactionType);
|
||||||
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
|
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
|
||||||
@@ -915,7 +924,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
||||||
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan,
|
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan,
|
||||||
commandProcessingResultBuilder, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId,
|
commandProcessingResultBuilder, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId,
|
||||||
isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||||
|
|
||||||
// Update loan transaction on repayment.
|
// Update loan transaction on repayment.
|
||||||
if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) {
|
if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) {
|
||||||
@@ -984,10 +993,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
|
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
|
||||||
}
|
}
|
||||||
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
||||||
|
final String chargeRefundChargeType = null;
|
||||||
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, loan,
|
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, loan,
|
||||||
commandProcessingResultBuilder, bulkRepaymentCommand.getTransactionDate(),
|
commandProcessingResultBuilder, bulkRepaymentCommand.getTransactionDate(),
|
||||||
singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail, bulkRepaymentCommand.getNote(), null,
|
singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail, bulkRepaymentCommand.getNote(), null,
|
||||||
isRecoveryRepayment, isAccountTransfer, holidayDetailDTO, isHolidayValidationDone);
|
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDTO, isHolidayValidationDone);
|
||||||
transactionIds.add(loanTransaction.getId());
|
transactionIds.add(loanTransaction.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1646,6 +1656,189 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
.build();
|
.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
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) {
|
public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) {
|
||||||
|
|||||||
+4
@@ -427,6 +427,10 @@ public final class LoanEnumerations {
|
|||||||
optionData = new LoanTransactionEnumData(LoanTransactionType.GOODWILL_CREDIT.getValue().longValue(),
|
optionData = new LoanTransactionEnumData(LoanTransactionType.GOODWILL_CREDIT.getValue().longValue(),
|
||||||
LoanTransactionType.GOODWILL_CREDIT.getCode(), "Goodwill Credit");
|
LoanTransactionType.GOODWILL_CREDIT.getCode(), "Goodwill Credit");
|
||||||
break;
|
break;
|
||||||
|
case CHARGE_REFUND:
|
||||||
|
optionData = new LoanTransactionEnumData(LoanTransactionType.CHARGE_REFUND.getValue().longValue(),
|
||||||
|
LoanTransactionType.CHARGE_REFUND.getCode(), "Charge Refund");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return optionData;
|
return optionData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,4 +46,6 @@
|
|||||||
<include file="parts/0024_add_audit_entries.xml" relativeToChangelogFile="true"/>
|
<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/0025_add_audit_entries_to_journal_entry.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0026_reversals_for_reversed_transactions.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>
|
</databaseChangeLog>
|
||||||
|
|||||||
+32
@@ -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>
|
||||||
+28
@@ -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>
|
||||||
+602
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+2
-2
@@ -263,7 +263,7 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest {
|
|||||||
disbursedLoanID, "resourceId");
|
disbursedLoanID, "resourceId");
|
||||||
Assertions.assertNotNull(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.assertNotNull(creditBalanceRefundMap.get("externalId"));
|
||||||
Assertions.assertEquals(creditBalanceRefundMap.get("externalId"), externalId, "Incorrect External Id Saved");
|
Assertions.assertEquals(creditBalanceRefundMap.get("externalId"), externalId, "Incorrect External Id Saved");
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest {
|
|||||||
HashMap loanStatusHashMap = (HashMap) this.loanTransactionHelper.makeRepaymentTypePayment(repaymentTransactionType,
|
HashMap loanStatusHashMap = (HashMap) this.loanTransactionHelper.makeRepaymentTypePayment(repaymentTransactionType,
|
||||||
"06 January 2022", 200.00f, this.disbursedLoanID, "");
|
"06 January 2022", 200.00f, this.disbursedLoanID, "");
|
||||||
Integer newTransactionId = (Integer) loanStatusHashMap.get("resourceId");
|
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");
|
HashMap typeMap = (HashMap) loanStatusHashMap.get("type");
|
||||||
Boolean isTypeCorrect = (Boolean) typeMap.get(repaymentTransactionType);
|
Boolean isTypeCorrect = (Boolean) typeMap.get(repaymentTransactionType);
|
||||||
|
|||||||
+26
-2
@@ -63,6 +63,7 @@ public class LoanTransactionHelper {
|
|||||||
private static final String WAIVE_INTEREST_COMMAND = "waiveinterest";
|
private static final String WAIVE_INTEREST_COMMAND = "waiveinterest";
|
||||||
private static final String MAKE_REPAYMENT_COMMAND = "repayment";
|
private static final String MAKE_REPAYMENT_COMMAND = "repayment";
|
||||||
private static final String UNDO = "undo";
|
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 CREDIT_BALANCE_REFUND_COMMAND = "creditBalanceRefund";
|
||||||
private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = "withdrawnByApplicant";
|
private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = "withdrawnByApplicant";
|
||||||
private static final String RECOVER_FROM_GUARANTORS_COMMAND = "recoverGuarantees";
|
private static final String RECOVER_FROM_GUARANTORS_COMMAND = "recoverGuarantees";
|
||||||
@@ -349,6 +350,12 @@ public class LoanTransactionHelper {
|
|||||||
getCreditBalanceRefundBodyAsJSON(date, amountToBePaid, externalId), jsonAttributeToGetback);
|
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,
|
public Object makeRepaymentTypePayment(final String repaymentTypeCommand, final String date, final Float amountToBePaid,
|
||||||
final Integer loanID, String jsonAttributeToGetback) {
|
final Integer loanID, String jsonAttributeToGetback) {
|
||||||
return performLoanTransaction(createLoanTransactionURL(repaymentTypeCommand, loanID), getRepaymentBodyAsJSON(date, amountToBePaid),
|
return performLoanTransaction(createLoanTransactionURL(repaymentTypeCommand, loanID), getRepaymentBodyAsJSON(date, amountToBePaid),
|
||||||
@@ -452,10 +459,10 @@ public class LoanTransactionHelper {
|
|||||||
return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL, "");
|
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 + "?"
|
final String GET_LOAN_CHARGES_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + txnId + "?"
|
||||||
+ Utils.TENANT_IDENTIFIER;
|
+ 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) {
|
public HashMap getPostDatedCheck(final Integer loanId, final Integer installmentId) {
|
||||||
@@ -535,6 +542,23 @@ public class LoanTransactionHelper {
|
|||||||
return new Gson().toJson(map);
|
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) {
|
private String getCreditBalanceRefundBodyAsJSON(final String transactionDate, final Float transactionAmount, final String externalId) {
|
||||||
final HashMap<String, String> map = new HashMap<>();
|
final HashMap<String, String> map = new HashMap<>();
|
||||||
map.put("locale", "en");
|
map.put("locale", "en");
|
||||||
|
|||||||
Reference in New Issue
Block a user