FINERACT-1675 New Paid loanCharge Refund Transaction
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
6f39c8ffed
commit
4034f4ae19
+2
@@ -56,4 +56,6 @@ public class LoanTransactionDTO {
|
||||
|
||||
@Setter
|
||||
private boolean isLoanToLoanTransfer;
|
||||
|
||||
private final String chargeRefundChargeType;
|
||||
}
|
||||
|
||||
+25
-1
@@ -124,6 +124,7 @@ public class AccountingProcessorHelper {
|
||||
final BigDecimal overPayments = (BigDecimal) map.get("overPaymentPortion");
|
||||
final boolean reversed = (Boolean) map.get("reversed");
|
||||
final Long paymentTypeId = (Long) map.get("paymentTypeId");
|
||||
final String chargeRefundChargeType = (String) map.get("chargeRefundChargeType");
|
||||
|
||||
final List<ChargePaymentDTO> feePaymentDetails = new ArrayList<>();
|
||||
final List<ChargePaymentDTO> penaltyPaymentDetails = new ArrayList<>();
|
||||
@@ -151,7 +152,7 @@ public class AccountingProcessorHelper {
|
||||
}
|
||||
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId,
|
||||
transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed,
|
||||
penaltyPaymentDetails, feePaymentDetails, isAccountTransfer);
|
||||
penaltyPaymentDetails, feePaymentDetails, isAccountTransfer, chargeRefundChargeType);
|
||||
Boolean isLoanToLoanTransfer = (Boolean) accountingBridgeData.get("isLoanToLoanTransfer");
|
||||
if (isLoanToLoanTransfer != null && isLoanToLoanTransfer) {
|
||||
transaction.setLoanToLoanTransfer(true);
|
||||
@@ -1301,4 +1302,27 @@ public class AccountingProcessorHelper {
|
||||
private GLAccount getGLAccountById(final Long accountId) {
|
||||
return this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
|
||||
}
|
||||
|
||||
public Integer getValueForFeeOrPenaltyIncomeAccount(final String chargeRefundChargeType) {
|
||||
if (chargeRefundChargeType == null
|
||||
|| !(chargeRefundChargeType.equalsIgnoreCase("P") || chargeRefundChargeType.equalsIgnoreCase("F"))) {
|
||||
String errorValue;
|
||||
if (chargeRefundChargeType == null) {
|
||||
errorValue = "Null";
|
||||
} else {
|
||||
errorValue = chargeRefundChargeType;
|
||||
}
|
||||
throw new PlatformDataIntegrityException("error.msg.chargeRefundChargeType.can.only.be.P.or.F",
|
||||
"chargeRefundChargeType can only be P (Penalty) or F(Fee) - Value is: " + errorValue);
|
||||
}
|
||||
Integer incomeAccount = null;
|
||||
if (chargeRefundChargeType.equalsIgnoreCase("P")) {
|
||||
incomeAccount = AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue();
|
||||
} else {
|
||||
incomeAccount = AccrualAccountsForLoan.INCOME_FROM_FEES.getValue();
|
||||
|
||||
}
|
||||
return incomeAccount;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
@@ -307,6 +307,19 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge Refunds (and their reversals) have an extra refund related pair of journal entries in addition to
|
||||
* those related to the repayment above
|
||||
***/
|
||||
if (!(totalDebitAmount.compareTo(BigDecimal.ZERO) == 0)) {
|
||||
if (loanTransactionDTO.getTransactionType().isChargeRefund()) {
|
||||
Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType());
|
||||
this.helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, currencyCode, incomeAccount,
|
||||
AccrualAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
|
||||
totalDebitAmount, isReversal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+14
@@ -24,6 +24,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
|
||||
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
|
||||
@@ -256,6 +257,19 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
|
||||
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge Refunds (and their reversals) have an extra refund related pair of journal entries in addition to
|
||||
* those related to the repayment above
|
||||
***/
|
||||
if (!(totalDebitAmount.compareTo(BigDecimal.ZERO) == 0)) {
|
||||
if (loanTransactionDTO.getTransactionType().isChargeRefund()) {
|
||||
Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType());
|
||||
this.helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, currencyCode, incomeAccount,
|
||||
AccrualAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
|
||||
totalDebitAmount, isReversal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
@@ -824,6 +824,15 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder refundLoanCharge(final Long loanId) {
|
||||
this.actionName = "CHARGEREFUND";
|
||||
this.entityName = "LOAN";
|
||||
this.entityId = null;
|
||||
this.loanId = loanId;
|
||||
this.href = "/loans/" + loanId + "/transactions/template?command=chargerefund";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder loanRecoveryPaymentTransaction(final Long loanId) {
|
||||
this.actionName = "RECOVERYPAYMENT";
|
||||
this.entityName = "LOAN";
|
||||
|
||||
+6
-4
@@ -177,9 +177,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
final Boolean isHolidayValidationDone = false;
|
||||
final HolidayDetailDTO holidayDetailDto = null;
|
||||
final boolean isRecoveryRepayment = false;
|
||||
final String chargeRefundChargeType = null;
|
||||
final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT,
|
||||
toLoanAccount, new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null,
|
||||
isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
|
||||
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(command,
|
||||
fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction);
|
||||
@@ -340,10 +341,11 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
final boolean isRecoveryRepayment = false;
|
||||
final Boolean isHolidayValidationDone = false;
|
||||
final HolidayDetailDTO holidayDetailDto = null;
|
||||
final String chargeRefundChargeType = null;
|
||||
loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount,
|
||||
new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(),
|
||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), null, null, isRecoveryRepayment,
|
||||
isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
}
|
||||
|
||||
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount,
|
||||
@@ -475,10 +477,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
LoanTransaction disburseTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(),
|
||||
accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
||||
accountTransferDTO.getNoteText(), accountTransferDTO.getTxnExternalId(), true);
|
||||
|
||||
final String chargeRefundChargeType = null;
|
||||
LoanTransaction repayTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount,
|
||||
new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
|
||||
accountTransferDTO.getPaymentDetail(), null, null, false, isAccountTransfer, null, false, true);
|
||||
accountTransferDTO.getPaymentDetail(), null, null, false, chargeRefundChargeType, isAccountTransfer, null, false, true);
|
||||
|
||||
AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToLoanTransfer(accountTransferDTO,
|
||||
fromLoanAccount, toLoanAccount, disburseTransaction, repayTransaction);
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.businessevent.domain.loan.transaction;
|
||||
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
public class LoanChargeRefundBusinessEvent extends LoanTransactionBusinessEvent {
|
||||
|
||||
public LoanChargeRefundBusinessEvent(LoanTransaction value) {
|
||||
super(value);
|
||||
}
|
||||
}
|
||||
+12
-9
@@ -120,7 +120,7 @@ public class LoanTransactionsApiResource {
|
||||
+ "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n"
|
||||
+ "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
|
||||
+ "loans/1/transactions/template?command=foreclosure" + "\n"
|
||||
+ "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value")
|
||||
+ "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
|
||||
public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId,
|
||||
@@ -228,14 +228,14 @@ public class LoanTransactionsApiResource {
|
||||
+ "Example Requests:\n" + "\n" + "loans/1/transactions?command=repayment" + " | Make a Repayment | \n"
|
||||
+ "loans/1/transactions?command=merchantIssuedRefund" + " | Merchant Issued Refund | \n"
|
||||
+ "loans/1/transactions?command=payoutRefund" + " | Payout Refund | \n" + "loans/1/transactions?command=goodwillCredit"
|
||||
+ " | Goodwil Credit | \n" + "loans/1/transactions?command=waiveinterest" + " | Waive Interest | \n"
|
||||
+ "loans/1/transactions?command=writeoff" + " | Write-off Loan | \n" + "loans/1/transactions?command=close-rescheduled"
|
||||
+ " | Close Rescheduled Loan | \n" + "loans/1/transactions?command=close" + " | Close Loan | \n"
|
||||
+ "loans/1/transactions?command=undowriteoff" + " | Undo Loan Write-off | \n" + "loans/1/transactions?command=recoverypayment"
|
||||
+ " | Make Recovery Payment | \n" + "loans/1/transactions?command=refundByCash"
|
||||
+ " | Make a Refund of an Active Loan by Cash | \n" + "loans/1/transactions?command=foreclosure"
|
||||
+ " | Foreclosure of an Active Loan | \n" + "loans/1/transactions?command=creditBalanceRefund" + " | Credit Balance Refund"
|
||||
+ " | \n")
|
||||
+ " | Goodwil Credit | \n" + "loans/1/transactions?command=chargeRefund" + " | Charge Refund | \n"
|
||||
+ "loans/1/transactions?command=waiveinterest" + " | Waive Interest | \n" + "loans/1/transactions?command=writeoff"
|
||||
+ " | Write-off Loan | \n" + "loans/1/transactions?command=close-rescheduled" + " | Close Rescheduled Loan | \n"
|
||||
+ "loans/1/transactions?command=close" + " | Close Loan | \n" + "loans/1/transactions?command=undowriteoff"
|
||||
+ " | Undo Loan Write-off | \n" + "loans/1/transactions?command=recoverypayment" + " | Make Recovery Payment | \n"
|
||||
+ "loans/1/transactions?command=refundByCash" + " | Make a Refund of an Active Loan by Cash | \n"
|
||||
+ "loans/1/transactions?command=foreclosure" + " | Foreclosure of an Active Loan | \n"
|
||||
+ "loans/1/transactions?command=creditBalanceRefund" + " | Credit Balance Refund" + " | \n")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsRequest.class)))
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) })
|
||||
@@ -258,6 +258,9 @@ public class LoanTransactionsApiResource {
|
||||
} else if (is(commandParam, "goodwillCredit")) {
|
||||
final CommandWrapper commandRequest = builder.loanGoodwillCreditTransaction(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "chargeRefund")) {
|
||||
final CommandWrapper commandRequest = builder.refundLoanCharge(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "waiveinterest")) {
|
||||
final CommandWrapper commandRequest = builder.waiveInterestPortionTransaction(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
|
||||
+7
-1
@@ -34,6 +34,7 @@ public class LoanTransactionEnumData {
|
||||
private final boolean merchantIssuedRefund;
|
||||
private final boolean payoutRefund;
|
||||
private final boolean goodwillCredit;
|
||||
private final boolean chargeRefund;
|
||||
private final boolean contra;
|
||||
private final boolean waiveInterest;
|
||||
private final boolean waiveCharges;
|
||||
@@ -59,6 +60,7 @@ public class LoanTransactionEnumData {
|
||||
this.merchantIssuedRefund = Long.valueOf(21).equals(this.id);
|
||||
this.payoutRefund = Long.valueOf(22).equals(this.id);
|
||||
this.goodwillCredit = Long.valueOf(23).equals(this.id);
|
||||
this.chargeRefund = Long.valueOf(24).equals(this.id);
|
||||
this.contra = Long.valueOf(3).equals(this.id);
|
||||
this.waiveInterest = Long.valueOf(4).equals(this.id);
|
||||
this.waiveCharges = Long.valueOf(9).equals(this.id);
|
||||
@@ -101,7 +103,7 @@ public class LoanTransactionEnumData {
|
||||
}
|
||||
|
||||
public boolean isRepaymentType() {
|
||||
if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit()) {
|
||||
if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -131,6 +133,10 @@ public class LoanTransactionEnumData {
|
||||
return this.goodwillCredit;
|
||||
}
|
||||
|
||||
public boolean isChargeRefund() {
|
||||
return this.chargeRefund;
|
||||
}
|
||||
|
||||
public boolean isWaiveInterest() {
|
||||
return this.waiveInterest;
|
||||
}
|
||||
|
||||
+17
-1
@@ -113,6 +113,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTrans
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
|
||||
@@ -3055,6 +3056,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
}
|
||||
validateRepaymentTypeAccountStatus(repaymentTransaction, event);
|
||||
validateActivityNotBeforeClientOrGroupTransferDate(event, repaymentTransaction.getTransactionDate());
|
||||
validateRepaymentTypeTransactionNotBeforeAChargeRefund(repaymentTransaction, "created");
|
||||
validateActivityNotBeforeLastTransactionDate(event, repaymentTransaction.getTransactionDate());
|
||||
if (!isHolidayValidationDone) {
|
||||
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(),
|
||||
@@ -3073,7 +3075,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
|
||||
private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) {
|
||||
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund()
|
||||
|| repaymentTransaction.isPayoutRefund()) {
|
||||
|| repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund()) {
|
||||
|
||||
if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
@@ -4917,6 +4919,20 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final LoanTransaction repaymentTransaction,
|
||||
final String reversedOrCreated) {
|
||||
if (repaymentTransaction.isRepaymentType() && !repaymentTransaction.isChargeRefund()) {
|
||||
for (LoanTransaction txn : this.getLoanTransactions()) {
|
||||
if (txn.isChargeRefund() && repaymentTransaction.getTransactionDate().isBefore(txn.getTransactionDate())) {
|
||||
final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists";
|
||||
final String details = "Loan Transaction: " + this.getId() + " Can't be " + reversedOrCreated
|
||||
+ " because a Later Charge Refund Exists.";
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LocalDate getLastUserTransactionDate() {
|
||||
LocalDate currentTransactionDate = getDisbursementDate();
|
||||
for (final LoanTransaction previousTransaction : this.loanTransactions) {
|
||||
|
||||
+4
-3
@@ -30,7 +30,8 @@ public interface LoanAccountDomainService {
|
||||
|
||||
LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
|
||||
LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
|
||||
boolean isRecoveryRepayment, boolean isAccountTransfer, HolidayDetailDTO holidatDetailDto, Boolean isHolidayValidationDone);
|
||||
boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidatDetailDto,
|
||||
Boolean isHolidayValidationDone);
|
||||
|
||||
LoanTransaction makeRefund(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId);
|
||||
@@ -63,8 +64,8 @@ public interface LoanAccountDomainService {
|
||||
|
||||
LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
|
||||
LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
|
||||
boolean isRecoveryRepayment, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone,
|
||||
boolean isLoanToLoanTransfer);
|
||||
boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
|
||||
Boolean isHolidayValidationDone, boolean isLoanToLoanTransfer);
|
||||
|
||||
void saveLoanWithDataIntegrityViolationChecks(Loan loan);
|
||||
|
||||
|
||||
+16
-8
@@ -129,9 +129,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan,
|
||||
final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount,
|
||||
final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment,
|
||||
boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone) {
|
||||
final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
|
||||
Boolean isHolidayValidationDone) {
|
||||
return makeRepayment(repaymentTransactionType, loan, builderResult, transactionDate, transactionAmount, paymentDetail, noteText,
|
||||
txnExternalId, isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone, false);
|
||||
txnExternalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone,
|
||||
false);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -154,8 +156,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan,
|
||||
final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount,
|
||||
final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment,
|
||||
boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone,
|
||||
final boolean isLoanToLoanTransfer) {
|
||||
final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
|
||||
Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) {
|
||||
AppUser currentUser = getAppUserIfPresent();
|
||||
checkClientOrGroupActive(loan);
|
||||
|
||||
@@ -181,7 +183,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
txnExternalId);
|
||||
} else {
|
||||
newRepaymentTransaction = LoanTransaction.repaymentType(repaymentTransactionType, loan.getOffice(), repaymentAmount,
|
||||
paymentDetail, transactionDate, txnExternalId);
|
||||
paymentDetail, transactionDate, txnExternalId, chargeRefundChargeType);
|
||||
}
|
||||
|
||||
LocalDate recalculateFrom = null;
|
||||
@@ -223,9 +225,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
|
||||
recalculateAccruals(loan);
|
||||
|
||||
LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType,
|
||||
isRecoveryRepayment, newRepaymentTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||
if (!repaymentTransactionType.isChargeRefund()) {
|
||||
LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType,
|
||||
isRecoveryRepayment, newRepaymentTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||
}
|
||||
|
||||
// disable all active standing orders linked to this loan if status
|
||||
// changes to closed
|
||||
@@ -277,6 +281,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionPayoutRefundPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
||||
repaymentEvent = new LoanTransactionGoodwillCreditPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isChargeRefund()) {
|
||||
repaymentEvent = new LoanChargePaymentPreBusinessEvent(loan);
|
||||
} else if (isRecoveryRepayment) {
|
||||
repaymentEvent = new LoanTransactionRecoveryPaymentPreBusinessEvent(loan);
|
||||
}
|
||||
@@ -294,6 +300,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionPayoutRefundPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
||||
repaymentEvent = new LoanTransactionGoodwillCreditPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isChargeRefund()) {
|
||||
repaymentEvent = new LoanChargePaymentPostBusinessEvent(transaction);
|
||||
} else if (isRecoveryRepayment) {
|
||||
repaymentEvent = new LoanTransactionRecoveryPaymentPostBusinessEvent(transaction);
|
||||
}
|
||||
|
||||
+28
-3
@@ -115,6 +115,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
@Column(name = "manually_adjusted_or_reversed", nullable = false)
|
||||
private boolean manuallyAdjustedOrReversed;
|
||||
|
||||
@Column(name = "charge_refund_charge_type", length = 1, nullable = true, unique = true)
|
||||
private String chargeRefundChargeType;
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction")
|
||||
private Set<LoanCollateralManagement> loanCollateralManagementSet = new HashSet<>();
|
||||
|
||||
@@ -147,8 +150,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
}
|
||||
|
||||
public static LoanTransaction repaymentType(final LoanTransactionType repaymentType, final Office office, final Money amount,
|
||||
final PaymentDetail paymentDetail, final LocalDate paymentDate, final String externalId) {
|
||||
return new LoanTransaction(null, office, repaymentType, paymentDetail, amount.getAmount(), paymentDate, externalId);
|
||||
final PaymentDetail paymentDetail, final LocalDate paymentDate, final String externalId, final String chargeRefundChargeType) {
|
||||
return new LoanTransaction(null, office, repaymentType, paymentDetail, amount.getAmount(), paymentDate, externalId,
|
||||
chargeRefundChargeType);
|
||||
}
|
||||
|
||||
public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) {
|
||||
@@ -344,7 +348,21 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
this.submittedOnDate = DateUtils.getBusinessLocalDate();
|
||||
}
|
||||
|
||||
private LoanTransaction(final Loan loan, final Office office, final LoanTransactionType type, final PaymentDetail paymentDetail,
|
||||
final BigDecimal amount, final LocalDate date, final String externalId, final String chargeRefundChargeType) {
|
||||
this.loan = loan;
|
||||
this.typeOf = type.getValue();
|
||||
this.paymentDetail = paymentDetail;
|
||||
this.amount = amount;
|
||||
this.dateOf = date;
|
||||
this.externalId = externalId;
|
||||
this.office = office;
|
||||
this.submittedOnDate = DateUtils.getBusinessLocalDate();
|
||||
this.chargeRefundChargeType = chargeRefundChargeType;
|
||||
}
|
||||
|
||||
public void reverse() {
|
||||
this.loan.validateRepaymentTypeTransactionNotBeforeAChargeRefund(this, "reversed");
|
||||
this.reversed = true;
|
||||
this.loanTransactionToRepaymentScheduleMappings.clear();
|
||||
}
|
||||
@@ -480,7 +498,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
}
|
||||
|
||||
public boolean isRepaymentType() {
|
||||
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit();
|
||||
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund();
|
||||
}
|
||||
|
||||
public boolean isRepayment() {
|
||||
@@ -499,6 +517,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
return LoanTransactionType.GOODWILL_CREDIT.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isChargeRefund() {
|
||||
return LoanTransactionType.CHARGE_REFUND.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isNotRepaymentType() {
|
||||
return !isRepaymentType();
|
||||
}
|
||||
@@ -640,6 +662,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
thisTransactionData.put("feeChargesPortion", this.feeChargesPortion);
|
||||
thisTransactionData.put("penaltyChargesPortion", this.penaltyChargesPortion);
|
||||
thisTransactionData.put("overPaymentPortion", this.overPaymentPortion);
|
||||
if (transactionType.isChargeRefund()) {
|
||||
thisTransactionData.put("chargeRefundChargeType", this.chargeRefundChargeType);
|
||||
}
|
||||
|
||||
if (this.paymentDetail != null) {
|
||||
thisTransactionData.put("paymentTypeId", this.paymentDetail.getPaymentType().getId());
|
||||
|
||||
+10
-2
@@ -55,7 +55,8 @@ public enum LoanTransactionType {
|
||||
CREDIT_BALANCE_REFUND(20, "loanTransactionType.creditBalanceRefund"), //
|
||||
MERCHANT_ISSUED_REFUND(21, "loanTransactionType.merchantIssuedRefund"), //
|
||||
PAYOUT_REFUND(22, "loanTransactionType.payoutRefund"), //
|
||||
GOODWILL_CREDIT(23, "loanTransactionType.goodwillCredit");
|
||||
GOODWILL_CREDIT(23, "loanTransactionType.goodwillCredit"), //
|
||||
CHARGE_REFUND(24, "loanTransactionType.chargeRefund");
|
||||
|
||||
private final Integer value;
|
||||
private final String code;
|
||||
@@ -147,6 +148,9 @@ public enum LoanTransactionType {
|
||||
case 23:
|
||||
loanTransactionType = LoanTransactionType.GOODWILL_CREDIT;
|
||||
break;
|
||||
case 24:
|
||||
loanTransactionType = LoanTransactionType.CHARGE_REFUND;
|
||||
break;
|
||||
default:
|
||||
loanTransactionType = LoanTransactionType.INVALID;
|
||||
break;
|
||||
@@ -178,8 +182,12 @@ public enum LoanTransactionType {
|
||||
return this.value.equals(LoanTransactionType.GOODWILL_CREDIT.getValue());
|
||||
}
|
||||
|
||||
public boolean isChargeRefund() {
|
||||
return this.value.equals(LoanTransactionType.CHARGE_REFUND.getValue());
|
||||
}
|
||||
|
||||
public boolean isRepaymentType() {
|
||||
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit());
|
||||
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund());
|
||||
}
|
||||
|
||||
public boolean isRecoveryRepayment() {
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class LoanChargeRefundException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public LoanChargeRefundException(final String defaultUserMessage, final Object... defaultUserMessageArgs) {
|
||||
super("error.msg." + defaultUserMessage, defaultUserMessage, defaultUserMessageArgs);
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@CommandType(entity = "LOAN", action = "CHARGEREFUND")
|
||||
public class LoanChargeRefundCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final LoanWritePlatformService writePlatformService;
|
||||
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
|
||||
try {
|
||||
return this.writePlatformService.loanChargeRefund(command.getLoanId(), command);
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loancharge.refund",
|
||||
"Loancharge Refund");
|
||||
return CommandProcessingResult.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -444,6 +444,41 @@ public final class LoanEventApiJsonValidator {
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
public void validateLoanChargeRefundTransaction(final String json) {
|
||||
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final String loanChargeIdParam = "loanChargeId";
|
||||
final String installmentNumberParam = "installmentNumber";
|
||||
final String dueDateParam = "dueDate";
|
||||
Set<String> transactionParameters = new HashSet<>(
|
||||
Arrays.asList(loanChargeIdParam, dueDateParam, "locale", "dateFormat", installmentNumberParam, //
|
||||
// remainder below relate to payment part of refund and not validated here
|
||||
"transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber",
|
||||
"routingCode", "receiptNumber", "bankNumber"));
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||
.resource("loan.charge.refund.transaction");
|
||||
|
||||
JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
final Long chargeId = this.fromApiJsonHelper.extractLongNamed(loanChargeIdParam, element);
|
||||
baseDataValidator.reset().parameter(loanChargeIdParam).value(chargeId).notNull().integerGreaterThanZero();
|
||||
|
||||
final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(installmentNumberParam, element);
|
||||
baseDataValidator.reset().parameter(installmentNumberParam).value(installmentNumber).ignoreIfNull().integerGreaterThanZero();
|
||||
|
||||
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
|
||||
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).ignoreIfNull().positiveAmount();
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
public void validateInstallmentChargeTransaction(final String json) {
|
||||
|
||||
if (StringUtils.isBlank(json)) {
|
||||
|
||||
+1
-1
@@ -1669,7 +1669,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
||||
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
|
||||
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||
sqlBuilder.append("select ").append(mapper.schema()).append(
|
||||
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
|
||||
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
|
||||
.append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))")
|
||||
.append(" or (ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
|
||||
.append(" or (ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
|
||||
|
||||
+2
@@ -68,6 +68,8 @@ public interface LoanWritePlatformService {
|
||||
|
||||
CommandProcessingResult undoWaiveLoanCharge(JsonCommand command);
|
||||
|
||||
CommandProcessingResult loanChargeRefund(Long loanId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult loanReassignment(Long loanId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult bulkLoanReassignment(JsonCommand command);
|
||||
|
||||
+195
-2
@@ -105,6 +105,7 @@ import org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanDelete
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanUpdateChargeBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanWaiveChargeBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.charge.LoanWaiveChargeUndoBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanChargePaymentPostBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
|
||||
import org.apache.fineract.portfolio.businessevent.domain.loan.transaction.LoanWrittenOffPostBusinessEvent;
|
||||
@@ -180,6 +181,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCount
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
|
||||
@@ -888,6 +890,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
public CommandProcessingResult makeLoanRepayment(final LoanTransactionType repaymentTransactionType, final Long loanId,
|
||||
final JsonCommand command, final boolean isRecoveryRepayment) {
|
||||
final String chargeRefundChargeType = null;
|
||||
return makeLoanRepaymentWithChargeRefundChargeType(repaymentTransactionType, loanId, command, isRecoveryRepayment,
|
||||
chargeRefundChargeType);
|
||||
}
|
||||
|
||||
private CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType repaymentTransactionType,
|
||||
final Long loanId, final JsonCommand command, final boolean isRecoveryRepayment, final String chargeRefundChargeType) {
|
||||
|
||||
this.loanUtilService.validateRepaymentTransactionType(repaymentTransactionType);
|
||||
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
|
||||
@@ -915,7 +924,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
||||
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan,
|
||||
commandProcessingResultBuilder, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId,
|
||||
isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
|
||||
|
||||
// Update loan transaction on repayment.
|
||||
if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) {
|
||||
@@ -984,10 +993,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
|
||||
}
|
||||
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
||||
final String chargeRefundChargeType = null;
|
||||
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, loan,
|
||||
commandProcessingResultBuilder, bulkRepaymentCommand.getTransactionDate(),
|
||||
singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail, bulkRepaymentCommand.getNote(), null,
|
||||
isRecoveryRepayment, isAccountTransfer, holidayDetailDTO, isHolidayValidationDone);
|
||||
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDTO, isHolidayValidationDone);
|
||||
transactionIds.add(loanTransaction.getId());
|
||||
}
|
||||
}
|
||||
@@ -1646,6 +1656,189 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult loanChargeRefund(final Long loanId, final JsonCommand command) {
|
||||
|
||||
this.loanEventApiJsonValidator.validateLoanChargeRefundTransaction(command.json());
|
||||
|
||||
final Long loanChargeId = command.longValueOfParameterNamed("loanChargeId");
|
||||
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
|
||||
|
||||
final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber");
|
||||
final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate");
|
||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||
|
||||
final LoanInstallmentCharge installmentChargeEntry = loanChargeRefundEntranceValidation(loanCharge, installmentNumber, dueDate);
|
||||
Integer installmentNumberIdentified = null;
|
||||
if (installmentChargeEntry != null) {
|
||||
installmentNumberIdentified = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber();
|
||||
}
|
||||
final BigDecimal fullRefundAbleAmount = loanChargeValidateRefundAmount(loanCharge, installmentChargeEntry, transactionAmount);
|
||||
|
||||
JsonCommand repaymentJsonCommand = adaptLoanChargeRefundCommandForFutherRepaymentProcessing(command, fullRefundAbleAmount);
|
||||
|
||||
boolean isRecoveryRepayment = false;
|
||||
String chargeRefundChargeType = "F";
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
chargeRefundChargeType = "P";
|
||||
}
|
||||
// chargeRefundChargeType only included as a parameter for accounting reason - in order to identify whether fee
|
||||
// or penalty GL account is relevant
|
||||
CommandProcessingResult result = makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType.CHARGE_REFUND,
|
||||
repaymentJsonCommand.getLoanId(), repaymentJsonCommand, isRecoveryRepayment, chargeRefundChargeType);
|
||||
|
||||
Long loanChargeRefundTransactionId = result.resourceId();
|
||||
LoanTransaction newChargeRefundTxn = null;
|
||||
for (LoanTransaction chargeRefundTxn : loanCharge.getLoan().getLoanTransactions()) {
|
||||
if (chargeRefundTxn.getId().equals(loanChargeRefundTransactionId)) {
|
||||
newChargeRefundTxn = chargeRefundTxn;
|
||||
final BigDecimal appliedRefundAmount = newChargeRefundTxn.getAmount(loanCharge.getLoan().getCurrency()).getAmount()
|
||||
.multiply(BigDecimal.valueOf(-1));
|
||||
final LoanChargePaidBy loanChargePaidByForChargeRefund = new LoanChargePaidBy(newChargeRefundTxn, loanCharge,
|
||||
appliedRefundAmount, installmentNumberIdentified);
|
||||
newChargeRefundTxn.getLoanChargesPaid().add(loanChargePaidByForChargeRefund);
|
||||
loanCharge.getLoanChargePaidBySet().add(loanChargePaidByForChargeRefund);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newChargeRefundTxn));
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
private JsonCommand adaptLoanChargeRefundCommandForFutherRepaymentProcessing(JsonCommand command, BigDecimal fullRefundAbleAmount) {
|
||||
// creates JsonCommand for onward repayment processing
|
||||
JsonObject jsonObject = (JsonObject) this.fromApiJsonHelper.parse(command.json());
|
||||
|
||||
String dateFormat;
|
||||
if (this.fromApiJsonHelper.parameterExists("dateFormat", jsonObject)) {
|
||||
dateFormat = this.fromApiJsonHelper.extractStringNamed("dateFormat", jsonObject);
|
||||
} else {
|
||||
dateFormat = "dd MMMM yyyy";
|
||||
jsonObject.addProperty("dateFormat", dateFormat);
|
||||
}
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
|
||||
LocalDate transactionDate = DateUtils.getLocalDateOfTenant();
|
||||
String transactionDateString = transactionDate.format(dateTimeFormatter);
|
||||
jsonObject.addProperty("transactionDate", transactionDateString);
|
||||
if (!this.fromApiJsonHelper.parameterExists("transactionAmount", jsonObject)) {
|
||||
jsonObject.addProperty("transactionAmount", fullRefundAbleAmount.toString());
|
||||
}
|
||||
jsonObject.remove("loanChargeId");
|
||||
jsonObject.remove("installmentNumber");
|
||||
jsonObject.remove("dueDate");
|
||||
|
||||
JsonCommand repaymentJsonCommand = JsonCommand.fromExistingCommand(command, jsonObject);
|
||||
return repaymentJsonCommand;
|
||||
}
|
||||
|
||||
private BigDecimal loanChargeValidateRefundAmount(LoanCharge loanCharge, LoanInstallmentCharge installmentChargeEntry,
|
||||
BigDecimal transactionAmount) {
|
||||
// if transactionAmount not provided return max refundable amount (amount paid minus previous refunds)
|
||||
BigDecimal chargeAmountPaid = BigDecimal.ZERO;
|
||||
BigDecimal chargeAmountRefunded = BigDecimal.ZERO;
|
||||
MonetaryCurrency loanCurrency = loanCharge.getLoan().getCurrency();
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
final Integer installmentNumber = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber();
|
||||
chargeAmountPaid = installmentChargeEntry.getAmountPaid(loanCurrency).getAmount();
|
||||
for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) {
|
||||
if (installmentNumber.equals(loanChargePaidBy.getInstallmentNumber())) {
|
||||
if (isRefundElementOfChargeRefund(loanChargePaidBy)) {
|
||||
chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chargeAmountPaid = loanCharge.getAmountPaid(loanCurrency).getAmount();
|
||||
for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) {
|
||||
if (isRefundElementOfChargeRefund(loanChargePaidBy)) {
|
||||
chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
chargeAmountRefunded = chargeAmountRefunded.multiply(BigDecimal.valueOf(-1));
|
||||
|
||||
if (chargeAmountRefunded.compareTo(chargeAmountPaid) > 0) {
|
||||
final String errorMessage = "loan.charge.more.refunded.than.paid.unexpected.system.error";
|
||||
final String details = "Paid: " + chargeAmountPaid.toString() + " Refunded: " + chargeAmountPaid.toString();
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
|
||||
BigDecimal refundableAmount = chargeAmountPaid.subtract(chargeAmountRefunded);
|
||||
if (transactionAmount != null) { // refund amount was provided.
|
||||
if (transactionAmount.compareTo(refundableAmount) > 0) {
|
||||
final String errorMessage = "loan.charge.transaction.amount.is.more.than.is.refundable";
|
||||
final String details = "transactionAmount: " + transactionAmount.toString() + " Refundable: "
|
||||
+ refundableAmount.toString();
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
}
|
||||
|
||||
return refundableAmount;
|
||||
}
|
||||
|
||||
private boolean isRefundElementOfChargeRefund(LoanChargePaidBy loanChargePaidBy) {
|
||||
// The Refund Element is always negative
|
||||
return (loanChargePaidBy.getLoanTransaction().isChargeRefund() && loanChargePaidBy.getAmount().compareTo(BigDecimal.ZERO) < 0);
|
||||
}
|
||||
|
||||
private LoanInstallmentCharge loanChargeRefundEntranceValidation(LoanCharge loanCharge, Integer installmentNumber, LocalDate dueDate) {
|
||||
|
||||
LoanInstallmentCharge installmentChargeEntry = null;
|
||||
|
||||
Loan loan = loanCharge.getLoan();
|
||||
if (!(loan.isOpen() || loan.status().isClosedObligationsMet() || loan.status().isOverpaid())) {
|
||||
final String errorMessage = "loan.charge.refund.invalid.status";
|
||||
throw new LoanChargeRefundException(errorMessage, loan.status().toString());
|
||||
}
|
||||
|
||||
if (dueDate != null && installmentNumber != null) {
|
||||
throwLoanChargeRefundException("loan.charge.refund.dueDate.and.installmentNumber.provided.use.only.one", installmentNumber,
|
||||
dueDate);
|
||||
}
|
||||
|
||||
if (loanCharge.isInstalmentFee()) { // identify specific installment
|
||||
if (dueDate == null && installmentNumber == null) {
|
||||
throwLoanChargeRefundException(
|
||||
"loan.charge.refund.neither.dueDate.nor.installmentNumber.provided.for.this.installment.charge", installmentNumber,
|
||||
dueDate);
|
||||
}
|
||||
|
||||
if (dueDate != null) {
|
||||
installmentChargeEntry = loanCharge.getInstallmentLoanCharge(dueDate);
|
||||
} else if (installmentNumber != null) {
|
||||
installmentChargeEntry = loanCharge.getInstallmentLoanCharge(installmentNumber);
|
||||
}
|
||||
|
||||
if (installmentChargeEntry == null) {
|
||||
throwLoanChargeRefundException("loan.charge.refund.installment.not.found", installmentNumber, dueDate);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (dueDate != null || installmentNumber != null) {
|
||||
throwLoanChargeRefundException(
|
||||
"loan.charge.refund.dueDate.or.installmentNumber.provided.but.this.is.not.an.installment.charge", installmentNumber,
|
||||
dueDate);
|
||||
}
|
||||
}
|
||||
|
||||
return installmentChargeEntry;
|
||||
}
|
||||
|
||||
private void throwLoanChargeRefundException(String errorMessage, Integer installmentNumber, LocalDate dueDate) {
|
||||
String dueDateValue = "";
|
||||
String installmentNumberValue = "";
|
||||
if (dueDate != null) {
|
||||
dueDateValue = dueDate.toString();
|
||||
}
|
||||
if (installmentNumber != null) {
|
||||
installmentNumberValue = installmentNumber.toString();
|
||||
}
|
||||
throw new LoanChargeRefundException(errorMessage, "dueDate: " + dueDateValue + " installmentNumber: " + installmentNumberValue);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) {
|
||||
|
||||
+4
@@ -427,6 +427,10 @@ public final class LoanEnumerations {
|
||||
optionData = new LoanTransactionEnumData(LoanTransactionType.GOODWILL_CREDIT.getValue().longValue(),
|
||||
LoanTransactionType.GOODWILL_CREDIT.getCode(), "Goodwill Credit");
|
||||
break;
|
||||
case CHARGE_REFUND:
|
||||
optionData = new LoanTransactionEnumData(LoanTransactionType.CHARGE_REFUND.getValue().longValue(),
|
||||
LoanTransactionType.CHARGE_REFUND.getCode(), "Charge Refund");
|
||||
break;
|
||||
}
|
||||
return optionData;
|
||||
}
|
||||
|
||||
@@ -46,4 +46,6 @@
|
||||
<include file="parts/0024_add_audit_entries.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0025_add_audit_entries_to_journal_entry.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0026_reversals_for_reversed_transactions.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0027_add_charge_refund_permission.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0028_add_charge_refund_charge_type_to_loan_transaction.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_loan" />
|
||||
<column name="code" value="CHARGEREFUND_LOAN" />
|
||||
<column name="entity_name" value="LOAN" />
|
||||
<column name="action_name" value="CHARGEREFUND" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<addColumn tableName="m_loan_transaction">
|
||||
<column name="charge_refund_charge_type" type="VARCHAR(1)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user