From 113a0dca30177e49521bd41230fa18fdcb65c487 Mon Sep 17 00:00:00 2001 From: Justin-MacIntosh Date: Mon, 16 Feb 2026 21:43:49 -0500 Subject: [PATCH 1/2] feat: Added support for configuring Date Fields for EligibilityChecks. --- bin/library/load-library-metadata.py | 158 ++++++++++++------ .../modals/ParameterModal.tsx | 5 +- .../modals/ConfigureCheckModal.tsx | 37 ++++ builder-frontend/src/types.ts | 6 +- 4 files changed, 151 insertions(+), 55 deletions(-) diff --git a/bin/library/load-library-metadata.py b/bin/library/load-library-metadata.py index 94225042..ee12d564 100644 --- a/bin/library/load-library-metadata.py +++ b/bin/library/load-library-metadata.py @@ -1,20 +1,57 @@ +from __future__ import annotations + from copy import deepcopy -import requests -import firebase_admin -from firebase_admin import credentials, storage, firestore -import json from datetime import datetime +from typing import Any, TypedDict +import json import os + +import firebase_admin +from firebase_admin import credentials, storage, firestore +from google.cloud.firestore import Client as FirestoreClient +from google.cloud.storage import Bucket import google.auth.credentials +import requests + + +# ----------------------------------- +# TYPE DEFINITIONS +# ----------------------------------- + +class ParameterDefinition(TypedDict): + key: str + label: str + required: bool + type: str + + +class CheckRecord(TypedDict, total=False): + id: str + evaluationUrl: str + method: str + name: str + module: str + version: str + inputs: dict[str, Any] + parameterDefinitions: list[ParameterDefinition] + inputDefinition: dict[str, Any] + + +# Type aliases for clarity +JsonSchema = dict[str, Any] +OpenAPIComponents = dict[str, JsonSchema] +OpenAPIDocument = dict[str, Any] class EmulatorCredentials(credentials.Base): """Mock credentials for use with Firebase emulators.""" - def __init__(self): + _mock_credential: google.auth.credentials.Credentials + + def __init__(self) -> None: self._mock_credential = google.auth.credentials.AnonymousCredentials() - def get_credential(self): + def get_credential(self) -> google.auth.credentials.Credentials: return self._mock_credential # ----------------------------------- @@ -69,8 +106,8 @@ def get_credential(self): firebase_admin.initialize_app(cred, firebase_options) -db = firestore.client() -bucket = storage.bucket() +db: FirestoreClient = firestore.client() +bucket: Bucket = storage.bucket() # -------------------------------------------- @@ -78,7 +115,8 @@ def get_credential(self): # -------------------------------------------- -def resolve_ref(ref, components): +def resolve_ref(ref: str, components: OpenAPIComponents) -> JsonSchema: + """Resolve a JSON Schema $ref to its target schema.""" ref_path = ref.replace("#/components/schemas/", "") if ref_path not in components: return {} @@ -91,7 +129,7 @@ def resolve_ref(ref, components): # -------------------------------------------- # Recursively expand schemas and resolve $ref # -------------------------------------------- -def expand_schema(schema, components): +def expand_schema(schema: Any, components: OpenAPIComponents) -> Any: """Recursively expand all $ref inside a schema node.""" if not isinstance(schema, dict): return schema @@ -101,7 +139,7 @@ def expand_schema(schema, components): target = resolve_ref(schema["$ref"], components) return expand_schema(target, components) - expanded = {} + expanded: dict[str, Any] = {} for key, value in schema.items(): # Recurse into lists (e.g., 'allOf', 'oneOf') @@ -120,11 +158,13 @@ def expand_schema(schema, components): return expanded -def extract_top_level_inputs(schema, components): +def extract_top_level_inputs( + schema: JsonSchema, components: OpenAPIComponents +) -> dict[str, JsonSchema]: """Return only top-level properties of requestBody schema.""" expanded = expand_schema(schema, components) - inputs = {} + inputs: dict[str, JsonSchema] = {} if expanded.get("type") == "object": for prop_name, prop_schema in expanded.get("properties", {}).items(): # Fully expand each property @@ -135,10 +175,11 @@ def extract_top_level_inputs(schema, components): # -------------------------------------------- # Convert schema to simple {field: type} # -------------------------------------------- -def flatten_schema(schema): - flat = {} +def flatten_schema(schema: JsonSchema) -> dict[str, JsonSchema]: + """Flatten a nested JSON schema into a flat dictionary with dotted keys.""" + flat: dict[str, JsonSchema] = {} - def walk(name, node): + def walk(name: str, node: Any) -> None: if not isinstance(node, dict): return @@ -201,18 +242,19 @@ def walk(name, node): # -------------------------------------------- # Process entire OpenAPI document # -------------------------------------------- -def extract_check_records(openapi, version): - components = openapi.get("components", {}).get("schemas", {}) - paths = openapi.get("paths", {}) +def extract_check_records(openapi: OpenAPIDocument, version: str) -> list[CheckRecord]: + """Extract check records from an OpenAPI document.""" + components: OpenAPIComponents = openapi.get("components", {}).get("schemas", {}) + paths: dict[str, Any] = openapi.get("paths", {}) - output = [] + output: list[CheckRecord] = [] for path, methods in paths.items(): # Only process check endpoints if "/checks" not in path: continue - for method, details in methods.items(): - method = method.upper() + for method_key, details in methods.items(): + method_upper = method_key.upper() # Split the URL into parts segments = path.strip("/").split("/") @@ -224,12 +266,12 @@ def extract_check_records(openapi, version): module = "/".join(segments[checks_index + 1:-1]) - id = 'L-' + module + '-' + name + '-' + version + check_id = 'L-' + module + '-' + name + '-' + version - entry = { - "id": id, + entry: CheckRecord = { + "id": check_id, "evaluationUrl": path, - "method": method, + "method": method_upper, "name": name, "module": module, "version": version, @@ -239,21 +281,21 @@ def extract_check_records(openapi, version): # ---------------------------------------- # 1. Path or query parameters # ---------------------------------------- - parameters = details.get("parameters", []) + parameters: list[dict[str, Any]] = details.get("parameters", []) for p in parameters: - name = p["name"] - dtype = p.get("schema", {}).get("type", "unknown") - entry["inputs"][name] = dtype + param_name: str = p["name"] + dtype: str = p.get("schema", {}).get("type", "unknown") + entry["inputs"][param_name] = dtype # ---------------------------------------- # 2. Request body parameters # ---------------------------------------- if "requestBody" in details: - content = details["requestBody"]["content"] + content: dict[str, Any] = details["requestBody"]["content"] if "application/json" in content: - schema = content["application/json"].get("schema", {}) + schema: JsonSchema = content["application/json"].get("schema", {}) # Only expand top-level 'parameters' and 'situation' entry["inputs"].update( extract_top_level_inputs(schema, components)) @@ -263,13 +305,18 @@ def extract_check_records(openapi, version): return output -def transform_parameters(properties_obj): +def transform_parameters(properties_obj: dict[str, JsonSchema]) -> list[ParameterDefinition]: """Convert properties dict to a list of {key, name, type} objects.""" - transformed = [] + transformed: list[ParameterDefinition] = [] for key, val in properties_obj.items(): # Determine the property's type - prop_type = val.get("type", "object") # fallback + prop_type: str = val.get("type", "object") # fallback + + # Check for date format - OpenAPI represents FEEL date as { type: "string", format: "date" } + prop_format: str | None = val.get("format") + if prop_type == "string" and prop_format == "date": + prop_type = "date" transformed.append({ "key": key, @@ -281,15 +328,15 @@ def transform_parameters(properties_obj): return transformed -def transform_parameters_format(data): +def transform_parameters_format(data: list[CheckRecord]) -> list[CheckRecord]: """Transform all `inputs.parameters.properties` in the provided list.""" for check in data: - inputs = check.get("inputs", {}) - parameters = inputs.get("parameters") + inputs: dict[str, Any] = check.get("inputs", {}) + parameters: dict[str, Any] | None = inputs.get("parameters") # Only transform objects that follow the original structure if isinstance(parameters, dict) and "properties" in parameters: - properties_obj = parameters["properties"] + properties_obj: dict[str, JsonSchema] = parameters["properties"] new_parameters = transform_parameters(properties_obj) # Replace object with the transformed list @@ -297,24 +344,27 @@ def transform_parameters_format(data): return data -def transform_situation_format(data): +def transform_situation_format(data: list[CheckRecord]) -> list[CheckRecord]: """Transform all `inputs.situation` in the provided list.""" for check in data: check["inputDefinition"] = check["inputs"]["situation"] return data -def save_json_to_storage_and_update_firestore(json_string, firestore_doc_path): +def save_json_to_storage_and_update_firestore( + json_string: str, firestore_doc_path: str +) -> str: """ Upload JSON string to Firebase Storage and update Firestore with the storage path or download URL of the uploaded file. """ + from datetime import timezone # --------------------- # Create filename # Example: exported_2025-02-12_14-30-59.json # --------------------- - timestamp = datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d_%H-%M-%S") filename = f"LibraryApiSchemaExports/export_{timestamp}.json" # --------------------- @@ -324,7 +374,7 @@ def save_json_to_storage_and_update_firestore(json_string, firestore_doc_path): blob.upload_from_string(json_string, content_type="application/json") # Get the storage path - storage_path = blob.name + storage_path: str = blob.name # --------------------- # Update Firestore @@ -344,32 +394,32 @@ def save_json_to_storage_and_update_firestore(json_string, firestore_doc_path): # -------------------------------------------- # Load your OpenAPI JSON here # -------------------------------------------- -if __name__ == "__main__": - - url = f"{LIBRARY_API_BASE_URL}/q/openapi.json" +def main() -> None: + """Main entry point for the library metadata sync script.""" + url: str = f"{LIBRARY_API_BASE_URL}/q/openapi.json" print(f"Fetching OpenAPI spec from: {url}") # Send a GET request - response = requests.get(url) + response: requests.Response = requests.get(url) # Raise an error if the request failed response.raise_for_status() # optional, but good practice # Parse JSON - data = response.json() + data: OpenAPIDocument = response.json() - version = data["info"]["version"] + version: str = data["info"]["version"] - check_records = extract_check_records(data, version) + check_records: list[CheckRecord] = extract_check_records(data, version) check_records = transform_parameters_format(check_records) check_records = transform_situation_format(check_records) for check in check_records: - check.pop("inputs") + check.pop("inputs") # type: ignore[misc] # Write JSON file using UTF-8 to avoid errors - json_string = json.dumps(check_records, indent=2, ensure_ascii=False) + json_string: str = json.dumps(check_records, indent=2, ensure_ascii=False) print("Parsed json") print(json_string) @@ -378,3 +428,7 @@ def save_json_to_storage_and_update_firestore(json_string, firestore_doc_path): json_string, firestore_doc_path="system/config" ) + + +if __name__ == "__main__": + main() diff --git a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/modals/ParameterModal.tsx b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/modals/ParameterModal.tsx index 707bd576..fe4f99cd 100644 --- a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/modals/ParameterModal.tsx +++ b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/modals/ParameterModal.tsx @@ -7,7 +7,7 @@ type ParamValues = { key: string; label: string; required: boolean; - type: "string" | "number" | "boolean"; + type: "string" | "number" | "boolean" | "date"; } const ParameterModal = ( { actionTitle, modalAction, closeModal, initialData }: @@ -55,11 +55,12 @@ const ParameterModal = (
diff --git a/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx b/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx index a1cd5c50..fb35b334 100644 --- a/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx +++ b/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx @@ -11,6 +11,7 @@ import type { BooleanParameter, NumberParameter, StringParameter, + DateParameter, } from "@/types"; const ConfigureCheckModal = ({ @@ -121,6 +122,14 @@ const ParameterInput = ({ currentValue={() => tempCheck().parameters[parameterKey()]} /> ); + } else if (parameterType() === "date") { + return ( + } + currentValue={() => tempCheck().parameters[parameterKey()]} + /> + ); } return
Unsupported parameter type: {parameterType()}
; }; @@ -226,4 +235,32 @@ const ParameterBooleanInput = ({ ); }; +const ParameterDateInput = ({ + onParameterChange, + parameter, + currentValue, +}: { + onParameterChange: (value: any) => void; + parameter: Accessor; + currentValue: Accessor; +}) => { + return ( +
+
+ {titleCase(parameter().key)}{" "} + {parameter().required && *} +
+
{parameter().label}
+ { + onParameterChange(e.target.value); + }} + type="date" + value={currentValue() ?? ""} + class="form-input" + /> +
+ ); +}; + export default ConfigureCheckModal; diff --git a/builder-frontend/src/types.ts b/builder-frontend/src/types.ts index 18004185..a0f57304 100644 --- a/builder-frontend/src/types.ts +++ b/builder-frontend/src/types.ts @@ -68,7 +68,8 @@ export type ParameterDefinition = // StringSelectParameter | // StringMultiInputParameter | | NumberParameter - | BooleanParameter; + | BooleanParameter + | DateParameter; interface BaseParameter { key: string; label: string; @@ -90,6 +91,9 @@ export interface NumberParameter extends BaseParameter { export interface BooleanParameter extends BaseParameter { type: "boolean"; } +export interface DateParameter extends BaseParameter { + type: "date"; +} /* Screener Evaluation Results */ export interface ScreenerResult { From b8b6c084aac5c2bb66e4bef17f959784d1d616c0 Mon Sep 17 00:00:00 2001 From: Justin-MacIntosh Date: Tue, 17 Feb 2026 10:55:00 -0500 Subject: [PATCH 2/2] feat: Updated screener results to show parameters and module of CheckConfigs --- .../org/acme/controller/DecisionResource.java | 8 ++- .../components/project/preview/Results.tsx | 65 ++++++++++--------- .../src/components/project/preview/types.ts | 5 ++ .../screener/EligibilityResults.tsx | 55 ++++++++++------ builder-frontend/src/types.ts | 5 +- 5 files changed, 88 insertions(+), 50 deletions(-) diff --git a/builder-api/src/main/java/org/acme/controller/DecisionResource.java b/builder-api/src/main/java/org/acme/controller/DecisionResource.java index c3717c46..4a2e48d6 100644 --- a/builder-api/src/main/java/org/acme/controller/DecisionResource.java +++ b/builder-api/src/main/java/org/acme/controller/DecisionResource.java @@ -156,7 +156,13 @@ private Map evaluateBenefit(Benefit benefit, Map resultsList.add(evaluationResult); String uniqueCheckKey = checkConfig.getCheckId() + checkNum; - checkResults.put(uniqueCheckKey, Map.of("name", checkConfig.getCheckName(), "result", evaluationResult)); + Map checkResultMap = new HashMap<>(); + checkResultMap.put("name", checkConfig.getCheckName()); + checkResultMap.put("result", evaluationResult); + checkResultMap.put("module", checkConfig.getCheckModule() != null ? checkConfig.getCheckModule() : ""); + checkResultMap.put("version", checkConfig.getCheckVersion() != null ? checkConfig.getCheckVersion() : ""); + checkResultMap.put("parameters", checkConfig.getParameters() != null ? checkConfig.getParameters() : Map.of()); + checkResults.put(uniqueCheckKey, checkResultMap); checkNum += 1; } diff --git a/builder-frontend/src/components/project/preview/Results.tsx b/builder-frontend/src/components/project/preview/Results.tsx index 7b48494b..a0ba174d 100644 --- a/builder-frontend/src/components/project/preview/Results.tsx +++ b/builder-frontend/src/components/project/preview/Results.tsx @@ -1,11 +1,18 @@ import { Accessor, For, Match, Show, Switch } from "solid-js"; import { PreviewFormData, ScreenerResult } from "./types"; +import type { ParameterValues } from "@/types"; import checkIcon from "../../../assets/images/checkIcon.svg"; import questionIcon from "../../../assets/images/questionIcon.svg"; import xIcon from "../../../assets/images/xIcon.svg"; +function formatParameters(params: ParameterValues): string { + return Object.entries(params) + .map(([key, value]) => `${key}=${value}`) + .join(", "); +} + export default function Results({ inputData, results, @@ -80,35 +87,35 @@ export default function Results({
{([checkKey, check]) => ( -
- {check.name}:{" "} - - - - - - - - - - - +
+
+ + + + + + + + + + + +
+
+
+ {check.name} + + + ({[check.module, check.version].filter(Boolean).join(" v")}) + + +
+ 0}> +
+ {formatParameters(check.parameters)} +
+
+
)} diff --git a/builder-frontend/src/components/project/preview/types.ts b/builder-frontend/src/components/project/preview/types.ts index e1b8926b..b5982d8f 100644 --- a/builder-frontend/src/components/project/preview/types.ts +++ b/builder-frontend/src/components/project/preview/types.ts @@ -1,3 +1,5 @@ +import type { ParameterValues } from "@/types"; + /* Screener Evaluation Results */ export interface ScreenerResult { [key: string]: BenefitResult @@ -12,6 +14,9 @@ interface BenefitResult { interface CheckResult { name: string; result: OptionalBoolean; + module: string; + version: string; + parameters: ParameterValues; } type OptionalBoolean = "TRUE" | "FALSE" | "UNABLE_TO_DETERMINE"; diff --git a/builder-frontend/src/components/screener/EligibilityResults.tsx b/builder-frontend/src/components/screener/EligibilityResults.tsx index 900d3f13..590748e6 100644 --- a/builder-frontend/src/components/screener/EligibilityResults.tsx +++ b/builder-frontend/src/components/screener/EligibilityResults.tsx @@ -1,4 +1,4 @@ -import { Switch, Match, For, Accessor } from "solid-js"; +import { Switch, Match, For, Accessor, Show } from "solid-js"; import type { ScreenerResult, BenefitResult } from "@/types"; @@ -6,6 +6,11 @@ import checkIcon from "@/assets/images/checkIcon.svg"; import questionIcon from "@/assets/images/questionIcon.svg"; import xIcon from "@/assets/images/xIcon.svg"; +function formatParameters(params: Record): string { + return Object.entries(params) + .map(([key, value]) => `${key}=${value}`) + .join(", "); +} export default function EligibilityResults( { screenerResult }: { screenerResult: Accessor } @@ -45,24 +50,36 @@ function BenefitResult({ benefitResult }: { benefitResult: BenefitResult }) {

{benefitResult.name}

{([checkKey, check]) => ( -

- - - - - - - - - - - - {check.name} -

+
+
+ + + + + + + + + + + +
+
+
+ {check.name} + + + ({[check.module, check.version].filter(Boolean).join(" v")}) + + +
+ 0}> +
+ {formatParameters(check.parameters)} +
+
+
+
)}
diff --git a/builder-frontend/src/types.ts b/builder-frontend/src/types.ts index a0f57304..47ab8f62 100644 --- a/builder-frontend/src/types.ts +++ b/builder-frontend/src/types.ts @@ -106,9 +106,12 @@ export interface BenefitResult { [key: string]: CheckResult; }; } -interface CheckResult { +export interface CheckResult { name: string; result: OptionalBoolean; + module: string; + version: string; + parameters: ParameterValues; } export type OptionalBoolean = "TRUE" | "FALSE" | "UNABLE_TO_DETERMINE";