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();