FINERACT-2327: Allow to create Interest Refund transaction manually

This commit is contained in:
mariiaKraievska
2025-07-23 11:32:30 +02:00
committed by Aleksandar Vidakovic
parent 59409550a7
commit c2e19bb68f
22 changed files with 799 additions and 8 deletions
@@ -3885,4 +3885,13 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId;
return this;
}
public CommandWrapperBuilder manualInterestRefund(final Long loanId, final Long transactionId) {
this.actionName = "MANUAL_INTEREST_REFUND";
this.entityName = "LOAN_TRANSACTION";
this.loanId = loanId;
this.entityId = transactionId;
this.href = "/loans/" + loanId + "/transactions/" + transactionId + "?command=interest-refund";
return this;
}
}
@@ -41,6 +41,7 @@ public enum TransactionType {
BUY_DOWN_FEE("buyDownFee"), //
BUY_DOWN_FEE_ADJUSTMENT("buyDownFeeAdjustment"), //
BUY_DOWN_FEE_AMORTIZATION("buyDownFeeAmortization"), //
INTEREST_REFUND("interestRefund"), //
;
public final String value;
@@ -68,6 +68,10 @@ public final class ErrorMessageHelper {
return "Loan can't be disbursed, disburse amount is exceeding approved principal.";
}
public static String addManualInterestRefundIfAlreadyExistsFailure() {
return "Interest Refund already exists for this transaction";
}
public static String addDisbursementExceedMaxAppliedAmountFailure(String totalDisbAmount, String maxDisbursalAmount) {
return String.format("Loan disbursal amount can't be greater than maximum applied loan amount calculation. "
+ "Total disbursed amount: %s Maximum disbursal amount: %s", totalDisbAmount, maxDisbursalAmount);
@@ -76,6 +76,7 @@ import org.apache.fineract.test.messaging.event.loan.transaction.LoanDisbursalTr
import org.apache.fineract.test.messaging.event.loan.transaction.LoanRefundPostBusinessEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionGoodwillCreditPostEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionInterestPaymentWaiverPostEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionInterestRefundPostEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionMakeRepaymentPostEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionMerchantIssuedRefundPostEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionPayoutRefundPostEvent;
@@ -308,7 +309,8 @@ public class EventCheckHelper {
Response<PostLoansLoanIdTransactionsResponse> transactionResponse, TransactionType transactionType, String externalOwnerId)
throws IOException {
Long loanId = transactionResponse.body().getLoanId();
Long transactionId = transactionResponse.body().getResourceId();
Long transactionId = transactionType.equals(TransactionType.INTEREST_REFUND) ? transactionResponse.body().getSubResourceId()
: transactionResponse.body().getResourceId();
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
List<GetLoansLoanIdTransactions> transactions = loanDetailsResponse.body().getTransactions();
GetLoansLoanIdTransactions transactionFound = transactions//
@@ -324,6 +326,7 @@ public class EventCheckHelper {
case MERCHANT_ISSUED_REFUND -> LoanTransactionMerchantIssuedRefundPostEvent.class;
case REFUND_BY_CASH -> LoanRefundPostBusinessEvent.class;
case INTEREST_PAYMENT_WAIVER -> LoanTransactionInterestPaymentWaiverPostEvent.class;
case INTEREST_REFUND -> LoanTransactionInterestRefundPostEvent.class;
default -> throw new IllegalStateException(String.format("transaction type %s cannot be found", transactionType.getValue()));
};
@@ -0,0 +1,27 @@
/**
* 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.test.messaging.event.loan.transaction;
public class LoanTransactionInterestRefundPostEvent extends AbstractLoanTransactionEvent {
@Override
public String getEventName() {
return "LoanTransactionInterestRefundPostBusinessEvent";
}
}
@@ -433,6 +433,63 @@ public class LoanStepDef extends AbstractStepDef {
eventCheckHelper.loanBalanceChangedEventCheck(loanId);
}
@When("Admin manually adds Interest Refund for {string} transaction made on {string} with {double} EUR interest refund amount")
public void addInterestRefundTransactionManually(final String transactionTypeInput, final String transactionDate, final double amount)
throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanResponse.body().getLoanId();
final TransactionType transactionType = TransactionType.valueOf(transactionTypeInput);
final Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
assert loanDetailsResponse.body() != null;
final List<GetLoansLoanIdTransactions> transactions = loanDetailsResponse.body().getTransactions();
assert transactions != null;
final GetLoansLoanIdTransactions refundTransaction = transactions.stream()
.filter(t -> t.getType() != null
&& (transactionType.equals(TransactionType.PAYOUT_REFUND) ? "Payout Refund" : "Merchant Issued Refund")
.equals(t.getType().getValue())
&& t.getDate() != null && transactionDate.equals(FORMATTER.format(t.getDate())))
.findFirst().orElseThrow(() -> new IllegalStateException("No refund transaction found for loan " + loanId));
final Response<PostLoansLoanIdTransactionsResponse> adjustmentResponse = addInterestRefundTransaction(amount,
refundTransaction.getId());
testContext().set(TestContextKey.LOAN_INTEREST_REFUND_RESPONSE, adjustmentResponse);
ErrorHelper.checkSuccessfulApiCall(adjustmentResponse);
eventCheckHelper.transactionEventCheck(adjustmentResponse, TransactionType.INTEREST_REFUND, null);
eventCheckHelper.loanBalanceChangedEventCheck(loanId);
}
@Then("Admin fails to add duplicate Interest Refund for {string} transaction made on {string} with {double} EUR interest refund amount")
public void failToAddManualInterestRefundIfAlreadyExists(final String transactionTypeInput, final String transactionDate,
final double amount) throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanResponse.body().getLoanId();
final TransactionType transactionType = TransactionType.valueOf(transactionTypeInput);
final Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
assert loanDetailsResponse.body() != null;
final List<GetLoansLoanIdTransactions> transactions = loanDetailsResponse.body().getTransactions();
assert transactions != null;
final GetLoansLoanIdTransactions refundTransaction = transactions.stream()
.filter(t -> t.getType() != null
&& (transactionType.equals(TransactionType.PAYOUT_REFUND) ? "Payout Refund" : "Merchant Issued Refund")
.equals(t.getType().getValue())
&& t.getDate() != null && transactionDate.equals(FORMATTER.format(t.getDate())))
.findFirst().orElseThrow(() -> new IllegalStateException("No refund transaction found for loan " + loanId));
final Response<PostLoansLoanIdTransactionsResponse> adjustmentResponse = addInterestRefundTransaction(amount,
refundTransaction.getId());
testContext().set(TestContextKey.LOAN_INTEREST_REFUND_RESPONSE, adjustmentResponse);
final ErrorResponse errorDetails = ErrorResponse.from(adjustmentResponse);
assertThat(errorDetails.getHttpStatusCode()).as(ErrorMessageHelper.addManualInterestRefundIfAlreadyExistsFailure()).isEqualTo(403);
assertThat(errorDetails.getSingleError().getDeveloperMessage())
.isEqualTo(ErrorMessageHelper.addManualInterestRefundIfAlreadyExistsFailure());
}
private void createTransactionWithAutoIdempotencyKeyAndWithExternalOwner(String transactionTypeInput, String transactionPaymentType,
String transactionDate, double transactionAmount, String externalOwnerId) throws IOException {
eventStore.reset();
@@ -4984,4 +5041,21 @@ public class LoanStepDef extends AbstractStepDef {
}
assertThat(developerMessage).isEqualTo(ErrorMessageHelper.updateApprovedLoanLessMinAllowedAmountFailure());
}
private Response<PostLoansLoanIdTransactionsResponse> addInterestRefundTransaction(final double amount, final Long transactionId)
throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
assert loanResponse.body() != null;
final long loanId = loanResponse.body().getLoanId();
final DefaultPaymentType paymentType = DefaultPaymentType.AUTOPAY;
final Long paymentTypeValue = paymentTypeResolver.resolve(paymentType);
final PostLoansLoanIdTransactionsTransactionIdRequest interestRefundRequest = new PostLoansLoanIdTransactionsTransactionIdRequest()
.dateFormat("dd MMMM yyyy").locale("en").transactionAmount(amount).paymentTypeId(paymentTypeValue)
.externalId("EXT-INT-REF-" + UUID.randomUUID()).note("");
return loanTransactionsApi.adjustLoanTransaction(loanId, transactionId, interestRefundRequest, "interest-refund").execute();
}
}
@@ -241,6 +241,7 @@ public abstract class TestContextKey {
public static final String LOAN_DISBURSEMENT_DETAIL_RESPONSE = "loanDisbursementDetailResponse";
public static final String LOAN_CAPITALIZED_INCOME_AMORTIZATION_ID = "loanCapitalizedIncomeAmortizationId";
public static final String LOAN_CAPITALIZED_INCOME_ADJUSTMENT_RESPONSE = "loanCapitalizedIncomeAdjustmentResponse";
public static final String LOAN_INTEREST_REFUND_RESPONSE = "loanInterestRefundResponse";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationContractTermination";
public static final String LOAN_CONTRACT_TERMINATION_RESPONSE = "loanContractTerminationResponse";
public static final String LOAN_UNDO_CONTRACT_TERMINATION_RESPONSE = "loanUndoContractTerminationResponse";
@@ -333,4 +333,202 @@ Feature: MerchantIssuedRefund
| 01 August 2024 | Accrual | 8.33 | 0.0 | 8.33 | 0.0 | 0.0 | 0.0 | false | false |
| 01 August 2024 | Accrual Activity | 8.33 | 0.0 | 8.33 | 0.0 | 0.0 | 0.0 | false | false |
| 05 August 2024 | Merchant Issued Refund | 10.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false |
Then Loan status will be "OVERPAID"
Then Loan status will be "OVERPAID"
@TestRailId:C3873
Scenario: Manual Interest Refund creation for Merchant Issued Refund with interestRefundCalculation = false
When Admin sets the business date to "01 July 2024"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 July 2024 | 1000 | 10 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 July 2024" with "1000" amount and expected disbursement date on "01 July 2024"
And Admin successfully disburse the loan on "01 July 2024" with "1000" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | | 669.43 | 330.57 | 8.33 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 2 | 31 | 01 September 2024 | | 336.11 | 333.32 | 5.58 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 336.11 | 2.8 | 0.0 | 0.0 | 338.91 | 0.0 | 0.0 | 0.0 | 338.91 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 16.71 | 0.0 | 0.0 | 1016.71 | 0.0 | 0.0 | 0.0 | 1016.71 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
When Admin sets the business date to "10 July 2024"
And Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "10 July 2024" with 338.9 EUR transaction amount and system-generated Idempotency key
When Admin sets the business date to "15 July 2024"
And Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "15 July 2024" with 50 EUR transaction amount and system-generated Idempotency key and interestRefundCalculation false
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.36 | 0.0 | 0.0 | 335.78 | 50.0 | 50.0 | 0.0 | 285.78 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 13.58 | 0.0 | 0.0 | 1013.58 | 388.9 | 388.9 | 0.0 | 624.68 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Merchant Issued Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
When Admin manually adds Interest Refund for "MERCHANT_ISSUED_REFUND" transaction made on "15 July 2024" with 0.19 EUR interest refund amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 0.19 | 0.19 | 0.0 | 338.71 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.36 | 0.0 | 0.0 | 335.78 | 50.0 | 50.0 | 0.0 | 285.78 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 13.58 | 0.0 | 0.0 | 1013.58 | 389.09 | 389.09 | 0.0 | 624.49 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Merchant Issued Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
| 15 July 2024 | Interest Refund | 0.19 | 0.0 | 0.19 | 0.0 | 0.0 | 613.52 | false | false |
Then Loan Transactions tab has a "MERCHANT_ISSUED_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 50.0 |
| LIABILITY | 145023 | Suspense/Clearing account | 50.0 | |
Then Loan Transactions tab has a "INTEREST_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112603 | Interest/Fee Receivable | | 0.19 |
| INCOME | 404000 | Interest Income | 0.19 | |
@TestRailId:C3874
Scenario: Undo Merchant Issued Refund with manual Interest Refund, both transactions are reversed
When Admin sets the business date to "01 July 2024"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 July 2024 | 1000 | 10 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 July 2024" with "1000" amount and expected disbursement date on "01 July 2024"
And Admin successfully disburse the loan on "01 July 2024" with "1000" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | | 669.43 | 330.57 | 8.33 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 2 | 31 | 01 September 2024 | | 336.11 | 333.32 | 5.58 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 336.11 | 2.8 | 0.0 | 0.0 | 338.91 | 0.0 | 0.0 | 0.0 | 338.91 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 16.71 | 0.0 | 0.0 | 1016.71 | 0.0 | 0.0 | 0.0 | 1016.71 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
When Admin sets the business date to "10 July 2024"
And Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "10 July 2024" with 338.9 EUR transaction amount and system-generated Idempotency key
When Admin sets the business date to "15 July 2024"
And Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "15 July 2024" with 50 EUR transaction amount and system-generated Idempotency key and interestRefundCalculation false
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.36 | 0.0 | 0.0 | 335.78 | 50.0 | 50.0 | 0.0 | 285.78 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 13.58 | 0.0 | 0.0 | 1013.58 | 388.9 | 388.9 | 0.0 | 624.68 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Merchant Issued Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
When Admin manually adds Interest Refund for "MERCHANT_ISSUED_REFUND" transaction made on "15 July 2024" with 0.19 EUR interest refund amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 0.19 | 0.19 | 0.0 | 338.71 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.36 | 0.0 | 0.0 | 335.78 | 50.0 | 50.0 | 0.0 | 285.78 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 13.58 | 0.0 | 0.0 | 1013.58 | 389.09 | 389.09 | 0.0 | 624.49 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Merchant Issued Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
| 15 July 2024 | Interest Refund | 0.19 | 0.0 | 0.19 | 0.0 | 0.0 | 613.52 | false | false |
Then Loan Transactions tab has a "MERCHANT_ISSUED_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 50.0 |
| LIABILITY | 145023 | Suspense/Clearing account | 50.0 | |
Then Loan Transactions tab has a "INTEREST_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112603 | Interest/Fee Receivable | | 0.19 |
| INCOME | 404000 | Interest Income | 0.19 | |
When Customer undo "1"th transaction made on "15 July 2024"
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 334.07 | 329.45 | 9.45 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 334.07 | 2.78 | 0.0 | 0.0 | 336.85 | 0.0 | 0.0 | 0.0 | 336.85 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 14.65 | 0.0 | 0.0 | 1014.65 | 338.9 | 338.9 | 0.0 | 675.75 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Merchant Issued Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | true | false |
| 15 July 2024 | Interest Refund | 0.19 | 0.0 | 0.19 | 0.0 | 0.0 | 613.52 | true | false |
Then Loan Transactions tab has a "MERCHANT_ISSUED_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 50.0 |
| LIABILITY | 145023 | Suspense/Clearing account | 50.0 | |
| ASSET | 112601 | Loans Receivable | 50.0 | |
| LIABILITY | 145023 | Suspense/Clearing account | | 50.0 |
Then Loan Transactions tab has a "INTEREST_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112603 | Interest/Fee Receivable | | 0.19 |
| INCOME | 404000 | Interest Income | 0.19 | |
| ASSET | 112603 | Interest/Fee Receivable | 0.19 | |
| INCOME | 404000 | Interest Income | | 0.19 |
@TestRailId:C3875
Scenario: Prevent manual Interest Refund creation if interestRefundCalculation = true and Interest Refund already exists
When Admin sets the business date to "01 July 2024"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 July 2024 | 1000 | 10 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 July 2024" with "1000" amount and expected disbursement date on "01 July 2024"
And Admin successfully disburse the loan on "01 July 2024" with "1000" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | | 669.43 | 330.57 | 8.33 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 2 | 31 | 01 September 2024 | | 336.11 | 333.32 | 5.58 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 336.11 | 2.8 | 0.0 | 0.0 | 338.91 | 0.0 | 0.0 | 0.0 | 338.91 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 16.71 | 0.0 | 0.0 | 1016.71 | 0.0 | 0.0 | 0.0 | 1016.71 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
When Admin sets the business date to "10 July 2024"
And Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "10 July 2024" with 338.9 EUR transaction amount and system-generated Idempotency key
When Admin sets the business date to "15 July 2024"
And Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "15 July 2024" with 50 EUR transaction amount and system-generated Idempotency key and interestRefundCalculation true
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 0.19 | 0.19 | 0.0 | 338.71 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.36 | 0.0 | 0.0 | 335.78 | 50.0 | 50.0 | 0.0 | 285.78 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 13.58 | 0.0 | 0.0 | 1013.58 | 389.09 | 389.09 | 0.0 | 624.49 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Merchant Issued Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
| 15 July 2024 | Interest Refund | 0.19 | 0.0 | 0.19 | 0.0 | 0.0 | 613.52 | false | false |
When Admin fails to add duplicate Interest Refund for "MERCHANT_ISSUED_REFUND" transaction made on "15 July 2024" with 0.19 EUR interest refund amount
@@ -192,4 +192,202 @@ Feature: PayoutRefund
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 950.0 | true | false |
| 10 July 2024 | Interest Refund | 0.12 | 0.0 | 0.12 | 0.0 | 0.0 | 950.0 | true | false |
| 15 July 2024 | Repayment | 100.0 | 96.24 | 3.76 | 0.0 | 0.0 | 903.76 | false | true |
| 15 July 2024 | Repayment | 100.0 | 96.24 | 3.76 | 0.0 | 0.0 | 903.76 | false | true |
@TestRailId:C3870
Scenario: Manual Interest Refund creation for Payout Refund with interestRefundCalculation = false
When Admin sets the business date to "01 July 2024"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 July 2024 | 1000 | 10 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 July 2024" with "1000" amount and expected disbursement date on "01 July 2024"
And Admin successfully disburse the loan on "01 July 2024" with "1000" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | | 669.43 | 330.57 | 8.33 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 2 | 31 | 01 September 2024 | | 336.11 | 333.32 | 5.58 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 336.11 | 2.8 | 0.0 | 0.0 | 338.91 | 0.0 | 0.0 | 0.0 | 338.91 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 16.71 | 0.0 | 0.0 | 1016.71 | 0.0 | 0.0 | 0.0 | 1016.71 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
When Admin sets the business date to "10 July 2024"
And Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "10 July 2024" with 338.9 EUR transaction amount and system-generated Idempotency key
When Admin sets the business date to "15 July 2024"
And Customer makes "PAYOUT_REFUND" transaction with "AUTOPAY" payment type on "15 July 2024" with 50 EUR transaction amount and system-generated Idempotency key and interestRefundCalculation false
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 50.0 | 50.0 | 0.0 | 288.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.78 | 0.0 | 0.0 | 336.2 | 0.0 | 0.0 | 0.0 | 336.2 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 14.0 | 0.0 | 0.0 | 1014.0 | 388.9 | 388.9 | 0.0 | 625.1 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
When Admin manually adds Interest Refund for "PAYOUT_REFUND" transaction made on "15 July 2024" with 0.19 EUR interest refund amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 50.19 | 50.19 | 0.0 | 288.71 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.78 | 0.0 | 0.0 | 336.2 | 0.0 | 0.0 | 0.0 | 336.2 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 14.0 | 0.0 | 0.0 | 1014.0 | 389.09 | 389.09 | 0.0 | 624.91 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
| 15 July 2024 | Interest Refund | 0.19 | 0.0 | 0.19 | 0.0 | 0.0 | 613.52 | false | false |
Then Loan Transactions tab has a "PAYOUT_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 50.0 |
| LIABILITY | 145023 | Suspense/Clearing account | 50.0 | |
Then Loan Transactions tab has a "INTEREST_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112603 | Interest/Fee Receivable | | 0.19 |
| INCOME | 404000 | Interest Income | 0.19 | |
@TestRailId:C3871
Scenario: Undo Payout Refund with manual Interest Refund, both transactions are reversed
When Admin sets the business date to "01 July 2024"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 July 2024 | 1000 | 10 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 July 2024" with "1000" amount and expected disbursement date on "01 July 2024"
And Admin successfully disburse the loan on "01 July 2024" with "1000" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | | 669.43 | 330.57 | 8.33 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 2 | 31 | 01 September 2024 | | 336.11 | 333.32 | 5.58 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 336.11 | 2.8 | 0.0 | 0.0 | 338.91 | 0.0 | 0.0 | 0.0 | 338.91 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 16.71 | 0.0 | 0.0 | 1016.71 | 0.0 | 0.0 | 0.0 | 1016.71 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
When Admin sets the business date to "10 July 2024"
And Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "10 July 2024" with 338.9 EUR transaction amount and system-generated Idempotency key
When Admin sets the business date to "15 July 2024"
And Customer makes "PAYOUT_REFUND" transaction with "AUTOPAY" payment type on "15 July 2024" with 50 EUR transaction amount and system-generated Idempotency key and interestRefundCalculation false
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 50.0 | 50.0 | 0.0 | 288.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.78 | 0.0 | 0.0 | 336.2 | 0.0 | 0.0 | 0.0 | 336.2 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 14.0 | 0.0 | 0.0 | 1014.0 | 388.9 | 388.9 | 0.0 | 625.1 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
When Admin manually adds Interest Refund for "PAYOUT_REFUND" transaction made on "15 July 2024" with 0.19 EUR interest refund amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 50.19 | 50.19 | 0.0 | 288.71 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.78 | 0.0 | 0.0 | 336.2 | 0.0 | 0.0 | 0.0 | 336.2 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 14.0 | 0.0 | 0.0 | 1014.0 | 389.09 | 389.09 | 0.0 | 624.91 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
| 15 July 2024 | Interest Refund | 0.19 | 0.0 | 0.19 | 0.0 | 0.0 | 613.52 | false | false |
Then Loan Transactions tab has a "PAYOUT_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 50.0 |
| LIABILITY | 145023 | Suspense/Clearing account | 50.0 | |
Then Loan Transactions tab has a "INTEREST_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112603 | Interest/Fee Receivable | | 0.19 |
| INCOME | 404000 | Interest Income | 0.19 | |
When Customer undo "1"th transaction made on "15 July 2024"
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 334.07 | 329.45 | 9.45 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 334.07 | 2.78 | 0.0 | 0.0 | 336.85 | 0.0 | 0.0 | 0.0 | 336.85 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 14.65 | 0.0 | 0.0 | 1014.65 | 338.9 | 338.9 | 0.0 | 675.75 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | true | false |
| 15 July 2024 | Interest Refund | 0.19 | 0.0 | 0.19 | 0.0 | 0.0 | 613.52 | true | false |
Then Loan Transactions tab has a "PAYOUT_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 50.0 |
| LIABILITY | 145023 | Suspense/Clearing account | 50.0 | |
| ASSET | 112601 | Loans Receivable | 50.0 | |
| LIABILITY | 145023 | Suspense/Clearing account | | 50.0 |
Then Loan Transactions tab has a "INTEREST_REFUND" transaction with date "15 July 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112603 | Interest/Fee Receivable | | 0.19 |
| INCOME | 404000 | Interest Income | 0.19 | |
| ASSET | 112603 | Interest/Fee Receivable | 0.19 | |
| INCOME | 404000 | Interest Income | | 0.19 |
@TestRailId:C3872
Scenario: Prevent manual Interest Refund creation if interestRefundCalculation = true and Interest Refund already exists
When Admin sets the business date to "01 July 2024"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 July 2024 | 1000 | 10 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 July 2024" with "1000" amount and expected disbursement date on "01 July 2024"
And Admin successfully disburse the loan on "01 July 2024" with "1000" EUR transaction amount
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | | 669.43 | 330.57 | 8.33 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 2 | 31 | 01 September 2024 | | 336.11 | 333.32 | 5.58 | 0.0 | 0.0 | 338.9 | 0.0 | 0.0 | 0.0 | 338.9 |
| 3 | 30 | 01 October 2024 | | 0.0 | 336.11 | 2.8 | 0.0 | 0.0 | 338.91 | 0.0 | 0.0 | 0.0 | 338.91 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 16.71 | 0.0 | 0.0 | 1016.71 | 0.0 | 0.0 | 0.0 | 1016.71 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
When Admin sets the business date to "10 July 2024"
And Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "10 July 2024" with 338.9 EUR transaction amount and system-generated Idempotency key
When Admin sets the business date to "15 July 2024"
And Customer makes "PAYOUT_REFUND" transaction with "AUTOPAY" payment type on "15 July 2024" with 50 EUR transaction amount and system-generated Idempotency key and interestRefundCalculation true
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 July 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 August 2024 | 10 July 2024 | 663.52 | 336.48 | 2.42 | 0.0 | 0.0 | 338.9 | 338.9 | 338.9 | 0.0 | 0.0 |
| 2 | 31 | 01 September 2024 | | 333.42 | 330.1 | 8.8 | 0.0 | 0.0 | 338.9 | 50.19 | 50.19 | 0.0 | 288.71 |
| 3 | 30 | 01 October 2024 | | 0.0 | 333.42 | 2.78 | 0.0 | 0.0 | 336.2 | 0.0 | 0.0 | 0.0 | 336.2 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 14.0 | 0.0 | 0.0 | 1014.0 | 389.09 | 389.09 | 0.0 | 624.91 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 July 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 10 July 2024 | Repayment | 338.9 | 336.48 | 2.42 | 0.0 | 0.0 | 663.52 | false | false |
| 15 July 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 613.52 | false | false |
| 15 July 2024 | Interest Refund | 0.19 | 0.0 | 0.19 | 0.0 | 0.0 | 613.52 | false | false |
When Admin fails to add duplicate Interest Refund for "PAYOUT_REFUND" transaction made on "15 July 2024" with 0.19 EUR interest refund amount
@@ -97,4 +97,7 @@ public interface LoanAccountDomainService {
LoanTransaction applyInterestRefund(Loan loan, LoanRefundRequestData loanRefundRequest);
void updateAndSaveLoanCollateralTransactionsForIndividualAccounts(Loan loan, LoanTransaction transaction);
LoanTransaction createManualInterestRefundWithAmount(Loan loan, LoanTransaction targetTransaction, BigDecimal amount,
PaymentDetail paymentDetail, ExternalId txnExternalId);
}
@@ -195,6 +195,11 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.INTEREST_REFUND, null, amount, date, externalId);
}
public static LoanTransaction interestRefund(final Loan loan, final BigDecimal amount, final LocalDate date,
final PaymentDetail paymentDetail, final ExternalId externalId) {
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.INTEREST_REFUND, paymentDetail, amount, date, externalId);
}
public static LoanTransaction chargeAdjustment(final Loan loan, final BigDecimal amount, final LocalDate transactionDate,
final ExternalId externalId, PaymentDetail paymentDetail) {
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_ADJUSTMENT, paymentDetail, amount, transactionDate,
@@ -0,0 +1,42 @@
/**
* 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.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@CommandType(entity = "LOAN_TRANSACTION", action = "MANUAL_INTEREST_REFUND")
public class ManualInterestRefundCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService loanWritePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return loanWritePlatformService.makeManualInterestRefund(command.getLoanId(), command.entityId(), command);
}
}
@@ -94,4 +94,6 @@ public interface LoanTransactionValidator {
void validateExternalId(DataValidatorBuilder baseDataValidator, JsonElement element);
void validateReversalExternalId(DataValidatorBuilder baseDataValidator, JsonElement element);
void validateManualInterestRefundTransaction(String json);
}
@@ -155,4 +155,6 @@ public interface LoanReadPlatformService {
List<Long> retrieveLoanIdsByExternalIds(List<ExternalId> externalIds);
boolean existsByLoanId(Long loanId);
LoanTransactionData retrieveManualInterestRefundTemplate(Long loanId, Long targetTransactionId);
}
@@ -125,4 +125,6 @@ public interface LoanWritePlatformService {
CommandProcessingResult undoChargeOff(JsonCommand command);
CommandProcessingResult makeRefund(Long loanId, LoanTransactionType loanTransactionType, JsonCommand command);
CommandProcessingResult makeManualInterestRefund(Long loanId, Long transactionId, JsonCommand command);
}
@@ -572,6 +572,11 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
loanTransactionValidator.validateReversalExternalId(baseDataValidator, element);
}
@Override
public void validateManualInterestRefundTransaction(final String json) {
loanTransactionValidator.validateManualInterestRefundTransaction(json);
}
private Set<String> getCapitalizedIncomeParameters() {
return new HashSet<>(
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
@@ -93,6 +93,7 @@ public class LoanTransactionsApiResource {
public static final String CAPITALIZED_INCOME = "capitalizedIncome";
public static final String CAPITALIZED_INCOME_ADJUSTMENT = "capitalizedIncomeAdjustment";
public static final String CONTRACT_TERMINATION = "contractTermination";
public static final String INTEREST_REFUND_COMMAND_VALUE = "interest-refund";
private final Set<String> responseDataParameters = new HashSet<>(Arrays.asList("id", "type", "date", "currency", "amount", "externalId",
LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME));
@@ -121,7 +122,8 @@ public class LoanTransactionsApiResource {
+ "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=interestPaymentWaiver"
+ "\n" + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)"
+ "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
+ "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n"
+ "loans/1/transactions/template?command=interest-refund")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
@@ -151,7 +153,8 @@ public class LoanTransactionsApiResource {
+ "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=interestPaymentWaiver"
+ "\n" + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)"
+ "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
+ "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n"
+ "loans/1/transactions/template?command=interest-refund")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
public String retrieveTransactionTemplate(
@@ -694,6 +697,8 @@ public class LoanTransactionsApiResource {
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_ADJUSTMENT_COMMAND)) {
transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId,
LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT, transactionId);
} else if (CommandParameterUtil.is(commandParam, INTEREST_REFUND_COMMAND_VALUE)) {
transactionData = this.loanReadPlatformService.retrieveManualInterestRefundTemplate(resolvedLoanId, transactionId);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
@@ -718,6 +723,8 @@ public class LoanTransactionsApiResource {
commandRequest = builder.capitalizedIncomeAdjustment(resolvedLoanId, resolvedTransactionId).build();
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_ADJUSTMENT_COMMAND)) {
commandRequest = builder.buyDownFeeAdjustment(resolvedLoanId, resolvedTransactionId).build();
} else if (CommandParameterUtil.is(commandParam, INTEREST_REFUND_COMMAND_VALUE)) {
commandRequest = builder.manualInterestRefund(resolvedLoanId, resolvedTransactionId).build();
} else { // Default to adjust the Loan Transaction
commandRequest = builder.adjustTransaction(resolvedLoanId, resolvedTransactionId).build();
}
@@ -206,7 +206,13 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final ExternalId txnExternalId = externalIdFactory.create();
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestRefundPreBusinessEvent(loan));
return LoanTransaction.interestRefund(loan, interestRefundAmount, refundTransaction.getDateOf(), txnExternalId);
}
@Override
public LoanTransaction createManualInterestRefundWithAmount(final Loan loan, final LoanTransaction targetTransaction,
final BigDecimal interestRefundAmount, final PaymentDetail paymentDetail, final ExternalId txnExternalId) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestRefundPreBusinessEvent(loan));
return LoanTransaction.interestRefund(loan, interestRefundAmount, targetTransaction.getDateOf(), paymentDetail, txnExternalId);
}
@Transactional
@@ -1089,4 +1089,35 @@ public final class LoanTransactionValidatorImpl implements LoanTransactionValida
baseDataValidator.reset().parameter("reversalExternalId").value(reversalExternalId).notExceedingLengthOf(100);
}
}
@Override
public void validateManualInterestRefundTransaction(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Set<String> transactionParameters = new HashSet<>(
Arrays.asList("transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId", "accountNumber",
"checkNumber", "routingCode", "receiptNumber", "bankNumber", "loanId", "numberOfRepayments"));
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.transaction");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element);
baseDataValidator.reset().parameter("externalId").value(externalId).ignoreIfNull().notExceedingLengthOf(100);
validatePaymentDetails(baseDataValidator, element);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
}
@@ -46,8 +46,10 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
import org.apache.fineract.infrastructure.core.api.ApiFacingEnum;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.data.StringEnumOptionData;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
@@ -192,6 +194,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
private final LoanBalanceService loanBalanceService;
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
private final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository;
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
@Override
public LoanAccountData retrieveOne(final Long loanId) {
@@ -2448,6 +2451,39 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
return loanRepositoryWrapper.existsByLoanId(loanId);
}
@Override
public LoanTransactionData retrieveManualInterestRefundTemplate(final Long loanId, final Long targetTransactionId) {
final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
final LoanTransaction targetTxn = loan.getLoanTransaction(txn -> txn.getId() != null && txn.getId().equals(targetTransactionId));
if (targetTxn == null) {
throw new LoanTransactionNotFoundException(targetTransactionId);
}
if (!(targetTxn.isMerchantIssuedRefund() || targetTxn.isPayoutRefund())) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.not.refund.type", "Only for refund transactions");
}
if (targetTxn.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.reversed", "Refund transaction is reversed");
}
final boolean alreadyExists = loan.getLoanTransactions().stream().anyMatch(txn -> txn.isInterestRefund() && !txn
.getLoanTransactionRelations(rel -> rel.getToTransaction() != null && rel.getToTransaction().equals(targetTxn)).isEmpty());
if (alreadyExists) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.interest.refund.already.exists",
"Interest Refund already exists for this refund");
}
final InterestRefundService interestRefundService = interestRefundServiceDelegate.lookupInterestRefundService(loan);
final Money totalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), targetTxn.getTransactionDate(),
List.of(), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList());
final Money newTotalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), targetTxn.getTransactionDate(),
List.of(targetTxn), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList());
final BigDecimal interestRefundAmount = totalInterest.minus(newTotalInterest).getAmount();
final Collection<PaymentTypeData> paymentTypeOptions = paymentTypeReadPlatformService.retrieveAllPaymentTypes();
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.INTEREST_REFUND);
return LoanTransactionData.loanTransactionDataForCreditTemplate(transactionType, targetTxn.getTransactionDate(),
interestRefundAmount, paymentTypeOptions, loan.getCurrency().toData());
}
private LoanTransaction deriveDefaultInterestWaiverTransaction(final Loan loan) {
final Money totalInterestOutstanding = loan.getTotalInterestOutstandingOnLoan();
Money possibleInterestToWaive = totalInterestOutstanding.copy();
@@ -98,6 +98,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestRefundPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoChargeOffBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
@@ -188,6 +189,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactio
import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
@@ -210,6 +212,7 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommand
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentParameter;
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.note.domain.Note;
@@ -3010,6 +3013,136 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build();
}
@Transactional
@Override
public CommandProcessingResult makeManualInterestRefund(final Long loanId, final Long transactionId, final JsonCommand command) {
loanTransactionValidator.validateManualInterestRefundTransaction(command.json());
Loan loan = loanAssembler.assembleFrom(loanId);
if (loan == null) {
throw new LoanNotFoundException(loanId);
}
final LoanTransaction targetTransaction = this.loanTransactionRepository.findByIdAndLoanId(transactionId, loanId)
.orElseThrow(() -> new LoanTransactionNotFoundException(transactionId, loanId));
final LocalDate transactionDate = targetTransaction.getDateOf();
if (!(targetTransaction.isMerchantIssuedRefund() || targetTransaction.isPayoutRefund())) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.not.refund.type",
"Target transaction must be Merchant Issued Refund or Payout Refund");
}
if (targetTransaction.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.reversed", "Target transaction is already reversed");
}
boolean alreadyHasInterestRefund = loan.getLoanTransactions().stream()
.anyMatch(txn -> txn.isInterestRefund()
&& !txn.getLoanTransactionRelations(rel -> rel.getToTransaction().equals(targetTransaction)).isEmpty()
&& !txn.isReversed());
if (alreadyHasInterestRefund) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.interest.refund.already.exists",
"Interest Refund already exists for this transaction");
}
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.interest.refund.amount.invalid",
"Amount must be provided and positive");
}
final boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream()
.map(LoanSupportedInterestRefundTypes::getTransactionType)
.anyMatch(transactionType -> transactionType.equals(targetTransaction.getTypeOf()));
if (!shouldCreateInterestRefundTransaction) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.interest.refund.not.supported",
"Interest Refund calculation is not supported for this loan");
}
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
changes.put(LoanApiConstants.externalIdParameterName, txnExternalId);
final LocalDate recalculateFrom = loan.isInterestBearingAndInterestRecalculationEnabled() ? transactionDate : null;
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, null);
this.loanTransactionValidator.validateRefund(loan, LoanTransactionType.INTEREST_REFUND, transactionDate, scheduleGeneratorDTO);
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
createNote(loan, command, changes);
final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
final LoanTransaction interestRefundTxn = loanAccountDomainService.createManualInterestRefundWithAmount(loan, targetTransaction,
amount, paymentDetail, txnExternalId);
interestRefundTxn.getLoanTransactionRelations().add(
LoanTransactionRelation.linkToTransaction(interestRefundTxn, targetTransaction, LoanTransactionRelationTypeEnum.RELATED));
final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan,
interestRefundTxn);
final LoanRepaymentScheduleInstallment currentInstallment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(transactionDate);
final boolean processLatest = isTransactionChronologicallyLatest //
&& !loan.isForeclosure() //
&& !loan.hasChargesAffectedByBackdatedRepaymentLikeTransaction(interestRefundTxn) //
&& loanTransactionProcessingService.canProcessLatestTransactionOnly(loan, interestRefundTxn, currentInstallment); //
if (processLatest) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), interestRefundTxn,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.addLoanTransaction(interestRefundTxn);
} else {
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
loan.addLoanTransaction(interestRefundTxn);
reprocessLoanTransactionsService.reprocessTransactions(loan);
}
// Update outstanding loan balances
loanBalanceService.updateLoanOutstandingBalances(loan);
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(interestRefundTxn);
businessEventNotifierService.notifyPostBusinessEvent(new LoanTransactionInterestRefundPostBusinessEvent(interestRefundTxn));
// Accrual reprocessing
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate);
loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
loanAccountDomainService.updateAndSavePostDatedChecksForIndividualAccount(loan, interestRefundTxn);
loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, interestRefundTxn);
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, loan.isInterestBearingAndInterestRecalculationEnabled(),
false);
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withLoanId(loan.getId()) //
.withEntityId(targetTransaction.getId()) //
.withEntityExternalId(targetTransaction.getExternalId()) //
.withSubEntityId(interestRefundTxn.getId()) //
.withSubEntityExternalId(interestRefundTxn.getExternalId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.with(changes) //
.build();
}
public void handleChargebackTransaction(final Loan loan, LoanTransaction chargebackTransaction) {
loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction);
@@ -3626,5 +3759,4 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.setExpectedMaturityDate(latestRepaymentDate);
}
}
}
@@ -110,6 +110,7 @@ import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlat
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundServiceDelegate;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccountServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualActivityProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualEventService;
@@ -177,6 +178,7 @@ import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
@@ -344,7 +346,8 @@ public class LoanAccountConfiguration {
LoanForeclosureValidator loanForeclosureValidator, LoanTransactionMapper loanTransactionMapper,
LoanTransactionProcessingService loanTransactionProcessingService, LoanBalanceService loanBalanceService,
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository,
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository) {
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository,
@Lazy InterestRefundServiceDelegate interestRefundServiceDelegate) {
return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository,
loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService,
fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService,
@@ -352,7 +355,7 @@ public class LoanAccountConfiguration {
loanUtilService, configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator,
delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService,
loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService, loanBalanceService,
loanCapitalizedIncomeBalanceRepository, loanBuyDownFeeBalanceRepository);
loanCapitalizedIncomeBalanceRepository, loanBuyDownFeeBalanceRepository, interestRefundServiceDelegate);
}
@Bean