From e433354de6be67cf1199521b51a1981853056ea8 Mon Sep 17 00:00:00 2001 From: joshwanf <17016446+joshwanf@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:25:57 -0400 Subject: [PATCH] Use today's date as default for Library Checks with an asOfDate parameter --- .../org/acme/controller/DecisionResource.java | 12 +- .../org/acme/service/LibraryApiService.java | 70 +++++++++++- .../acme/service/LibraryApiServiceTest.java | 105 ++++++++++++++++++ .../SelectedEligibilityCheck.tsx | 22 +++- .../modals/ConfigureCheckModal.tsx | 24 +++- .../components/project/preview/Results.tsx | 26 ++++- .../src/components/project/preview/types.ts | 2 + .../screener/EligibilityResults.tsx | 21 +++- builder-frontend/src/types.ts | 2 + 9 files changed, 264 insertions(+), 20 deletions(-) create mode 100644 builder-api/src/test/java/org/acme/service/LibraryApiServiceTest.java 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 07b760f3..1891d0ad 100644 --- a/builder-api/src/main/java/org/acme/controller/DecisionResource.java +++ b/builder-api/src/main/java/org/acme/controller/DecisionResource.java @@ -22,6 +22,7 @@ import org.acme.service.DmnService; import org.acme.service.FormDataTransformer; import org.acme.service.LibraryApiService; +import org.acme.service.LibraryApiService.LibraryCheckEvaluation; import java.util.*; @@ -136,8 +137,15 @@ private Map evaluateBenefit(Benefit benefit, Map int checkNum = 0; for (CheckConfig checkConfig : benefit.getChecks()) { EvaluationResult evaluationResult; + Map effectiveParameters = checkConfig.getParameters() != null + ? new HashMap<>(checkConfig.getParameters()) + : new HashMap<>(); + List defaultedParameters = List.of(); if (isLibraryCheck(checkConfig)){ - evaluationResult = libraryApi.evaluateCheck(checkConfig, formData); + LibraryCheckEvaluation libraryCheckEvaluation = libraryApi.evaluateCheck(checkConfig, formData); + evaluationResult = libraryCheckEvaluation.result(); + effectiveParameters = libraryCheckEvaluation.effectiveParameters(); + defaultedParameters = libraryCheckEvaluation.defaultedParameters(); } else { Map customFormValues = (Map) formData.get("custom"); if (customFormValues == null) { @@ -159,6 +167,8 @@ private Map evaluateBenefit(Benefit benefit, Map 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()); + checkResultMap.put("effectiveParameters", effectiveParameters); + checkResultMap.put("defaultedParameters", defaultedParameters); checkResults.put(uniqueCheckKey, checkResultMap); checkNum += 1; } diff --git a/builder-api/src/main/java/org/acme/service/LibraryApiService.java b/builder-api/src/main/java/org/acme/service/LibraryApiService.java index 0009075c..f6a40d0a 100644 --- a/builder-api/src/main/java/org/acme/service/LibraryApiService.java +++ b/builder-api/src/main/java/org/acme/service/LibraryApiService.java @@ -18,6 +18,8 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.LocalDate; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,6 +29,18 @@ @ApplicationScoped public class LibraryApiService { private static final String DEFAULT_LIBRARY_API_URL = "http://localhost:8083"; + private static final String AS_OF_DATE_PARAMETER = "asOfDate"; + + public record LibraryCheckEvaluation( + EvaluationResult result, + Map effectiveParameters, + List defaultedParameters + ) {} + + record EffectiveParameters( + Map parameters, + List defaultedParameters + ) {} @Inject private StorageService storageService; @@ -100,12 +114,13 @@ public Optional getById(String id) { return Optional.of(matches.getFirst()); } - public EvaluationResult evaluateCheck(CheckConfig checkConfig, Map inputs) throws JsonProcessingException { + public LibraryCheckEvaluation evaluateCheck(CheckConfig checkConfig, Map inputs) throws JsonProcessingException { // TODO: Check that checkConfig has required attributes and handle null values + EffectiveParameters effectiveParameters = buildEffectiveParameters(checkConfig); Map data = new HashMap<>(); - data.put("parameters", checkConfig.getParameters()); + data.put("parameters", effectiveParameters.parameters()); data.put("situation", inputs); ObjectMapper mapper = new ObjectMapper(); String bodyJson = mapper.writeValueAsString(data); @@ -139,7 +154,11 @@ public EvaluationResult evaluateCheck(CheckConfig checkConfig, Map responseBody = mapper.readValue( @@ -150,13 +169,52 @@ public EvaluationResult evaluateCheck(CheckConfig checkConfig, Map configuredParameters = checkConfig.getParameters() != null + ? checkConfig.getParameters() + : Map.of(); + Map parameters = new HashMap<>(configuredParameters); + List defaultedParameters = new ArrayList<>(); + + if (declaresAsOfDateParameter(checkConfig) && isMissingParameter(parameters.get(AS_OF_DATE_PARAMETER))) { + parameters.put(AS_OF_DATE_PARAMETER, LocalDate.now().toString()); + defaultedParameters.add(AS_OF_DATE_PARAMETER); } + + return new EffectiveParameters(parameters, defaultedParameters); + } + + private boolean declaresAsOfDateParameter(CheckConfig checkConfig) { + if (checkConfig.getParameterDefinitions() == null) { + return false; + } + return checkConfig.getParameterDefinitions().stream() + .anyMatch(parameterDefinition -> AS_OF_DATE_PARAMETER.equals(parameterDefinition.getKey())); + } + + private boolean isMissingParameter(Object value) { + return value == null || (value instanceof String && ((String) value).isBlank()); } } diff --git a/builder-api/src/test/java/org/acme/service/LibraryApiServiceTest.java b/builder-api/src/test/java/org/acme/service/LibraryApiServiceTest.java new file mode 100644 index 00000000..192e82a9 --- /dev/null +++ b/builder-api/src/test/java/org/acme/service/LibraryApiServiceTest.java @@ -0,0 +1,105 @@ +package org.acme.service; + +import org.acme.model.domain.CheckConfig; +import org.acme.model.domain.ParameterDefinition; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LibraryApiServiceTest { + + @Test + void buildEffectiveParameters_defaultsMissingAsOfDateWhenDeclared() { + LibraryApiService service = new LibraryApiService(); + CheckConfig checkConfig = checkConfigWithParameters(Map.of("minAge", 65), asOfDateParameter()); + + LibraryApiService.EffectiveParameters result = service.buildEffectiveParameters(checkConfig); + + assertEquals(LocalDate.now().toString(), result.parameters().get("asOfDate")); + assertEquals(List.of("asOfDate"), result.defaultedParameters()); + assertFalse(checkConfig.getParameters().containsKey("asOfDate")); + } + + @Test + void buildEffectiveParameters_defaultsBlankAsOfDateWhenDeclared() { + LibraryApiService service = new LibraryApiService(); + Map parameters = new HashMap<>(); + parameters.put("asOfDate", " "); + parameters.put("minAge", 65); + CheckConfig checkConfig = checkConfigWithParameters(parameters, asOfDateParameter()); + + LibraryApiService.EffectiveParameters result = service.buildEffectiveParameters(checkConfig); + + assertEquals(LocalDate.now().toString(), result.parameters().get("asOfDate")); + assertEquals(List.of("asOfDate"), result.defaultedParameters()); + assertEquals(" ", checkConfig.getParameters().get("asOfDate")); + } + + @Test + void buildEffectiveParameters_keepsExplicitAsOfDate() { + LibraryApiService service = new LibraryApiService(); + CheckConfig checkConfig = checkConfigWithParameters( + Map.of("asOfDate", "2025-12-31", "minAge", 65), + asOfDateParameter() + ); + + LibraryApiService.EffectiveParameters result = service.buildEffectiveParameters(checkConfig); + + assertEquals("2025-12-31", result.parameters().get("asOfDate")); + assertTrue(result.defaultedParameters().isEmpty()); + } + + @Test + void buildEffectiveParameters_doesNotDefaultWhenAsOfDateIsNotDeclared() { + LibraryApiService service = new LibraryApiService(); + CheckConfig checkConfig = checkConfigWithParameters(Map.of("minAge", 65), minAgeParameter()); + + LibraryApiService.EffectiveParameters result = service.buildEffectiveParameters(checkConfig); + + assertFalse(result.parameters().containsKey("asOfDate")); + assertTrue(result.defaultedParameters().isEmpty()); + } + + @Test + void buildEffectiveParameters_returnsMutableCopy() { + LibraryApiService service = new LibraryApiService(); + Map parameters = Map.of("minAge", 65); + CheckConfig checkConfig = checkConfigWithParameters(parameters, minAgeParameter()); + + LibraryApiService.EffectiveParameters result = service.buildEffectiveParameters(checkConfig); + + assertNotSame(parameters, result.parameters()); + } + + private CheckConfig checkConfigWithParameters( + Map parameters, + ParameterDefinition parameterDefinition + ) { + CheckConfig checkConfig = new CheckConfig(); + checkConfig.setParameters(parameters); + checkConfig.setParameterDefinitions(List.of(parameterDefinition)); + return checkConfig; + } + + private ParameterDefinition asOfDateParameter() { + ParameterDefinition parameterDefinition = new ParameterDefinition(); + parameterDefinition.setKey("asOfDate"); + parameterDefinition.setType("date"); + return parameterDefinition; + } + + private ParameterDefinition minAgeParameter() { + ParameterDefinition parameterDefinition = new ParameterDefinition(); + parameterDefinition.setKey("minAge"); + parameterDefinition.setType("number"); + return parameterDefinition; + } +} diff --git a/builder-frontend/src/components/project/manageBenefits/configureBenefit/SelectedEligibilityCheck.tsx b/builder-frontend/src/components/project/manageBenefits/configureBenefit/SelectedEligibilityCheck.tsx index 18bb0972..db8fb87c 100644 --- a/builder-frontend/src/components/project/manageBenefits/configureBenefit/SelectedEligibilityCheck.tsx +++ b/builder-frontend/src/components/project/manageBenefits/configureBenefit/SelectedEligibilityCheck.tsx @@ -30,6 +30,19 @@ const SelectedEligibilityCheck = ({ const displayName = () => checkConfig().aliasName || checkConfig().checkName; + const isBlankParameterValue = (value: unknown) => { + return value === undefined || value === null || value === ""; + }; + + const usesAsOfDateDefault = (parameter: ParameterDefinition) => { + return ( + !!checkConfig().evaluationUrl && + parameter.key === "asOfDate" && + parameter.type === "date" && + isBlankParameterValue(checkConfig().parameters[parameter.key]) + ); + }; + const unfilledRequiredParameters = () => { return []; }; @@ -88,7 +101,14 @@ const SelectedEligibilityCheck = ({ {(parameter: ParameterDefinition) => { const getLabel = () => { let value = checkConfig().parameters[parameter.key]; - return value !== undefined ? ( + if (usesAsOfDateDefault(parameter)) { + return ( + + Uses today's date by default + + ); + } + return !isBlankParameterValue(value) ? ( value.toString() ) : ( Not configured 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 1a43069d..780f9fa7 100644 --- a/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx +++ b/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx @@ -1,4 +1,4 @@ -import { Accessor, For, createSignal } from "solid-js"; +import { Accessor, For, Show, createSignal } from "solid-js"; import { createStore, SetStoreFunction } from "solid-js/store"; import { titleCase } from "@/utils/title_case"; @@ -7,7 +7,6 @@ import type { CheckConfig, ParameterDefinition, ParameterValues, - BooleanParameter, } from "@/types"; const ConfigureCheckModal = ({ @@ -66,6 +65,12 @@ const ConfigureCheckModal = ({ setTempCheck={setTempCheck} parameter={() => parameter} /> + +
+ Leave blank to use today's date when this screener + is evaluated. +
+
); @@ -91,6 +96,17 @@ const ConfigureCheckModal = ({ ); }; +const usesAsOfDateDefault = ( + checkConfig: CheckConfig, + parameter: ParameterDefinition +) => { + return ( + !!checkConfig.evaluationUrl && + parameter.key === "asOfDate" && + parameter.type === "date" + ); +}; + const ParameterInput = ({ tempCheck, setTempCheck, @@ -125,7 +141,7 @@ const ParameterInput = ({ return ( } + parameter={parameter} currentValue={() => tempCheck().parameters[parameterKey()]} /> ); @@ -198,7 +214,7 @@ const ParameterBooleanInput = ({ currentValue, }: { onParameterChange: (value: any) => void; - parameter: Accessor; + parameter: Accessor; currentValue: Accessor; }) => { return ( diff --git a/builder-frontend/src/components/project/preview/Results.tsx b/builder-frontend/src/components/project/preview/Results.tsx index c127f048..7dc562d5 100644 --- a/builder-frontend/src/components/project/preview/Results.tsx +++ b/builder-frontend/src/components/project/preview/Results.tsx @@ -7,9 +7,17 @@ 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 { +function formatParameters( + params: ParameterValues, + defaultedParameters: string[] = [] +): string { return Object.entries(params) - .map(([key, value]) => `${key}=${value}`) + .map(([key, value]) => { + const defaultedLabel = defaultedParameters.includes(key) + ? " (defaulted to today's date)" + : ""; + return `${key}=${value}${defaultedLabel}`; + }) .join(", "); } @@ -119,9 +127,19 @@ export default function Results({ - 0}> + 0) || + (check.parameters && + Object.keys(check.parameters).length > 0) + } + >
- {formatParameters(check.parameters)} + {formatParameters( + check.effectiveParameters ?? check.parameters, + check.defaultedParameters + )}
diff --git a/builder-frontend/src/components/project/preview/types.ts b/builder-frontend/src/components/project/preview/types.ts index 8f414141..ca000d86 100644 --- a/builder-frontend/src/components/project/preview/types.ts +++ b/builder-frontend/src/components/project/preview/types.ts @@ -18,6 +18,8 @@ interface CheckResult { module: string; version: string; parameters: ParameterValues; + effectiveParameters?: ParameterValues; + defaultedParameters?: string[]; } 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 db3b701d..81718df7 100644 --- a/builder-frontend/src/components/screener/EligibilityResults.tsx +++ b/builder-frontend/src/components/screener/EligibilityResults.tsx @@ -6,9 +6,17 @@ 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 { +function formatParameters( + params: Record, + defaultedParameters: string[] = [] +): string { return Object.entries(params) - .map(([key, value]) => `${key}=${value}`) + .map(([key, value]) => { + const defaultedLabel = defaultedParameters.includes(key) + ? " (defaulted to today's date)" + : ""; + return `${key}=${value}${defaultedLabel}`; + }) .join(", "); } @@ -83,11 +91,16 @@ function BenefitResult({ benefitResult }: { benefitResult: BenefitResult }) { 0 + (check.effectiveParameters && + Object.keys(check.effectiveParameters).length > 0) || + (check.parameters && Object.keys(check.parameters).length > 0) } >
- {formatParameters(check.parameters)} + {formatParameters( + check.effectiveParameters ?? check.parameters, + check.defaultedParameters + )}
diff --git a/builder-frontend/src/types.ts b/builder-frontend/src/types.ts index 3ef4c4a8..01d8f318 100644 --- a/builder-frontend/src/types.ts +++ b/builder-frontend/src/types.ts @@ -109,6 +109,8 @@ export interface CheckResult { module: string; version: string; parameters: ParameterValues; + effectiveParameters?: ParameterValues; + defaultedParameters?: string[]; } export type OptionalBoolean = "TRUE" | "FALSE" | "UNABLE_TO_DETERMINE";