Skip to content

Commit 221a2db

Browse files
ruzeynalovJose Alberto Hernandez
authored andcommitted
FINERACT-2421: added e2e test for verifying reprocess Interest Refund txn amount if no txn was changed
1 parent 8ba1e15 commit 221a2db

File tree

8 files changed

+139
-33
lines changed

8 files changed

+139
-33
lines changed

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public enum DefaultLoanProduct implements LoanProduct {
8787
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND, //
8888
LP1_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, //
8989
LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, //
90+
LP2_ADV_PMT_ALLOC_ACTUAL_ACTUAL_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, //
9091
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_WHOLE_TERM, //
9192
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_PRECLOSE_LAST_INSTALLMENT_STRATEGY, //
9293
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION, //

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4230,6 +4230,27 @@ public void initialize() throws Exception {
42304230
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_CHARGEBACK,
42314231
responseLoanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcDailyMultiDisburseChargeback);
42324232

4233+
// LP2 + interest recalculation + advanced custom payment allocation + progressive loan schedule + horizontal
4234+
// (LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL)
4235+
String name152 = DefaultLoanProduct.LP2_ADV_PMT_ALLOC_ACTUAL_ACTUAL_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL.getName();
4236+
4237+
PostLoanProductsRequest loanProductsRequestAdvPaymentAllocationActualActualProgressiveLoanSchedule = loanProductsRequestFactory
4238+
.defaultLoanProductsRequestLP2InterestDailyRecalculation()//
4239+
.name(name152)//
4240+
.supportedInterestRefundTypes(Arrays.asList("MERCHANT_ISSUED_REFUND", "PAYOUT_REFUND"))//
4241+
.enableAccrualActivityPosting(true) //
4242+
.daysInYearType(DaysInYearType.ACTUAL.value)//
4243+
.daysInMonthType(DaysInMonthType.ACTUAL.value)//
4244+
.paymentAllocation(List.of(//
4245+
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
4246+
createPaymentAllocation("PAYOUT_REFUND", "LAST_INSTALLMENT"), //
4247+
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "LAST_INSTALLMENT")));//
4248+
PostLoanProductsResponse responseLoanProductsRequestAdvPaymentAllocationActualActualProgressiveLoanSchedule = createLoanProductIdempotent(
4249+
loanProductsRequestAdvPaymentAllocationActualActualProgressiveLoanSchedule);
4250+
TestContext.INSTANCE.set(
4251+
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_PAYMENT_ALLOCATION_ACTUAL_ACTUAL_PROGRESSIVE_LOAN_SCHEDULE,
4252+
responseLoanProductsRequestAdvPaymentAllocationActualActualProgressiveLoanSchedule);
4253+
42334254
// LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_PRINCIPAL_FIRST
42344255
// Same as LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL but with PRINCIPAL before INTEREST
42354256
// in payment allocation order

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public abstract class TestContextKey {
8888
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_AUTO = "loanProductCreateResponseLP2DownPaymentAuto";
8989
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE = "loanProductCreateResponseLP2DownPaymentProgressiveLoanSchedule";
9090
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_CUSTOM_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE = "loanProductCreateResponseLP2ProgressiveLoanScheduleCustomPaymentAllocation";
91+
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_PAYMENT_ALLOCATION_ACTUAL_ACTUAL_PROGRESSIVE_LOAN_SCHEDULE = "loanProductCreateResponseLP2ProgressiveLoanScheduleActualActualAdvancedPaymentAllocation";
9192
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE_VERTICAL = "loanProductCreateResponseLP2DownPaymentProgressiveLoanScheduleVertical";
9293
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE_INSTALLMENT_LEVEL_DELINQUENCY = "loanProductCreateResponseLP2DownPaymentProgressiveLoanScheduleInstallmentLevelDelinquency";
9394
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROG_SCHEDULE_HOR_INST_LVL_DELINQUENCY_CREDIT_ALLOCATION = "loanProductCreateResponseLP2DownPaymentProgressiveLoanScheduleHorizontalInstallmentLevelDelinquencyCreditAllocation";

fineract-e2e-tests-runner/src/test/resources/features/LoanMerchantIssuedRefund.feature

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,3 +637,62 @@ Feature: MerchantIssuedRefund
637637
| 02 October 2025 | Interest Refund | 17.07 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false |
638638
| 02 October 2025 | Accrual | 18.33 | 0.0 | 18.33 | 0.0 | 0.0 | 0.0 | false | false |
639639
| 02 October 2025 | Accrual Activity | 8.97 | 0.0 | 8.97 | 0.0 | 0.0 | 0.0 | false | false |
640+
641+
@TestRailId:C4355
642+
Scenario: Verify manual Interest Refund is recalculated during reprocessing even if no prior transaction was reverse-replayed
643+
When Admin sets the business date to "01 January 2025"
644+
And Admin creates a client with random data
645+
And Admin creates a fully customized loan with the following data:
646+
| 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 |
647+
| LP2_ADV_PMT_ALLOC_ACTUAL_ACTUAL_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 100 | 26 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
648+
And Admin successfully approves the loan on "01 January 2025" with "100" amount and expected disbursement date on "01 January 2025"
649+
And Admin successfully disburse the loan on "01 January 2025" with "100" EUR transaction amount
650+
And Loan Transactions tab has the following data:
651+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
652+
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
653+
# MIR with interestRefundCalculation=false (no auto Interest Refund)
654+
When Admin sets the business date to "01 February 2025"
655+
And Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "01 February 2025" with 66.41 EUR transaction amount and system-generated Idempotency key and interestRefundCalculation false
656+
Then Loan Transactions tab has the following data:
657+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
658+
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
659+
| 01 February 2025 | Merchant Issued Refund | 66.41 | 64.2 | 2.21 | 0.0 | 0.0 | 35.8 | false | false |
660+
# Manually create Interest Refund with arbitrary amount (0.47 EUR)
661+
When Admin manually adds Interest Refund for "MERCHANT_ISSUED_REFUND" transaction made on "01 February 2025" with 0.47 EUR interest refund amount
662+
Then Loan Transactions tab has the following data:
663+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
664+
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
665+
| 01 February 2025 | Merchant Issued Refund | 66.41 | 64.2 | 2.21 | 0.0 | 0.0 | 35.8 | false | false |
666+
| 01 February 2025 | Interest Refund | 0.47 | 0.47 | 0.0 | 0.0 | 0.0 | 35.33 | false | false |
667+
# Backdated repayment on 20 January (before MIR) - triggers replay of MIR
668+
And Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "20 January 2025" with 17.94 EUR transaction amount and system-generated Idempotency key
669+
Then Loan Transactions tab has the following data:
670+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
671+
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
672+
| 20 January 2025 | Repayment | 17.94 | 16.59 | 1.35 | 0.0 | 0.0 | 83.41 | false | false |
673+
| 01 February 2025 | Merchant Issued Refund | 66.41 | 66.41 | 0.0 | 0.0 | 0.0 | 17.0 | false | true |
674+
| 01 February 2025 | Interest Refund | 1.48 | 1.48 | 0.0 | 0.0 | 0.0 | 15.52 | false | true |
675+
# Step 4: Make another repayment on 25 January (also before MIR)
676+
And Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "25 January 2025" with 10.94 EUR transaction amount and system-generated Idempotency key
677+
Then Loan Transactions tab has the following data:
678+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
679+
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
680+
| 20 January 2025 | Repayment | 17.94 | 16.59 | 1.35 | 0.0 | 0.0 | 83.41 | false | false |
681+
| 25 January 2025 | Repayment | 10.94 | 10.94 | 0.0 | 0.0 | 0.0 | 72.47 | false | false |
682+
| 01 February 2025 | Merchant Issued Refund | 66.41 | 66.41 | 0.0 | 0.0 | 0.0 | 6.06 | false | true |
683+
| 01 February 2025 | Interest Refund | 1.47 | 1.47 | 0.0 | 0.0 | 0.0 | 4.59 | false | true |
684+
# Step 5: Reverse the 1st repayment (20 Jan) - this triggers full transaction reprocessing
685+
# Key expectation: Interest Refund should be RECALCULATED even though MIR wasn't modified
686+
# Before fix: Interest Refund kept 1.47 because no txn before it was changed
687+
# After fix: Interest Refund recalculated to correct value
688+
When Customer undo "1"th "Repayment" transaction made on "20 January 2025"
689+
Then Loan Transactions tab has the following data:
690+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
691+
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
692+
| 20 January 2025 | Repayment | 17.94 | 16.59 | 1.35 | 0.0 | 0.0 | 83.41 | true | false |
693+
| 25 January 2025 | Repayment | 10.94 | 10.94 | 0.0 | 0.0 | 0.0 | 89.06 | false | false |
694+
| 01 February 2025 | Merchant Issued Refund | 66.41 | 64.26 | 2.15 | 0.0 | 0.0 | 24.8 | false | true |
695+
#Interest Refund value is only slightly different (1.46 vs 1.47) which I believe is wrong
696+
| 01 February 2025 | Interest Refund | 1.46 | 1.46 | 0.0 | 0.0 | 0.0 | 23.34 | false | true |
697+
#following steps will fail if Interest Refund is not recalculated properly
698+
Then Loan has 23.97 outstanding amount

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/TransactionChangeData.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import lombok.AllArgsConstructor;
2222
import lombok.Getter;
2323
import lombok.Setter;
24+
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
2425
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
2526

2627
/**
@@ -33,4 +34,11 @@ public class TransactionChangeData {
3334

3435
private LoanTransaction oldTransaction;
3536
private LoanTransaction newTransaction;
37+
38+
public boolean isBeingReprocessed(final LoanTransaction loanTransaction) {
39+
final MonetaryCurrency currency = loanTransaction.getLoan().getCurrency();
40+
return (oldTransaction.getId() != null && oldTransaction.getTypeOf().equals(loanTransaction.getTypeOf()) //
41+
&& oldTransaction.getTransactionDate().equals(loanTransaction.getTransactionDate()) //
42+
&& (oldTransaction.getAmount(currency).compareTo(loanTransaction.getAmount(currency)) == 0));
43+
}
3644
}

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -568,16 +568,26 @@ private void handleInterestRefund(final LoanTransaction loanTransaction, final T
568568
.filter(LoanTransaction::isNotReversed).map(AbstractPersistableCustom::getId).toList();
569569
final List<LoanTransaction> modifiedTransactions = new ArrayList<>(progCtx.getAlreadyProcessedTransactions().stream()
570570
.filter(LoanTransaction::isNotReversed).filter(tr -> tr.getId() == null).toList());
571-
final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), targetDate,
572-
modifiedTransactions, unmodifiedTransactionIds);
573-
final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund);
574-
loanTransaction.updateAmount(newAmount.getAmount());
571+
if (validatePreviousInterestRefundTransaction(progCtx.getChangedTransactionDetail().getTransactionChanges(),
572+
loanTransaction)) {
573+
final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), targetDate,
574+
modifiedTransactions, unmodifiedTransactionIds);
575+
final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund);
576+
loanTransaction.updateAmount(newAmount.getAmount());
577+
}
575578
progCtx.setSumOfInterestRefundAmount(progCtx.getSumOfInterestRefundAmount().add(loanTransaction.getAmount()));
576579
}
577580
}
578581
handleRepayment(loanTransaction, ctx);
579582
}
580583

584+
private boolean validatePreviousInterestRefundTransaction(final List<TransactionChangeData> transactionChanges,
585+
final LoanTransaction interestRefundTransaction) {
586+
Optional<TransactionChangeData> optionalTransactionChangeData = transactionChanges.stream()
587+
.filter(tc -> tc.isBeingReprocessed(interestRefundTransaction)).findFirst();
588+
return optionalTransactionChangeData.isPresent();
589+
}
590+
581591
private boolean chargeOffIsInEffect(TransactionCtx ctx, LoanTransaction chargeOffTransaction, LoanTransaction loanTransaction) {
582592
if (ctx instanceof ProgressiveTransactionCtx progressiveCtx && progressiveCtx.isChargedOff()) {
583593
return true;

0 commit comments

Comments
 (0)