You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2026/10/19 12:58:13 UTC

(fineract) 01/02: FINERACT-2455: WC - Past due days calculation bug

This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch maintenance/1.16.0
in repository https://gitbox.apache.org/repos/asf/fineract.git

commit 85397461e8fec744b60de46bc0bf4f91708023ac
Author: mariiaKraievska <ri...@gmail.com>
AuthorDate: Mon Oct 12 13:07:34 2026 +0300

    FINERACT-2455: WC - Past due days calculation bug
    
    (cherry picked from commit c7d74f71a58bb6bc2f681a4e03dc2343f8c5c352)
---
 .../features/working-capital-loan-start-dates.adoc |  2 +-
 .../features/WorkingCapitalDelinquency.feature     | 85 ++++++++++++++++++++++
 .../api/WorkingCapitalLoanApiResourceSwagger.java  |  1 +
 ...italLoanDelinquencyRangeScheduleRepository.java | 17 ++++-
 ...italLoanApplicationReadPlatformServiceImpl.java |  8 +-
 ...italLoanDelinquencyReadPlatformServiceImpl.java |  2 +-
 ...LoanDelinquencyReadPlatformServiceImplTest.java | 13 ++++
 7 files changed, 121 insertions(+), 7 deletions(-)

diff --git a/fineract-doc/src/docs/en/chapters/features/working-capital-loan-start-dates.adoc b/fineract-doc/src/docs/en/chapters/features/working-capital-loan-start-dates.adoc
index bd718f20ae..bc2a6a2070 100644
--- a/fineract-doc/src/docs/en/chapters/features/working-capital-loan-start-dates.adoc
+++ b/fineract-doc/src/docs/en/chapters/features/working-capital-loan-start-dates.adoc
@@ -158,7 +158,7 @@ Every WC loan business event carries the same `breach` and `delinquency` blocks
 
 === Delinquency Start Date
 
-* `delinquencyStartDate` is the `fromDate` of the earliest `m_wc_loan_delinquency_range_schedule` row with `min_payment_criteria_met = false` (`findTopByLoanIdAndMinPaymentCriteriaMetFalseOrderByFromDateAsc`), with no grace arithmetic applied.
+* `delinquencyStartDate` is the `fromDate` of the earliest still-open past delinquency-range-schedule period that already carries a positive classified `delinquentDays` (`findOpenPastDuePeriodsOrderByFromDateAsc`: `min_payment_criteria_met` is `null` or `false`, `reset = false`, `to_date` on or before the business date), with no grace arithmetic applied. `null` is included because instant classification can mark a period delinquent before COB evaluates `min_payment_criteria_met`.
 * The delinquency schedule itself is anchored according to `delinquencyStartType`: `LOAN_CREATION` anchors the first period on the loan submitted-on date, while `DISBURSEMENT` (or an unset type) anchors it on the actual disbursement date. This anchor is resolved in `WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.generateInitialPeriod`.
 * When no delinquent period exists, `delinquencyStartDate` is `null`.
 
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature
index 8f733a2dc3..bb99eb4df7 100644
--- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature
+++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature
@@ -1430,3 +1430,88 @@ Feature: Working Capital Delinquency
       | 1            | 2026-02-13  |              | D00            | 1              | 30             |
     Then Admin closes the Working Capital loan with all obligations met with a full repayment on "13 February 2026"
 
