diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java index 82972057d..4408de249 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java @@ -69,7 +69,9 @@ public enum DefaultLoanProduct implements LoanProduct { LP1_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, // LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, // LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_WHOLE_TERM, // - LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_PRECLOSE_LAST_INSTALLMENT_STRATEGY;// + LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_PRECLOSE_LAST_INSTALLMENT_STRATEGY, // + LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION, // + ; @Override public String getName() { diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java index 0fecd42a6..09a4f02e0 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java @@ -990,6 +990,28 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ TestContext.INSTANCE.set( TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_LAST_INSTALLMENT, loanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcDailyTillPrecloseLastInstallmentResponse); + + // LP2 with progressive loan schedule + horizontal + interest EMI + actual/actual + interest refund with + // Merchant issued and Payment refund + interest recalculation + // (LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION) + String name48 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION.getName(); + PostLoanProductsRequest loanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundRecalculation = loanProductsRequestFactory + .defaultLoanProductsRequestLP2Emi()// + .isInterestRecalculationEnabled(true)// + .preClosureInterestCalculationStrategy(1)// + .rescheduleStrategyMethod(4)// + .interestRecalculationCompoundingMethod(0)// + .recalculationRestFrequencyType(2)// + .recalculationRestFrequencyInterval(1)// + .name(name48)// + .supportedInterestRefundTypes(Arrays.asList("MERCHANT_ISSUED_REFUND", "PAYOUT_REFUND"))// + .paymentAllocation(List.of(// + createPaymentAllocation("DEFAULT", "LAST_INSTALLMENT")));// + Response responseLoanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundInterestRecalculation = loanProductsApi + .createLoanProduct(loanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundRecalculation).execute(); + TestContext.INSTANCE.set( + TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION, + responseLoanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundInterestRecalculation); } public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule, diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java index 6cc1f37a7..62c01d592 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java @@ -22,6 +22,7 @@ import static org.apache.fineract.test.data.TransactionProcessingStrategyCode.AD import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.gson.Gson; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -41,6 +42,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -56,6 +58,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdDelinquencySummary; import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData; import org.apache.fineract.client.models.GetLoansLoanIdLoanChargePaidByData; import org.apache.fineract.client.models.GetLoansLoanIdLoanTermVariations; +import org.apache.fineract.client.models.GetLoansLoanIdLoanTransactionRelation; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule; import org.apache.fineract.client.models.GetLoansLoanIdResponse; @@ -2498,6 +2501,29 @@ public class LoanStepDef extends AbstractStepDef { }); } + @Then("In Loan Transactions the {string}th Transaction has relationship type={} with the {string}th Transaction") + public void loanTransactionsRelationshipCheck(String nthTransactionFromStr, String relationshipType, String nthTransactionToStr) + throws IOException { + final Response loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final long loanId = loanCreateResponse.body().getLoanId(); + + final Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); + ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); + + final List transactions = loanDetailsResponse.body().getTransactions(); + final int nthTransactionFrom = nthTransactionFromStr == null ? transactions.size() - 1 + : Integer.parseInt(nthTransactionFromStr) - 1; + final int nthTransactionTo = nthTransactionToStr == null ? transactions.size() - 1 : Integer.parseInt(nthTransactionToStr) - 1; + final GetLoansLoanIdTransactions transactionFrom = transactions.get(nthTransactionFrom); + final GetLoansLoanIdTransactions transactionTo = transactions.get(nthTransactionTo); + + final Optional relationshipOptional = transactionFrom.getTransactionRelations().stream() + .filter(r -> r.getRelationType().equals(relationshipType)) + .filter(r -> r.getToLoanTransaction().equals(transactionTo.getId())).findFirst(); + + assertTrue(relationshipOptional.isPresent(), "Missed relationship between transactions"); + } + private void createCustomizedLoan(final List loanData, final boolean withEmi) throws IOException { final String loanProduct = loanData.get(0); final String submitDate = loanData.get(1); diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java index 0502844ce..3397a2e97 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java @@ -102,6 +102,7 @@ public abstract class TestContextKey { public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmiActualActualInterestRefund"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_ADVANCED_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL = "loanProductCreateResponseLP1ProgressiveLoanScheduleHorizontal"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_WHOLE_TERM = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationSameAsRepTillPreCloseWholeTerm"; + public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmiActualActualInterestRefundFInterestRecalculation"; public static final String CHARGE_FOR_LOAN_PERCENT_LATE_CREATE_RESPONSE = "ChargeForLoanPercentLateCreateResponse"; public static final String CHARGE_FOR_LOAN_PERCENT_LATE_AMOUNT_PLUS_INTEREST_CREATE_RESPONSE = "ChargeForLoanPercentLateAmountPlusInterestCreateResponse"; public static final String CHARGE_FOR_LOAN_PERCENT_PROCESSING_CREATE_RESPONSE = "ChargeForLoanPercentProcessingCreateResponse"; diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature index e4f439412..6dc3164e5 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature @@ -3397,3 +3397,42 @@ Feature: LoanRepayment When Admin makes Credit Balance Refund transaction on "10 September 2024" with 91.21 EUR transaction amount When Customer undo "1"th "Repayment" transaction made on "24 June 2024" Then Loan status will be "ACTIVE" + + Scenario: Verify the relationship for Interest Refund transaction after repayment by reverting related transaction + When Admin sets the business date to "30 January 2024" + When Admin creates a client with random data + When 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_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION | 01 January 2024 | 200 | 15 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "200" amount and expected disbursement date on "01 January 2024" + And Admin successfully disburse the loan on "01 January 2024" with "200" EUR transaction amount + When Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "15 January 2024" with 50 EUR transaction amount and self-generated Idempotency key + And Customer makes "PAYOUT_REFUND" transaction with "AUTOPAY" payment type on "16 January 2024" with 50 EUR transaction amount and self-generated Idempotency key + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 | false | + | 15 January 2024 | Merchant Issued Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | + | 15 January 2024 | Interest Refund | 0.29 | 0.29 | 0.0 | 0.0 | 0.0 | 149.71 | false | + | 16 January 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 99.71 | false | + | 16 January 2024 | Interest Refund | 0.31 | 0.31 | 0.0 | 0.0 | 0.0 | 99.4 | false | + When Customer makes "AUTOPAY" repayment on "10 January 2024" with 25 EUR transaction amount (and transaction fails because of wrong date) + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 | false | + | 10 January 2024 | Repayment | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | 175.0 | false | + | 15 January 2024 | Merchant Issued Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 125.0 | false | + | 15 January 2024 | Interest Refund | 0.29 | 0.29 | 0.0 | 0.0 | 0.0 | 124.71 | false | + | 16 January 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 74.71 | false | + | 16 January 2024 | Interest Refund | 0.31 | 0.31 | 0.0 | 0.0 | 0.0 | 74.4 | false | + Then In Loan Transactions the "4"th Transaction has relationship type=RELATED with the "3"th Transaction + Then In Loan Transactions the "6"th Transaction has relationship type=RELATED with the "5"th Transaction + When Customer undo "1"th "Merchant Issued Refund" transaction made on "15 January 2024" + When Customer undo "1"th "Payout Refund" transaction made on "16 January 2024" + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 | false | + | 10 January 2024 | Repayment | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | 175.0 | false | + | 15 January 2024 | Merchant Issued Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 125.0 | true | + | 15 January 2024 | Interest Refund | 0.29 | 0.29 | 0.0 | 0.0 | 0.0 | 124.71 | true | + | 16 January 2024 | Payout Refund | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 125.0 | true | + | 16 January 2024 | Interest Refund | 0.31 | 0.31 | 0.0 | 0.0 | 0.0 | 124.69 | true | \ No newline at end of file diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java index d74f7757c..7e6de7248 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java @@ -27,6 +27,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import lombok.Getter; +import lombok.Setter; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; @Getter @@ -38,6 +39,7 @@ public class LoanTransactionRelation extends AbstractAuditableWithUTCDateTimeCus @JoinColumn(name = "from_loan_transaction_id", nullable = false) private LoanTransaction fromTransaction; + @Setter @ManyToOne @JoinColumn(name = "to_loan_transaction_id") private LoanTransaction toTransaction; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index fde017758..d2d97484c 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -743,9 +743,22 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep return null; } - protected void createNewTransaction(LoanTransaction oldTransaction, LoanTransaction newTransaction, TransactionCtx ctx) { + protected void createNewTransaction(final LoanTransaction oldTransaction, final LoanTransaction newTransaction, + final TransactionCtx ctx) { oldTransaction.updateExternalId(null); oldTransaction.getLoanChargesPaid().clear(); + + if (newTransaction.getTypeOf().isInterestRefund()) { + newTransaction.getLoanTransactionRelations().stream().filter( + r -> r.getToTransaction().getTypeOf().isMerchantIssuedRefund() || r.getToTransaction().getTypeOf().isPayoutRefund()) + .filter(r -> r.getToTransaction().isReversed()) + .forEach(newRelation -> oldTransaction.getLoanTransactionRelations().stream() + .filter(oldRelation -> LoanTransactionRelationTypeEnum.RELATED.equals(oldRelation.getRelationType())) + .findFirst().map(oldRelation -> oldRelation.getToTransaction().getId()) + .ifPresent(oldToTransactionId -> newRelation.setToTransaction( + ctx.getChangedTransactionDetail().getNewTransactionMappings().get(oldToTransactionId)))); + } + // Adding Replayed relation from newly created transaction to reversed transaction newTransaction.getLoanTransactionRelations() .add(LoanTransactionRelation.linkToTransaction(newTransaction, oldTransaction, LoanTransactionRelationTypeEnum.REPLAYED));