diff --git a/.claude/commands/new-dmn-check.md b/.claude/commands/new-dmn-check.md new file mode 100644 index 00000000..06a7ae4f --- /dev/null +++ b/.claude/commands/new-dmn-check.md @@ -0,0 +1,469 @@ +--- +name: new-dmn-check +description: Create a new DMN eligibility check in library-api — generates the DMN XML and Bruno test files +argument-hint: "[CheckName] [category]" +disable-model-invocation: true +--- + +Create a new DMN eligibility check in library-api. Arguments: `$ARGUMENTS` (optional — check name in PascalCase and/or category, e.g. `PersonMinIncome income`). + +## Step 1 — Gather Requirements + +Parse `$ARGUMENTS` if provided: +- First token (PascalCase word) → check name (e.g. `PersonMinIncome`) +- Second token (lowercase word) → category (e.g. `income`) + +If either is missing, ask the user for: +1. **Check name** — PascalCase, globally unique (e.g. `PersonMinIncome`). Derive the file name by converting to kebab-case (e.g. `person-min-income.dmn`) and the service name by appending `Service` (e.g. `PersonMinIncomeService`). +2. **Category** — existing (`age`, `enrollment`) or a new category name. +3. **Description** — one sentence describing what eligibility condition is checked. +4. **Parameters** beyond `situation` — for each: name, FEEL type (`string`, `number`, `date`, `boolean`), and purpose. If there are none, the `parameters` input will be omitted entirely from the DMN model. +5. **FEEL logic** — either a FEEL expression or a plain-English description. Describe which `situation` fields are used (e.g. `situation.people`, `situation.enrollments`, `situation.simpleChecks.*`). +6. **Does the logic need intermediate values?** — Yes → context-chain pattern; No → simple literal expression pattern. + +## Step 2 — Validate Before Generating + +Run these checks before writing any files: + +**A. Model name uniqueness** — Search all existing DMN files: +``` +Grep for: name="{CheckName}" +Path: library-api/src/main/resources/**/*.dmn +``` +If a match is found, warn the user and stop. The name must be globally unique. + +**B. Category directory** — Check if `library-api/src/main/resources/checks/{category}/` exists. +- If it exists, note the base module file (e.g. `Age.dmn`, `Enrollment.dmn`) and its namespace URI for importing. +- If it doesn't exist, warn the user that a base module DMN must be created first for the new category (with its own namespace URI, types, and BKMs), and offer to create a minimal one. + +**C. Existing checks in category** — List existing check DMNs in the category directory so the user can confirm there are no near-duplicate checks. + +## Step 3 — Generate the DMN File + +**File path**: `library-api/src/main/resources/checks/{category}/{check-name}.dmn` + +**Rules**: +- Generate fresh UUID v4 values for every `id` attribute (format: `_XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX` using uppercase hex). Each UUID must be unique within the file. +- The model's own `namespace` attribute is a fresh UUID URI: `https://kie.apache.org/dmn/_{UUID}`. +- BDT namespace is always: `https://kie.apache.org/dmn/_1B91A885-130A-4E0B-A762-E12AA6DD5C79` +- BDT `locationURI`: use `../BDT.dmn` for checks directly in `checks/{category}/` (one level deep). Use `../../BDT.dmn` if nested deeper. +- Category base module import is **mandatory** — always import `{Category}.dmn` (same directory) even if no types from it are referenced. This establishes the category association. +- Decision Service name: exactly `{CheckName}Service`. +- Decision Service output type: `BDT.tCheckResponse`. +- The output decision must be named `checkResult` with `typeRef="boolean"`. +- **Parameters**: if the check needs caller-supplied values, define a `tParameters` item definition and include the `parameters` inputData node, its `dmn:informationRequirement`, and its DMNDI shape/edge. If the check has no parameters, omit all of these entirely — no `tParameters` type, no `parameters` inputData element. This way the generated OpenAPI schema accurately reflects that the endpoint takes only `situation`. +- `tSituation` item definition includes only the fields the check actually reads (keep it minimal — don't copy BDT's full tSituation). +- If a `tSituation` field is itself a complex type (e.g. `simpleChecks`), define a **local** version of that nested type containing only the specific properties this check uses. Reference the local type, not the BDT one. Example: if the check reads only `situation.simpleChecks.ownerOccupant`, define a local `tSimpleChecks` with just `ownerOccupant: boolean`, and use `typeRef="tSimpleChecks"` in `tSituation` (not `BDT.tSimpleChecks`). + +### Inverse Check Pattern + +If the new check is the logical inverse of an **existing** check (e.g. `NoTenYearTaxAbatement` is the inverse of `TenYearTaxAbatement`), model it by importing and negating — do **not** duplicate the logic: + +1. Add a third import for the sibling check DMN (e.g. `xmlns:included3="{siblingNamespace}"` and a `` element). +2. Add a `` in the `checkResult` decision referencing the sibling's Decision Service id: `href="{siblingNamespace}#{siblingServiceId}"`. +3. Write the FEEL expression as: `not({SiblingAlias}.{SiblingCheckName}Service(situation: situation))` — or include `parameters: parameters` if the sibling takes parameters. +4. In DMNDI, add a `` for the imported service (using `dmnElementRef="included3:{siblingServiceId}"`) and a `` for the knowledge requirement edge. Place the imported service shape above and to the right of the main service box. +5. The sibling check's namespace URI and Decision Service id can be found in its DMN file — read it before generating. + +**Example**: `no-ten-year-tax-abatement.dmn` imports `ten-year-tax-abatement.dmn` and evaluates: +``` +not(TenYearTaxAbatement.TenYearTaxAbatementService(situation: situation)) +``` +This mirrors `person-not-enrolled-in-benefit.dmn` which imports `person-enrolled-in-benefit.dmn`. + +### Template A — Simple Literal Expression + +Use when the FEEL logic is a single expression with no intermediate values needed. + +Based on `person-enrolled-in-benefit.dmn`: + +```xml + + + {One sentence description} + + + + + + + + + + {feelType} + + + + + + + + Enrollment.tEnrollmentList + + + + + + + + + + + + + + + + + + + + + + + + {FEEL expression} + + + + + + + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**DMNDI layout coordinate guide** (fill in the placeholders above): +- `{DS_WIDTH}` — approximately `max(200, len("{CheckName}Service") * 12 + 40)`; round to nearest 10 +- `{DS_X}` — choose so the service box is centered around x≈310; e.g. `310 - DS_WIDTH/2` +- `{DS_X_RIGHT}` — `DS_X + DS_WIDTH` +- `{DECISION_X}` — `DS_X + (DS_WIDTH - 88) / 2` (horizontally centers the 88px decision inside the service box); y is always **147** — this leaves a 41px gap below the service box top (y=106) so the service name label doesn't overlap the decision node +- `{DECISION_CENTER_X}` — `DECISION_X + 44` +- `{SITUATION_X}` — with parameters: `DS_X` (left-align); without parameters: `DS_X + (DS_WIDTH - 100) / 2` (center under service box) +- `{PARAMS_X}` — `DS_X + DS_WIDTH - 100` (right-align with service box); omit if no parameters +- `{SITUATION_CENTER_X}` — `SITUATION_X + 50` +- `{PARAMS_CENTER_X}` — `PARAMS_X + 50`; omit if no parameters + +**DMNDI rules** (always enforce): +- Input nodes (`situation`, `parameters`) must be at a higher y-value than the service box (i.e. below it visually). Use y=336 when the service box occupies y=106–305. +- Edge waypoints go FROM the center of the input node (x+50, 361) TO the bottom-center of `checkResult` (DECISION_CENTER_X, 181). +- Do **not** add `DMNShape` entries for BKMs or decisions imported from BDT or the category module unless they are directly called (via `dmn:knowledgeRequirement`) by this check. Unused imported elements clutter the diagram. + +### Template B — Context Chain + +Use when the FEEL logic needs intermediate computed values (e.g. extract a field, call a BKM, then compare). + +Based on `person-min-age.dmn`. The decision body uses a `dmn:context` instead of `dmn:literalExpression`: + +```xml + + + + + + + + + + + + + + + + {FEEL expression for this step} + + + + + + + {final boolean expression, e.g.: result} + + + + +``` + +The rest of the file structure (imports, itemDefinitions, inputData, decisionService, DMNDI) is identical to Template A. + +**BKM knowledge requirement note**: When calling a BKM from an imported module (e.g. `Age.as of date(...)`), the `` href must reference the imported module's namespace URI followed by `#` and the BKM's `id` attribute in that file. Find the BKM id by reading the base module DMN. Use namespace-qualified calls in FEEL: `{ImportAlias}.{bkmName}(...)`. + +## Step 4 — Generate Bruno Test Files + +**Directory**: `library-api/test/bdt/checks/{category}/{CheckName}/` + +Create three files: + +**Pass.bru** — a request that should make `checkResult` evaluate to `true`: +```bru +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/{category}/{check-name} + body: json + auth: inherit +} + +body:json { + { + "situation": { + {situationFieldsForPassCase} + }, + "parameters": { + {parametersForPassCase} + } + } +} + +assert { + res.body.checkResult: eq true + res.status: eq 200 +} +``` + +**Fail.bru** — a request that should make `checkResult` evaluate to `false`: +```bru +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/{category}/{check-name} + body: json + auth: inherit +} + +body:json { + { + "situation": { + {situationFieldsForFailCase} + }, + "parameters": { + {parametersForFailCase} + } + } +} + +assert { + res.body.checkResult: eq false + res.status: eq 200 +} +``` + +**Null.bru** — a request that should make `checkResult` evaluate to `null` (missing/unknown data): +```bru +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/{category}/{check-name} + body: json + auth: inherit +} + +body:json { + { + "situation": { + {situationFieldsForNullCase} + }, + "parameters": { + {parametersForNullCase} + } + } +} + +assert { + res.body.checkResult: eq null + res.status: eq 200 +} +``` + +Use a situation where the relevant field is absent or set to `null`, triggering the null-return branch of the FEEL expression. + +**simpleChecks-based checks**: when the check reads `situation.simpleChecks.`, the Null case must set the **specific boolean field** to `null` — not the entire `simpleChecks` object. This tests the inner null guard in the FEEL expression: +```json +"situation": { + "simpleChecks": { + "tenYearTaxAbatement": null + } +} +``` +Not: +```json +"situation": { + "simpleChecks": null +} +``` + +**Parameterless checks**: omit `"parameters"` from the request body entirely in all three test files. Do not send `"parameters": {}` or `"parameters": null`. + +Use realistic test data: concrete IDs (e.g. `"p1"`), dates (ISO 8601), and parameter values that clearly demonstrate the pass vs. fail condition. + +## Step 5 — Run Tests + +Run the full test suites to verify the new check integrates correctly. + +**1. Maven tests** — compile and run the Java test suite: +```bash +cd library-api && mvn test +``` +If tests fail, diagnose the error (most likely a malformed DMN or namespace conflict) and fix the DMN file before proceeding. + +**2. Bruno tests** — run the full API test suite (requires the library-api dev server running at `http://localhost:8083`): +```bash +cd library-api/test/bdt && bru run +``` +If the server is not running, start it first (`cd library-api && quarkus dev`), wait for it to be ready, then run `bru run`. + +If any Bruno tests fail: +- Check that the endpoint URL in the `.bru` files matches the actual generated route (verify in Swagger UI at `http://localhost:8083/q/swagger-ui`) +- Verify the FEEL logic produces the expected `true`/`false`/`null` outputs for each test case +- Fix the DMN or `.bru` files and re-run until all tests pass + +Only proceed to Step 6 once **both** `mvn test` and `bru run` pass cleanly. + +## Step 6 — Print a Summary and Next-Steps Checklist + +After all tests pass, print: + +``` +## Files Created + +- library-api/src/main/resources/checks/{category}/{check-name}.dmn +- library-api/test/bdt/checks/{category}/{CheckName}/Pass.bru +- library-api/test/bdt/checks/{category}/{CheckName}/Fail.bru +- library-api/test/bdt/checks/{category}/{CheckName}/Null.bru + +## Next Steps + +- [ ] Verify the endpoint appears in Swagger UI: + http://localhost:8083/q/swagger-ui (look for POST /checks/{category}/{check-name}) +- [ ] If you created a new category, create the base module DMN first: + library-api/src/main/resources/checks/{category}/{Category}.dmn +``` + +--- + +## Critical Constraints (always enforce) + +1. **Model name uniqueness** — check all `.dmn` files before generating. Stop if duplicate found. +2. **Service name** — must be exactly `{CheckName}Service`. No variations. +3. **File name** — kebab-case, `.dmn` extension (e.g. `person-min-income.dmn`). +4. **Namespace-qualified references** — when calling imported BKMs or decisions, always prefix with the import alias (e.g. `Age.as of date(...)`, `BDT.tCheckResponse`). +5. **No circular imports** — checks import BDT.dmn and their category base module, and may also import one sibling check when implementing the "inverse check" pattern (see Step 3). They must never create cycles (A imports B imports A). +6. **BDT import path** — relative to the check file: `../BDT.dmn` for `checks/{category}/` files. +7. **Fresh UUIDs** — generate a new UUID v4 for every `id` attribute. Never reuse UUIDs from example files. +8. **tSituation is local and minimal** — define only the `situation` fields this check actually reads. Do not copy BDT's full tSituation definition. If a field's type is itself complex (e.g. `simpleChecks`), define a local version of that nested type too, containing only the specific properties used. Never reference BDT's version of a nested type (e.g. `BDT.tSimpleChecks`) when a local minimal definition suffices. +9. **Output decision named `checkResult`** — the boolean output decision must always be named `checkResult`. +10. **Always import the category base module** — every check must import its category's base module DMN (e.g. `Residence.dmn`, `Age.dmn`), even if no types or BKMs from it are used in this check. +11. **DMNDI: inputs below, no unused imported shapes** — input nodes must always be placed below the decision service box (higher y). Never add `DMNShape` entries for BKMs or elements from imported modules unless they are explicitly called by a `dmn:knowledgeRequirement` in this check. +12. **Parameterless checks omit `parameters` entirely** — if the check has no caller-supplied parameters, remove `tParameters`, the `parameters` inputData element, its `dmn:informationRequirement`, its DMNDI shape and edge, and the `"parameters"` key from all Bruno test request bodies. An absent `parameters` input in the DMN type system is the correct way to express this; do not use an empty context or null type. +13. **simpleChecks Null.bru targets the specific field** — when the check reads `situation.simpleChecks.`, the Null test must set that specific boolean field to `null`, not the entire `simpleChecks` object to `null`. Setting the parent object to null tests a different (shallower) branch of the FEEL null guard than is most useful. diff --git a/library-api/src/main/resources/benefits/pa/phl/homestead-exemption.dmn b/library-api/src/main/resources/benefits/pa/phl/homestead-exemption.dmn index e11e85ae..c28524c0 100644 --- a/library-api/src/main/resources/benefits/pa/phl/homestead-exemption.dmn +++ b/library-api/src/main/resources/benefits/pa/phl/homestead-exemption.dmn @@ -1,10 +1,13 @@ - + + + + string @@ -43,9 +46,18 @@ + + + + + + + + + - + PersonNotEnrolledInBenefit.PersonNotEnrolledInBenefitService @@ -60,13 +72,13 @@ - + situation.primaryPersonId - + "PhlHomesteadExemption" @@ -76,22 +88,46 @@ - - - not(situation.simpleChecks.tenYearTaxAbatement) - + + + + NoTenYearTaxAbatement.NoTenYearTaxAbatementService + + + + + situation + + + - - - situation.simpleChecks.ownerOccupant - + + + + OwnerOccupant.OwnerOccupantService + + + + + situation + + + - - - situation.simpleChecks.livesInPhiladelphiaPa - + + + + LivesInPhiladelphiaPa.LivesInPhiladelphiaPaService + + + + + situation + + + @@ -160,14 +196,32 @@ all(checksAsList) 100 - - 599 + + 50 + 239 + 290 - - 599 + + + 290 - - 599 + + 50 + 239 + 290 + + + + 290 + + + 50 + 239 + 290 + + + + 290 50 @@ -191,11 +245,11 @@ all(checksAsList) - + - - + + @@ -204,11 +258,50 @@ all(checksAsList) - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -217,7 +310,7 @@ all(checksAsList) - + @@ -226,7 +319,7 @@ all(checksAsList) - + @@ -235,20 +328,32 @@ all(checksAsList) - + - - + + - - + + + + + + + + + + + + + + - - + + diff --git a/library-api/src/main/resources/checks/age/person-max-age.dmn b/library-api/src/main/resources/checks/age/person-max-age.dmn index fb4fd162..9b53d866 100644 --- a/library-api/src/main/resources/checks/age/person-max-age.dmn +++ b/library-api/src/main/resources/checks/age/person-max-age.dmn @@ -51,21 +51,24 @@ - + situation.people[id = parameters.personId].dateOfBirth[1] - + Age.as of date(dateOfBirth, parameters.asOfDate) - + - age <= parameters.maxAge + if dateOfBirth != null then + age in [0..parameters.maxAge] +else + null diff --git a/library-api/src/main/resources/checks/age/person-min-age.dmn b/library-api/src/main/resources/checks/age/person-min-age.dmn index b125ff0a..1b2de111 100644 --- a/library-api/src/main/resources/checks/age/person-min-age.dmn +++ b/library-api/src/main/resources/checks/age/person-min-age.dmn @@ -52,21 +52,24 @@ - + situation.people[id = parameters.personId].dateOfBirth[1] - + Age.as of date(dateOfBirth, parameters.asOfDate) - + - age >= parameters.minAge + if dateOfBirth != null then + age >= parameters.minAge +else + null diff --git a/library-api/src/main/resources/checks/age/someone-min-age.dmn b/library-api/src/main/resources/checks/age/someone-min-age.dmn index 62f3d2b3..0a7b4268 100644 --- a/library-api/src/main/resources/checks/age/someone-min-age.dmn +++ b/library-api/src/main/resources/checks/age/someone-min-age.dmn @@ -66,7 +66,7 @@ - count(ages[item >= parameters.minAge]) > 0 + if DOBs = null or count(DOBs) = 0 then null else count(ages[item >= parameters.minAge]) > 0 diff --git a/library-api/src/main/resources/checks/enrollment/person-enrolled-in-benefit.dmn b/library-api/src/main/resources/checks/enrollment/person-enrolled-in-benefit.dmn index 9c9c35ef..c0b2d5b6 100644 --- a/library-api/src/main/resources/checks/enrollment/person-enrolled-in-benefit.dmn +++ b/library-api/src/main/resources/checks/enrollment/person-enrolled-in-benefit.dmn @@ -34,7 +34,7 @@ - if situation.enrollments != null and parameters.personId != null and parameters.benefit != null then + if situation.enrollments != null and count(situation.enrollments) > 0 and parameters.personId != null and parameters.benefit != null then some enrollment in situation.enrollments satisfies enrollment.personId = parameters.personId and enrollment.benefit = parameters.benefit else null diff --git a/library-api/src/main/resources/checks/residence/Residence.dmn b/library-api/src/main/resources/checks/residence/Residence.dmn new file mode 100644 index 00000000..e7ea4442 --- /dev/null +++ b/library-api/src/main/resources/checks/residence/Residence.dmn @@ -0,0 +1,23 @@ + + + + + BKMs, data types, and/or Decision Services that will be included in every eligibility check categorized as "Residence" + + + + + + + + + + + + + + + + + + diff --git a/library-api/src/main/resources/checks/residence/lives-in-philadelphia-pa.dmn b/library-api/src/main/resources/checks/residence/lives-in-philadelphia-pa.dmn new file mode 100644 index 00000000..5bd0b575 --- /dev/null +++ b/library-api/src/main/resources/checks/residence/lives-in-philadelphia-pa.dmn @@ -0,0 +1,90 @@ + + + Checks that a person lives in Philadelphia, PA. + + + + + + boolean + + + + + tSimpleChecks + + + + + + + + + + + + + + + + if situation.simpleChecks != null and situation.simpleChecks.livesInPhiladelphiaPa != null then + situation.simpleChecks.livesInPhiladelphiaPa = true +else + null + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library-api/src/main/resources/checks/residence/no-ten-year-tax-abatement.dmn b/library-api/src/main/resources/checks/residence/no-ten-year-tax-abatement.dmn new file mode 100644 index 00000000..6d18d36d --- /dev/null +++ b/library-api/src/main/resources/checks/residence/no-ten-year-tax-abatement.dmn @@ -0,0 +1,133 @@ + + + Checks that a property does not have an active 10-year tax abatement. + + + + + + + boolean + + + + + tSimpleChecks + + + + + + + + + + + + + + + + + + + not(TenYearTaxAbatement.TenYearTaxAbatementService(situation: situation)) + + + + + + + + + + + + 553 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library-api/src/main/resources/checks/residence/owner-occupant.dmn b/library-api/src/main/resources/checks/residence/owner-occupant.dmn new file mode 100644 index 00000000..cbafbada --- /dev/null +++ b/library-api/src/main/resources/checks/residence/owner-occupant.dmn @@ -0,0 +1,90 @@ + + + Checks that a person owns and occupies their primary residence. + + + + + + boolean + + + + + tSimpleChecks + + + + + + + + + + + + + + + + if situation.simpleChecks != null and situation.simpleChecks.ownerOccupant != null then + situation.simpleChecks.ownerOccupant = true +else + null + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library-api/src/main/resources/checks/residence/ten-year-tax-abatement.dmn b/library-api/src/main/resources/checks/residence/ten-year-tax-abatement.dmn new file mode 100644 index 00000000..05fbdd92 --- /dev/null +++ b/library-api/src/main/resources/checks/residence/ten-year-tax-abatement.dmn @@ -0,0 +1,109 @@ + + + Checks that a property has an active 10-year tax abatement. + + + + + + boolean + + + + + tSimpleChecks + + + + + + + + + + + + + + + + if situation.simpleChecks != null and situation.simpleChecks.tenYearTaxAbatement != null then + situation.simpleChecks.tenYearTaxAbatement = true +else + null + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library-api/src/test/java/org/codeforphilly/bdt/api/DynamicEndpointPatternTest.java b/library-api/src/test/java/org/codeforphilly/bdt/api/DynamicEndpointPatternTest.java index a42e8608..22e26d8d 100644 --- a/library-api/src/test/java/org/codeforphilly/bdt/api/DynamicEndpointPatternTest.java +++ b/library-api/src/test/java/org/codeforphilly/bdt/api/DynamicEndpointPatternTest.java @@ -14,6 +14,7 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.*; @QuarkusTest @@ -54,8 +55,37 @@ public void testAllCheckEndpointsReturnCheckResult() { .then() .statusCode(200) .body("checkResult", notNullValue()) - .body("situation", notNullValue()) - .body("parameters", notNullValue()); + .body("situation", notNullValue()); + // 'parameters' is only echoed back for checks that declare a parameters input + } + } + + @Test + public void testAllCheckEndpointsReturnNullWithEmptyInputs() { + Map allModels = modelRegistry.getAllModels(); + + List checkModels = allModels.values().stream() + .filter(model -> model.getPath().startsWith("checks/")) + .filter(model -> model.getDecisionServices().contains(model.getModelName() + "Service")) + .collect(Collectors.toList()); + + assertTrue(checkModels.size() > 0, "Should have at least one check model"); + + for (ModelInfo model : checkModels) { + String path = "/api/v1/" + model.getPath(); + + String checkResult = given() + .contentType(ContentType.JSON) + .body(Map.of("situation", Map.of())) + .when() + .post(path) + .then() + .statusCode(200) + .extract() + .jsonPath() + .getString("checkResult"); + + assertNull(checkResult, path + " should return null checkResult with empty situation"); } } diff --git a/library-api/src/test/java/org/codeforphilly/bdt/api/OpenAPISchemaPatternTest.java b/library-api/src/test/java/org/codeforphilly/bdt/api/OpenAPISchemaPatternTest.java index d1fd096f..beb33ab0 100644 --- a/library-api/src/test/java/org/codeforphilly/bdt/api/OpenAPISchemaPatternTest.java +++ b/library-api/src/test/java/org/codeforphilly/bdt/api/OpenAPISchemaPatternTest.java @@ -56,8 +56,7 @@ public void testAllCheckEndpointsHaveCheckResultInSchema() { path + " should have 'checkResult' in response schema, not 'result'"); assertTrue(properties.containsKey("situation"), path + " should have 'situation' in response schema"); - assertTrue(properties.containsKey("parameters"), - path + " should have 'parameters' in response schema"); + // 'parameters' is only present for checks that declare a parameters input in their DMN model } } @@ -110,8 +109,7 @@ public void testAllCheckEndpointsHaveCheckResultInExamples() { path + " example should have 'checkResult', not 'result'"); assertTrue(exampleValue.containsKey("situation"), path + " example should have 'situation'"); - assertTrue(exampleValue.containsKey("parameters"), - path + " example should have 'parameters'"); + // 'parameters' is only present for checks that declare a parameters input in their DMN model } } diff --git a/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Eligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Eligible.bru new file mode 100644 index 00000000..090fe5de --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Eligible.bru @@ -0,0 +1,39 @@ +meta { + name: Eligible + type: http + seq: 2 +} + +post { + url: {{host}}/benefits/pa/phl/homestead-exemption + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p2", + "benefit": "PhlHomesteadExemption" + } + ], + "simpleChecks": { + "tenYearTaxAbatement": false, + "ownerOccupant": true, + "livesInPhiladelphiaPa": true + } + } + } +} + +assert { + res.body.isEligible: eq true +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Ineligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Ineligible.bru new file mode 100644 index 00000000..d676eb0b --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Ineligible.bru @@ -0,0 +1,39 @@ +meta { + name: Ineligible + type: http + seq: 3 +} + +post { + url: {{host}}/benefits/pa/phl/homestead-exemption + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p2", + "benefit": "PhlHomesteadExemption" + } + ], + "simpleChecks": { + "tenYearTaxAbatement": true, + "ownerOccupant": true, + "livesInPhiladelphiaPa": true + } + } + } +} + +assert { + res.body.isEligible: eq false +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Smoke Test.bru b/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Smoke Test.bru index 68bd7a3c..354b63f4 100644 --- a/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Smoke Test.bru +++ b/library-api/test/bdt/benefits/pa/phl/PhlHomesteadExemption/Smoke Test.bru @@ -27,6 +27,10 @@ body:json { } } +assert { + res.body.isEligible: eq null +} + settings { encodeUrl: true timeout: 0 diff --git a/library-api/test/bdt/checks/age/PersonMaxAge/DOB is null.bru b/library-api/test/bdt/checks/age/PersonMaxAge/DOB is null.bru new file mode 100644 index 00000000..3c5edf00 --- /dev/null +++ b/library-api/test/bdt/checks/age/PersonMaxAge/DOB is null.bru @@ -0,0 +1,38 @@ +meta { + name: DOB is null + type: http + seq: 2 +} + +post { + url: {{host}}/checks/age/person-max-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "people": [ + { + "id": "p1", + "dateOfBirth": null + } + ] + }, + "parameters": { + "personId": "p1", + "asOfDate": "2025-12-31", + "maxAge": 5 + } + } +} + +assert { + res.body.checkResult: eq null +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/library-api/test/bdt/checks/age/PersonMinAge/Person with Null DOB.bru b/library-api/test/bdt/checks/age/PersonMinAge/Person with Null DOB.bru new file mode 100644 index 00000000..01b57731 --- /dev/null +++ b/library-api/test/bdt/checks/age/PersonMinAge/Person with Null DOB.bru @@ -0,0 +1,38 @@ +meta { + name: Person with Null DOB + type: http + seq: 3 +} + +post { + url: {{host}}/checks/age/person-min-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "people": [ + { + "id": "p1", + "dateOfBirth": null + } + ] + }, + "parameters": { + "personId": "p1", + "asOfDate": "2024-01-01", + "minAge": 4 + } + } +} + +assert { + res.body.checkResult: eq null +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments empty.bru b/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments empty.bru new file mode 100644 index 00000000..7eff128e --- /dev/null +++ b/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments empty.bru @@ -0,0 +1,38 @@ +meta { + name: Enrollments empty + type: http + seq: 2 +} + +post { + url: {{host}}/checks/enrollment/person-enrolled-in-benefit + body: json + auth: inherit +} + +body:json { + { + "situation": { + "people": [ + { + "id": "p1" + } + ], + "enrollments": [ + ] + }, + "parameters": { + "personId": "p1", + "benefit": "bogus" + } + } +} + +assert { + res.body.checkResult: eq null +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments null.bru b/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments null.bru new file mode 100644 index 00000000..2e158d2e --- /dev/null +++ b/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments null.bru @@ -0,0 +1,37 @@ +meta { + name: Enrollments null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/enrollment/person-enrolled-in-benefit + body: json + auth: inherit +} + +body:json { + { + "situation": { + "people": [ + { + "id": "p1" + } + ], + "enrollments": null + }, + "parameters": { + "personId": "p1", + "benefit": "bogus" + } + } +} + +assert { + res.body.checkResult: eq null +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrolled.bru b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrolled.bru new file mode 100644 index 00000000..4099930a --- /dev/null +++ b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrolled.bru @@ -0,0 +1,42 @@ +meta { + name: Enrolled + type: http + seq: 4 +} + +post { + url: {{host}}/checks/enrollment/person-not-enrolled-in-benefit + body: json + auth: inherit +} + +body:json { + { + "situation": { + "people": [ + { + "id": "p1" + } + ], + "enrollments": [ + { + "personId": "p1", + "benefit": "bogus" + } + ] + }, + "parameters": { + "personId": "p1", + "benefit": "bogus" + } + } +} + +assert { + res.body.checkResult: eq false +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments empty.bru b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments empty.bru new file mode 100644 index 00000000..11c4b173 --- /dev/null +++ b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments empty.bru @@ -0,0 +1,38 @@ +meta { + name: Enrollments empty + type: http + seq: 4 +} + +post { + url: {{host}}/checks/enrollment/person-not-enrolled-in-benefit + body: json + auth: inherit +} + +body:json { + { + "situation": { + "people": [ + { + "id": "p1" + } + ], + "enrollments": [ + ] + }, + "parameters": { + "personId": "p1", + "benefit": "bogus" + } + } +} + +assert { + res.body.checkResult: eq null +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments null.bru b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments null.bru new file mode 100644 index 00000000..107d7abc --- /dev/null +++ b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments null.bru @@ -0,0 +1,37 @@ +meta { + name: Enrollments null + type: http + seq: 4 +} + +post { + url: {{host}}/checks/enrollment/person-not-enrolled-in-benefit + body: json + auth: inherit +} + +body:json { + { + "situation": { + "people": [ + { + "id": "p1" + } + ], + "enrollments": null + }, + "parameters": { + "personId": "p1", + "benefit": "bogus" + } + } +} + +assert { + res.body.checkResult: eq null +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/NOT Enrolled in Bogus Benefit.bru b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/NOT Enrolled in Bogus Benefit.bru index 653ba716..20368af5 100644 --- a/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/NOT Enrolled in Bogus Benefit.bru +++ b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/NOT Enrolled in Bogus Benefit.bru @@ -32,6 +32,10 @@ body:json { } } +assert { + res.body.checkResult: eq true +} + settings { encodeUrl: true timeout: 0 diff --git a/library-api/test/bdt/checks/residence/LivesInPhiladelphiaPa/Fail.bru b/library-api/test/bdt/checks/residence/LivesInPhiladelphiaPa/Fail.bru new file mode 100644 index 00000000..a0a22c11 --- /dev/null +++ b/library-api/test/bdt/checks/residence/LivesInPhiladelphiaPa/Fail.bru @@ -0,0 +1,26 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/residence/lives-in-philadelphia-pa + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "livesInPhiladelphiaPa": false + } + } + } +} + +assert { + res.body.checkResult: eq false + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/LivesInPhiladelphiaPa/Null.bru b/library-api/test/bdt/checks/residence/LivesInPhiladelphiaPa/Null.bru new file mode 100644 index 00000000..2b045c7b --- /dev/null +++ b/library-api/test/bdt/checks/residence/LivesInPhiladelphiaPa/Null.bru @@ -0,0 +1,26 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/residence/lives-in-philadelphia-pa + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "livesInPhiladelphiaPa": null + } + } + } +} + +assert { + res.body.checkResult: eq null + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/LivesInPhiladelphiaPa/Pass.bru b/library-api/test/bdt/checks/residence/LivesInPhiladelphiaPa/Pass.bru new file mode 100644 index 00000000..ab57f0ee --- /dev/null +++ b/library-api/test/bdt/checks/residence/LivesInPhiladelphiaPa/Pass.bru @@ -0,0 +1,26 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/residence/lives-in-philadelphia-pa + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "livesInPhiladelphiaPa": true + } + } + } +} + +assert { + res.body.checkResult: eq true + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/NoTenYearTaxAbatement/Fail.bru b/library-api/test/bdt/checks/residence/NoTenYearTaxAbatement/Fail.bru new file mode 100644 index 00000000..16b78a8e --- /dev/null +++ b/library-api/test/bdt/checks/residence/NoTenYearTaxAbatement/Fail.bru @@ -0,0 +1,26 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/residence/no-ten-year-tax-abatement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "tenYearTaxAbatement": true + } + } + } +} + +assert { + res.body.checkResult: eq false + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/NoTenYearTaxAbatement/Null.bru b/library-api/test/bdt/checks/residence/NoTenYearTaxAbatement/Null.bru new file mode 100644 index 00000000..bbbe93f0 --- /dev/null +++ b/library-api/test/bdt/checks/residence/NoTenYearTaxAbatement/Null.bru @@ -0,0 +1,26 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/residence/no-ten-year-tax-abatement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "tenYearTaxAbatement": null + } + } + } +} + +assert { + res.body.checkResult: eq null + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/NoTenYearTaxAbatement/Pass.bru b/library-api/test/bdt/checks/residence/NoTenYearTaxAbatement/Pass.bru new file mode 100644 index 00000000..ea5a8161 --- /dev/null +++ b/library-api/test/bdt/checks/residence/NoTenYearTaxAbatement/Pass.bru @@ -0,0 +1,26 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/residence/no-ten-year-tax-abatement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "tenYearTaxAbatement": false + } + } + } +} + +assert { + res.body.checkResult: eq true + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/OwnerOccupant/Fail.bru b/library-api/test/bdt/checks/residence/OwnerOccupant/Fail.bru new file mode 100644 index 00000000..8f34b4dc --- /dev/null +++ b/library-api/test/bdt/checks/residence/OwnerOccupant/Fail.bru @@ -0,0 +1,26 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/residence/owner-occupant + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "ownerOccupant": false + } + } + } +} + +assert { + res.body.checkResult: eq false + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/OwnerOccupant/Null.bru b/library-api/test/bdt/checks/residence/OwnerOccupant/Null.bru new file mode 100644 index 00000000..727d43d2 --- /dev/null +++ b/library-api/test/bdt/checks/residence/OwnerOccupant/Null.bru @@ -0,0 +1,26 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/residence/owner-occupant + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "ownerOccupant": null + } + } + } +} + +assert { + res.body.checkResult: eq null + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/OwnerOccupant/Pass.bru b/library-api/test/bdt/checks/residence/OwnerOccupant/Pass.bru new file mode 100644 index 00000000..40701022 --- /dev/null +++ b/library-api/test/bdt/checks/residence/OwnerOccupant/Pass.bru @@ -0,0 +1,26 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/residence/owner-occupant + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "ownerOccupant": true + } + } + } +} + +assert { + res.body.checkResult: eq true + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/TenYearTaxAbatement/Fail.bru b/library-api/test/bdt/checks/residence/TenYearTaxAbatement/Fail.bru new file mode 100644 index 00000000..00105191 --- /dev/null +++ b/library-api/test/bdt/checks/residence/TenYearTaxAbatement/Fail.bru @@ -0,0 +1,26 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/residence/ten-year-tax-abatement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "tenYearTaxAbatement": false + } + } + } +} + +assert { + res.body.checkResult: eq false + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/TenYearTaxAbatement/Null.bru b/library-api/test/bdt/checks/residence/TenYearTaxAbatement/Null.bru new file mode 100644 index 00000000..9b63bb47 --- /dev/null +++ b/library-api/test/bdt/checks/residence/TenYearTaxAbatement/Null.bru @@ -0,0 +1,26 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/residence/ten-year-tax-abatement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "tenYearTaxAbatement": null + } + } + } +} + +assert { + res.body.checkResult: eq null + res.status: eq 200 +} diff --git a/library-api/test/bdt/checks/residence/TenYearTaxAbatement/Pass.bru b/library-api/test/bdt/checks/residence/TenYearTaxAbatement/Pass.bru new file mode 100644 index 00000000..65fca7fd --- /dev/null +++ b/library-api/test/bdt/checks/residence/TenYearTaxAbatement/Pass.bru @@ -0,0 +1,26 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/residence/ten-year-tax-abatement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "tenYearTaxAbatement": true + } + } + } +} + +assert { + res.body.checkResult: eq true + res.status: eq 200 +}