+  Scenario: Verify pastDueDays when instant classification runs before COB evaluates minPaymentCriteriaMet
+    When Admin sets the business date to "01 January 2026"
+    And Admin creates a client with random data
+    And Admin creates a Working Capital Loan Product with delinquencyGraceDays 3 and delinquencyStartType "DISBURSEMENT" for loan test
+    And Admin creates a working capital loan with the grace days product and the following data:
+      | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate |
+      | 01 January 2026 | 01 January 2026          | 9000.0          | 100000.0           | 18.0              |
+    And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026"
+    And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount
+    # Period 1 toDate includes the 3-day grace window (2026-02-02). Advance past it without COB so minPaymentCriteriaMet stays NULL.
+    When Admin sets the business date to "03 February 2026"
+    And Customer makes repayment on "03 February 2026" with 0.01 transaction amount on Working Capital loan
+    Then Working Capital loan delinquency range schedule has the following data:
+      | periodNumber | fromDate   | toDate     | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays |
+      | 1            | 2026-01-01 | 2026-02-02 | 270.0          | 0.01       | 269.99            | null                  | 269.99           | 1              |
+    And Delinquency Tag History for Working Capital loan has lines:
+      | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
+      | 1            | 2026-02-03  |              | D00            | 1              | 30             |
+    And Working capital loan details has the following field values:
+      | delinquent.delinquentAmount   | 269.99     |
+      | delinquent.pastDueDays        | 1          |
+      | delinquencyStartDate          | 2026-01-01 |
+      | delinquencyEffectiveStartDate | 2026-01-04 |
+      | summary.overdueSinceDate      | 2026-02-02 |
+    Then Admin closes the Working Capital loan with a full repayment on "03 February 2026"
+
+  Scenario: Verify pastDueDays for backdated disbursement and partial repayment without COB
+    When Admin sets the business date to "01 April 2026"
+    And Admin creates a client with random data
+    And Admin creates a Working Capital Loan Product with delinquencyGraceDays 3 and delinquencyStartType "DISBURSEMENT" for loan test
+    And Admin creates a working capital loan with the grace days product and the following data:
+      | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate |
+      | 01 January 2026 | 01 January 2026          | 9000.0          | 100000.0           | 18.0              |
+    And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026"
+    And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount
+    # Partial repayment 50 days backdated: does not meet the minimum, triggers instant classification while minPaymentCriteriaMet stays NULL
+    And Customer makes repayment on "10 February 2026" with 0.01 transaction amount on Working Capital loan
+    Then Working Capital loan delinquency range schedule has the following data:
+      | periodNumber | fromDate   | toDate     | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays |
+      | 1            | 2026-01-01 | 2026-02-02 | 270.0          | 0.01       | 269.99            | null                  | 269.99           | 58             |
+    And Delinquency Tag History for Working Capital loan has lines:
+      | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
+      | 1            | 2026-04-01  |              | D30            | 31             | 60             |
+    And Working capital loan details has the following field values:
+      | delinquent.delinquentAmount   | 269.99     |
+      | delinquent.pastDueDays        | 58         |
+      | delinquencyStartDate          | 2026-01-01 |
+      | delinquencyEffectiveStartDate | 2026-01-04 |
+      | summary.overdueSinceDate      | 2026-02-02 |
+    Then Admin closes the Working Capital loan with a full repayment on "01 April 2026"
+
+  Scenario: Verify pastDueDays after first COB and subsequent partial repayment on next day
+    When Admin sets the business date to "01 January 2026"
+    And Admin creates a client with random data
+    And Admin creates a Working Capital Loan Product with delinquencyGraceDays 3 and delinquencyStartType "DISBURSEMENT" for loan test
+    And Admin creates a working capital loan with the grace days product and the following data:
+      | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate |
+      | 01 January 2026 | 01 January 2026          | 9000.0          | 100000.0           | 18.0              |
+    And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026"
+    And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount
+    When Admin sets the business date to "03 February 2026"
+    And Admin runs inline COB job for Working Capital Loan by loanId
+    Then Working Capital loan delinquency range schedule has the following data:
+      | periodNumber | fromDate   | toDate     | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays |
+      | 1            | 2026-01-01 | 2026-02-02 | 270.0          | 0.0        | 270.0             | false                 | 270.0            | 1              |
+      | 2            | 2026-02-03 | 2026-03-04 | 270.0          | 0.0        | 270.0             | null                  | null             | null           |
+    And Working capital loan details has the following field values:
+      | delinquent.delinquentAmount   | 270.0      |
+      | delinquent.pastDueDays        | 1          |
+      | delinquencyStartDate          | 2026-01-01 |
+      | delinquencyEffectiveStartDate | 2026-01-04 |
+      | summary.overdueSinceDate      | 2026-02-02 |
+    When Admin sets the business date to "04 February 2026"
+    And Customer makes repayment on "04 February 2026" with 0.01 transaction amount on Working Capital loan
+    Then Working Capital loan delinquency range schedule has the following data:
+      | periodNumber | fromDate   | toDate     | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays |
+      | 1            | 2026-01-01 | 2026-02-02 | 270.0          | 0.01       | 269.99            | false                 | 269.99           | 2              |
+      | 2            | 2026-02-03 | 2026-03-04 | 270.0          | 0.0        | 270.0             | null                  | null             | null           |
+    And Working capital loan details has the following field values:
+      | delinquent.delinquentAmount   | 269.99     |
+      | delinquent.pastDueDays        | 2          |
+      | delinquencyStartDate          | 2026-01-01 |
+      | delinquencyEffectiveStartDate | 2026-01-04 |
+      | summary.overdueSinceDate      | 2026-02-02 |
+    Then Admin closes the Working Capital loan with a full repayment on "04 February 2026"
diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java
index d819cdb77f..90d435393a 100644
--- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java
+++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java
@@ -396,6 +396,7 @@ public final class WorkingCapitalLoanApiResourceSwagger {
             public BigDecimal totalExpectedRepayment;
             public BigDecimal totalRepayment;
             public BigDecimal totalOutstanding;
+            public LocalDate overdueSinceDate;
         }
 
         @Schema(description = "Working Capital Delinquency Collection Data")
diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java
index 60f834c1a0..3fcf40fa6e 100644
--- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java
+++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java
@@ -100,7 +100,22 @@ public interface WorkingCapitalLoanDelinquencyRangeScheduleRepository
 
     List<WorkingCapitalLoanDelinquencyRangeSchedule> findByLoanIdAndToDateIsBeforeOrderByPeriodNumberAsc(Long loanId, LocalDate toDate);
 
