FINERACT-2211: Fix-Incorrect LoanSchedule after Full Refund with LAST_INSTALLMENT rule on 12m interest bearing Term loan

This commit is contained in:
Adam Saghy
2025-03-12 15:00:14 +00:00
parent a28b9332b9
commit 4ea0acdcf1
3 changed files with 115 additions and 16 deletions
@@ -6362,4 +6362,83 @@ Feature: Loan
| 23 December 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 |
| 22 March 2025 | Accrual | 0.67 | 0.0 | 0.67 | 0.0 | 0.0 | 0.0 |
@TestRailId:C3534 @AdvancedPaymentAllocation
Scenario: Verify advanced payment allocation - future installments: LAST_INSTALLMENT, full Merchant issued Refund on the disbursement date, 2nd disbursement on the same date
When Admin sets the business date to "11 March 2025"
And Admin creates a client with random data
When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_FULL" loan product "MERCHANT_ISSUED_REFUND" transaction type to "LAST_INSTALLMENT" future installment allocation rule
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_FULL | 11 March 2025 | 1000 | 26 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 12 | MONTHS | 1 | MONTHS | 12 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "11 March 2025" with "1000" amount and expected disbursement date on "11 March 2025"
And Admin successfully disburse the loan on "11 March 2025" with "200" EUR transaction amount
Then Loan Repayment schedule has 12 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 |
| | | 11 March 2025 | | 200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 11 April 2025 | | 185.3 | 14.7 | 4.42 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 2 | 30 | 11 May 2025 | | 170.14 | 15.16 | 3.96 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 3 | 31 | 11 June 2025 | | 154.78 | 15.36 | 3.76 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 4 | 30 | 11 July 2025 | | 138.97 | 15.81 | 3.31 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 5 | 31 | 11 August 2025 | | 122.92 | 16.05 | 3.07 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 6 | 31 | 11 September 2025 | | 106.51 | 16.41 | 2.71 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 7 | 30 | 11 October 2025 | | 89.67 | 16.84 | 2.28 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 8 | 31 | 11 November 2025 | | 72.53 | 17.14 | 1.98 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 9 | 30 | 11 December 2025 | | 54.96 | 17.57 | 1.55 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 10 | 31 | 11 January 2026 | | 37.05 | 17.91 | 1.21 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 11 | 31 | 11 February 2026 | | 18.75 | 18.3 | 0.82 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
| 12 | 28 | 11 March 2026 | | 0.0 | 18.75 | 0.37 | 0.0 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 200.0 | 29.44 | 0.0 | 0 | 229.44 | 0.0 | 0.0 | 0.0 | 229.44 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 11 March 2025 | Disbursement | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 |
When Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "11 March 2025" with 200 EUR transaction amount and system-generated Idempotency key
Then Loan Repayment schedule has 12 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 |
| | | 11 March 2025 | | 200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 11 April 2025 | 11 March 2025 | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2 | 30 | 11 May 2025 | 11 March 2025 | 191.2 | 8.8 | 0.0 | 0.0 | 0.0 | 8.8 | 8.8 | 8.8 | 0.0 | 0.0 |
| 3 | 31 | 11 June 2025 | 11 March 2025 | 172.08 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
| 4 | 30 | 11 July 2025 | 11 March 2025 | 152.96 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
| 5 | 31 | 11 August 2025 | 11 March 2025 | 133.84 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
| 6 | 31 | 11 September 2025 | 11 March 2025 | 114.72 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
| 7 | 30 | 11 October 2025 | 11 March 2025 | 95.6 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
| 8 | 31 | 11 November 2025 | 11 March 2025 | 76.48 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
| 9 | 30 | 11 December 2025 | 11 March 2025 | 57.36 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
| 10 | 31 | 11 January 2026 | 11 March 2025 | 38.24 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
| 11 | 31 | 11 February 2026 | 11 March 2025 | 19.12 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
| 12 | 28 | 11 March 2026 | 11 March 2025 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 200.0 | 0 | 0.0 | 0 | 200.0 | 200.0 | 200.0 | 0.0 | 0.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 11 March 2025 | Disbursement | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 |
| 11 March 2025 | Merchant Issued Refund | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 |
And Admin successfully disburse the loan on "11 March 2025" with "200" EUR transaction amount
Then Loan Repayment schedule has 12 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 |
| | | 11 March 2025 | | 200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| | | 11 March 2025 | | 200.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 11 April 2025 | | 366.18 | 33.82 | 4.42 | 0.0 | 0.0 | 38.24 | 0.0 | 0.0 | 0.0 | 38.24 |
| 2 | 30 | 11 May 2025 | | 331.49 | 34.69 | 3.55 | 0.0 | 0.0 | 38.24 | 8.8 | 8.8 | 0.0 | 29.44 |
| 3 | 31 | 11 June 2025 | | 296.35 | 35.14 | 3.1 | 0.0 | 0.0 | 38.24 | 19.12 | 19.12 | 0.0 | 19.12 |
| 4 | 30 | 11 July 2025 | | 260.77 | 35.58 | 2.66 | 0.0 | 0.0 | 38.24 | 19.12 | 19.12 | 0.0 | 19.12 |
| 5 | 31 | 11 August 2025 | | 224.91 | 35.86 | 2.38 | 0.0 | 0.0 | 38.24 | 19.12 | 19.12 | 0.0 | 19.12 |
| 6 | 31 | 11 September 2025 | | 188.68 | 36.23 | 2.01 | 0.0 | 0.0 | 38.24 | 19.12 | 19.12 | 0.0 | 19.12 |
| 7 | 30 | 11 October 2025 | | 152.02 | 36.66 | 1.58 | 0.0 | 0.0 | 38.24 | 19.12 | 19.12 | 0.0 | 19.12 |
| 8 | 31 | 11 November 2025 | | 115.03 | 36.99 | 1.25 | 0.0 | 0.0 | 38.24 | 19.12 | 19.12 | 0.0 | 19.12 |
| 9 | 30 | 11 December 2025 | | 77.61 | 37.42 | 0.82 | 0.0 | 0.0 | 38.24 | 19.12 | 19.12 | 0.0 | 19.12 |
| 10 | 31 | 11 January 2026 | | 39.82 | 37.79 | 0.45 | 0.0 | 0.0 | 38.24 | 19.12 | 19.12 | 0.0 | 19.12 |
| 11 | 31 | 11 February 2026 | | 19.12 | 20.7 | 0.03 | 0.0 | 0.0 | 20.73 | 19.12 | 19.12 | 0.0 | 1.61 |
| 12 | 28 | 11 March 2026 | 11 March 2025 | 0.0 | 19.12 | 0.0 | 0.0 | 0.0 | 19.12 | 19.12 | 19.12 | 0.0 | 0.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 400.0 | 22.25 | 0.0 | 0 | 422.25 | 200.0 | 200.0 | 0.0 | 222.25 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 11 March 2025 | Disbursement | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 |
| 11 March 2025 | Merchant Issued Refund | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 11 March 2025 | Disbursement | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 200.0 |
When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product "MERCHANT_ISSUED_REFUND" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
@@ -1063,4 +1063,9 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
return DateUtils.isAfter(referenceDate, getFromDate()) && !DateUtils.isAfter(referenceDate, getDueDate());
}
public boolean isDueBalanceZero() {
return MathUtil.isZero(
MathUtil.nullToZero(MathUtil.add(getPrincipal(), getInterestCharged(), getFeeChargesCharged(), getPenaltyCharges())));
}
}
@@ -1861,27 +1861,42 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
.toList(); //
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
.toList(); //
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
inAdvanceInstallments = installments.stream()
// In case of Last installment strategy it could occur the projected EMI of an installment is
// zero,
// but we should still involve this period to allocated further amounts and pushing this till we
// run ouf of unallocated amounts
.filter(i -> i.isNotFullyPaidOff() || i.isDueBalanceZero()) //
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
.toList(); //
} else if (FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
// try to resolve as current installment ( not due )
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate())).filter(f -> loanTransaction.isAfter(f.getFromDate())
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1))
.toList();
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
.filter(f -> loanTransaction.isAfter(f.getFromDate())
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1)) //
.toList(); //
// if there is no current installment, resolve similar to LAST_INSTALLMENT
if (inAdvanceInstallments.isEmpty()) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
inAdvanceInstallments = installments.stream()
// In case of Last installment strategy it could occur the projected EMI of an installment
// is zero,
// but we should still involve this period to allocated further amounts and pushing this
// till we run ouf of unallocated amounts
.filter(i -> i.isNotFullyPaidOff() || i.isDueBalanceZero()) //
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
.toList(); //
}
}