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
|
||||
private boolean isLoanToLoanTransfer;
|
||||
|
||||
private final String chargeRefundChargeType;
|
||||
}
|
||||
|
||||
+25
-1
@@ -124,6 +124,7 @@ public class AccountingProcessorHelper {
|
||||
final BigDecimal overPayments = (BigDecimal) map.get("overPaymentPortion");
|
||||
final boolean reversed = (Boolean) map.get("reversed");
|
||||
final Long paymentTypeId = (Long) map.get("paymentTypeId");
|
||||
final String chargeRefundChargeType = (String) map.get("chargeRefundChargeType");
|
||||
|
||||
final List<ChargePaymentDTO> feePaymentDetails = new ArrayList<>();
|
||||
final List<ChargePaymentDTO> penaltyPaymentDetails = new ArrayList<>();
|
||||
@@ -151,7 +152,7 @@ public class AccountingProcessorHelper {
|
||||
}
|
||||
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId,
|
||||
transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed,
|
||||
penaltyPaymentDetails, feePaymentDetails, isAccountTransfer);
|
||||
penaltyPaymentDetails, feePaymentDetails, isAccountTransfer, chargeRefundChargeType);
|
||||
Boolean isLoanToLoanTransfer = (Boolean) accountingBridgeData.get("isLoanToLoanTransfer");
|
||||
if (isLoanToLoanTransfer != null && isLoanToLoanTransfer) {
|
||||
transaction.setLoanToLoanTransfer(true);
|
||||
@@ -1301,4 +1302,27 @@ public class AccountingProcessorHelper {
|
||||
private GLAccount getGLAccountById(final Long accountId) {
|
||||
return this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
|
||||
}
|
||||
|
||||
public Integer getValueForFeeOrPenaltyIncomeAccount(final String chargeRefundChargeType) {
|
||||
if (chargeRefundChargeType == null
|
||||
|| !(chargeRefundChargeType.equalsIgnoreCase("P") || chargeRefundChargeType.equalsIgnoreCase("F"))) {
|
||||
String errorValue;
|
||||
if (chargeRefundChargeType == null) {
|
||||
errorValue = "Null";
|
||||
} else {
|
||||
errorValue = chargeRefundChargeType;
|
||||
}
|
||||
throw new PlatformDataIntegrityException("error.msg.chargeRefundChargeType.can.only.be.P.or.F",
|
||||
"chargeRefundChargeType can only be P (Penalty) or F(Fee) - Value is: " + errorValue);
|
||||
}
|
||||
Integer incomeAccount = null;
|
||||
if (chargeRefundChargeType.equalsIgnoreCase("P")) {
|
||||
incomeAccount = AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue();
|
||||
} else {
|
||||
incomeAccount = AccrualAccountsForLoan.INCOME_FROM_FEES.getValue();
|
||||
|
||||
}
|
||||
return incomeAccount;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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 lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
|
||||
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
|
||||
@@ -256,6 +257,19 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
|
||||
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge Refunds (and their reversals) have an extra refund related pair of journal entries in addition to
|
||||
* those related to the repayment above
|
||||
***/
|
||||
if (!(totalDebitAmount.compareTo(BigDecimal.ZERO) == 0)) {
|
||||
if (loanTransactionDTO.getTransactionType().isChargeRefund()) {
|
||||
Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType());
|
||||
this.helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, currencyCode, incomeAccount,
|
||||
AccrualAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
|
||||
totalDebitAmount, isReversal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
@@ -824,6 +824,15 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder refundLoanCharge(final Long loanId) {
|
||||
this.actionName = "CHARGEREFUND";
|
||||
this.entityName = "LOAN";
|
||||
this.entityId = null;
|
||||
this.loanId = loanId;
|
||||
this.href = "/loans/" + loanId + "/transactions/template?command=chargerefund";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder loanRecoveryPaymentTransaction(final Long loanId) {
|
||||
this.actionName = "RECOVERYPAYMENT";
|
||||
this.entityName = "LOAN";
|
||||
|
||||
+6
-4
@@ -177,9 +177,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
final Boolean isHolidayValidationDone = false;
|
||||
final HolidayDetailDTO holidayDetailDto = null;
|
||||
final boolean isRecoveryRepayment = false;
|
||||
final String chargeRefundChargeType = null;
|
||||
final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT,
|
||||
toLoanAccount, new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null,
|
||||
isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
|
||||
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(command,
|
||||
fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction);
|
||||
@@ -340,10 +341,11 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
final boolean isRecoveryRepayment = false;
|
||||
final Boolean isHolidayValidationDone = false;
|
||||
final HolidayDetailDTO holidayDetailDto = null;
|
||||
final String chargeRefundChargeType = null;
|
||||
loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount,
|
||||
new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(),
|
||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), null, null, isRecoveryRepayment,
|
||||
isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
}
|
||||
|
||||
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount,
|
||||
@@ -475,10 +477,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
LoanTransaction disburseTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(),
|
||||
accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
||||
accountTransferDTO.getNoteText(), accountTransferDTO.getTxnExternalId(), true);
|
||||
|
||||
final String chargeRefundChargeType = null;
|
||||
LoanTransaction repayTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount,
|
||||
new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
|
||||
accountTransferDTO.getPaymentDetail(), null, null, false, isAccountTransfer, null, false, true);
|
||||
accountTransferDTO.getPaymentDetail(), null, null, false, chargeRefundChargeType, isAccountTransfer, null, false, true);
|
||||
|
||||
AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToLoanTransfer(accountTransferDTO,
|
||||
fromLoanAccount, toLoanAccount, disburseTransaction, repayTransaction);
|
||||
|
||||
+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=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
|
||||
+ "loans/1/transactions/template?command=foreclosure" + "\n"
|
||||
+ "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value")
|
||||
+ "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
|
||||
public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId,
|
||||
@@ -228,14 +228,14 @@ public class LoanTransactionsApiResource {
|
||||
+ "Example Requests:\n" + "\n" + "loans/1/transactions?command=repayment" + " | Make a Repayment | \n"
|
||||
+ "loans/1/transactions?command=merchantIssuedRefund" + " | Merchant Issued Refund | \n"
|
||||
+ "loans/1/transactions?command=payoutRefund" + " | Payout Refund | \n" + "loans/1/transactions?command=goodwillCredit"
|
||||
+ " | Goodwil Credit | \n" + "loans/1/transactions?command=waiveinterest" + " | Waive Interest | \n"
|
||||
+ "loans/1/transactions?command=writeoff" + " | Write-off Loan | \n" + "loans/1/transactions?command=close-rescheduled"
|
||||
+ " | Close Rescheduled Loan | \n" + "loans/1/transactions?command=close" + " | Close Loan | \n"
|
||||
+ "loans/1/transactions?command=undowriteoff" + " | Undo Loan Write-off | \n" + "loans/1/transactions?command=recoverypayment"
|
||||
+ " | Make Recovery Payment | \n" + "loans/1/transactions?command=refundByCash"
|
||||
+ " | Make a Refund of an Active Loan by Cash | \n" + "loans/1/transactions?command=foreclosure"
|
||||
+ " | Foreclosure of an Active Loan | \n" + "loans/1/transactions?command=creditBalanceRefund" + " | Credit Balance Refund"
|
||||
+ " | \n")
|
||||
+ " | Goodwil Credit | \n" + "loans/1/transactions?command=chargeRefund" + " | Charge Refund | \n"
|
||||
+ "loans/1/transactions?command=waiveinterest" + " | Waive Interest | \n" + "loans/1/transactions?command=writeoff"
|
||||
+ " | Write-off Loan | \n" + "loans/1/transactions?command=close-rescheduled" + " | Close Rescheduled Loan | \n"
|
||||
+ "loans/1/transactions?command=close" + " | Close Loan | \n" + "loans/1/transactions?command=undowriteoff"
|
||||
+ " | Undo Loan Write-off | \n" + "loans/1/transactions?command=recoverypayment" + " | Make Recovery Payment | \n"
|
||||
+ "loans/1/transactions?command=refundByCash" + " | Make a Refund of an Active Loan by Cash | \n"
|
||||
+ "loans/1/transactions?command=foreclosure" + " | Foreclosure of an Active Loan | \n"
|
||||
+ "loans/1/transactions?command=creditBalanceRefund" + " | Credit Balance Refund" + " | \n")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsRequest.class)))
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) })
|
||||
@@ -258,6 +258,9 @@ public class LoanTransactionsApiResource {
|
||||
} else if (is(commandParam, "goodwillCredit")) {
|
||||
final CommandWrapper commandRequest = builder.loanGoodwillCreditTransaction(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "chargeRefund")) {
|
||||
final CommandWrapper commandRequest = builder.refundLoanCharge(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "waiveinterest")) {
|
||||
final CommandWrapper commandRequest = builder.waiveInterestPortionTransaction(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
|
||||
+7
-1
@@ -34,6 +34,7 @@ public class LoanTransactionEnumData {
|
||||
private final boolean merchantIssuedRefund;
|
||||
private final boolean payoutRefund;
|
||||
private final boolean goodwillCredit;
|
||||
private final boolean chargeRefund;
|
||||
private final boolean contra;
|
||||
private final boolean waiveInterest;
|
||||
private final boolean waiveCharges;
|
||||
@@ -59,6 +60,7 @@ public class LoanTransactionEnumData {
|
||||
this.merchantIssuedRefund = Long.valueOf(21).equals(this.id);
|
||||
this.payoutRefund = Long.valueOf(22).equals(this.id);
|
||||
this.goodwillCredit = Long.valueOf(23).equals(this.id);
|
||||
this.chargeRefund = Long.valueOf(24).equals(this.id);
|
||||
this.contra = Long.valueOf(3).equals(this.id);
|
||||
this.waiveInterest = Long.valueOf(4).equals(this.id);
|
||||
this.waiveCharges = Long.valueOf(9).equals(this.id);
|
||||
@@ -101,7 +103,7 @@ public class LoanTransactionEnumData {
|
||||
}
|
||||
|
||||
public boolean isRepaymentType() {
|
||||
if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit()) {
|
||||
if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -131,6 +133,10 @@ public class LoanTransactionEnumData {
|
||||
return this.goodwillCredit;
|
||||
}
|
||||
|
||||
public boolean isChargeRefund() {
|
||||
return this.chargeRefund;
|
||||
}
|
||||
|
||||
public boolean isWaiveInterest() {
|
||||
return this.waiveInterest;
|
||||
}
|
||||
|
||||
+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.InvalidRefundDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
|
||||
@@ -3055,6 +3056,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
}
|
||||
validateRepaymentTypeAccountStatus(repaymentTransaction, event);
|
||||
validateActivityNotBeforeClientOrGroupTransferDate(event, repaymentTransaction.getTransactionDate());
|
||||
validateRepaymentTypeTransactionNotBeforeAChargeRefund(repaymentTransaction, "created");
|
||||
validateActivityNotBeforeLastTransactionDate(event, repaymentTransaction.getTransactionDate());
|
||||
if (!isHolidayValidationDone) {
|
||||
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(),
|
||||
@@ -3073,7 +3075,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
|
||||
private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) {
|
||||
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund()
|
||||
|| repaymentTransaction.isPayoutRefund()) {
|
||||
|| repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund()) {
|
||||
|
||||
if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
@@ -4917,6 +4919,20 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final LoanTransaction repaymentTransaction,
|
||||
final String reversedOrCreated) {
|
||||
if (repaymentTransaction.isRepaymentType() && !repaymentTransaction.isChargeRefund()) {
|
||||
for (LoanTransaction txn : this.getLoanTransactions()) {
|
||||
if (txn.isChargeRefund() && repaymentTransaction.getTransactionDate().isBefore(txn.getTransactionDate())) {
|
||||
final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists";
|
||||
final String details = "Loan Transaction: " + this.getId() + " Can't be " + reversedOrCreated
|
||||
+ " because a Later Charge Refund Exists.";
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LocalDate getLastUserTransactionDate() {
|
||||
LocalDate currentTransactionDate = getDisbursementDate();
|
||||
for (final LoanTransaction previousTransaction : this.loanTransactions) {
|
||||
|
||||
+4
-3
@@ -30,7 +30,8 @@ public interface LoanAccountDomainService {
|
||||
|
||||
LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
|
||||
LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
|
||||
boolean isRecoveryRepayment, boolean isAccountTransfer, HolidayDetailDTO holidatDetailDto, Boolean isHolidayValidationDone);
|
||||
boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidatDetailDto,
|
||||
Boolean isHolidayValidationDone);
|
||||
|
||||
LoanTransaction makeRefund(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId);
|
||||
@@ -63,8 +64,8 @@ public interface LoanAccountDomainService {
|
||||
|
||||
LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
|
||||
LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
|
||||
boolean isRecoveryRepayment, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone,
|
||||
boolean isLoanToLoanTransfer);
|
||||
boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
|
||||
Boolean isHolidayValidationDone, boolean isLoanToLoanTransfer);
|
||||
|
||||
void saveLoanWithDataIntegrityViolationChecks(Loan loan);
|
||||
|
||||
|
||||
+16
-8
@@ -129,9 +129,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan,
|
||||
final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount,
|
||||
final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment,
|
||||
boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone) {
|
||||
final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
|
||||
Boolean isHolidayValidationDone) {
|
||||
return makeRepayment(repaymentTransactionType, loan, builderResult, transactionDate, transactionAmount, paymentDetail, noteText,
|
||||
txnExternalId, isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone, false);
|
||||
txnExternalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone,
|
||||
false);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -154,8 +156,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan,
|
||||
final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount,
|
||||
final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment,
|
||||
boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone,
|
||||
final boolean isLoanToLoanTransfer) {
|
||||
final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
|
||||
Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) {
|
||||
AppUser currentUser = getAppUserIfPresent();
|
||||
checkClientOrGroupActive(loan);
|
||||
|
||||
@@ -181,7 +183,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
txnExternalId);
|
||||
} else {
|
||||
newRepaymentTransaction = LoanTransaction.repaymentType(repaymentTransactionType, loan.getOffice(), repaymentAmount,
|
||||
paymentDetail, transactionDate, txnExternalId);
|
||||
paymentDetail, transactionDate, txnExternalId, chargeRefundChargeType);
|
||||
}
|
||||
|
||||
LocalDate recalculateFrom = null;
|
||||
@@ -223,9 +225,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
|
||||
recalculateAccruals(loan);
|
||||
|
||||
LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType,
|
||||
isRecoveryRepayment, newRepaymentTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||
if (!repaymentTransactionType.isChargeRefund()) {
|
||||
LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType,
|
||||
isRecoveryRepayment, newRepaymentTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||
}
|
||||
|
||||
// disable all active standing orders linked to this loan if status
|
||||
// changes to closed
|
||||
@@ -277,6 +281,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionPayoutRefundPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
||||
repaymentEvent = new LoanTransactionGoodwillCreditPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isChargeRefund()) {
|
||||
repaymentEvent = new LoanChargePaymentPreBusinessEvent(loan);
|
||||
} else if (isRecoveryRepayment) {
|
||||
repaymentEvent = new LoanTransactionRecoveryPaymentPreBusinessEvent(loan);
|
||||
}
|
||||
@@ -294,6 +300,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionPayoutRefundPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
||||
repaymentEvent = new LoanTransactionGoodwillCreditPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isChargeRefund()) {
|
||||
repaymentEvent = new LoanChargePaymentPostBusinessEvent(transaction);
|
||||
} else if (isRecoveryRepayment) {
|
||||
repaymentEvent = new LoanTransactionRecoveryPaymentPostBusinessEvent(transaction);
|
||||
}
|
||||
|
||||
+28
-3
@@ -115,6 +115,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
@Column(name = "manually_adjusted_or_reversed", nullable = false)
|
||||
private boolean manuallyAdjustedOrReversed;
|
||||
|
||||
@Column(name = "charge_refund_charge_type", length = 1, nullable = true, unique = true)
|
||||
private String chargeRefundChargeType;
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction")
|
||||
private Set<LoanCollateralManagement> loanCollateralManagementSet = new HashSet<>();
|
||||
|
||||
@@ -147,8 +150,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
}
|
||||
|
||||
public static LoanTransaction repaymentType(final LoanTransactionType repaymentType, final Office office, final Money amount,
|
||||
final PaymentDetail paymentDetail, final LocalDate paymentDate, final String externalId) {
|
||||
return new LoanTransaction(null, office, repaymentType, paymentDetail, amount.getAmount(), paymentDate, externalId);
|
||||
final PaymentDetail paymentDetail, final LocalDate paymentDate, final String externalId, final String chargeRefundChargeType) {
|
||||
return new LoanTransaction(null, office, repaymentType, paymentDetail, amount.getAmount(), paymentDate, externalId,
|
||||
chargeRefundChargeType);
|
||||
}
|
||||
|
||||
public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) {
|
||||
@@ -344,7 +348,21 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
this.submittedOnDate = DateUtils.getBusinessLocalDate();
|
||||
}
|
||||
|
||||
private LoanTransaction(final Loan loan, final Office office, final LoanTransactionType type, final PaymentDetail paymentDetail,
|
||||
final BigDecimal amount, final LocalDate date, final String externalId, final String chargeRefundChargeType) {
|
||||
this.loan = loan;
|
||||
this.typeOf = type.getValue();
|
||||
this.paymentDetail = paymentDetail;
|
||||
this.amount = amount;
|
||||
this.dateOf = date;
|
||||
this.externalId = externalId;
|
||||
this.office = office;
|
||||
this.submittedOnDate = DateUtils.getBusinessLocalDate();
|
||||
this.chargeRefundChargeType = chargeRefundChargeType;
|
||||
}
|
||||
|
||||
public void reverse() {
|
||||
this.loan.validateRepaymentTypeTransactionNotBeforeAChargeRefund(this, "reversed");
|
||||
this.reversed = true;
|
||||
this.loanTransactionToRepaymentScheduleMappings.clear();
|
||||
}
|
||||
@@ -480,7 +498,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
}
|
||||
|
||||
public boolean isRepaymentType() {
|
||||
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit();
|
||||
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund();
|
||||
}
|
||||
|
||||
public boolean isRepayment() {
|
||||
@@ -499,6 +517,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
return LoanTransactionType.GOODWILL_CREDIT.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isChargeRefund() {
|
||||
return LoanTransactionType.CHARGE_REFUND.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isNotRepaymentType() {
|
||||
return !isRepaymentType();
|
||||
}
|
||||
@@ -640,6 +662,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
thisTransactionData.put("feeChargesPortion", this.feeChargesPortion);
|
||||
thisTransactionData.put("penaltyChargesPortion", this.penaltyChargesPortion);
|
||||
thisTransactionData.put("overPaymentPortion", this.overPaymentPortion);
|
||||
if (transactionType.isChargeRefund()) {
|
||||
thisTransactionData.put("chargeRefundChargeType", this.chargeRefundChargeType);
|
||||
}
|
||||
|
||||
if (this.paymentDetail != null) {
|
||||
thisTransactionData.put("paymentTypeId", this.paymentDetail.getPaymentType().getId());
|
||||
|
||||
+10
-2
@@ -55,7 +55,8 @@ public enum LoanTransactionType {
|
||||
CREDIT_BALANCE_REFUND(20, "loanTransactionType.creditBalanceRefund"), //
|
||||
MERCHANT_ISSUED_REFUND(21, "loanTransactionType.merchantIssuedRefund"), //
|
||||
PAYOUT_REFUND(22, "loanTransactionType.payoutRefund"), //
|
||||
GOODWILL_CREDIT(23, "loanTransactionType.goodwillCredit");
|
||||
GOODWILL_CREDIT(23, "loanTransactionType.goodwillCredit"), //
|
||||
CHARGE_REFUND(24, "loanTransactionType.chargeRefund");
|
||||
|
||||
private final Integer value;
|
||||
private final String code;
|
||||
@@ -147,6 +148,9 @@ public enum LoanTransactionType {
|
||||
case 23:
|
||||
loanTransactionType = LoanTransactionType.GOODWILL_CREDIT;
|
||||
break;
|
||||
case 24:
|
||||
loanTransactionType = LoanTransactionType.CHARGE_REFUND;
|
||||
break;
|
||||
default:
|
||||
loanTransactionType = LoanTransactionType.INVALID;
|
||||
break;
|
||||
@@ -178,8 +182,12 @@ public enum LoanTransactionType {
|
||||
return this.value.equals(LoanTransactionType.GOODWILL_CREDIT.getValue());
|
||||
}
|
||||
|
||||
public boolean isChargeRefund() {
|
||||
return this.value.equals(LoanTransactionType.CHARGE_REFUND.getValue());
|
||||
}
|
||||
|
||||
public boolean isRepaymentType() {
|
||||
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit());
|
||||
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund());
|
||||
}
|
||||
|
||||
public boolean isRecoveryRepayment() {
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
public void validateLoanChargeRefundTransaction(final String json) {
|
||||
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final String loanChargeIdParam = "loanChargeId";
|
||||
final String installmentNumberParam = "installmentNumber";
|
||||
final String dueDateParam = "dueDate";
|
||||
Set<String> transactionParameters = new HashSet<>(
|
||||
Arrays.asList(loanChargeIdParam, dueDateParam, "locale", "dateFormat", installmentNumberParam, //
|
||||
// remainder below relate to payment part of refund and not validated here
|
||||
"transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber",
|
||||
"routingCode", "receiptNumber", "bankNumber"));
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||
.resource("loan.charge.refund.transaction");
|
||||
|
||||
JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
final Long chargeId = this.fromApiJsonHelper.extractLongNamed(loanChargeIdParam, element);
|
||||
baseDataValidator.reset().parameter(loanChargeIdParam).value(chargeId).notNull().integerGreaterThanZero();
|
||||
|
||||
final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(installmentNumberParam, element);
|
||||
baseDataValidator.reset().parameter(installmentNumberParam).value(installmentNumber).ignoreIfNull().integerGreaterThanZero();
|
||||
|
||||
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
|
||||
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).ignoreIfNull().positiveAmount();
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
public void validateInstallmentChargeTransaction(final String json) {
|
||||
|
||||
if (StringUtils.isBlank(json)) {
|
||||
|
||||
+1
-1
@@ -1669,7 +1669,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
||||
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
|
||||
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||
sqlBuilder.append("select ").append(mapper.schema()).append(
|
||||
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
|
||||
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
|
||||
.append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))")
|
||||
.append(" or (ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
|
||||
.append(" or (ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
|
||||
|
||||
+2
@@ -68,6 +68,8 @@ public interface LoanWritePlatformService {
|
||||
|
||||
CommandProcessingResult undoWaiveLoanCharge(JsonCommand command);
|
||||
|
||||
CommandProcessingResult loanChargeRefund(Long loanId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult loanReassignment(Long loanId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult bulkLoanReassignment(JsonCommand command);
|
||||
|
||||
+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.LoanWaiveChargeBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanWaiveChargeUndoBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanChargePaymentPostBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanWrittenOffPostBusinessEvent;
|
||||
@@ -180,6 +181,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCount
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
|
||||
@@ -888,6 +890,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
public CommandProcessingResult makeLoanRepayment(final LoanTransactionType repaymentTransactionType, final Long loanId,
|
||||
final JsonCommand command, final boolean isRecoveryRepayment) {
|
||||
final String chargeRefundChargeType = null;
|
||||
return makeLoanRepaymentWithChargeRefundChargeType(repaymentTransactionType, loanId, command, isRecoveryRepayment,
|
||||
chargeRefundChargeType);
|
||||
}
|
||||
|
||||
private CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType repaymentTransactionType,
|
||||
final Long loanId, final JsonCommand command, final boolean isRecoveryRepayment, final String chargeRefundChargeType) {
|
||||
|
||||
this.loanUtilService.validateRepaymentTransactionType(repaymentTransactionType);
|
||||
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
|
||||
@@ -915,7 +924,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
||||
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan,
|
||||
commandProcessingResultBuilder, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId,
|
||||
isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
|
||||
// Update loan transaction on repayment.
|
||||
if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) {
|
||||
@@ -984,10 +993,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
|
||||
}
|
||||
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
||||
final String chargeRefundChargeType = null;
|
||||
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, loan,
|
||||
commandProcessingResultBuilder, bulkRepaymentCommand.getTransactionDate(),
|
||||
singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail, bulkRepaymentCommand.getNote(), null,
|
||||
isRecoveryRepayment, isAccountTransfer, holidayDetailDTO, isHolidayValidationDone);
|
||||
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDTO, isHolidayValidationDone);
|
||||
transactionIds.add(loanTransaction.getId());
|
||||
}
|
||||
}
|
||||
@@ -1646,6 +1656,189 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult loanChargeRefund(final Long loanId, final JsonCommand command) {
|
||||
|
||||
this.loanEventApiJsonValidator.validateLoanChargeRefundTransaction(command.json());
|
||||
|
||||
final Long loanChargeId = command.longValueOfParameterNamed("loanChargeId");
|
||||
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
|
||||
|
||||
final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber");
|
||||
final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate");
|
||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||
|
||||
final LoanInstallmentCharge installmentChargeEntry = loanChargeRefundEntranceValidation(loanCharge, installmentNumber, dueDate);
|
||||
Integer installmentNumberIdentified = null;
|
||||
if (installmentChargeEntry != null) {
|
||||
installmentNumberIdentified = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber();
|
||||
}
|
||||
final BigDecimal fullRefundAbleAmount = loanChargeValidateRefundAmount(loanCharge, installmentChargeEntry, transactionAmount);
|
||||
|
||||
JsonCommand repaymentJsonCommand = adaptLoanChargeRefundCommandForFutherRepaymentProcessing(command, fullRefundAbleAmount);
|
||||
|
||||
boolean isRecoveryRepayment = false;
|
||||
String chargeRefundChargeType = "F";
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
chargeRefundChargeType = "P";
|
||||
}
|
||||
// chargeRefundChargeType only included as a parameter for accounting reason - in order to identify whether fee
|
||||
// or penalty GL account is relevant
|
||||
CommandProcessingResult result = makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType.CHARGE_REFUND,
|
||||
repaymentJsonCommand.getLoanId(), repaymentJsonCommand, isRecoveryRepayment, chargeRefundChargeType);
|
||||
|
||||
Long loanChargeRefundTransactionId = result.resourceId();
|
||||
LoanTransaction newChargeRefundTxn = null;
|
||||
for (LoanTransaction chargeRefundTxn : loanCharge.getLoan().getLoanTransactions()) {
|
||||
if (chargeRefundTxn.getId().equals(loanChargeRefundTransactionId)) {
|
||||
newChargeRefundTxn = chargeRefundTxn;
|
||||
final BigDecimal appliedRefundAmount = newChargeRefundTxn.getAmount(loanCharge.getLoan().getCurrency()).getAmount()
|
||||
.multiply(BigDecimal.valueOf(-1));
|
||||
final LoanChargePaidBy loanChargePaidByForChargeRefund = new LoanChargePaidBy(newChargeRefundTxn, loanCharge,
|
||||
appliedRefundAmount, installmentNumberIdentified);
|
||||
newChargeRefundTxn.getLoanChargesPaid().add(loanChargePaidByForChargeRefund);
|
||||
loanCharge.getLoanChargePaidBySet().add(loanChargePaidByForChargeRefund);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newChargeRefundTxn));
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
private JsonCommand adaptLoanChargeRefundCommandForFutherRepaymentProcessing(JsonCommand command, BigDecimal fullRefundAbleAmount) {
|
||||
// creates JsonCommand for onward repayment processing
|
||||
JsonObject jsonObject = (JsonObject) this.fromApiJsonHelper.parse(command.json());
|
||||
|
||||
String dateFormat;
|
||||
if (this.fromApiJsonHelper.parameterExists("dateFormat", jsonObject)) {
|
||||
dateFormat = this.fromApiJsonHelper.extractStringNamed("dateFormat", jsonObject);
|
||||
} else {
|
||||
dateFormat = "dd MMMM yyyy";
|
||||
jsonObject.addProperty("dateFormat", dateFormat);
|
||||
}
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
|
||||
LocalDate transactionDate = DateUtils.getLocalDateOfTenant();
|
||||
String transactionDateString = transactionDate.format(dateTimeFormatter);
|
||||
jsonObject.addProperty("transactionDate", transactionDateString);
|
||||
if (!this.fromApiJsonHelper.parameterExists("transactionAmount", jsonObject)) {
|
||||
jsonObject.addProperty("transactionAmount", fullRefundAbleAmount.toString());
|
||||
}
|
||||
jsonObject.remove("loanChargeId");
|
||||
jsonObject.remove("installmentNumber");
|
||||
jsonObject.remove("dueDate");
|
||||
|
||||
JsonCommand repaymentJsonCommand = JsonCommand.fromExistingCommand(command, jsonObject);
|
||||
return repaymentJsonCommand;
|
||||
}
|
||||
|
||||
private BigDecimal loanChargeValidateRefundAmount(LoanCharge loanCharge, LoanInstallmentCharge installmentChargeEntry,
|
||||
BigDecimal transactionAmount) {
|
||||
// if transactionAmount not provided return max refundable amount (amount paid minus previous refunds)
|
||||
BigDecimal chargeAmountPaid = BigDecimal.ZERO;
|
||||
BigDecimal chargeAmountRefunded = BigDecimal.ZERO;
|
||||
MonetaryCurrency loanCurrency = loanCharge.getLoan().getCurrency();
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
final Integer installmentNumber = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber();
|
||||
chargeAmountPaid = installmentChargeEntry.getAmountPaid(loanCurrency).getAmount();
|
||||
for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) {
|
||||
if (installmentNumber.equals(loanChargePaidBy.getInstallmentNumber())) {
|
||||
if (isRefundElementOfChargeRefund(loanChargePaidBy)) {
|
||||
chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chargeAmountPaid = loanCharge.getAmountPaid(loanCurrency).getAmount();
|
||||
for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) {
|
||||
if (isRefundElementOfChargeRefund(loanChargePaidBy)) {
|
||||
chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
chargeAmountRefunded = chargeAmountRefunded.multiply(BigDecimal.valueOf(-1));
|
||||
|
||||
if (chargeAmountRefunded.compareTo(chargeAmountPaid) > 0) {
|
||||
final String errorMessage = "loan.charge.more.refunded.than.paid.unexpected.system.error";
|
||||
final String details = "Paid: " + chargeAmountPaid.toString() + " Refunded: " + chargeAmountPaid.toString();
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
|
||||
BigDecimal refundableAmount = chargeAmountPaid.subtract(chargeAmountRefunded);
|
||||
if (transactionAmount != null) { // refund amount was provided.
|
||||
if (transactionAmount.compareTo(refundableAmount) > 0) {
|
||||
final String errorMessage = "loan.charge.transaction.amount.is.more.than.is.refundable";
|
||||
final String details = "transactionAmount: " + transactionAmount.toString() + " Refundable: "
|
||||
+ refundableAmount.toString();
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
}
|
||||
|
||||
return refundableAmount;
|
||||
}
|
||||
|
||||
private boolean isRefundElementOfChargeRefund(LoanChargePaidBy loanChargePaidBy) {
|
||||
// The Refund Element is always negative
|
||||
return (loanChargePaidBy.getLoanTransaction().isChargeRefund() && loanChargePaidBy.getAmount().compareTo(BigDecimal.ZERO) < 0);
|
||||
}
|
||||
|
||||
private LoanInstallmentCharge loanChargeRefundEntranceValidation(LoanCharge loanCharge, Integer installmentNumber, LocalDate dueDate) {
|
||||
|
||||
LoanInstallmentCharge installmentChargeEntry = null;
|
||||
|
||||
Loan loan = loanCharge.getLoan();
|
||||
if (!(loan.isOpen() || loan.status().isClosedObligationsMet() || loan.status().isOverpaid())) {
|
||||
final String errorMessage = "loan.charge.refund.invalid.status";
|
||||
throw new LoanChargeRefundException(errorMessage, loan.status().toString());
|
||||
}
|
||||
|
||||
if (dueDate != null && installmentNumber != null) {
|
||||
throwLoanChargeRefundException("loan.charge.refund.dueDate.and.installmentNumber.provided.use.only.one", installmentNumber,
|
||||
dueDate);
|
||||
}
|
||||
|
||||
if (loanCharge.isInstalmentFee()) { // identify specific installment
|
||||
if (dueDate == null && installmentNumber == null) {
|
||||
throwLoanChargeRefundException(
|
||||
"loan.charge.refund.neither.dueDate.nor.installmentNumber.provided.for.this.installment.charge", installmentNumber,
|
||||
dueDate);
|
||||
}
|
||||
|
||||
if (dueDate != null) {
|
||||
installmentChargeEntry = loanCharge.getInstallmentLoanCharge(dueDate);
|
||||
} else if (installmentNumber != null) {
|
||||
installmentChargeEntry = loanCharge.getInstallmentLoanCharge(installmentNumber);
|
||||
}
|
||||
|
||||
if (installmentChargeEntry == null) {
|
||||
throwLoanChargeRefundException("loan.charge.refund.installment.not.found", installmentNumber, dueDate);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (dueDate != null || installmentNumber != null) {
|
||||
throwLoanChargeRefundException(
|
||||
"loan.charge.refund.dueDate.or.installmentNumber.provided.but.this.is.not.an.installment.charge", installmentNumber,
|
||||
dueDate);
|
||||
}
|
||||
}
|
||||
|
||||
return installmentChargeEntry;
|
||||
}
|
||||
|
||||
private void throwLoanChargeRefundException(String errorMessage, Integer installmentNumber, LocalDate dueDate) {
|
||||
String dueDateValue = "";
|
||||
String installmentNumberValue = "";
|
||||
if (dueDate != null) {
|
||||
dueDateValue = dueDate.toString();
|
||||
}
|
||||
if (installmentNumber != null) {
|
||||
installmentNumberValue = installmentNumber.toString();
|
||||
}
|
||||
throw new LoanChargeRefundException(errorMessage, "dueDate: " + dueDateValue + " installmentNumber: " + installmentNumberValue);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) {
|
||||
|
||||
+4
@@ -427,6 +427,10 @@ public final class LoanEnumerations {
|
||||
optionData = new LoanTransactionEnumData(LoanTransactionType.GOODWILL_CREDIT.getValue().longValue(),
|
||||
LoanTransactionType.GOODWILL_CREDIT.getCode(), "Goodwill Credit");
|
||||
break;
|
||||
case CHARGE_REFUND:
|
||||
optionData = new LoanTransactionEnumData(LoanTransactionType.CHARGE_REFUND.getValue().longValue(),
|
||||
LoanTransactionType.CHARGE_REFUND.getCode(), "Charge Refund");
|
||||
break;
|
||||
}
|
||||
return optionData;
|
||||
}
|
||||
|
||||
@@ -46,4 +46,6 @@
|
||||
<include file="parts/0024_add_audit_entries.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0025_add_audit_entries_to_journal_entry.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0026_reversals_for_reversed_transactions.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0027_add_charge_refund_permission.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0028_add_charge_refund_charge_type_to_loan_transaction.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+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");
|
||||
Assertions.assertNotNull(resourceId);
|
||||
|
||||
HashMap creditBalanceRefundMap = this.loanTransactionHelper.getLoanTransactionDetails(disbursedLoanID, resourceId);
|
||||
HashMap creditBalanceRefundMap = (HashMap) this.loanTransactionHelper.getLoanTransactionDetails(disbursedLoanID, resourceId, "");
|
||||
Assertions.assertNotNull(creditBalanceRefundMap.get("externalId"));
|
||||
Assertions.assertEquals(creditBalanceRefundMap.get("externalId"), externalId, "Incorrect External Id Saved");
|
||||
|
||||
@@ -347,7 +347,7 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest {
|
||||
HashMap loanStatusHashMap = (HashMap) this.loanTransactionHelper.makeRepaymentTypePayment(repaymentTransactionType,
|
||||
"06 January 2022", 200.00f, this.disbursedLoanID, "");
|
||||
Integer newTransactionId = (Integer) loanStatusHashMap.get("resourceId");
|
||||
loanStatusHashMap = this.loanTransactionHelper.getLoanTransactionDetails(this.disbursedLoanID, newTransactionId);
|
||||
loanStatusHashMap = (HashMap) this.loanTransactionHelper.getLoanTransactionDetails(this.disbursedLoanID, newTransactionId, "");
|
||||
|
||||
HashMap typeMap = (HashMap) loanStatusHashMap.get("type");
|
||||
Boolean isTypeCorrect = (Boolean) typeMap.get(repaymentTransactionType);
|
||||
|
||||
+26
-2
@@ -63,6 +63,7 @@ public class LoanTransactionHelper {
|
||||
private static final String WAIVE_INTEREST_COMMAND = "waiveinterest";
|
||||
private static final String MAKE_REPAYMENT_COMMAND = "repayment";
|
||||
private static final String UNDO = "undo";
|
||||
private static final String LOANCHARGE_REFUND_REPAYMENT_COMMAND = "chargeRefund";
|
||||
private static final String CREDIT_BALANCE_REFUND_COMMAND = "creditBalanceRefund";
|
||||
private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = "withdrawnByApplicant";
|
||||
private static final String RECOVER_FROM_GUARANTORS_COMMAND = "recoverGuarantees";
|
||||
@@ -349,6 +350,12 @@ public class LoanTransactionHelper {
|
||||
getCreditBalanceRefundBodyAsJSON(date, amountToBePaid, externalId), jsonAttributeToGetback);
|
||||
}
|
||||
|
||||
public Object loanChargeRefund(final Integer loanChargeId, final Integer installmentNumber, final Float amountToBePaid,
|
||||
final String externalId, final Integer loanID, String jsonAttributeToGetback) {
|
||||
return performLoanTransaction(createLoanTransactionURL(LOANCHARGE_REFUND_REPAYMENT_COMMAND, loanID),
|
||||
getLoanChargeRefundBodyAsJSON(loanChargeId, installmentNumber, amountToBePaid, externalId), jsonAttributeToGetback);
|
||||
}
|
||||
|
||||
public Object makeRepaymentTypePayment(final String repaymentTypeCommand, final String date, final Float amountToBePaid,
|
||||
final Integer loanID, String jsonAttributeToGetback) {
|
||||
return performLoanTransaction(createLoanTransactionURL(repaymentTypeCommand, loanID), getRepaymentBodyAsJSON(date, amountToBePaid),
|
||||
@@ -452,10 +459,10 @@ public class LoanTransactionHelper {
|
||||
return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL, "");
|
||||
}
|
||||
|
||||
public HashMap getLoanTransactionDetails(final Integer loanId, final Integer txnId) {
|
||||
public Object getLoanTransactionDetails(final Integer loanId, final Integer txnId, final String param) {
|
||||
final String GET_LOAN_CHARGES_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + txnId + "?"
|
||||
+ Utils.TENANT_IDENTIFIER;
|
||||
return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL, "");
|
||||
return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL, param);
|
||||
}
|
||||
|
||||
public HashMap getPostDatedCheck(final Integer loanId, final Integer installmentId) {
|
||||
@@ -535,6 +542,23 @@ public class LoanTransactionHelper {
|
||||
return new Gson().toJson(map);
|
||||
}
|
||||
|
||||
private String getLoanChargeRefundBodyAsJSON(final Integer loanChargeId, final Integer installmentNumber, final Float transactionAmount,
|
||||
final String externalId) {
|
||||
final HashMap<String, String> map = new HashMap<>();
|
||||
map.put("locale", "en");
|
||||
map.put("dateFormat", "dd MMMM yyyy");
|
||||
map.put("loanChargeId", loanChargeId.toString());
|
||||
map.put("transactionAmount", transactionAmount.toString());
|
||||
map.put("note", "Loancharge Refund Made!!!");
|
||||
if (externalId != null) {
|
||||
map.put("externalId", externalId);
|
||||
}
|
||||
if (installmentNumber != null) {
|
||||
map.put("installmentNumber", installmentNumber.toString());
|
||||
}
|
||||
return new Gson().toJson(map);
|
||||
}
|
||||
|
||||
private String getCreditBalanceRefundBodyAsJSON(final String transactionDate, final Float transactionAmount, final String externalId) {
|
||||
final HashMap<String, String> map = new HashMap<>();
|
||||
map.put("locale", "en");
|
||||
|
||||
Reference in New Issue
Block a user