-    Optional<WorkingCapitalLoanDelinquencyRangeSchedule> findTopByLoanIdAndMinPaymentCriteriaMetFalseOrderByFromDateAsc(Long loanId);
+    /**
+     * Still-open past periods that already carry a positive classified {@code delinquentDays} value, earliest first.
+     * Includes {@code minPaymentCriteriaMet IS NULL} because instant classification can populate delinquent days before
+     * COB evaluates the period. Reset periods are excluded: they are skipped by classification and must not drive
+     * pastDueDays / delinquency start dates.
+     */
+    @Query("""
+            SELECT s FROM WorkingCapitalLoanDelinquencyRangeSchedule s
+            WHERE s.loan.id = :loanId
+              AND s.toDate <= :businessDate
+              AND (s.minPaymentCriteriaMet IS NULL OR s.minPaymentCriteriaMet = FALSE)
+              AND s.reset = false
+              AND s.delinquentDays > 0
+            ORDER BY s.fromDate ASC""")
+    List<WorkingCapitalLoanDelinquencyRangeSchedule> findOpenPastDuePeriodsOrderByFromDateAsc(@Param("loanId") Long loanId,
+            @Param("businessDate") LocalDate businessDate);
 
     List<WorkingCapitalLoanDelinquencyRangeSchedule> findByLoanIdAndResetIsNotAndToDateBeforeOrderByPeriodNumberAsc(Long id, Boolean reset,
             LocalDate startDate);
diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java
index 90abc20b48..145b36aa06 100644
--- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java
+++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java
@@ -244,10 +244,10 @@ public class WorkingCapitalLoanApplicationReadPlatformServiceImpl implements Wor
             data.setBreachEffectiveStartDate(resolveBreachEffectiveStartDate(period, data.getBreachGraceDays()));
         });
 
-        // delinquencyStartDate: fromDate of the earliest delinquent period. The delinquency range schedule bakes the
-        // delinquency grace days into the toDate of its first period, so the fromDate is the raw anchor date.
-        delinquencyRangeScheduleRepository.findTopByLoanIdAndMinPaymentCriteriaMetFalseOrderByFromDateAsc(loan.getId())
-                .ifPresent(period -> {
+        // Same open-past-due definition as pastDueDays: instant classification can mark a period delinquent while
+        // minPaymentCriteriaMet is still NULL (before COB evaluates it), so FALSE-only would leave these empty.
+        delinquencyRangeScheduleRepository.findOpenPastDuePeriodsOrderByFromDateAsc(loan.getId(), ThreadLocalContextUtil.getBusinessDate())
+                .stream().findFirst().ifPresent(period -> {
                     data.setDelinquencyStartDate(period.getFromDate());
                     data.setDelinquencyEffectiveStartDate(resolveDelinquencyEffectiveStartDate(period, data.getDelinquencyGraceDays()));
                     Optional.ofNullable(data.getSummary()).ifPresent(summary -> summary.setOverdueSinceDate(period.getToDate()));
diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java
index b9dc99a9a0..cc95832abe 100644
--- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java
+++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java
@@ -76,7 +76,7 @@ public class WorkingCapitalLoanDelinquencyReadPlatformServiceImpl implements Wor
             template.setDelinquentPrincipal(delinquentAmount);
         });
 
-        delinquencyRangeScheduleRepository.findTopByLoanIdAndMinPaymentCriteriaMetFalseOrderByFromDateAsc(loanId)
+        delinquencyRangeScheduleRepository.findOpenPastDuePeriodsOrderByFromDateAsc(loanId, businessDate).stream().findFirst()
                 .map(WorkingCapitalLoanDelinquencyRangeSchedule::getDelinquentDays).ifPresent(template::setPastDueDays);
 
         template.setInstallmentLevelDelinquency(list);
diff --git a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImplTest.java b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImplTest.java
index bf119f55c4..af1b8706ca 100644
--- a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImplTest.java
+++ b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImplTest.java
@@ -204,6 +204,19 @@ public class WorkingCapitalLoanDelinquencyReadPlatformServiceImplTest {
         assertThat(result.getDelinquentDays()).isEqualTo(36L);
     }
 
+    @Test
+    void pastDueDays_usesOpenPastDuePeriodWhenMinPaymentCriteriaMetIsStillNull() {
+        final WorkingCapitalLoanDelinquencyRangeSchedule period = period(1L, 1, "269.99");
+        period.setMinPaymentCriteriaMet(null);
+        period.setDelinquentDays(1L);
+        when(delinquencyRangeScheduleRepository.findOpenPastDuePeriodsOrderByFromDateAsc(LOAN_ID, BUSINESS_DATE))
+                .thenReturn(List.of(period));
+
+        final WorkingCapitalLoanCollectionData result = service.getCollectionData(LOAN_ID, BUSINESS_DATE);
+
+        assertThat(result.getPastDueDays()).isEqualTo(1L);
+    }
+
     private WorkingCapitalLoanDelinquencyRangeScheduleTagHistory tag(final WorkingCapitalLoanDelinquencyRangeSchedule period,
             final DelinquencyRange range, final String addedOnDate, final String liftedOnDate, final String outstandingAmount) {
         final WorkingCapitalLoanDelinquencyRangeScheduleTagHistory tag = new WorkingCapitalLoanDelinquencyRangeScheduleTagHistory();