diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature index 4cc80a459..d31af72ae 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -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 diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 307e1355d..e3c64e4b3 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -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()))); + } + } 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 0762a4217..14dde4885 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 @@ -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 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(); // } }