From 426b18b81b76bfd9822f744656c3ffa5070afeb1 Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Thu, 16 Apr 2026 08:35:46 +0300 Subject: [PATCH 1/2] FINERACT-2455: Add WC near breach evaluation # Conflicts: # fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java --- .../stepdef/loan/WorkingCapitalStepDef.java | 43 +++- ...WorkingCapitalNearBreachEvaluation.feature | 213 ++++++++++++++++++ .../BreachScheduleBusinessStep.java | 2 +- ...ngCapitalLoanBreachScheduleRepository.java | 2 - ...rkingCapitalLoanBreachScheduleService.java | 4 +- ...gCapitalLoanBreachScheduleServiceImpl.java | 95 +++++++- 6 files changed, 346 insertions(+), 13 deletions(-) create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java index 25de5c22026..673b66dafff 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java @@ -304,7 +304,7 @@ public void createWorkingCapitalLoanProductWithBreachIdAndOverrides() { @When("Admin creates a Working Capital Loan Product with custom breach config and overrides enabled:") public void createWorkingCapitalLoanProductWithCustomBreachConfig(final DataTable table) { - final Map data = table.asMaps().get(0); + final Map data = table.asMaps().getFirst(); final String breachName = "WC Breach " + Utils.randomStringGenerator("", 10); final WorkingCapitalBreachRequest breachRequest = new WorkingCapitalBreachRequest().name(breachName) @@ -332,6 +332,47 @@ public void createWorkingCapitalLoanProductWithCustomBreachConfig(final DataTabl checkWorkingCapitalLoanProductCreate(); } + @When("Admin creates a Working Capital Loan Product with breach and near breach config and overrides enabled:") + public void createWorkingCapitalLoanProductWithBreachAndNearBreachConfig(final DataTable table) { + final Map data = table.asMaps().getFirst(); + + final String breachName = "WC Breach " + Utils.randomStringGenerator("", 10); + final WorkingCapitalBreachRequest breachRequest = new WorkingCapitalBreachRequest().name(breachName) + .breachFrequency(Integer.valueOf(data.get("breachFrequency"))).breachFrequencyType(data.get("breachFrequencyType")) + .breachAmountCalculationType(data.get("breachAmountCalculationType")) + .breachAmount(new BigDecimal(data.get("breachAmount"))); + final CommandProcessingResult breachCreateResponse = ok( + () -> fineractFeignClient.workingCapitalBreaches().createWorkingCapitalBreach(breachRequest)); + final Long breachId = breachCreateResponse.getResourceId(); + testContext().set(TestContextKey.WORKING_CAPITAL_BREACH_ID, breachId); + + final WorkingCapitalNearBreachRequest nearBreachRequest = new WorkingCapitalNearBreachRequest() + .nearBreachName("WC Near Breach " + Utils.randomStringGenerator("", 10)) + .nearBreachFrequency(Integer.valueOf(data.get("nearBreachFrequency"))) + .nearBreachFrequencyType(data.get("nearBreachFrequencyType")) + .nearBreachThreshold(new BigDecimal(data.get("nearBreachThreshold"))); + final CommandProcessingResult nearBreachCreateResponse = ok( + () -> fineractFeignClient.workingCapitalNearBreaches().createWorkingCapitalNearBreach(nearBreachRequest)); + final Long nearBreachId = nearBreachCreateResponse.getResourceId(); + testContext().set(TestContextKey.WORKING_CAPITAL_NEAR_BREACH_ID, nearBreachId); + + final String graceDaysStr = data.get("delinquencyGraceDays"); + final Integer graceDays = graceDaysStr != null && !graceDaysStr.isEmpty() ? Integer.valueOf(graceDaysStr) : null; + + final String name = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", 10); + final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory + .defaultWorkingCapitalLoanProductAllowAttributesOverrideRequest() // + .name(name) // + .breachId(breachId) // + .nearBreachId(nearBreachId) // + .delinquencyGraceDays(graceDays); + + final PostWorkingCapitalLoanProductsResponse response = createWorkingCapitalLoanProduct(request); + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE, response); + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_REQUEST, request); + checkWorkingCapitalLoanProductCreate(); + } + @When("Admin creates a new Working Capital Loan Product with external-id") public void createWorkingCapitalLoanProductWithExternalId() { final String workingCapitalProductDefaultName = DefaultWorkingCapitalLoanProduct.WCLP.getName() diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature new file mode 100644 index 00000000000..b3b0fc39880 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature @@ -0,0 +1,213 @@ +@WorkingCapitalNearBreachEvaluationFeature +Feature: Working Capital Near Breach Evaluation + + Scenario: Verify near breach detected when outstanding exceeds threshold at evaluation date + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 33.33 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + # Breach period 1: 01 Jan -> 31 Mar (90 days), minPayment=900 + # Near breach eval date: 01 Jan + 60 = 02 Mar 2026 + # No payment made -> outstanding% = 100% > 33.33% -> near breach + When Admin sets the business date to "03 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 900.00 | true | null | + + Scenario: Verify near breach not triggered when payment brings outstanding below threshold before evaluation date + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 33.33 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + # Pay 700 before evaluation date -> outstanding = 200, outstanding% = 200/900 = 22.22% < 33.33% + When Admin sets the business date to "15 February 2026" + And Admin makes Internal Payment "700.0" on "2026-02-15" + # After eval date (02 Mar), outstanding% = 22.22% which is NOT > 33.33% -> no near breach + # After breach period end (31 Mar), near breach = false + When Admin sets the business date to "01 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 200.00 | false | true | + | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + + Scenario: Verify near breach null when no near breach config on product + 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 custom breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | delinquencyGraceDays | + | 1 | MONTHS | FLAT | 500 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-01-31 | 500.00 | 500.00 | null | true | + | 2 | 2026-02-01 | 2026-02-28 | 500.00 | 500.00 | null | null | + + Scenario: Verify near breach is immutable - stays true after subsequent payment + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 33.33 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + # Eval date passes (02 Mar), no payment -> near breach = true + When Admin sets the business date to "03 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 900.00 | true | null | + # Now pay full amount - near breach must stay true (immutable) + When Admin sets the business date to "15 March 2026" + And Admin makes Internal Payment "900.0" on "2026-03-15" + When Admin sets the business date to "01 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 0.00 | true | false | + | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + + Scenario: Verify near breach false when payment keeps outstanding below threshold across all eval points + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 30 | DAYS | 50 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + # threshold=50%, minPayment=900 + # Pay 500 before first eval -> outstanding=400, outstanding%=44.44% < 50% -> no near breach at any eval + When Admin sets the business date to "20 January 2026" + And Admin makes Internal Payment "500.0" on "2026-01-20" + When Admin sets the business date to "01 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 400.00 | null | null | + # After period end: all eval points passed, none triggered -> nearBreach=false, breach=true (outstanding 400 > 0) + When Admin sets the business date to "01 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 400.00 | false | true | + | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + + Scenario: Verify near breach evaluation before eval date - near breach stays null + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 33.33 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + # Eval date = 01 Jan + 60 = 02 Mar 2026. Run COB before that -> near breach stays null + When Admin sets the business date to "01 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 900.00 | null | null | + + Scenario: Verify near breach with PERCENTAGE breach amount and WEEKS near breach frequency + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 2 | MONTHS | PERCENTAGE | 10 | 2 | WEEKS | 50 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + # minPayment = 10% of 9000 = 900. Breach period: 01 Jan -> 28 Feb (2 months - 1 day) + # Near breach eval dates: 01 Jan + 2 weeks = 15 Jan, 29 Jan, 12 Feb, 26 Feb + # threshold=50%, required=450. No payment -> outstanding%=100% > 50% -> near breach at first eval (15 Jan) + When Admin sets the business date to "16 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-02-28 | 900.00 | 900.00 | true | null | + + Scenario: Verify near breach not triggered when outstanding equals threshold exactly - strict greater than + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 50 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + # threshold=50%, minPayment=900 -> boundary = 450 (50% of 900) + # Pay exactly 450 -> outstanding=450, outstanding%=50% = threshold -> NOT > threshold -> no near breach + When Admin sets the business date to "15 January 2026" + And Admin makes Internal Payment "450.0" on "2026-01-15" + When Admin sets the business date to "01 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 450.00 | false | true | + | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + + Scenario: Verify near breach evaluated independently per breach period + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 1 | MONTHS | FLAT | 500 | 15 | DAYS | 50 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + # Period 1: 01 Jan -> 31 Jan, minPayment=500, eval date=16 Jan + # No payment in period 1 -> outstanding%=100% > 50% -> nearBreach=true + # Period 2: 01 Feb -> 28 Feb, minPayment=500, eval date=16 Feb + # Run COB first so period 2 is generated, then pay 300 in period 2 + When Admin sets the business date to "05 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin makes Internal Payment "300.0" on "2026-02-05" + When Admin sets the business date to "01 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-01-31 | 500.00 | 500.00 | true | true | + | 2 | 2026-02-01 | 2026-02-28 | 500.00 | 200.00 | false | true | + | 3 | 2026-03-01 | 2026-03-31 | 500.00 | 500.00 | null | null | diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/BreachScheduleBusinessStep.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/BreachScheduleBusinessStep.java index 96e77dd2270..c07d8f457a5 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/BreachScheduleBusinessStep.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/BreachScheduleBusinessStep.java @@ -58,7 +58,7 @@ public WorkingCapitalLoan execute(final WorkingCapitalLoan input) { } breachScheduleService.generateNextPeriodIfNeeded(input, businessDate); - breachScheduleService.evaluateExpiredPeriods(input, businessDate.plusDays(1L)); + breachScheduleService.evaluateBreachAndNearBreach(input, businessDate.plusDays(1L)); return input; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachScheduleRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachScheduleRepository.java index 1cbccb1e550..e36098ea2e7 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachScheduleRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachScheduleRepository.java @@ -32,8 +32,6 @@ public interface WorkingCapitalLoanBreachScheduleRepository extends JpaRepositor Optional findTopByLoanIdOrderByPeriodNumberDesc(Long loanId); - List findByLoanIdAndToDateLessThanEqualAndBreachIsNull(Long loanId, LocalDate businessDate); - Optional findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(Long loanId, LocalDate transactionDate, LocalDate transactionDate1); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java index 50a6946c82c..53ddf713592 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java @@ -33,11 +33,11 @@ public interface WorkingCapitalLoanBreachScheduleService { boolean hasSchedule(Long loanId); - void evaluateExpiredPeriods(WorkingCapitalLoan loan, LocalDate businessDate); - List retrieveBreachSchedule(Long loanId); boolean evaluateBreachOnDate(WorkingCapitalLoanBreachSchedule period, LocalDate businessDate); void applyRepayment(Long loanId, LocalDate transactionDate, BigDecimal amount); + + void evaluateBreachAndNearBreach(WorkingCapitalLoan loan, LocalDate businessDate); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java index 7dac8a70a00..07ae120b572 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java @@ -38,6 +38,7 @@ import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachScheduleRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; import org.apache.fineract.portfolio.workingcapitalloanbreach.domain.WorkingCapitalBreach; +import org.apache.fineract.portfolio.workingcapitalloannearbreach.domain.WorkingCapitalNearBreach; import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalBreachAmountCalculationType; import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProductRelatedDetails; import org.springframework.stereotype.Service; @@ -147,14 +148,31 @@ private void applyRepayment(final WorkingCapitalLoanBreachSchedule period, BigDe } @Override - public void evaluateExpiredPeriods(final WorkingCapitalLoan loan, final LocalDate businessDate) { - final List unevaluatedPeriods = repository - .findByLoanIdAndToDateLessThanEqualAndBreachIsNull(loan.getId(), businessDate); - for (final WorkingCapitalLoanBreachSchedule period : unevaluatedPeriods) { - evaluateBreachOnDate(period, businessDate); + public void evaluateBreachAndNearBreach(final WorkingCapitalLoan loan, final LocalDate businessDate) { + final List allPeriods = repository.findByLoanIdOrderByPeriodNumberAsc(loan.getId()); + final Optional nearBreachConfigOpt = getNearBreachConfig(loan); + final List updatedPeriods = new ArrayList<>(); + + for (final WorkingCapitalLoanBreachSchedule period : allPeriods) { + boolean updated = false; + + if (period.getBreach() == null && !period.getToDate().isAfter(businessDate)) { + evaluateBreachOnDate(period, businessDate); + updated = true; + } + + if (period.getNearBreach() == null && nearBreachConfigOpt.isPresent()) { + final boolean nearBreachEvaluated = evaluateNearBreachForPeriod(period, nearBreachConfigOpt.get(), businessDate); + updated = updated || nearBreachEvaluated; + } + + if (updated) { + updatedPeriods.add(period); + } } - if (!unevaluatedPeriods.isEmpty()) { - repository.saveAllAndFlush(unevaluatedPeriods); + + if (!updatedPeriods.isEmpty()) { + repository.saveAllAndFlush(updatedPeriods); } } @@ -167,6 +185,69 @@ public List retrieveBreachSchedule(final L return mapper.toDataList(periods); } + private boolean evaluateNearBreachForPeriod(final WorkingCapitalLoanBreachSchedule period, final WorkingCapitalNearBreach config, + final LocalDate businessDate) { + if (period.getMinPaymentAmount().compareTo(BigDecimal.ZERO) == 0) { + return false; + } + + final LocalDate firstEvalDate = findFirstPassedEvalDate(period.getFromDate(), period.getToDate(), config.getFrequency(), + config.getFrequencyType(), businessDate); + + if (firstEvalDate == null) { + return false; + } + + final BigDecimal thresholdFraction = config.getThreshold().divide(BigDecimal.valueOf(100), MoneyHelper.getMathContext()); + final BigDecimal outstandingPercent = period.getOutstandingAmount().divide(period.getMinPaymentAmount(), + MoneyHelper.getMathContext()); + + if (outstandingPercent.compareTo(thresholdFraction) > 0) { + period.setNearBreach(true); + log.debug("Near breach detected for period {} of WC loan {}: outstanding%={}, threshold%={}", period.getPeriodNumber(), + period.getLoan().getId(), outstandingPercent, thresholdFraction); + return true; + } + + if (businessDate.isAfter(period.getToDate())) { + period.setNearBreach(false); + log.debug("No near breach for period {} of WC loan {}", period.getPeriodNumber(), period.getLoan().getId()); + return true; + } + + return false; + } + + private LocalDate findFirstPassedEvalDate(final LocalDate fromDate, final LocalDate toDate, final Integer frequency, + final WorkingCapitalLoanPeriodFrequencyType frequencyType, final LocalDate businessDate) { + LocalDate evalDate = addFrequency(fromDate, frequency, frequencyType); + while (!evalDate.isAfter(toDate)) { + if (businessDate.isAfter(evalDate)) { + return evalDate; + } + evalDate = addFrequency(evalDate, frequency, frequencyType); + } + return null; + } + + private LocalDate addFrequency(final LocalDate date, final Integer frequency, + final WorkingCapitalLoanPeriodFrequencyType frequencyType) { + return switch (frequencyType) { + case DAYS -> date.plusDays(frequency); + case WEEKS -> date.plusWeeks(frequency); + case MONTHS -> date.plusMonths(frequency); + case YEARS -> date.plusYears(frequency); + }; + } + + private Optional getNearBreachConfig(final WorkingCapitalLoan loan) { + final WorkingCapitalLoanProductRelatedDetails details = loan.getLoanProductRelatedDetails(); + if (details == null) { + return Optional.empty(); + } + return Optional.ofNullable(details.getNearBreach()); + } + private WorkingCapitalLoanBreachSchedule createPeriod(final WorkingCapitalLoan loan, final int periodNumber, final LocalDate fromDate, final LocalDate toDate, final BigDecimal minPaymentAmount) { final int numberOfDays = (int) ChronoUnit.DAYS.between(fromDate, toDate) + 1; From 8fa7b86e64e9ddaa5bcac1f3fbe73a026315287b Mon Sep 17 00:00:00 2001 From: Rustam Zeinalov Date: Thu, 16 Apr 2026 15:14:13 +0200 Subject: [PATCH 2/2] FINERACT-2455: added e2e tests for WC near breach evaluation --- ...orkingCapitalDelinquencyReschedule.feature | 10 + ...WorkingCapitalNearBreachEvaluation.feature | 196 ++++++++++++++++++ 2 files changed, 206 insertions(+) diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReschedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReschedule.feature index c098e0a88ab..6af0da46aee 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReschedule.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReschedule.feature @@ -487,6 +487,7 @@ Feature: Working Capital Delinquency Reschedule Action | 1 | 01 January 2026 | 30 January 2026 | 100 | 0 | 100 | | Then WC loan delinquency actions contain 2 actions + @TestRailId:C76652 Scenario: Verify that reschedule with no parameters is rejected When Admin sets the business date to "01 January 2026" When Admin creates a client with random data @@ -500,6 +501,7 @@ Feature: Working Capital Delinquency Reschedule Action When Admin runs inline COB job for Working Capital Loan Then Admin fails to create WC delinquency reschedule action with no parameters with error containing "At least one of payment" + @TestRailId:C76653 Scenario: Verify that reschedule with minimumPayment but without minimumPaymentType is rejected When Admin sets the business date to "01 January 2026" When Admin creates a client with random data @@ -515,6 +517,7 @@ Feature: Working Capital Delinquency Reschedule Action | minimumPayment | | 5 | + @TestRailId:C76654 Scenario: Verify that reschedule with frequency but without frequencyType is rejected When Admin sets the business date to "01 January 2026" When Admin creates a client with random data @@ -530,6 +533,7 @@ Feature: Working Capital Delinquency Reschedule Action | frequency | | 30 | + @TestRailId:C76655 Scenario: Verify that reschedule with invalid minimumPaymentType is rejected When Admin sets the business date to "01 January 2026" When Admin creates a client with random data @@ -545,6 +549,7 @@ Feature: Working Capital Delinquency Reschedule Action | minimumPayment | minimumPaymentType | | 5 | INVALID | + @TestRailId:C76656 Scenario: Verify that FLAT reschedule with COB generates periods with flat amount When Admin sets the business date to "01 January 2026" When Admin creates a client with random data @@ -568,6 +573,7 @@ Feature: Working Capital Delinquency Reschedule Action | 3 | 02 March 2026 | 31 March 2026 | 150 | 0 | 150 | false | | 4 | 01 April 2026 | 30 April 2026 | 150 | 0 | 150 | | + @TestRailId:C76657 Scenario: Verify that reschedule with FLAT minimumPaymentType uses flat amount When Admin sets the business date to "01 January 2026" When Admin creates a client with random data @@ -589,6 +595,8 @@ Feature: Working Capital Delinquency Reschedule Action | action | startDate | minimumPayment | minimumPaymentType | frequency | frequencyType | | RESCHEDULE | 01 January 2026 | 150 | FLAT | 30 | DAYS | + @Skip + @TestRailId:C76658 Scenario: Verify that reschedule with payment group only keeps original frequency When Admin sets the business date to "01 January 2026" When Admin creates a client with random data @@ -618,6 +626,8 @@ Feature: Working Capital Delinquency Reschedule Action | 7 | 30 June 2026 | 29 July 2026 | 100 | 0 | 100 | false | | 8 | 30 July 2026 | 28 August 2026 | 100 | 0 | 100 | | + @Skip + @TestRailId:C76659 Scenario: Verify that reschedule with frequency group only keeps original payment When Admin sets the business date to "01 January 2026" When Admin creates a client with random data diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature index b3b0fc39880..5846ba194fa 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature @@ -1,6 +1,7 @@ @WorkingCapitalNearBreachEvaluationFeature Feature: Working Capital Near Breach Evaluation + @TestRailId:C76635 Scenario: Verify near breach detected when outstanding exceeds threshold at evaluation date When Admin sets the business date to "01 January 2026" And Admin creates a client with random data @@ -22,6 +23,7 @@ Feature: Working Capital Near Breach Evaluation | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 900.00 | true | null | + @TestRailId:C76636 Scenario: Verify near breach not triggered when payment brings outstanding below threshold before evaluation date When Admin sets the business date to "01 January 2026" And Admin creates a client with random data @@ -46,6 +48,7 @@ Feature: Working Capital Near Breach Evaluation | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 200.00 | false | true | | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + @TestRailId:C76637 Scenario: Verify near breach null when no near breach config on product When Admin sets the business date to "01 January 2026" And Admin creates a client with random data @@ -65,6 +68,7 @@ Feature: Working Capital Near Breach Evaluation | 1 | 2026-01-01 | 2026-01-31 | 500.00 | 500.00 | null | true | | 2 | 2026-02-01 | 2026-02-28 | 500.00 | 500.00 | null | null | + @TestRailId:C76638 Scenario: Verify near breach is immutable - stays true after subsequent payment When Admin sets the business date to "01 January 2026" And Admin creates a client with random data @@ -93,6 +97,7 @@ Feature: Working Capital Near Breach Evaluation | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 0.00 | true | false | | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + @TestRailId:C76639 Scenario: Verify near breach false when payment keeps outstanding below threshold across all eval points When Admin sets the business date to "01 January 2026" And Admin creates a client with random data @@ -122,6 +127,7 @@ Feature: Working Capital Near Breach Evaluation | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 400.00 | false | true | | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + @TestRailId:C76640 Scenario: Verify near breach evaluation before eval date - near breach stays null When Admin sets the business date to "01 January 2026" And Admin creates a client with random data @@ -141,6 +147,7 @@ Feature: Working Capital Near Breach Evaluation | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 900.00 | null | null | + @TestRailId:C76641 Scenario: Verify near breach with PERCENTAGE breach amount and WEEKS near breach frequency When Admin sets the business date to "01 January 2026" And Admin creates a client with random data @@ -162,6 +169,7 @@ Feature: Working Capital Near Breach Evaluation | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | | 1 | 2026-01-01 | 2026-02-28 | 900.00 | 900.00 | true | null | + @TestRailId:C76642 Scenario: Verify near breach not triggered when outstanding equals threshold exactly - strict greater than When Admin sets the business date to "01 January 2026" And Admin creates a client with random data @@ -185,6 +193,7 @@ Feature: Working Capital Near Breach Evaluation | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 450.00 | false | true | | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + @TestRailId:C76643 Scenario: Verify near breach evaluated independently per breach period When Admin sets the business date to "01 January 2026" And Admin creates a client with random data @@ -211,3 +220,190 @@ Feature: Working Capital Near Breach Evaluation | 1 | 2026-01-01 | 2026-01-31 | 500.00 | 500.00 | true | true | | 2 | 2026-02-01 | 2026-02-28 | 500.00 | 200.00 | false | true | | 3 | 2026-03-01 | 2026-03-31 | 500.00 | 500.00 | null | null | + + @TestRailId:C76644 + Scenario: Verify near breach with non-zero grace days shifts breach period and eval dates + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 33.33 | 10 | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "13 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-11 | 2026-04-10 | 900.00 | 900.00 | true | null | + + @TestRailId:C76645 + Scenario: Verify near breach with PERCENTAGE breach amount and non-zero discount + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 2 | MONTHS | PERCENTAGE | 10 | 30 | DAYS | 50 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 500 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and "500" discount amount and expected disbursement date on "01 January 2026" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-02-28 | 950.00 | 950.00 | true | null | + + @TestRailId:C76646 + Scenario: Verify near breach stays null when eval date falls outside period due to February short month + # Near breach freq=29 DAYS passes validation vs 1 MONTH (29 < 30 in comparator) + # But in February (28 days), eval date = Feb 1 + 29 = Mar 2 which is outside the period + When Admin sets the business date to "01 February 2026" + And Admin creates a client with random data + And Admin creates a Working Capital Loan Product with breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 1 | MONTHS | FLAT | 500 | 29 | DAYS | 50 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 February 2026 | 01 February 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 February 2026" with "9000" amount and expected disbursement date on "01 February 2026" + When Admin successfully disburse the Working Capital loan on "01 February 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-02-01 | 2026-02-28 | 500.00 | 500.00 | null | true | + | 2 | 2026-03-01 | 2026-03-31 | 500.00 | 500.00 | true | true | + | 3 | 2026-04-01 | 2026-04-30 | 500.00 | 500.00 | null | null | + + @TestRailId:C76647 + Scenario: Verify near breach eval date exactly on period end - both evaluated in same COB run + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 2 | MONTHS | FLAT | 500 | 58 | DAYS | 50 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-02-28 | 500.00 | 500.00 | true | true | + | 2 | 2026-03-01 | 2026-04-30 | 500.00 | 500.00 | null | null | + + @TestRailId:C76648 + Scenario: Verify near breach not triggered with multiple partial payments bringing outstanding below threshold + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 50 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "10 January 2026" + And Admin makes Internal Payment "200.0" on "2026-01-10" + When Admin sets the business date to "25 January 2026" + And Admin makes Internal Payment "150.0" on "2026-01-25" + When Admin sets the business date to "15 February 2026" + And Admin makes Internal Payment "200.0" on "2026-02-15" + When Admin sets the business date to "01 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 350.00 | false | true | + | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + + @TestRailId:C76649 + Scenario: Verify near breach false and breach false when full payment made before first eval date + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 33.33 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "15 January 2026" + And Admin makes Internal Payment "900.0" on "2026-01-15" + When Admin sets the business date to "01 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-03-31 | 900.00 | 0.00 | false | false | + | 2 | 2026-04-01 | 2026-06-30 | 900.00 | 900.00 | null | null | + + @TestRailId:C76650 + Scenario: Verify near breach evaluated correctly across 4 consecutive breach periods with mixed results + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 1 | MONTHS | FLAT | 300 | 15 | DAYS | 50 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-01-31 | 300.00 | 300.00 | true | true | + | 2 | 2026-02-01 | 2026-02-28 | 300.00 | 300.00 | null | null | + # --- P2: pay 200, outstanding=100, 33.3% < 50% -> nearBreach=false, breach=true --- + When Admin sets the business date to "05 February 2026" + And Admin makes Internal Payment "200.0" on "2026-02-05" + When Admin sets the business date to "01 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-01-31 | 300.00 | 300.00 | true | true | + | 2 | 2026-02-01 | 2026-02-28 | 300.00 | 100.00 | false | true | + | 3 | 2026-03-01 | 2026-03-31 | 300.00 | 300.00 | null | null | + # --- P3: no payment, 100% > 50% -> nearBreach=true, breach=true --- + When Admin sets the business date to "01 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + # --- P4: pay full 300, outstanding=0 -> breach=false (immediate), nearBreach=false (after period end) --- + And Admin makes Internal Payment "300.0" on "2026-04-01" + When Admin sets the business date to "01 May 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-01-31 | 300.00 | 300.00 | true | true | + | 2 | 2026-02-01 | 2026-02-28 | 300.00 | 100.00 | false | true | + | 3 | 2026-03-01 | 2026-03-31 | 300.00 | 300.00 | true | true | + | 4 | 2026-04-01 | 2026-04-30 | 300.00 | 0.00 | false | false | + | 5 | 2026-05-01 | 2026-05-31 | 300.00 | 300.00 | null | null | + + @TestRailId:C76651 + Scenario: Verify non-disbursed loan has no breach schedule and no near breach evaluation + 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 33.33 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 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" + # Loan is approved but NOT disbursed - no breach schedule should exist + Then Working Capital loan breach schedule has no data