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