diff --git a/fineract-doc/src/docs/en/chapters/features/index.adoc b/fineract-doc/src/docs/en/chapters/features/index.adoc index 2a1dab1fc63..bcc3155cc38 100644 --- a/fineract-doc/src/docs/en/chapters/features/index.adoc +++ b/fineract-doc/src/docs/en/chapters/features/index.adoc @@ -31,3 +31,4 @@ include::working-capital-planned-projected-balances-eir.adoc[leveloffset=+1] include::working-capital-discount.adoc[leveloffset=+1] include::savings-interest-posting.adoc[leveloffset=+1] include::working-capital-breach-management.adoc[leveloffset=+1] +include::working-capital-breach-grace-days.adoc[leveloffset=+1] diff --git a/fineract-doc/src/docs/en/chapters/features/working-capital-breach-grace-days.adoc b/fineract-doc/src/docs/en/chapters/features/working-capital-breach-grace-days.adoc new file mode 100644 index 00000000000..ba109ecd588 --- /dev/null +++ b/fineract-doc/src/docs/en/chapters/features/working-capital-breach-grace-days.adoc @@ -0,0 +1,264 @@ += Working Capital Breach Grace Days + +== Overview + +Breach Grace Days is a per-product (and per-loan) integer setting that shifts the start date of the first period of the Working Capital (WC) Breach Schedule by N days after the actual disbursement date. Because Near-Breach evaluation runs over the same breach schedule, shifting the first breach period implicitly shifts the first Near-Breach evaluation window — no separate Near-Breach grace days field is required. + +The feature is implemented in the `fineract-working-capital-loan` module and is exposed on the Working Capital Loan Product and Working Capital Loan REST APIs as `breachGraceDays`. + +=== Purpose + +Breach Grace Days gives lenders an explicit, intent-revealing knob to delay the start of breach tracking after disbursement, decoupled from the pre-existing `delinquencyGraceDays` (which now exclusively drives delinquency machinery). This allows a product configuration such as "begin enforcing minimum payment obligations 5 days after disbursement" without conflating breach and delinquency semantics. + +=== Scope + +The scope of this document includes: + +* New `breachGraceDays` column on `m_wc_loan_product` and `m_wc_loan`. +* New `breachGraceDays` field on the product and loan REST APIs (create, update, retrieve). +* Validation rule `breachGraceDays >= 0` at both product and loan levels. +* Application of `breachGraceDays` only to the first period generated by `BreachScheduleBusinessStep`. +* Implicit behavior on Near-Breach evaluation as a consequence of the shifted first period. + +The scope explicitly excludes: + +* No post-period grace — once a period closes with `outstanding_amount > 0`, the period is flagged `breach = true` immediately, regardless of `breachGraceDays`. +* `delinquencyGraceDays` is not renamed and not removed; it retains its original semantics. +* No automatic data migration from `delinquency_grace_days` to `breach_grace_days`. + +=== Applicability + +* Applies to Working Capital loans whose product (or individual loan) has a breach configuration assigned (`breach_id` not null). When no breach configuration is set, the breach schedule is not generated and `breachGraceDays` is inert. +* Applies only to the first period in the schedule. Subsequent periods chain from `previousPeriod.toDate + 1` and do not re-apply the grace. +* Can be overridden at the loan-application level only when the product allows breach overrides (`allowAttributeOverrides.breach = true`). + +=== Definitions and Key Concepts + +*`breachGraceDays`:* Optional non-negative integer representing the number of calendar days added to the actual disbursement date before the first breach period begins. Stored on `m_wc_loan_product.breach_grace_days` (product default) and `m_wc_loan.breach_grace_days` (loan-level value, copied from product or overridden). Columns are nullable with DB-level `DEFAULT 0`; runtime treats a `null` value as `0`. + +*First Breach Period Start:* `firstPeriod.fromDate = actualDisbursementDate + breachGraceDays days`. Computed by `WorkingCapitalLoanBreachScheduleServiceImpl.generateInitialPeriod` using the helper `getBreachGraceDays(loan)`. + +*Implicit Near-Breach Shift:* `WorkingCapitalLoanNearBreachEvaluationServiceImpl.evaluatePeriod` computes its first checkpoint as `firstEvalDate = addFrequency(period.fromDate, frequency, frequencyType)`. Since `period.fromDate` is already shifted by `breachGraceDays`, the first near-breach checkpoint is shifted by the same amount. + +== Design Decisions and Considerations + +=== Decoupled from `delinquencyGraceDays` + +Prior to this feature, the breach schedule generator read `delinquencyGraceDays` to offset the first period — a coupling that conflated two unrelated business concepts. `breachGraceDays` is a new, independent field. The two fields can now be set to different values on the same product. + +=== Sibling Helper Instead of Rename + +`WorkingCapitalLoanBreachScheduleServiceImpl` exposes a new private helper `getBreachGraceDays(loan)`. The original `getGraceDays(loan)` (which reads `delinquencyGraceDays`) is intentionally retained to keep the delinquency-side concept available without confusion. + +=== Hard Cut-Over Without SQL Backfill + +The Liquibase changeset adds `breach_grace_days` as a nullable column with DB-level `DEFAULT 0`; existing rows receive `0` via that default. No automatic copy from `delinquency_grace_days` is performed. Tenants that previously relied on `delinquencyGraceDays` to shift the breach schedule must update `breachGraceDays` explicitly via the REST API or SQL. + +=== Null Treated as Zero + +The column is nullable with DB-level `DEFAULT 0`, and the Java field has no in-class initializer — so `breachGraceDays` may legitimately be `null` in memory (e.g., after a create request that omits the parameter, before the entity is reloaded from the DB). The service helper `getBreachGraceDays` returns `0` when it encounters a `null` value, so `null` and an explicit `0` are semantically equivalent at runtime. + +=== No Override Flag of Its Own + +`breachGraceDays` inherits the existing `allowAttributeOverrides.breach` flag. No new boolean is added to `m_wc_loan_product_configurable_attributes`. + +== Database Design + +=== Overview + +Two existing tables gain one column each. No new tables are introduced. + +=== Changes to Existing Tables + +==== m_wc_loan_product + +New column: + +[cols="1,2,1,3",options="header"] +|=== +| Column Name | Type | Constraints | Description +| `breach_grace_days` | INT | nullable, default `0` | Product-level default number of days the first breach period start is shifted past the actual disbursement date +|=== + +==== m_wc_loan + +New column: + +[cols="1,2,1,3",options="header"] +|=== +| Column Name | Type | Constraints | Description +| `breach_grace_days` | INT | nullable, default `0` | Loan-level value (copied from product at origination; may be overridden when the product allows breach overrides) +|=== + +The Liquibase changeset is `fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0043_wc_breach_grace_days.xml`. Both add-column changesets are guarded by `columnExists` preconditions so they are idempotent. + +== Configuration + +=== Loan Product Configuration + +`breachGraceDays` is optional on the product create/update request. When omitted on create, the DB column default (`0`) applies. + +[source,json] +---- +{ + "breachId": 1, // breach config required for the schedule to be generated + "breachGraceDays": 5, // optional; integer >= 0; column has DB default 0 + "allowAttributeOverrides": { + "breach": true // optional; required to allow per-loan override of breachGraceDays + } +} +---- + +=== Loan-Level Override + +When the loan application omits `breachGraceDays`, the loan inherits the product's value. When provided, the loan stores its own value. Override only takes effect when the product allows breach overrides; otherwise the assembler keeps the product default. + +== API Design + +=== Endpoints + +==== Working Capital Loan Product — Create + +[source] +---- +POST /v1/working-capital/products +---- + +**Request Body (relevant fields):** + +[source,json] +---- +{ + "name": "WC Product", + "shortName": "WCP", + "breachId": 1, + "breachGraceDays": 5, // optional; integer >= 0; column has DB default 0 + "delinquencyGraceDays": 0, // unrelated field, retained for delinquency + "locale": "en_US", + "dateFormat": "yyyy-MM-dd" +} +---- + +==== Working Capital Loan Product — Update + +[source] +---- +PUT /v1/working-capital/products/{productId} +---- + +Accepts `breachGraceDays` as a partial-update field. When present, the change is tracked in the response `changes` map under the key `breachGraceDays`. + +==== Working Capital Loan Product — Retrieve + +[source] +---- +GET /v1/working-capital/products/{productId} +GET /v1/working-capital/products +---- + +The response includes `breachGraceDays` (integer) at the top level. + +==== Working Capital Loan — Create + +[source] +---- +POST /v1/working-capital/loans +---- + +Accepts an optional `breachGraceDays` integer. Inherited from the product when omitted; overrides only apply when the product allows breach overrides. + +==== Working Capital Loan — Update + +[source] +---- +PUT /v1/working-capital/loans/{loanId} +---- + +Accepts `breachGraceDays` as a partial-update field. + +==== Working Capital Loan — Retrieve + +[source] +---- +GET /v1/working-capital/loans/{loanId} +---- + +The response includes the loan's effective `breachGraceDays` value. + +== Validation Rules + +=== Product + +* `breachGraceDays` is optional on create; when present must be `>= 0`. Error code: `[breachGraceDays] The parameter \`breachGraceDays\` must be zero or greater.` (HTTP 400). +* Enforced by `WorkingCapitalLoanProductDataValidator.validateSettingsFields` via `integerZeroOrGreater()` (applied both for create and partial update). + +=== Loan Application + +* Same `integerZeroOrGreater()` validation in `WorkingCapitalLoanApplicationDataValidator` for both create and update paths. +* No upper bound is enforced. + +== Business Rules + +=== Breach Schedule Generation + +* `WorkingCapitalLoanBreachScheduleServiceImpl.generateInitialPeriod` reads `loan.getLoanProductRelatedDetails().getBreachGraceDays()` and computes `fromDate = actualDisbursementDate.plusDays(breachGraceDays)`. +* `numberOfDays = (toDate − fromDate) + 1` (inclusive count). +* The grace is applied only when generating period number `1`. `generateNextPeriodIfNeeded` chains subsequent periods as `nextFromDate = latestPeriod.toDate + 1 day` and never re-applies the grace. +* When the product has no breach configuration, `BreachScheduleBusinessStep` short-circuits and no schedule is created — `breachGraceDays` has no effect. + +=== Near-Breach Evaluation + +* No code change in `WorkingCapitalLoanNearBreachEvaluationServiceImpl`. +* Near-breach checkpoints are derived from `period.fromDate`. Because the first period's `fromDate` is already shifted by `breachGraceDays`, the first near-breach checkpoint within period `1` is shifted by the same amount. +* Checkpoints in later periods are unaffected (their periods are chained without re-applying the grace). + +=== Loan Assembly From Product + +* `WorkingCapitalLoanAssemblerImpl` copies `breachGraceDays` from the product's `WorkingCapitalLoanProductRelatedDetail` into the loan's `WorkingCapitalLoanProductRelatedDetails` at origination. +* If the loan create/update request includes `breachGraceDays`, the assembler honours the override only when the product allows breach overrides; otherwise the product default is retained. + +== Example Scenarios + +=== Scenario #1: Breach Schedule First Period Shifted + +**Setup:** + +* Product configured with `breachId` (FLAT, `breachAmount = 500`, `breachFrequency = 1`, `breachFrequencyType = MONTHS`) and `breachGraceDays = 5`. +* Loan disbursed on `2026-01-01` with principal `9000`. + +**Action:** + +The Close-of-Business pipeline runs `BreachScheduleBusinessStep` on the day of disbursement. + +**Expected Behavior:** + +* Period `1` is created with `fromDate = 2026-01-06`, `toDate = 2026-02-05`, `numberOfDays = 31`, `minPaymentAmount = 500.00`, `breach = null`, `nearBreach = null`. +* Subsequent periods chain from `2026-02-06` and do not re-apply the 5-day grace. + +=== Scenario #2: Near-Breach Window Implicitly Shifted + +**Setup:** + +* Product configured with breach (FLAT, `breachAmount = 900`, `breachFrequency = 3`, `breachFrequencyType = MONTHS`), near-breach (`nearBreachFrequency = 60 DAYS`, `nearBreachThreshold = 33.33`), and `breachGraceDays = 5`. +* Loan disbursed on `2026-01-01`. No repayment is made. + +**Action:** + +Business date advances to `2026-03-07` and COB runs. + +**Expected Behavior:** + +* Period `1` of the breach schedule starts on `2026-01-06` (disbursement + 5 days grace) and ends on `2026-04-05`. +* The first near-breach checkpoint is `2026-01-06 + 60 days = 2026-03-07`. +* Since `paidAmount = 0` at `2026-03-07` and `requiredCumulative = (1) × 33.33% × 900 ≈ 299.97`, the period is flagged `nearBreach = true`. +* `breach` remains `null` because the period has not yet ended. + +== Summary + +`breachGraceDays` is an independent, validated configuration field on the Working Capital Loan Product and Loan that delays the start of the first breach period (and, by chaining, the first near-breach evaluation window). Key aspects: + +* Stored as a nullable `INT` column with DB-level `DEFAULT 0` on `m_wc_loan_product` and `m_wc_loan` (changeset `0043_wc_breach_grace_days.xml`); the breach-schedule service treats `null` as `0`. +* Exposed via `breachGraceDays` in product and loan REST APIs (create / update / retrieve). +* Applied only to period `1` of the breach schedule; chained periods are not regraced. +* Drives Near-Breach evaluation shift implicitly — no separate near-breach grace field is needed. +* Decoupled from the legacy `delinquencyGraceDays`, which keeps its original delinquency semantics. diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java index 15365305a40..73d35035d07 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java @@ -182,6 +182,15 @@ public void createWorkingCapitalLoan(final DataTable table) { @When("Admin creates a working capital loan using created product with the following data:") public void createWorkingCapitalLoanUsingCreatedProduct(final DataTable table) { + submitLoanUsingCreatedProduct(table, null); + } + + @When("Admin creates a working capital loan using created product with breachGraceDays {int} and the following data:") + public void createWorkingCapitalLoanUsingCreatedProductWithBreachGraceDays(final int breachGraceDays, final DataTable table) { + submitLoanUsingCreatedProduct(table, breachGraceDays); + } + + private void submitLoanUsingCreatedProduct(final DataTable table, final Integer breachGraceDays) { final List> data = table.asLists(); final List rawData = data.get(1); final Long clientId = extractClientId(); @@ -201,6 +210,9 @@ public void createWorkingCapitalLoanUsingCreatedProduct(final DataTable table) { .principalAmount(new BigDecimal(principal)).totalPaymentVolume(new BigDecimal(totalPaymentVolume)) .periodPaymentRate(new BigDecimal(periodPaymentRate)) .discount(discount != null && !discount.isEmpty() ? new BigDecimal(discount) : null); + if (breachGraceDays != null) { + loansRequest.breachGraceDays(breachGraceDays); + } testContext().set(TestContextKey.LOAN_CREATE_REQUEST, loansRequest); final PostWorkingCapitalLoansResponse response = ok( @@ -210,6 +222,14 @@ public void createWorkingCapitalLoanUsingCreatedProduct(final DataTable table) { log.info("Working Capital Loan created with dynamic product ID: {}, Loan ID: {}", loanProductId, response.getLoanId()); } + @Then("Working capital loan account has breachGraceDays {int}") + public void verifyLoanBreachGraceDays(final int expectedBreachGraceDays) { + final Long loanId = getCreatedLoanId(); + final GetWorkingCapitalLoansLoanIdResponse response = ok( + () -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); + assertThat(response.getBreachGraceDays()).as("breachGraceDays").isEqualTo(expectedBreachGraceDays); + } + @Then("Working capital loan creation was successful") public void verifyWorkingCapitalLoanCreationSuccess() { final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); 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 56eb5ee4a7d..f4b6a885aa8 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 @@ -122,6 +122,7 @@ public class WorkingCapitalStepDef extends AbstractStepDef { public static final String DELINQUENCY_BUCKET_ID_FIELD_NAME = "delinquencyBucketId"; public static final String DELINQUENCY_GRACE_DAYS_FIELD_NAME = "delinquencyGraceDays"; public static final String DELINQUENCY_START_TYPE_FIELD_NAME = "delinquencyStartType"; + public static final String BREACH_GRACE_DAYS_FIELD_NAME = "breachGraceDays"; public static final String BREACH_ID_FIELD_NAME = "breachId"; public static final String NEAR_BREACH_ID_FIELD_NAME = "nearBreachId"; public static final String LOCALE_FIELD_NAME = "locale"; @@ -340,13 +341,17 @@ public void createWorkingCapitalLoanProductWithCustomBreachConfig(final DataTabl final String graceDaysStr = data.get("delinquencyGraceDays"); final Integer graceDays = graceDaysStr != null && !graceDaysStr.isEmpty() ? Integer.valueOf(graceDaysStr) : null; + final String breachGraceDaysStr = data.get(BREACH_GRACE_DAYS_FIELD_NAME); + final Integer breachGraceDays = breachGraceDaysStr != null && !breachGraceDaysStr.isEmpty() ? Integer.valueOf(breachGraceDaysStr) + : null; final String name = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", 10); final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory .defaultWorkingCapitalLoanProductAllowAttributesOverrideRequest() // .name(name) // .breachId(breachId) // - .delinquencyGraceDays(graceDays); + .delinquencyGraceDays(graceDays) // + .breachGraceDays(breachGraceDays); final PostWorkingCapitalLoanProductsResponse response = createWorkingCapitalLoanProduct(request); testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE, response); @@ -380,6 +385,9 @@ public void createWorkingCapitalLoanProductWithBreachAndNearBreachConfig(final D final String graceDaysStr = data.get("delinquencyGraceDays"); final Integer graceDays = graceDaysStr != null && !graceDaysStr.isEmpty() ? Integer.valueOf(graceDaysStr) : null; + final String breachGraceDaysStr = data.get(BREACH_GRACE_DAYS_FIELD_NAME); + final Integer breachGraceDays = breachGraceDaysStr != null && !breachGraceDaysStr.isEmpty() ? Integer.valueOf(breachGraceDaysStr) + : null; final String name = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", 10); final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory @@ -387,7 +395,8 @@ public void createWorkingCapitalLoanProductWithBreachAndNearBreachConfig(final D .name(name) // .breachId(breachId) // .nearBreachId(nearBreachId) // - .delinquencyGraceDays(graceDays); + .delinquencyGraceDays(graceDays) // + .breachGraceDays(breachGraceDays); final PostWorkingCapitalLoanProductsResponse response = createWorkingCapitalLoanProduct(request); testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE, response); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachEvaluation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachEvaluation.feature index e0e04fd1ea2..a7c2b35dfd7 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachEvaluation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachEvaluation.feature @@ -144,41 +144,39 @@ Feature: Working Capital Breach Evaluation | 1 | 2026-01-01 | 2026-01-31 | 31 | 500.00 | 0.00 | null | false | | 2 | 2026-02-01 | 2026-02-28 | 28 | 500.00 | 500.00 | null | null | - #TODO pending implementation of grace days support for breach management - @Skip @TestRailId:C76614 Scenario: Verify that breach evaluation matches CSV example with 90 DAY frequency and partial payment When Admin sets the business date to "01 January 2019" 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 | - | 90 | DAYS | PERCENTAGE | 9 | 3 | + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | delinquencyGraceDays | breachGraceDays | + | 90 | DAYS | PERCENTAGE | 9 | 3 | 3 | And Admin creates a working capital loan using created product with the following data: | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | | 01 January 2019 | 01 January 2019 | 9000 | 100000 | 18 | 1000 | And Admin successfully approves the working capital loan on "01 January 2019" with "9000" amount and expected disbursement date on "01 January 2019" - When Admin successfully disburse the Working Capital loan on "01 January 2019" with "9000" EUR transaction amount + When Admin successfully disburse the Working Capital loan on "01 January 2019" with "9000" EUR transaction amount and "1000" discount amount And Admin runs inline COB job for Working Capital Loan by loanId - # Period 1: fromDate=Jan04 (disburse+3 grace), 90 days, min=(9000+1000)*9%=900 - # CSV reference: row 29 — Start=1/4/2019, End=4/4/2019, Days=90, Min=900 + # Period 1: fromDate=Jan04 (disburse+3 breach grace days), 90 days, min=(9000+1000)*9%=900 + # CSV reference: row 29 — Start=1/4/2019, 90 days, Min=900 (CSV end 4/4/2019 is exclusive; schedule stores inclusive toDate 4/3/2019) Then Working Capital loan breach schedule has the following data: | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | - | 1 | 2019-01-04 | 2019-04-04 | 90 | 900.00 | 900.00 | null | null | + | 1 | 2019-01-04 | 2019-04-03 | 90 | 900.00 | 900.00 | null | null | # Payment of 250 on Jan 5 (within period 1) When Admin sets the business date to "05 January 2019" And Admin makes Internal Payment "250.0" on "2019-01-05" # Outstanding should decrease mid-period Then Working Capital loan breach schedule has the following data: | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | - | 1 | 2019-01-04 | 2019-04-04 | 90 | 900.00 | 650.00 | null | null | + | 1 | 2019-01-04 | 2019-04-03 | 90 | 900.00 | 650.00 | null | null | # Advance past period 1 end: 250 < 900 -> breach=true - # CSV reference: row 30 — Period 2 Start=4/4/2019, End=7/3/2019, Days=90 + # CSV reference: row 30 — Period 2 Start=4/4/2019, 90 days (inclusive toDate 7/2/2019) When Admin sets the business date to "05 April 2019" 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 | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | - | 1 | 2019-01-04 | 2019-04-04 | 90 | 900.00 | 650.00 | null | true | - | 2 | 2019-04-04 | 2019-07-03 | 90 | 900.00 | 900.00 | null | null | + | 1 | 2019-01-04 | 2019-04-03 | 90 | 900.00 | 650.00 | null | true | + | 2 | 2019-04-04 | 2019-07-02 | 90 | 900.00 | 900.00 | null | null | @TestRailId:C76615 Scenario: Verify that paidAmount and outstandingAmount update immediately on payment diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachSchedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachSchedule.feature index a14d6244ecf..89c53c598b0 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachSchedule.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachSchedule.feature @@ -118,8 +118,8 @@ Feature: Working Capital Breach Schedule 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 | - | 7 | DAYS | PERCENTAGE | 9 | 3 | + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | delinquencyGraceDays | breachGraceDays | + | 7 | DAYS | PERCENTAGE | 9 | 3 | 3 | And Admin creates a working capital loan using created product with the following data: | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 1000 | @@ -357,8 +357,8 @@ Feature: Working Capital Breach Schedule 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 | - | 7 | DAYS | PERCENTAGE | 10 | 45 | + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | delinquencyGraceDays | breachGraceDays | + | 7 | DAYS | PERCENTAGE | 10 | 45 | 45 | And Admin creates a working capital loan using created product with the following data: | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | @@ -391,3 +391,74 @@ Feature: Working Capital Breach Schedule | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | | 1 | 2026-01-01 | 2026-01-31 | 31 | 0.00 | 0.00 | null | false | | 2 | 2026-02-01 | 2026-02-28 | 28 | 0.00 | 0.00 | null | null | + + @TestRailId:C77000 + Scenario: Verify working capital loan breach schedule - first period start shifted by breachGraceDays + 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 | breachGraceDays | + | 1 | MONTHS | FLAT | 500 | 5 | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | 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 + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-06 | 2026-02-05 | 31 | 500.00 | 500.00 | null | null | + + @TestRailId:C77002 + Scenario: Verify working capital loan account inherits breachGraceDays from product and breach schedule is shifted accordingly + 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 | breachGraceDays | + | 1 | MONTHS | FLAT | 500 | 4 | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan account has breachGraceDays 4 + 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 + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-05 | 2026-02-04 | 31 | 500.00 | 500.00 | null | null | + + @TestRailId:C77003 + Scenario: Verify working capital loan account breachGraceDays override takes precedence over product value in breach schedule + 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 | breachGraceDays | + | 1 | MONTHS | FLAT | 500 | 4 | + And Admin creates a working capital loan using created product with breachGraceDays 9 and the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan account has breachGraceDays 9 + 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 + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-10 | 2026-02-09 | 31 | 500.00 | 500.00 | null | null | + + @TestRailId:C77004 + Scenario: Verify breach schedule is not shifted by delinquencyGraceDays when breachGraceDays is not set + 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 | breachGraceDays | + | 1 | MONTHS | FLAT | 500 | 3 | | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | 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 + # delinquencyGraceDays must shift ONLY the delinquency machinery — breach schedule starts on disbursement date + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-01-31 | 31 | 500.00 | 500.00 | null | null | 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 dfb1e43946c..20fc7b4e52b 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature @@ -197,11 +197,11 @@ Feature: Working Capital 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 | 10 | + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | breachGraceDays | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | + | 3 | MONTHS | FLAT | 900 | 10 | 60 | DAYS | 33.33 | 10 | And Admin creates a working capital loan using created product with the following data: | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | - | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + | 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 @@ -570,8 +570,8 @@ Feature: Working Capital 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 | - | 9 | DAYS | PERCENTAGE | 50 | 3 | DAYS | 33 | 3 | + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays | breachGraceDays | + | 9 | DAYS | PERCENTAGE | 50 | 3 | DAYS | 33 | 3 | 3 | And Admin creates a working capital loan using created product with the following data: | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | | 01 January 2026 | 01 January 2026 | 800 | 100000 | 18 | 0 | @@ -655,3 +655,24 @@ Feature: Working Capital Near Breach Evaluation Then Working Capital loan breach schedule has the following data: | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach | | 1 | 2026-01-01 | 2026-01-09 | 90.00 | 0.00 | true | false | + + @TestRailId:C77001 + Scenario: Verify near breach evaluation window shifts when breachGraceDays is set on the 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 breach and near breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | breachGraceDays | + | 3 | MONTHS | FLAT | 900 | 60 | DAYS | 33.33 | 5 | + And Admin creates a working capital loan using created product with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | 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 + # breachGraceDays=5 -> Period 1: 01-06 -> 04-05; freq=60d -> 1 eval at 03-07 (cumulative required = 33.33% of 900 = 299.97) + # No payment by 03-07 -> cumulative paid=0 < 299.97 -> trigger Y + When Admin sets the business date to "08 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-06 | 2026-04-05 | 900.00 | 900.00 | true | null | 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 4d81130155c..439e406d568 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 @@ -217,6 +217,8 @@ private GetWorkingCapitalLoansLoanIdResponse() {} public Integer delinquencyGraceDays; @Schema(description = "Delinquency start type: LOAN_CREATION or DISBURSEMENT") public StringEnumOptionData delinquencyStartType; + @Schema(example = "0", description = "Number of days to shift the start of the first breach schedule period after disbursement") + public Integer breachGraceDays; @Schema(example = "[2024, 1, 14]", description = "Last closed business date (COB)") public LocalDate lastClosedBusinessDate; public List paymentAllocation; @@ -358,6 +360,8 @@ private PostWorkingCapitalLoansRequest() {} public Integer delinquencyGraceDays; @Schema(example = "LOAN_CREATION", description = "Delinquency start type: LOAN_CREATION or DISBURSEMENT") public String delinquencyStartType; + @Schema(example = "0", description = "Number of days to shift the start of the first breach schedule period after disbursement") + public Integer breachGraceDays; public List paymentAllocation; @Schema(example = "en_GB") @@ -465,6 +469,8 @@ private PutWorkingCapitalLoansLoanIdRequest() {} public Integer delinquencyGraceDays; @Schema(example = "LOAN_CREATION", description = "Delinquency start type: LOAN_CREATION or DISBURSEMENT") public String delinquencyStartType; + @Schema(example = "0", description = "Number of days to shift the start of the first breach schedule period after disbursement") + public Integer breachGraceDays; public List paymentAllocation; @Schema(example = "en_GB") diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java index d245e3e6717..7fa66cd9df7 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java @@ -86,6 +86,7 @@ public class WorkingCapitalLoanData implements Serializable { private WorkingCapitalLoanBalanceData balance; private Integer delinquencyGraceDays; private StringEnumOptionData delinquencyStartType; + private Integer breachGraceDays; private BigDecimal totalPaymentVolume; private WorkingCapitalLoanCollectionData collectionData; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java index 2e7a3ca52ac..7b4ef79faa5 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java @@ -72,6 +72,7 @@ public interface WorkingCapitalLoanMapper { @Mapping(target = "disbursementDetails", source = "disbursementDetails") @Mapping(target = "delinquencyGraceDays", source = "loanProductRelatedDetails.delinquencyGraceDays") @Mapping(target = "delinquencyStartType", source = "loanProductRelatedDetails", qualifiedByName = "delinquencyStartTypeData") + @Mapping(target = "breachGraceDays", source = "loanProductRelatedDetails.breachGraceDays") @Mapping(target = "collectionData", ignore = true) @Mapping(target = "totalNoPayments", ignore = true) @Mapping(target = "periodPaymentAmount", ignore = true) diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanApplicationDataValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanApplicationDataValidator.java index 7913e4ca204..97b77e6e01a 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanApplicationDataValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanApplicationDataValidator.java @@ -93,7 +93,8 @@ public class WorkingCapitalLoanApplicationDataValidator { WorkingCapitalLoanProductConstants.repaymentFrequencyTypeParamName, WorkingCapitalLoanConstants.submittedOnNoteParameterName, WorkingCapitalLoanProductConstants.breachIdParamName, WorkingCapitalLoanProductConstants.allowAttributeOverridesParamName, WorkingCapitalLoanProductConstants.paymentAllocationParamName, WorkingCapitalLoanProductConstants.delinquencyGraceDaysParamName, - WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, WorkingCapitalLoanProductConstants.nearBreachIdParamName)); + WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, WorkingCapitalLoanProductConstants.nearBreachIdParamName, + WorkingCapitalLoanProductConstants.breachGraceDaysParamName)); private final FromJsonHelper fromApiJsonHelper; private final WorkingCapitalPaymentAllocationDataValidator paymentAllocationDataValidator; @@ -217,6 +218,13 @@ public void validateForCreate(final JsonCommand command) { .value(delinquencyGraceDays).ignoreIfNull().integerZeroOrGreater(); } + if (this.fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element)) { + final Integer breachGraceDays = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element); + baseDataValidator.reset().parameter(WorkingCapitalLoanProductConstants.breachGraceDaysParamName).value(breachGraceDays) + .ignoreIfNull().integerZeroOrGreater(); + } + if (this.fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, element)) { final String delinquencyStartTypeValue = this.fromApiJsonHelper .extractStringNamed(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, element); @@ -464,6 +472,14 @@ private void validateForUpdate(final JsonCommand command, final Long existingPro .value(delinquencyGraceDays).ignoreIfNull().integerZeroOrGreater(); } + if (this.fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element)) { + atLeastOneParameterPassedForUpdate = true; + final Integer breachGraceDays = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element); + baseDataValidator.reset().parameter(WorkingCapitalLoanProductConstants.breachGraceDaysParamName).value(breachGraceDays) + .ignoreIfNull().integerZeroOrGreater(); + } + if (this.fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, element)) { final String delinquencyStartTypeValue = this.fromApiJsonHelper .extractStringNamed(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, element); 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 06a361ded73..b047dccaa70 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 @@ -107,7 +107,8 @@ public WorkingCapitalLoanTemplateData retrieveTemplate(final Long productId, fin .discount(product.getDiscount()) // .paymentAllocation(product.getPaymentAllocation()) // .breach(product.getBreach()) // - .nearBreach(product.getNearBreach()); // + .nearBreach(product.getNearBreach()) // + .breachGraceDays(product.getBreachGraceDays()); // } } if (clientId != null) { diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAssemblerImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAssemblerImpl.java index c3f7f327d97..605a16793b2 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAssemblerImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAssemblerImpl.java @@ -209,6 +209,10 @@ private WorkingCapitalLoanProductRelatedDetails buildLoanProductRelatedDetails(f ? fromApiJsonHelper.extractIntegerNamed(WorkingCapitalLoanProductConstants.delinquencyGraceDaysParamName, element, new HashSet<>()) : productDetail.getDelinquencyGraceDays()); + detail.setBreachGraceDays(fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element) + ? fromApiJsonHelper.extractIntegerNamed(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element, + new HashSet<>()) + : productDetail.getBreachGraceDays()); final String delinquencyStartTypeValue = fromApiJsonHelper .parameterExists(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, element) ? fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, element) @@ -417,6 +421,15 @@ public Map updateFrom(final JsonCommand command, final WorkingCa changes.put(WorkingCapitalLoanProductConstants.delinquencyGraceDaysParamName, delinquencyGraceDays); } } + if (fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element)) { + final Integer breachGraceDays = fromApiJsonHelper + .extractIntegerWithLocaleNamed(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element); + if (command.isChangeInIntegerParameterNamed(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, + detail.getBreachGraceDays())) { + detail.setBreachGraceDays(breachGraceDays); + changes.put(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, breachGraceDays); + } + } if (fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, element)) { final String existingValue = detail.getDelinquencyStartType() != null ? detail.getDelinquencyStartType().name() : null; if (command.isChangeInStringParameterNamed(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, 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 b34a18c8fc1..b57b257cbbb 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 @@ -64,8 +64,7 @@ public void generateInitialPeriod(final WorkingCapitalLoan loan) { return; } - final int graceDays = getGraceDays(loan); - final LocalDate fromDate = disbursementDateOptional.get().plusDays(graceDays); + final LocalDate fromDate = disbursementDateOptional.get().plusDays(getBreachGraceDays(loan)); final WorkingCapitalBreach breach = breachOpt.get(); final LocalDate toDate = calculateToDate(fromDate, breach.getBreachFrequency(), breach.getBreachFrequencyType()); final BigDecimal minPaymentAmount = calculateMinPaymentAmount(loan, breach); @@ -196,12 +195,9 @@ private Optional getBreachConfig(final WorkingCapitalLoan return Optional.ofNullable(details.getBreach()); } - private int getGraceDays(final WorkingCapitalLoan loan) { + private Integer getBreachGraceDays(final WorkingCapitalLoan loan) { final WorkingCapitalLoanProductRelatedDetails details = loan.getLoanProductRelatedDetails(); - if (details == null || details.getDelinquencyGraceDays() == null) { - return 0; - } - return details.getDelinquencyGraceDays(); + return (details == null || details.getBreachGraceDays() == null) ? 0 : details.getBreachGraceDays(); } private LocalDate calculateToDate(final LocalDate fromDate, final Integer frequency, diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/WorkingCapitalLoanProductConstants.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/WorkingCapitalLoanProductConstants.java index 6540eb92075..7408e7d907c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/WorkingCapitalLoanProductConstants.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/WorkingCapitalLoanProductConstants.java @@ -69,6 +69,9 @@ private WorkingCapitalLoanProductConstants() { public static final String delinquencyGraceDaysParamName = "delinquencyGraceDays"; public static final String delinquencyStartTypeParamName = "delinquencyStartType"; + // Breach grace + public static final String breachGraceDaysParamName = "breachGraceDays"; + // Accounting public static final String accountingRuleParamName = "accountingRule"; public static final String fundSourceAccountIdParamName = "fundSourceAccountId"; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/api/WorkingCapitalLoanProductApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/api/WorkingCapitalLoanProductApiResourceSwagger.java index 355e138cd0a..6e374dc1f74 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/api/WorkingCapitalLoanProductApiResourceSwagger.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/api/WorkingCapitalLoanProductApiResourceSwagger.java @@ -127,6 +127,8 @@ private PostWorkingCapitalLoanProductsRequest() {} public String delinquencyStartType; @Schema(example = "1") public Long nearBreachId; + @Schema(example = "0", description = "Number of days to shift the start of the first breach schedule period after disbursement") + public Integer breachGraceDays; // Configurable attributes public PostAllowAttributeOverrides allowAttributeOverrides; @@ -329,6 +331,8 @@ private GetWorkingCapitalLoanProductsResponse() {} @Schema(example = "1") public Integer delinquencyGraceDays; public StringEnumOptionData delinquencyStartType; + @Schema(example = "0", description = "Number of days to shift the start of the first breach schedule period after disbursement") + public Integer breachGraceDays; // Configurable attributes public GetConfigurableAttributes allowAttributeOverrides; @@ -510,6 +514,8 @@ private GetWorkingCapitalLoanProductsProductIdResponse() {} public Integer delinquencyGraceDays; public StringEnumOptionData delinquencyStartType; public GetWorkingCapitalLoanNearBreach nearBreach; + @Schema(example = "0", description = "Number of days to shift the start of the first breach schedule period after disbursement") + public Integer breachGraceDays; // Configurable attributes public GetWorkingCapitalLoanProductsResponse.GetConfigurableAttributes allowAttributeOverrides; @@ -591,6 +597,8 @@ private PutWorkingCapitalLoanProductsProductIdRequest() {} public String delinquencyStartType; @Schema(example = "1") public Long nearBreachId; + @Schema(example = "0", description = "Number of days to shift the start of the first breach schedule period after disbursement") + public Integer breachGraceDays; // Configurable attributes public PostWorkingCapitalLoanProductsRequest.PostAllowAttributeOverrides allowAttributeOverrides; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/data/WorkingCapitalLoanProductData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/data/WorkingCapitalLoanProductData.java index 3f25bf2c4cf..a50c1eeefb3 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/data/WorkingCapitalLoanProductData.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/data/WorkingCapitalLoanProductData.java @@ -88,6 +88,7 @@ public class WorkingCapitalLoanProductData implements Serializable { private StringEnumOptionData repaymentFrequencyType; private Integer delinquencyGraceDays; private StringEnumOptionData delinquencyStartType; + private Integer breachGraceDays; // Configurable attributes (allowAttributeOverrides) private WorkingCapitalLoanProductConfigurableAttributesData allowAttributeOverrides; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductRelatedDetail.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductRelatedDetail.java index 0aca25e434c..81a5260963b 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductRelatedDetail.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductRelatedDetail.java @@ -71,4 +71,7 @@ public class WorkingCapitalLoanProductRelatedDetail { @Enumerated(EnumType.STRING) @Column(name = "delinquency_start_type", nullable = false) private WorkingCapitalLoanDelinquencyStartType delinquencyStartType; + + @Column(name = "breach_grace_days", nullable = true) + private Integer breachGraceDays; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductRelatedDetails.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductRelatedDetails.java index d8a693ffe60..b8560720604 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductRelatedDetails.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductRelatedDetails.java @@ -95,4 +95,7 @@ public class WorkingCapitalLoanProductRelatedDetails { @Enumerated(EnumType.STRING) @Column(name = "delinquency_start_type", nullable = false) private WorkingCapitalLoanDelinquencyStartType delinquencyStartType; + + @Column(name = "breach_grace_days", nullable = true) + private Integer breachGraceDays; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/mapper/WorkingCapitalLoanProductMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/mapper/WorkingCapitalLoanProductMapper.java index 064fa9f8324..34c4ada78e1 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/mapper/WorkingCapitalLoanProductMapper.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/mapper/WorkingCapitalLoanProductMapper.java @@ -72,6 +72,7 @@ public interface WorkingCapitalLoanProductMapper { @Mapping(target = "allowAttributeOverrides", source = "configurableAttributes", qualifiedByName = "configurableAttributesToData") @Mapping(target = "delinquencyGraceDays", source = "relatedDetail.delinquencyGraceDays") @Mapping(target = "delinquencyStartType", source = "relatedDetail.delinquencyStartType", qualifiedByName = "delinquencyStartTypeToStringEnumOptionData") + @Mapping(target = "breachGraceDays", source = "relatedDetail.breachGraceDays") @Mapping(target = "accountingRule", source = "accountingRule", qualifiedByName = "accountingRuleToStringEnumOptionData") @Mapping(target = "accountingMappings", ignore = true) @Mapping(target = "paymentChannelToFundSourceMappings", ignore = true) diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/serialization/WorkingCapitalLoanProductDataValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/serialization/WorkingCapitalLoanProductDataValidator.java index 86e22c8fdd3..e1b00ce8620 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/serialization/WorkingCapitalLoanProductDataValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/serialization/WorkingCapitalLoanProductDataValidator.java @@ -99,6 +99,7 @@ public class WorkingCapitalLoanProductDataValidator { WorkingCapitalLoanProductConstants.allowAttributeOverridesParamName, // WorkingCapitalLoanProductConstants.delinquencyGraceDaysParamName, // WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, // + WorkingCapitalLoanProductConstants.breachGraceDaysParamName, // WorkingCapitalLoanProductConstants.accountingRuleParamName, // WorkingCapitalLoanProductConstants.fundSourceAccountIdParamName, // WorkingCapitalLoanProductConstants.loanPortfolioAccountIdParamName, // @@ -384,6 +385,13 @@ private void validateSettingsFields(final JsonElement element, final DataValidat .value(delinquencyGraceDays).ignoreIfNull().integerZeroOrGreater(); } + if (this.fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element)) { + final Integer breachGraceDays = this.fromApiJsonHelper + .extractIntegerNamed(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, element, locale); + baseDataValidator.reset().parameter(WorkingCapitalLoanProductConstants.breachGraceDaysParamName).value(breachGraceDays) + .ignoreIfNull().integerZeroOrGreater(); + } + if (this.fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, element)) { final String delinquencyStartTypeValue = this.fromApiJsonHelper .extractStringNamed(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName, element); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductUpdateUtil.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductUpdateUtil.java index 8dbbd94c824..6a886c1bba9 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductUpdateUtil.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductUpdateUtil.java @@ -127,6 +127,12 @@ public Map updateRelatedDetail(final WorkingCapitalLoanProductRe changes.put(WorkingCapitalLoanProductConstants.delinquencyGraceDaysParamName, newValue); relatedDetail.setDelinquencyGraceDays(newValue); } + if (command.isChangeInIntegerParameterNamed(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, + relatedDetail.getBreachGraceDays())) { + final Integer newValue = command.integerValueOfParameterNamed(WorkingCapitalLoanProductConstants.breachGraceDaysParamName); + changes.put(WorkingCapitalLoanProductConstants.breachGraceDaysParamName, newValue); + relatedDetail.setBreachGraceDays(newValue); + } final String currentDelinquencyStartType = (relatedDetail.getDelinquencyStartType() != null) ? relatedDetail.getDelinquencyStartType().name() : null; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductWritePlatformServiceImpl.java index 0eec569cab4..e9cc619ba94 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductWritePlatformServiceImpl.java @@ -401,10 +401,13 @@ private WorkingCapitalLoanProduct createProductFromCommand(final Fund fund, fina .stringValueOfParameterNamed(WorkingCapitalLoanProductConstants.delinquencyStartTypeParamName); final WorkingCapitalLoanDelinquencyStartType delinquencyStartType = WorkingCapitalLoanDelinquencyStartType .fromString(delinquencyStartTypeValue); + final Integer breachGraceDays = command.parameterExists(WorkingCapitalLoanProductConstants.breachGraceDaysParamName) + ? command.integerValueOfParameterNamed(WorkingCapitalLoanProductConstants.breachGraceDaysParamName) + : 0; final WorkingCapitalLoanProductRelatedDetail relatedDetail = new WorkingCapitalLoanProductRelatedDetail(amortizationType, npvDayCount, principal, periodPaymentRate, repaymentEvery, repaymentFrequencyType, discount, delinquencyGraceDays, - delinquencyStartType); + delinquencyStartType, breachGraceDays); // Min/max constraints final BigDecimal minPrincipal = command.parameterExists(WorkingCapitalLoanProductConstants.minPrincipalParamName) diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml index e598a31fa98..25190c291cb 100644 --- a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml @@ -64,4 +64,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0043_wc_breach_grace_days.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0043_wc_breach_grace_days.xml new file mode 100644 index 00000000000..4adb9791783 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0043_wc_breach_grace_days.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductCRUDTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductCRUDTest.java index c658e06af99..5c69c5cdd11 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductCRUDTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductCRUDTest.java @@ -211,6 +211,30 @@ public void testUpdateWorkingCapitalLoanProduct() { wclProductHelper.deleteWorkingCapitalLoanProductById(productId); } + @Test + public void testUpdateWorkingCapitalLoanProductBreachGraceDays() { + // Given - product created with default breachGraceDays = 0 + final String uniqueName = "Test wcl Product " + UUID.randomUUID().toString().substring(0, 8); + final String uniqueShortName = Utils.uniqueRandomStringGenerator("", 4); + final PostWorkingCapitalLoanProductsRequest createRequest = new WorkingCapitalLoanProductTestBuilder().withName(uniqueName) + .withShortName(uniqueShortName).build(); + final Long productId = wclProductHelper.createWorkingCapitalLoanProduct(createRequest).getResourceId(); + final GetWorkingCapitalLoanProductsProductIdResponse afterCreate = wclProductHelper + .retrieveWorkingCapitalLoanProductById(productId); + assertEquals(0, afterCreate.getBreachGraceDays()); + + // When - update breachGraceDays to 7 + final PutWorkingCapitalLoanProductsProductIdRequest updateRequest = new WorkingCapitalLoanProductTestBuilder().withName(uniqueName) + .withShortName(uniqueShortName).withBreachGraceDays(7).buildUpdateRequest(); + wclProductHelper.updateWorkingCapitalLoanProductById(productId, updateRequest); + + // Then + final GetWorkingCapitalLoanProductsProductIdResponse afterUpdate = wclProductHelper + .retrieveWorkingCapitalLoanProductById(productId); + assertEquals(7, afterUpdate.getBreachGraceDays()); + wclProductHelper.deleteWorkingCapitalLoanProductById(productId); + } + @Test public void testUpdateWorkingCapitalLoanProductByExternalId() { // Given @@ -397,6 +421,7 @@ public void testHappyPath_CreateAndRetrieve_VerifyAllFields() { .withPaymentAllocationTypes(paymentAllocationTypes) // .withDelinquencyGraceDays(1) // .withDelinquencyStartType(WorkingCapitalLoanDelinquencyStartType.DISBURSEMENT.getCode()) // + .withBreachGraceDays(5) // // Term category .withPrincipalAmountMin(BigDecimal.valueOf(1000)) // .withPrincipalAmountDefault(BigDecimal.valueOf(5000)) // @@ -463,6 +488,7 @@ public void testHappyPath_CreateAndRetrieve_VerifyAllFields() { assertEquals(365, retrieved.getNpvDayCount()); assertEquals(1, retrieved.getDelinquencyGraceDays()); assertEquals("DISBURSEMENT", retrieved.getDelinquencyStartType().getCode()); + assertEquals(5, retrieved.getBreachGraceDays()); // Verify Payment Allocation (if present) if (retrieved.getPaymentAllocation() != null && !retrieved.getPaymentAllocation().isEmpty()) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductValidationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductValidationTest.java index f7cfca73913..7c9d13e4b5b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductValidationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductValidationTest.java @@ -743,4 +743,18 @@ public void testUpdateWorkingCapitalLoanProductWithInvalidDateRange() { wclProductHelper.deleteWorkingCapitalLoanProductById(productId); } + + @Test + public void testCreateWorkingCapitalLoanProductWithNegativeBreachGraceDays() { + // Given - breachGraceDays must be >= 0 + final PostWorkingCapitalLoanProductsRequest request = new WorkingCapitalLoanProductTestBuilder().withBreachGraceDays(-1).build(); + + // When & Then + final CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> wclProductHelper.createWorkingCapitalLoanProduct(request)); + assertEquals(400, exception.getStatus()); + assertNotNull(exception.getDeveloperMessage()); + assertEquals("Validation errors: [breachGraceDays] The parameter `breachGraceDays` must be zero or greater.", + exception.getDeveloperMessage()); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationTestBuilder.java index 57944d01662..285c6dfba4f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationTestBuilder.java @@ -58,6 +58,7 @@ public class WorkingCapitalLoanApplicationTestBuilder { private List paymentAllocationTypes; private Integer delinquencyGraceDays; private String delinquencyStartType; + private Integer breachGraceDays; public WorkingCapitalLoanApplicationTestBuilder withClientId(final Long clientId) { this.clientId = clientId; @@ -154,6 +155,11 @@ public WorkingCapitalLoanApplicationTestBuilder withDelinquencyStartType(final S return this; } + public WorkingCapitalLoanApplicationTestBuilder withBreachGraceDays(final Integer breachGraceDays) { + this.breachGraceDays = breachGraceDays; + return this; + } + public WorkingCapitalLoanApplicationTestBuilder withPaymentAllocationTypes(final List paymentAllocationTypes) { this.paymentAllocationTypes = paymentAllocationTypes; return this; @@ -206,6 +212,9 @@ private PostWorkingCapitalLoansRequest populateSubmitRequest(final PostWorkingCa if (delinquencyStartType != null) { request.delinquencyStartType(delinquencyStartType); } + if (breachGraceDays != null) { + request.breachGraceDays(breachGraceDays); + } if (breachId != null) { request.breachId(breachId); } @@ -268,6 +277,7 @@ private PutWorkingCapitalLoansLoanIdRequest populateModifyRequest(final PutWorki } request.delinquencyGraceDays(delinquencyGraceDays); request.delinquencyStartType(delinquencyStartType); + request.breachGraceDays(breachGraceDays); if (breachId != null) { request.breachId(breachId); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductTestBuilder.java index 2e35244f8b6..d892db3441e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductTestBuilder.java @@ -77,6 +77,7 @@ public class WorkingCapitalLoanProductTestBuilder { private Map allowAttributeOverrides; private Integer delinquencyGraceDays; private String delinquencyStartType; + private Integer breachGraceDays; private AccountingRuleEnum accountingRule = DEFAULT_ACCOUNTING_RULE; private Long nearBreachId; @@ -224,6 +225,11 @@ public WorkingCapitalLoanProductTestBuilder withDelinquencyStartType(final Strin return this; } + public WorkingCapitalLoanProductTestBuilder withBreachGraceDays(final Integer breachGraceDays) { + this.breachGraceDays = breachGraceDays; + return this; + } + public WorkingCapitalLoanProductTestBuilder withAccountingRule(final AccountingRuleEnum accountingRule) { this.accountingRule = accountingRule; return this; @@ -333,6 +339,7 @@ private void populateCommonFields(final PostWorkingCapitalLoanProductsRequest re } request.setDelinquencyGraceDays(this.delinquencyGraceDays); request.setDelinquencyStartType(this.delinquencyStartType); + request.setBreachGraceDays(this.breachGraceDays); request.setBreachId(this.breachId); request.setAccountingRule(this.accountingRule); request.setNearBreachId(this.nearBreachId); @@ -379,6 +386,7 @@ private void populateCommonFields(final PutWorkingCapitalLoanProductsProductIdRe } request.setDelinquencyGraceDays(this.delinquencyGraceDays); request.setDelinquencyStartType(this.delinquencyStartType); + request.setBreachGraceDays(this.breachGraceDays); request.setBreachId(this.breachId); if (this.accountingRule != null) { request.setAccountingRule(PutWorkingCapitalLoanProductsProductIdRequest.AccountingRuleEnum.valueOf(this.accountingRule.name()));