diff --git a/builder-api/src/main/java/org/acme/controller/AccountResource.java b/builder-api/src/main/java/org/acme/controller/AccountResource.java index 5b522a6d..40d6cda6 100644 --- a/builder-api/src/main/java/org/acme/controller/AccountResource.java +++ b/builder-api/src/main/java/org/acme/controller/AccountResource.java @@ -1,5 +1,7 @@ package org.acme.controller; +import io.quarkus.logging.Log; +import io.quarkus.runtime.LaunchMode; import io.quarkus.security.identity.SecurityIdentity; import jakarta.inject.Inject; import jakarta.ws.rs.*; @@ -16,6 +18,7 @@ import org.acme.functions.AccountHooks; import org.acme.model.dto.Auth.AccountHookRequest; import org.acme.model.dto.Auth.AccountHookResponse; +import org.acme.service.ExampleScreenerExportService; @Path("/api") public class AccountResource { @@ -23,10 +26,13 @@ public class AccountResource { @Inject AccountHooks accountHooks; + @Inject + ExampleScreenerExportService exampleScreenerExportService; + @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Path("/account-hooks") + @Path("/account/hooks") public Response accountHooks(@Context SecurityIdentity identity, AccountHookRequest request) { @@ -61,4 +67,47 @@ public Response accountHooks(@Context SecurityIdentity identity, return Response.ok(responseBody).build(); } + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Path("/account/export-example-screener") + public Response exportExampleScreener(@Context SecurityIdentity identity) { + String userId = AuthUtils.getUserId(identity); + + if (userId == null) { + return Response.status(Response.Status.UNAUTHORIZED) + .entity(new ApiError(true, "Unauthorized.")).build(); + } + + if (LaunchMode.current() != LaunchMode.DEVELOPMENT) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + try { + ExampleScreenerExportService.ExportSummary summary = exampleScreenerExportService + .exportForUser(userId); + return Response.ok( + Map.of( + "success", + true, + "outputPath", + summary.outputPath(), + "screenerCount", + summary.screenerCount(), + "firestoreDocuments", + summary.firestoreDocuments(), + "storageFiles", + summary.storageFiles())) + .build(); + } catch (Exception e) { + Log.error( + "Failed to export example screener seed data for user " + + userId, + e); + return Response.serverError().entity( + new ApiError(true, + "Failed to export example screener seed data.")) + .build(); + } + } } diff --git a/builder-api/src/main/java/org/acme/functions/AccountHooks.java b/builder-api/src/main/java/org/acme/functions/AccountHooks.java index b91b166e..c8b734be 100644 --- a/builder-api/src/main/java/org/acme/functions/AccountHooks.java +++ b/builder-api/src/main/java/org/acme/functions/AccountHooks.java @@ -4,6 +4,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import java.util.List; + import org.acme.service.ExampleScreenerImportService; @ApplicationScoped @@ -14,8 +16,11 @@ public class AccountHooks { public Boolean addExampleScreenerToAccount(String userId) { try { Log.info("Running ADD_EXAMPLE_SCREENER hook for user: " + userId); - String screenerId = exampleScreenerImportService.importForUser(userId); - Log.info("Imported example screener " + screenerId + " for user " + userId); + List screenerIds = exampleScreenerImportService + .importForUser(userId); + Log.info( + "Imported example screeners " + screenerIds + " for user " + + userId); return true; } catch (Exception e) { Log.error( diff --git a/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/Manifest.java b/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/Manifest.java new file mode 100644 index 00000000..ac8b7a13 --- /dev/null +++ b/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/Manifest.java @@ -0,0 +1,8 @@ +package org.acme.model.dto.ExampleScreener; + +import java.util.List; + +public record Manifest(List screeners, + List workingCustomChecks, List publishedCustomChecks, + List dmnPaths) { +} diff --git a/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/ScreenerManifest.java b/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/ScreenerManifest.java new file mode 100644 index 00000000..449cd277 --- /dev/null +++ b/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/ScreenerManifest.java @@ -0,0 +1,7 @@ +package org.acme.model.dto.ExampleScreener; + +import java.util.List; + +public record ScreenerManifest(String screenerPath, List benefits, + String formSchema) { +} diff --git a/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java b/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java new file mode 100644 index 00000000..5df421a9 --- /dev/null +++ b/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java @@ -0,0 +1,390 @@ +package org.acme.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.Timestamp; +import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.acme.constants.CollectionNames; +import org.acme.constants.FieldNames; +import org.acme.model.dto.ExampleScreener.ScreenerManifest; +import org.acme.persistence.FirestoreUtils; +import org.acme.persistence.StorageService; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +@ApplicationScoped +public class ExampleScreenerExportService { + private static final Path EXPORT_ROOT = Paths + .get("src", "main", "resources", "seed-data", "example-screener"); + private static final String SYSTEM_COLLECTION = "system"; + private static final String SYSTEM_CONFIG_ID = "config"; + + private final StorageService storageService; + private final String bucketName; + private final ObjectMapper objectMapper; + + @Inject + public ExampleScreenerExportService(StorageService storageService, + @ConfigProperty( + name = "GCS_BUCKET_NAME", + defaultValue = "demo-bdt-dev.appspot.com") String bucketName) { + this.storageService = storageService; + this.bucketName = bucketName; + this.objectMapper = new ObjectMapper(); + } + + public ExportSummary exportForUser(String userId) throws Exception { + resetExportRoot(); + + List> workingScreeners = getDocumentsByOwner( + CollectionNames.WORKING_SCREENER_COLLECTION, + userId); + List> workingCustomChecks = getDocumentsByOwner( + CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, + userId); + List> publishedCustomChecks = getDocumentsByOwner( + CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, + userId); + + int firestoreDocuments = 0; + ExportScreenerResult exportedScreeners = exportScreeners( + workingScreeners); + ExportDocumentResult exportedWorkingChecks = exportChecks( + CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, + workingCustomChecks); + ExportDocumentResult exportedPublishedChecks = exportChecks( + CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, + publishedCustomChecks); + + firestoreDocuments += exportedScreeners.numExported() + + exportedWorkingChecks.numExported() + + exportedPublishedChecks.numExported(); + firestoreDocuments += exportSystemConfig(); + + int storageFiles = 0; + ExportDocumentResult exportedWorkingCheckDmns = exportCheckDmns( + workingCustomChecks); + ExportDocumentResult exportedPublishedCheckDmns = exportCheckDmns( + publishedCustomChecks); + + storageFiles += exportedWorkingCheckDmns.numExported() + + exportedPublishedCheckDmns.numExported(); + + writeManifest( + exportedScreeners, + exportedWorkingChecks, + exportedPublishedChecks, + exportedWorkingCheckDmns, + exportedPublishedCheckDmns); + + Log.info( + "Exported Firebase seed data for user " + userId + " to " + + EXPORT_ROOT.toAbsolutePath().normalize()); + return new ExportSummary( + EXPORT_ROOT.toAbsolutePath().normalize().toString(), + workingScreeners.size(), firestoreDocuments, storageFiles); + } + + private List> getDocumentsByOwner(String collectionName, + String userId) { + List> documents = new ArrayList<>( + FirestoreUtils.getFirestoreDocsByField( + collectionName, + FieldNames.OWNER_ID, + userId)); + documents.sort( + Comparator.comparing( + document -> requiredString( + document, + FieldNames.ID, + collectionName))); + return documents; + } + + private ExportScreenerResult exportScreeners( + List> workingScreeners) throws IOException { + int numExported = 0; + List screeners = new ArrayList<>(); + + for (Map screener : workingScreeners) { + String screenerId = requiredString( + screener, + FieldNames.ID, + CollectionNames.WORKING_SCREENER_COLLECTION); + Path screenerPath = EXPORT_ROOT.resolve("firestore") + .resolve("workingScreener").resolve(screenerId + ".json"); + writeJsonFile( + screenerPath, + firestoreDocumentForExport(screener, screenerId)); + numExported++; + + ExportDocumentResult exportedBenefits = exportBenefits(screenerId); + String exportedFormPath = exportScreenerForm(screenerId); + + numExported += exportedBenefits.numExported(); + + screeners.add( + new ScreenerManifest(skipFirstPath(screenerPath).toString(), + exportedBenefits.outputPaths(), exportedFormPath)); + } + + return new ExportScreenerResult(numExported, screeners); + } + + private ExportDocumentResult exportBenefits(String screenerId) + throws IOException { + String collectionPath = CollectionNames.WORKING_SCREENER_COLLECTION + + "/" + screenerId + "/customBenefit"; + List> benefits = new ArrayList<>( + FirestoreUtils.getAllDocsInCollection(collectionPath)); + benefits.sort( + Comparator.comparing( + benefit -> requiredString( + benefit, + FieldNames.ID, + collectionPath))); + + int numExported = 0; + List outputPaths = new ArrayList<>(); + for (Map benefit : benefits) { + String benefitId = requiredString( + benefit, + FieldNames.ID, + collectionPath); + Path outputPath = EXPORT_ROOT.resolve("firestore") + .resolve("workingScreener").resolve(screenerId) + .resolve("customBenefit").resolve(benefitId + ".json"); + writeJsonFile( + outputPath, + firestoreDocumentForExport(benefit, benefitId)); + numExported++; + outputPaths.add(skipFirstPath(outputPath).toString()); + } + + return new ExportDocumentResult(numExported, outputPaths); + } + + private ExportDocumentResult exportChecks(String collectionName, + List> checks) throws IOException { + int numExported = 0; + List checkPaths = new ArrayList<>(); + + for (Map check : checks) { + String checkId = requiredString( + check, + FieldNames.ID, + collectionName); + Path checkPath = EXPORT_ROOT.resolve("firestore") + .resolve(collectionName).resolve(checkId + ".json"); + writeJsonFile( + checkPath, + firestoreDocumentForExport(check, checkId)); + numExported++; + checkPaths.add(skipFirstPath(checkPath).toString()); + } + return new ExportDocumentResult(numExported, checkPaths); + } + + private int exportSystemConfig() throws IOException { + Optional> config = FirestoreUtils + .getFirestoreDocById(SYSTEM_COLLECTION, SYSTEM_CONFIG_ID); + if (config.isEmpty()) { + return 0; + } + + writeJsonFile( + EXPORT_ROOT.resolve("firestore").resolve(SYSTEM_COLLECTION) + .resolve(SYSTEM_CONFIG_ID + ".json"), + firestoreDocumentForExport(config.get(), SYSTEM_CONFIG_ID)); + return 1; + } + + private String exportScreenerForm(String screenerId) throws IOException { + + Optional formSchema = storageService.getStringFromStorage( + storageService.getScreenerWorkingFormSchemaPath(screenerId)); + + Path formPath = !formSchema.isEmpty() + ? EXPORT_ROOT.resolve("storage").resolve("form") + .resolve("working").resolve(screenerId + ".json") + : Path.of(""); + if (!formSchema.isEmpty()) { + writeStringFile(formPath, formSchema.get()); + } + + return skipFirstPath(formPath).toString(); + } + + private ExportDocumentResult exportCheckDmns( + List> checks) throws IOException { + int numExported = 0; + List exportedDmns = new ArrayList<>(); + Set exportedIds = new LinkedHashSet<>(); + + for (Map check : checks) { + String checkId = requiredString( + check, + FieldNames.ID, + "customCheck"); + if (!exportedIds.add(checkId)) { + continue; + } + + Optional dmnModel = storageService.getStringFromStorage( + storageService.getCheckDmnModelPath(checkId)); + if (dmnModel.isEmpty()) { + continue; + } + Path exportPath = EXPORT_ROOT.resolve("storage").resolve("check") + .resolve(checkId + ".dmn"); + writeStringFile(exportPath, dmnModel.get()); + numExported++; + exportedDmns.add(skipFirstPath(exportPath).toString()); + } + + return new ExportDocumentResult(numExported, exportedDmns); + } + + private void writeManifest(ExportScreenerResult exportedScreeners, + ExportDocumentResult exportedWorkingChecks, + ExportDocumentResult exportedPublishedChecks, + ExportDocumentResult exportedWorkingCheckDmns, + ExportDocumentResult exportedPublishedCheckDmns) + throws IOException { + List combinedDmnPaths = new ArrayList<>( + exportedWorkingCheckDmns.outputPaths()); + combinedDmnPaths.addAll(exportedPublishedCheckDmns.outputPaths()); + + Map manifest = new LinkedHashMap<>(); + manifest.put("screeners", exportedScreeners.screeners()); + manifest.put( + "workingCustomChecks", + exportedWorkingChecks.outputPaths()); + manifest.put( + "publishedCustomChecks", + exportedPublishedChecks.outputPaths()); + manifest.put("dmnPaths", combinedDmnPaths); + + writeJsonFile(EXPORT_ROOT.resolve("manifest.json"), manifest); + } + + private Map firestoreDocumentForExport( + Map rawData, String documentId) { + Map exportData = new LinkedHashMap<>(); + for (Map.Entry entry : rawData.entrySet()) { + exportData.put( + entry.getKey(), + normalizeFirestoreValue(entry.getValue())); + } + exportData.put("_id", documentId); + return exportData; + } + + private Object normalizeFirestoreValue(Object value) { + if (value instanceof Timestamp timestamp) { + Map exportedTimestamp = new LinkedHashMap<>(); + exportedTimestamp.put("_type", "timestamp"); + exportedTimestamp + .put("value", timestamp.toDate().toInstant().toString()); + return exportedTimestamp; + } + + if (value instanceof Map mapValue) { + Map normalizedMap = new LinkedHashMap<>(); + for (Map.Entry entry : mapValue.entrySet()) { + normalizedMap.put( + String.valueOf(entry.getKey()), + normalizeFirestoreValue(entry.getValue())); + } + return normalizedMap; + } + + if (value instanceof List listValue) { + List normalizedList = new ArrayList<>(); + for (Object item : listValue) { + normalizedList.add(normalizeFirestoreValue(item)); + } + return normalizedList; + } + + return value; + } + + private void resetExportRoot() throws IOException { + if (Files.exists(EXPORT_ROOT)) { + try (var walk = Files.walk(EXPORT_ROOT)) { + walk.sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException("Failed to delete " + path, + e); + } + }); + } catch (RuntimeException e) { + if (e.getCause() instanceof IOException ioException) { + throw ioException; + } + throw e; + } + } + + Files.createDirectories(EXPORT_ROOT); + } + + private void writeJsonFile(Path path, Object data) throws IOException { + Files.createDirectories(path.getParent()); + Files.writeString( + path, + objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(data)); + } + + private void writeStringFile(Path path, String data) throws IOException { + Files.createDirectories(path.getParent()); + Files.writeString(path, data); + } + + private String requiredString(Map data, String fieldName, + String context) { + Object value = data.get(fieldName); + if (!(value instanceof String stringValue) || stringValue.isBlank()) { + throw new IllegalStateException( + "Missing field '" + fieldName + "' for " + context); + } + return stringValue; + } + + private Path skipFirstPath(Path path) { + int pathCount = path.getNameCount(); + if (pathCount <= 3) { + return path; + } + return path.subpath(3, path.getNameCount()); + } + + private record ExportDocumentResult(int numExported, + List outputPaths) { + } + + private record ExportScreenerResult(int numExported, + List screeners) { + } + + public record ExportSummary(String outputPath, int screenerCount, + int firestoreDocuments, int storageFiles) { + } +} diff --git a/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java b/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java index 54cd3989..b100976f 100644 --- a/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java +++ b/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java @@ -2,7 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; + import io.quarkus.logging.Log; +import io.quarkus.runtime.LaunchMode; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.acme.model.domain.Benefit; @@ -10,18 +13,21 @@ import org.acme.model.domain.CheckConfig; import org.acme.model.domain.EligibilityCheck; import org.acme.model.domain.Screener; +import org.acme.model.dto.ExampleScreener.Manifest; +import org.acme.model.dto.ExampleScreener.ScreenerManifest; import org.acme.persistence.EligibilityCheckRepository; import org.acme.persistence.ScreenerRepository; import org.acme.persistence.StorageService; import org.eclipse.microprofile.config.inject.ConfigProperty; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -33,148 +39,179 @@ @ApplicationScoped public class ExampleScreenerImportService { + private static final String BUNDLED_SEED_MANIFEST = "seed-data/example-screener/manifest.json"; private final ScreenerRepository screenerRepository; private final EligibilityCheckRepository eligibilityCheckRepository; private final StorageService storageService; - private final Optional configuredSeedPath; private final ObjectMapper objectMapper; @Inject - public ExampleScreenerImportService( - ScreenerRepository screenerRepository, - EligibilityCheckRepository eligibilityCheckRepository, - StorageService storageService, - @ConfigProperty(name = "example-screener.seed-path") Optional configuredSeedPath - ) { + public ExampleScreenerImportService(ScreenerRepository screenerRepository, + EligibilityCheckRepository eligibilityCheckRepository, + StorageService storageService, @ConfigProperty( + name = "example-screener.seed-path") Optional configuredSeedPath) { this.screenerRepository = screenerRepository; this.eligibilityCheckRepository = eligibilityCheckRepository; this.storageService = storageService; - this.configuredSeedPath = configuredSeedPath; this.objectMapper = new ObjectMapper(); } - public String importForUser(String userId) throws Exception { - Path seedRoot = resolveSeedRoot(); - SeedData seedData = loadSeedData(seedRoot); - - Map importedCustomCheckIds = importReferencedCustomChecks(seedData, userId); - - List importedBenefits = new ArrayList<>(); - List importedBenefitDetails = new ArrayList<>(); - for (Benefit seedBenefit : seedData.benefits()) { - Benefit importedBenefit = cloneBenefit(seedBenefit, userId, importedCustomCheckIds); - importedBenefits.add(importedBenefit); - importedBenefitDetails.add(new BenefitDetail( - importedBenefit.getId(), - importedBenefit.getName(), - importedBenefit.getDescription() - )); - } + public List importForUser(String userId) throws Exception { + Manifest manifest = readJsonRes(BUNDLED_SEED_MANIFEST, Manifest.class); + SeedData seedData = loadSeedData(manifest); + + Map importedCustomCheckIds = importReferencedCustomChecks( + seedData, + userId); + List importedScreenerIds = new ArrayList<>(); + + for (SeedScreenerData seedScreener : seedData.screeners()) { + List importedBenefits = new ArrayList<>(); + List importedBenefitDetails = new ArrayList<>(); + for (Benefit seedBenefit : seedScreener.benefits()) { + Benefit importedBenefit = cloneBenefit( + seedBenefit, + userId, + importedCustomCheckIds); + importedBenefits.add(importedBenefit); + importedBenefitDetails.add( + new BenefitDetail(importedBenefit.getId(), + importedBenefit.getName(), + importedBenefit.getDescription())); + } - Screener importedScreener = new Screener(); - importedScreener.setOwnerId(userId); - importedScreener.setScreenerName(seedData.screener().getScreenerName()); - importedScreener.setBenefits(importedBenefitDetails); + Screener importedScreener = new Screener(); + importedScreener.setOwnerId(userId); + importedScreener + .setScreenerName(seedScreener.screener().getScreenerName()); + importedScreener.setBenefits(importedBenefitDetails); - String newScreenerId = screenerRepository.saveNewWorkingScreener(importedScreener); - importedScreener.setId(newScreenerId); + String newScreenerId = screenerRepository + .saveNewWorkingScreener(importedScreener); + importedScreener.setId(newScreenerId); - for (Benefit importedBenefit : importedBenefits) { - screenerRepository.saveNewCustomBenefit(newScreenerId, importedBenefit); - } + for (Benefit importedBenefit : importedBenefits) { + screenerRepository + .saveNewCustomBenefit(newScreenerId, importedBenefit); + } - String formPath = storageService.getScreenerWorkingFormSchemaPath(newScreenerId); - storageService.writeJsonToStorage(formPath, seedData.formSchema()); + if (seedScreener.formSchema() != null) { + String formPath = storageService + .getScreenerWorkingFormSchemaPath(newScreenerId); + storageService.writeJsonToStorage( + formPath, + seedScreener.formSchema()); + } + + importedScreenerIds.add(newScreenerId); + Log.info( + "Imported example screener " + newScreenerId + " for user " + + userId); + } - Log.info("Imported example screener " + newScreenerId + " for user " + userId); - return newScreenerId; + return importedScreenerIds; } - private Map importReferencedCustomChecks(SeedData seedData, String userId) throws Exception { + private Map importReferencedCustomChecks(SeedData seedData, + String userId) throws Exception { Set referencedCustomCheckIds = new LinkedHashSet<>(); - for (Benefit benefit : seedData.benefits()) { - if (benefit.getChecks() == null) { - continue; - } - for (CheckConfig checkConfig : benefit.getChecks()) { - String sourceCheckId = resolveSourceCheckId(checkConfig); - if (sourceCheckId != null && !isLibraryCheckId(sourceCheckId)) { - referencedCustomCheckIds.add(sourceCheckId); + for (SeedScreenerData seedScreener : seedData.screeners()) { + for (Benefit benefit : seedScreener.benefits()) { + if (benefit.getChecks() == null) { + continue; + } + for (CheckConfig checkConfig : benefit.getChecks()) { + String sourceCheckId = resolveSourceCheckId(checkConfig); + if (sourceCheckId != null + && !isLibraryCheckId(sourceCheckId)) { + referencedCustomCheckIds.add(sourceCheckId); + } } } } Map remappedCheckIds = new HashMap<>(); for (String seedSourceCheckId : referencedCustomCheckIds) { - SeedCustomCheckVersions seedCustomCheckVersions = findSeedCustomCheckVersions(seedData, seedSourceCheckId); + SeedCustomCheckVersions seedCustomCheckVersions = findSeedCustomCheckVersions( + seedData, + seedSourceCheckId); if (seedCustomCheckVersions.workingCheck() != null) { String newWorkingId = upsertWorkingCustomCheck( - userId, - seedCustomCheckVersions.workingCheck(), - seedData.dmnByCheckId() - ); - remappedCheckIds.put(seedCustomCheckVersions.workingCheck().getId(), newWorkingId); + userId, + seedCustomCheckVersions.workingCheck(), + seedData.dmnByCheckId()); + remappedCheckIds.put( + seedCustomCheckVersions.workingCheck().getId(), + newWorkingId); } if (seedCustomCheckVersions.publishedCheck() != null) { String newPublishedId = upsertPublishedCustomCheck( - userId, - seedCustomCheckVersions.publishedCheck(), - seedData.dmnByCheckId() - ); - remappedCheckIds.put(seedCustomCheckVersions.publishedCheck().getId(), newPublishedId); + userId, + seedCustomCheckVersions.publishedCheck(), + seedData.dmnByCheckId()); + remappedCheckIds.put( + seedCustomCheckVersions.publishedCheck().getId(), + newPublishedId); } if (!remappedCheckIds.containsKey(seedSourceCheckId)) { - throw new IllegalStateException("No imported check mapping found for seed check " + seedSourceCheckId); + throw new IllegalStateException( + "No imported check mapping found for seed check " + + seedSourceCheckId); } } return remappedCheckIds; } - private SeedCustomCheckVersions findSeedCustomCheckVersions(SeedData seedData, String seedSourceCheckId) { - EligibilityCheck referencedCheck = seedData.workingCustomChecks().get(seedSourceCheckId); + private SeedCustomCheckVersions findSeedCustomCheckVersions( + SeedData seedData, String seedSourceCheckId) { + EligibilityCheck referencedCheck = seedData.workingCustomChecks() + .get(seedSourceCheckId); if (referencedCheck == null) { - referencedCheck = seedData.publishedCustomChecks().get(seedSourceCheckId); + referencedCheck = seedData.publishedCustomChecks() + .get(seedSourceCheckId); } if (referencedCheck == null) { - throw new IllegalStateException("Missing seed custom check for referenced id " + seedSourceCheckId); + throw new IllegalStateException( + "Missing seed custom check for referenced id " + + seedSourceCheckId); } String seedWorkingId = buildWorkingCheckId( - referencedCheck.getOwnerId(), - referencedCheck.getModule(), - referencedCheck.getName() - ); + referencedCheck.getOwnerId(), + referencedCheck.getModule(), + referencedCheck.getName()); String seedPublishedId = buildPublishedCheckId( - referencedCheck.getOwnerId(), - referencedCheck.getModule(), - referencedCheck.getName(), - referencedCheck.getVersion() - ); + referencedCheck.getOwnerId(), + referencedCheck.getModule(), + referencedCheck.getName(), + referencedCheck.getVersion()); return new SeedCustomCheckVersions( - seedData.workingCustomChecks().get(seedWorkingId), - seedData.publishedCustomChecks().get(seedPublishedId) - ); + seedData.workingCustomChecks().get(seedWorkingId), + seedData.publishedCustomChecks().get(seedPublishedId)); } - private String upsertWorkingCustomCheck( - String userId, - EligibilityCheck seedCheck, - Map dmnByCheckId - ) throws Exception { + private String upsertWorkingCustomCheck(String userId, + EligibilityCheck seedCheck, Map dmnByCheckId) + throws Exception { EligibilityCheck importedCheck = cloneEligibilityCheck(seedCheck); importedCheck.setOwnerId(userId); importedCheck.setIsArchived(false); - String newWorkingId = buildWorkingCheckId(userId, importedCheck.getModule(), importedCheck.getName()); + String newWorkingId = buildWorkingCheckId( + userId, + importedCheck.getModule(), + importedCheck.getName()); importedCheck.setId(newWorkingId); - if (eligibilityCheckRepository.getWorkingCustomCheck(userId, newWorkingId, true).isPresent()) { + if (eligibilityCheckRepository + .getWorkingCustomCheck(userId, newWorkingId, true) + .isPresent()) { eligibilityCheckRepository.updateWorkingCustomCheck(importedCheck); } else { eligibilityCheckRepository.saveNewWorkingCustomCheck(importedCheck); @@ -184,31 +221,32 @@ private String upsertWorkingCustomCheck( return newWorkingId; } - private String upsertPublishedCustomCheck( - String userId, - EligibilityCheck seedCheck, - Map dmnByCheckId - ) throws Exception { + private String upsertPublishedCustomCheck(String userId, + EligibilityCheck seedCheck, Map dmnByCheckId) + throws Exception { EligibilityCheck importedCheck = cloneEligibilityCheck(seedCheck); importedCheck.setOwnerId(userId); importedCheck.setIsArchived(false); String newPublishedId = buildPublishedCheckId( - userId, - importedCheck.getModule(), - importedCheck.getName(), - importedCheck.getVersion() - ); + userId, + importedCheck.getModule(), + importedCheck.getName(), + importedCheck.getVersion()); importedCheck.setId(newPublishedId); - if (eligibilityCheckRepository.getPublishedCustomCheck(userId, newPublishedId).isPresent()) { - eligibilityCheckRepository.updatePublishedCustomCheck(importedCheck); + if (eligibilityCheckRepository + .getPublishedCustomCheck(userId, newPublishedId).isPresent()) { + eligibilityCheckRepository + .updatePublishedCustomCheck(importedCheck); } else { try { - eligibilityCheckRepository.saveNewPublishedCustomCheck(importedCheck); + eligibilityCheckRepository + .saveNewPublishedCustomCheck(importedCheck); } catch (Exception e) { Log.info(e); - eligibilityCheckRepository.updatePublishedCustomCheck(importedCheck); + eligibilityCheckRepository + .updatePublishedCustomCheck(importedCheck); } } @@ -216,38 +254,47 @@ private String upsertPublishedCustomCheck( return newPublishedId; } - private void writeCheckDmn(String newCheckId, EligibilityCheck seedCheck, Map dmnByCheckId) throws Exception { + private void writeCheckDmn(String newCheckId, EligibilityCheck seedCheck, + Map dmnByCheckId) throws Exception { String dmnModel = dmnByCheckId.get(seedCheck.getId()); - if ((dmnModel == null || dmnModel.isBlank()) && seedCheck.getDmnModel() != null) { + if ((dmnModel == null || dmnModel.isBlank()) + && seedCheck.getDmnModel() != null) { dmnModel = seedCheck.getDmnModel(); } if (dmnModel == null || dmnModel.isBlank()) { - throw new IllegalStateException("Missing DMN model for seed check " + seedCheck.getId()); + throw new IllegalStateException( + "Missing DMN model for seed check " + seedCheck.getId()); } storageService.writeStringToStorage( - storageService.getCheckDmnModelPath(newCheckId), - dmnModel, - "application/xml" - ); + storageService.getCheckDmnModelPath(newCheckId), + dmnModel, + "application/xml"); } - private Benefit cloneBenefit(Benefit seedBenefit, String userId, Map importedCustomCheckIds) { - Benefit importedBenefit = objectMapper.convertValue(seedBenefit, Benefit.class); + private Benefit cloneBenefit(Benefit seedBenefit, String userId, + Map importedCustomCheckIds) { + Benefit importedBenefit = objectMapper + .convertValue(seedBenefit, Benefit.class); importedBenefit.setId(UUID.randomUUID().toString()); importedBenefit.setOwnerId(userId); - importedBenefit.setChecks(remapCheckConfigs(seedBenefit.getChecks(), importedCustomCheckIds)); + importedBenefit.setChecks( + remapCheckConfigs( + seedBenefit.getChecks(), + importedCustomCheckIds)); return importedBenefit; } - private List remapCheckConfigs(List seedChecks, Map importedCustomCheckIds) { + private List remapCheckConfigs(List seedChecks, + Map importedCustomCheckIds) { if (seedChecks == null || seedChecks.isEmpty()) { return Collections.emptyList(); } List importedChecks = new ArrayList<>(); for (CheckConfig seedCheck : seedChecks) { - CheckConfig importedCheck = objectMapper.convertValue(seedCheck, CheckConfig.class); + CheckConfig importedCheck = objectMapper + .convertValue(seedCheck, CheckConfig.class); importedCheck.setCheckId(UUID.randomUUID().toString()); String sourceCheckId = resolveSourceCheckId(seedCheck); @@ -255,9 +302,12 @@ private List remapCheckConfigs(List seedChecks, Map screenerFiles = listJsonFiles(workingScreenersDir); - if (screenerFiles.size() != 1) { - throw new IllegalStateException("Expected exactly one working screener seed document, found " + screenerFiles.size()); - } + private SeedData loadSeedData(Manifest manifest) throws IOException { + List screenerFiles = manifest.screeners(); - Path screenerFile = screenerFiles.get(0); - Screener screener = readJsonFile(screenerFile, Screener.class); - String screenerDocId = stripExtension(screenerFile.getFileName().toString()); + List screeners = new ArrayList<>(); + for (ScreenerManifest screenerManifest : screenerFiles) { + String screenerPath = screenerManifest.screenerPath(); + Screener screener = readJsonRes(screenerPath, Screener.class); + + List benefitsFiles = screenerManifest.benefits(); + List benefits = new ArrayList<>(); + for (String benefitPath : benefitsFiles) { + benefits.add(readJsonRes(benefitPath, Benefit.class)); + } - Path benefitsDir = workingScreenersDir.resolve(screenerDocId).resolve("customBenefit"); - List benefits = new ArrayList<>(); - for (Path benefitFile : listJsonFiles(benefitsDir)) { - benefits.add(readJsonFile(benefitFile, Benefit.class)); + JsonNode formSchema = screenerManifest.formSchema().length() > 0 + ? loadFormSchema(screenerManifest.formSchema()) + : JsonNodeFactory.instance.objectNode(); + + screeners.add(new SeedScreenerData(screener, benefits, formSchema)); } - JsonNode formSchema = objectMapper.readTree( - Files.readString(seedRoot.resolve("storage").resolve("form").resolve("working").resolve(screenerDocId + ".json")) - ); - - return new SeedData( - screener, - benefits, - formSchema, - loadChecks(seedRoot.resolve("firestore").resolve("workingCustomCheck")), - loadChecks(seedRoot.resolve("firestore").resolve("publishedCustomCheck")), - loadDmnFiles(seedRoot.resolve("storage").resolve("check")) - ); + return new SeedData(screeners, + loadChecks(manifest.workingCustomChecks()), + loadChecks(manifest.publishedCustomChecks()), + loadDmnFiles(manifest.dmnPaths())); + } + + private JsonNode loadFormSchema(String formPath) { + return readJsonRes(formPath, JsonNode.class); } - private Map loadChecks(Path checksDir) throws IOException { + private Map loadChecks(List checksPaths) { Map checksById = new LinkedHashMap<>(); - if (!Files.isDirectory(checksDir)) { - return checksById; - } - for (Path checkFile : listJsonFiles(checksDir)) { - EligibilityCheck check = readJsonFile(checkFile, EligibilityCheck.class); + for (String checkPath : checksPaths) { + EligibilityCheck check = readJsonRes( + checkPath, + EligibilityCheck.class); checksById.put(check.getId(), check); } return checksById; } - private Map loadDmnFiles(Path dmnDir) throws IOException { + private Map loadDmnFiles(List dmnPaths) { Map dmnByCheckId = new HashMap<>(); - if (!Files.isDirectory(dmnDir)) { - return dmnByCheckId; - } - try (var stream = Files.list(dmnDir)) { - stream - .filter(Files::isRegularFile) - .filter(path -> path.getFileName().toString().endsWith(".dmn")) - .sorted(Comparator.comparing(path -> path.getFileName().toString())) - .forEach(path -> { - try { - dmnByCheckId.put(stripExtension(path.getFileName().toString()), Files.readString(path)); - } catch (IOException e) { - throw new RuntimeException("Failed to read DMN file " + path, e); - } - }); - } catch (RuntimeException e) { - if (e.getCause() instanceof IOException ioException) { - throw ioException; + dmnPaths.stream().forEach(path -> { + try { + InputStream stream = getPathStream(path); + String contents = new String(stream.readAllBytes(), + StandardCharsets.UTF_8); + dmnByCheckId.put(stripExtension(getIdFromPath(path)), contents); + } catch (IOException exception) { + Log.info("Error reading DMN file: " + path); } - throw e; - } + }); return dmnByCheckId; } - private T readJsonFile(Path path, Class clazz) throws IOException { - return objectMapper.readValue(Files.readString(path), clazz); - } - - private List listJsonFiles(Path directory) throws IOException { - if (!Files.isDirectory(directory)) { - return Collections.emptyList(); - } - - try (var stream = Files.list(directory)) { - return stream - .filter(Files::isRegularFile) - .filter(path -> path.getFileName().toString().endsWith(".json")) - .sorted(Comparator.comparing(path -> path.getFileName().toString())) - .toList(); + private T readJsonRes(String path, Class clazz) { + try { + InputStream stream = getPathStream(path); + return objectMapper.readValue(stream, clazz); + } catch (IOException exception) { + Log.info("Failed to read resource: " + path); + throw new IllegalStateException(exception); } } - private Path resolveSeedRoot() { - List candidates = new ArrayList<>(); - configuredSeedPath - .map(String::trim) - .filter(path -> !path.isBlank()) - .map(Paths::get) - .ifPresent(candidates::add); - candidates.add(Paths.get("seed-data", "example-screener")); - candidates.add(Paths.get("..", "seed-data", "example-screener")); - - for (Path candidate : candidates) { - Path absoluteCandidate = candidate.toAbsolutePath().normalize(); - if (Files.isDirectory(absoluteCandidate)) { - return absoluteCandidate; + private InputStream getPathStream(String path) throws IOException { + try { + if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { + return Files.newInputStream( + Path.of("src", "main", "resources").resolve(path)); + } else { + return Thread.currentThread().getContextClassLoader() + .getResourceAsStream(path); } + } catch (IOException exception) { + throw new IOException("Could not find: " + path); } - - throw new IllegalStateException("Could not find example screener seed data in any expected location"); } - private String buildWorkingCheckId(String ownerId, String module, String name) { + private String buildWorkingCheckId(String ownerId, String module, + String name) { return "W-" + ownerId + "-" + module + "-" + name; } - private String buildPublishedCheckId(String ownerId, String module, String name, String version) { + private String buildPublishedCheckId(String ownerId, String module, + String name, String version) { return "P-" + ownerId + "-" + module + "-" + name + "-" + version; } + private String getIdFromPath(String path) { + return stripExtension(Paths.get(path).getFileName().toString()); + } + private String stripExtension(String filename) { int extensionIndex = filename.lastIndexOf('.'); if (extensionIndex == -1) { @@ -410,17 +441,17 @@ private String stripExtension(String filename) { return filename.substring(0, extensionIndex); } - private record SeedData( - Screener screener, - List benefits, - JsonNode formSchema, - Map workingCustomChecks, - Map publishedCustomChecks, - Map dmnByCheckId - ) {} - - private record SeedCustomCheckVersions( - EligibilityCheck workingCheck, - EligibilityCheck publishedCheck - ) {} + private record SeedData(List screeners, + Map workingCustomChecks, + Map publishedCustomChecks, + Map dmnByCheckId) { + } + + private record SeedScreenerData(Screener screener, List benefits, + JsonNode formSchema) { + } + + private record SeedCustomCheckVersions(EligibilityCheck workingCheck, + EligibilityCheck publishedCheck) { + } } diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.json new file mode 100644 index 00000000..ac172c51 --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.json @@ -0,0 +1,31 @@ +{ + "datePublished" : 1780418141112, + "dmnModel" : "\n\n \n \n \n \n \n \n \n \n \n \n \n \n true\n \n \n \n \n \n \n \n 300\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "isArchived" : false, + "module" : "testchecks", + "inputDefinition" : { + "type" : "object", + "properties" : { + "custom" : { + "type" : "object", + "properties" : { + "testinput" : { } + }, + "required" : [ "testinput" ] + } + }, + "required" : [ "custom" ] + }, + "parameterDefinitions" : [ { + "label" : "P 1", + "type" : "string", + "key" : "p1", + "required" : true + } ], + "name" : "test", + "description" : "desc", + "id" : "P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "version" : "2.0.0", + "_id" : "P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/system/config.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/system/config.json new file mode 100644 index 00000000..8a181974 --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/system/config.json @@ -0,0 +1,9 @@ +{ + "latestJsonStoragePath" : "LibraryApiSchemaExports/export_2026-06-02_15-50-34.json", + "id" : "config", + "updatedAt" : { + "_type" : "timestamp", + "value" : "2026-06-02T15:50:34.539Z" + }, + "_id" : "config" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.json new file mode 100644 index 00000000..ac31796c --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.json @@ -0,0 +1,27 @@ +{ + "dmnModel" : "\n\n \n \n \n \n \n \n \n \n \n \n \n \n true\n \n \n \n \n \n \n \n 300\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "isArchived" : false, + "module" : "testchecks", + "inputDefinition" : { + "type" : "object", + "properties" : { + "custom" : { + "type" : "object", + "required" : [ "testinput" ] + } + }, + "required" : [ "custom" ] + }, + "parameterDefinitions" : [ { + "label" : "P 1", + "type" : "string", + "key" : "p1", + "required" : true + } ], + "name" : "test", + "description" : "desc", + "id" : "W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "version" : "2.0.0", + "_id" : "W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh.json new file mode 100644 index 00000000..e677f296 --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh.json @@ -0,0 +1,15 @@ +{ + "benefits" : [ { + "name" : "OOPA", + "description" : "desc", + "id" : "b481f008-5ed6-4d5f-a0d1-a08733ab91f9" + }, { + "name" : "Homestead Exemption", + "description" : "If you own your primary residence, you are eligible for the Homestead Exemption on your Real Estate Tax. The Homestead Exemption reduces the taxable portion of your property’s assessed value.\n\nWith this exemption, the property’s assessed value is reduced by $100,000. Most homeowners will save about $1,399 a year on their Real Estate Tax bill starting in 2025.\n\nmore info:\nhttps://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/get-the-homestead-exemption/", + "id" : "ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba" + } ], + "id" : "Am3FMS6ID2keNa82MbPh", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "screenerName" : "Example - Philly Property Tax Relief", + "_id" : "Am3FMS6ID2keNa82MbPh" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json new file mode 100644 index 00000000..9363c374 --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json @@ -0,0 +1,99 @@ +{ + "checks" : [ { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "taxDelinquent" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-tax-delinquent-0.7.1", + "checkName" : "tax-delinquent", + "evaluationUrl" : "/api/v1/checks/residence/tax-delinquent", + "checkId" : "b33675f3-5ec6-4f53-80ce-494a257905ed", + "parameters" : { } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "livesInPhiladelphiaPa" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-lives-in-philadelphia-pa-0.7.1", + "checkName" : "lives-in-philadelphia-pa", + "evaluationUrl" : "/api/v1/checks/residence/lives-in-philadelphia-pa", + "checkId" : "9ae0d755-22b8-4d91-91b7-8e3f20e2baf7", + "parameters" : { } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "ownerOccupant" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-owner-occupant-0.7.1", + "checkName" : "owner-occupant", + "evaluationUrl" : "/api/v1/checks/residence/owner-occupant", + "checkId" : "a6f46b66-9867-4353-a8d7-a18e13933d68", + "parameters" : { } + }, { + "checkModule" : "testchecks", + "checkVersion" : "2.0.0", + "inputDefinition" : { + "type" : "object", + "properties" : { + "custom" : { + "type" : "object", + "properties" : { + "testinput" : { } + }, + "required" : [ "testinput" ] + } + }, + "required" : [ "custom" ] + }, + "parameterDefinitions" : [ { + "label" : "P 1", + "type" : "string", + "key" : "p1", + "required" : true + } ], + "sourceCheckId" : "P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0", + "checkName" : "test", + "checkId" : "9490bea9-be68-465c-a11e-263cb98c4ef3", + "parameters" : { + "p1" : "blah" + } + } ], + "name" : "OOPA", + "description" : "desc", + "id" : "b481f008-5ed6-4d5f-a0d1-a08733ab91f9", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "_id" : "b481f008-5ed6-4d5f-a0d1-a08733ab91f9" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json new file mode 100644 index 00000000..fa4d1a3c --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json @@ -0,0 +1,112 @@ +{ + "checks" : [ { + "checkModule" : "enrollment", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "enrollments" : { + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "personId" : { + "type" : "string" + }, + "benefit" : { + "type" : "string" + } + } + } + } + } + }, + "parameterDefinitions" : [ { + "label" : "personId", + "type" : "string", + "key" : "personId", + "required" : false + }, { + "label" : "benefit", + "type" : "string", + "key" : "benefit", + "required" : false + } ], + "sourceCheckId" : "L-enrollment-person-not-enrolled-in-benefit-0.7.1", + "checkName" : "person-not-enrolled-in-benefit", + "evaluationUrl" : "/api/v1/checks/enrollment/person-not-enrolled-in-benefit", + "checkId" : "b3b28b1e-64da-41d2-b9c2-e086a7dc0ec3", + "parameters" : { + "personId" : "client", + "benefit" : "PhlHomesteadExemption" + } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "ownerOccupant" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-owner-occupant-0.7.1", + "checkName" : "owner-occupant", + "evaluationUrl" : "/api/v1/checks/residence/owner-occupant", + "checkId" : "c2ba3e25-9fa4-42fa-a8c9-9624a4b0348d", + "parameters" : { } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "livesInPhiladelphiaPa" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-lives-in-philadelphia-pa-0.7.1", + "checkName" : "lives-in-philadelphia-pa", + "evaluationUrl" : "/api/v1/checks/residence/lives-in-philadelphia-pa", + "checkId" : "6af91242-a6d7-40f9-8693-ec75ad70903a", + "parameters" : { } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "tenYearTaxAbatement" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-no-ten-year-tax-abatement-0.7.1", + "checkName" : "no-ten-year-tax-abatement", + "evaluationUrl" : "/api/v1/checks/residence/no-ten-year-tax-abatement", + "checkId" : "f8495889-388c-4b06-935c-cac3f056b68d", + "parameters" : { } + } ], + "name" : "Homestead Exemption", + "description" : "If you own your primary residence, you are eligible for the Homestead Exemption on your Real Estate Tax. The Homestead Exemption reduces the taxable portion of your property’s assessed value.\n\nWith this exemption, the property’s assessed value is reduced by $100,000. Most homeowners will save about $1,399 a year on their Real Estate Tax bill starting in 2025.\n\nmore info:\nhttps://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/get-the-homestead-exemption/", + "id" : "ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "_id" : "ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/manifest.json b/builder-api/src/main/resources/seed-data/example-screener/manifest.json new file mode 100644 index 00000000..65557621 --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/manifest.json @@ -0,0 +1,10 @@ +{ + "screeners" : [ { + "screenerPath" : "seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh.json", + "benefits" : [ "seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json", "seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json" ], + "formSchema" : "seed-data/example-screener/storage/form/working/Am3FMS6ID2keNa82MbPh.json" + } ], + "workingCustomChecks" : [ "seed-data/example-screener/firestore/workingCustomCheck/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.json" ], + "publishedCustomChecks" : [ "seed-data/example-screener/firestore/publishedCustomCheck/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.json" ], + "dmnPaths" : [ "seed-data/example-screener/storage/check/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.dmn", "seed-data/example-screener/storage/check/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.dmn" ] +} \ No newline at end of file diff --git a/seed-data/example-screener/storage/check/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn b/builder-api/src/main/resources/seed-data/example-screener/storage/check/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.dmn similarity index 100% rename from seed-data/example-screener/storage/check/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn rename to builder-api/src/main/resources/seed-data/example-screener/storage/check/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.dmn diff --git a/seed-data/example-screener/storage/check/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn b/builder-api/src/main/resources/seed-data/example-screener/storage/check/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.dmn similarity index 100% rename from seed-data/example-screener/storage/check/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn rename to builder-api/src/main/resources/seed-data/example-screener/storage/check/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.dmn diff --git a/seed-data/example-screener/storage/form/working/hEStvPeFmEte58GQTC7Y.json b/builder-api/src/main/resources/seed-data/example-screener/storage/form/working/Am3FMS6ID2keNa82MbPh.json similarity index 100% rename from seed-data/example-screener/storage/form/working/hEStvPeFmEte58GQTC7Y.json rename to builder-api/src/main/resources/seed-data/example-screener/storage/form/working/Am3FMS6ID2keNa82MbPh.json diff --git a/builder-frontend/src/api/account.ts b/builder-frontend/src/api/account.ts index 28dad71a..025df1e3 100644 --- a/builder-frontend/src/api/account.ts +++ b/builder-frontend/src/api/account.ts @@ -5,7 +5,7 @@ import { authPost } from "@/api/auth"; const apiUrl = env.apiUrl; export const runAccountHooks = async () => { - const accountHookUrl = new URL(`${apiUrl}/account-hooks`); + const accountHookUrl = new URL(`${apiUrl}/account/hooks`); const hooksToCall = ["add example screener"]; @@ -24,3 +24,20 @@ export const runAccountHooks = async () => { throw error; // rethrow so you can handle it in your component if needed } }; + +export const exportExampleScreener = async () => { + const url = new URL(`${apiUrl}/account/export-example-screener`); + + try { + const response = await authPost(url.toString()); + + if (!response.ok) { + return { success: false }; + } + const data = (await response.json()) as { success: boolean }; + return data; + } catch (err) { + console.error("Error calling account hooks:", err); + return { success: false }; + } +}; diff --git a/builder-frontend/src/components/Header/ExportExampleScreener.tsx b/builder-frontend/src/components/Header/ExportExampleScreener.tsx new file mode 100644 index 00000000..2faa1bda --- /dev/null +++ b/builder-frontend/src/components/Header/ExportExampleScreener.tsx @@ -0,0 +1,45 @@ +import { exportExampleScreener } from "@/api/account"; +import { Button } from "@/components/shared/Button"; +import { createSignal, JSX, Match, Setter, Switch } from "solid-js"; + +interface Props { + setShowExportMenu: Setter; +} +export const ExportExampleScreener = (props: Props) => { + const [exportingMessage, setExportingMessage] = createSignal(""); + const [isExportingExample, setIsExportingExample] = createSignal(false); + + const handleExportExampleScreener: JSX.EventHandler< + HTMLButtonElement, + MouseEvent + > = async (e) => { + setIsExportingExample(true); + setExportingMessage(""); + const result = await exportExampleScreener(); + if (!result.success) { + setExportingMessage("An error occurred exporting."); + } else { + setExportingMessage("Successfully exported screeners."); + } + setIsExportingExample(false); + }; + + return ( + <> +
Ready to save changes to the example screener?
+ + + + + + Waiting for export + +
{exportingMessage()}
+ + ); +}; diff --git a/builder-frontend/src/components/Header/Header.tsx b/builder-frontend/src/components/Header/Header.tsx index 290e80b0..50766ee6 100644 --- a/builder-frontend/src/components/Header/Header.tsx +++ b/builder-frontend/src/components/Header/Header.tsx @@ -1,11 +1,14 @@ import { useAuth } from "../../context/AuthContext"; import { useLocation, useNavigate } from "@solidjs/router"; -import { Component, createMemo, For, Show } from "solid-js"; +import { Component, createMemo, createSignal, DEV, For, Show } from "solid-js"; import { HamburgerMenu } from "@/components/shared/HamburgerMenu"; import "./Header.css"; import { Menu } from "lucide-solid"; +import { Button } from "@/components/shared/Button"; +import { Modal } from "@/components/shared/Modal"; +import { ExportExampleScreener } from "@/components/Header/ExportExampleScreener"; const HeaderButton = ({ buttonText, @@ -35,6 +38,8 @@ interface MenuProps { const HeaderMenu: Component = (props) => { const navigate = useNavigate(); + const [showExportMenu, setShowExportMenu] = createSignal(false); + const menuItems: { label: string; onClick: () => void }[] = [ { label: "Custom Checks", @@ -62,6 +67,14 @@ const HeaderMenu: Component = (props) => { )} + + + setShowExportMenu(false)}> + + + ); }; diff --git a/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenu.css b/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenu.css index 6a5f0b8c..91ebfd02 100644 --- a/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenu.css +++ b/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenu.css @@ -19,7 +19,7 @@ width: 25%; height: 100%; - z-index: 1000; + z-index: 5; background-color: white; padding: 0.5rem; diff --git a/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenuWrapper.tsx b/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenuWrapper.tsx index 336b4038..a7647aba 100644 --- a/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenuWrapper.tsx +++ b/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenuWrapper.tsx @@ -30,8 +30,20 @@ export const HamburgerMenuWrapper: ParentComponent = (props) => { const [showMenu, setShowMenu] = createSignal(false); const handleClickOutside = (ev: MouseEvent) => { + if (!showMenu()) return; + const el = root(); - if (showMenu() && el && !el.contains(ev.target as Node)) { + if (!el) return; + + const path = ev.composedPath(); + + const clickedInsideMenu = path.includes(el); + const clickedInsideModal = path.some( + (node) => + node instanceof HTMLElement && node.hasAttribute("data-modal-root"), + ); + + if (!clickedInsideMenu && !clickedInsideModal) { setShowMenu(false); } }; diff --git a/builder-frontend/src/components/shared/Modal.tsx b/builder-frontend/src/components/shared/Modal.tsx index 7d0db47f..85561add 100644 --- a/builder-frontend/src/components/shared/Modal.tsx +++ b/builder-frontend/src/components/shared/Modal.tsx @@ -12,7 +12,11 @@ export const Modal: Component> = (props) => { return ( -
props.onClose()}> +
props.onClose()} + data-modal-root + >
e.stopPropagation()} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1ca62a68..00000000 --- a/package-lock.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "benefit-decision-toolkit", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@fontsource/open-sans": "^5.2.7", - "@fontsource/ramaraja": "^5.2.8" - } - }, - "node_modules/@fontsource/open-sans": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.2.7.tgz", - "integrity": "sha512-8yfgDYjE5O0vmTPdrcjV35y4yMnctsokmi9gN49Gcsr0sjzkMkR97AnKDe6OqZh2SFkYlR28fxOvi21bYEgMSw==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@fontsource/ramaraja": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource/ramaraja/-/ramaraja-5.2.8.tgz", - "integrity": "sha512-MIrLpCbfo+L750CnkLmvkV/UKbvOXLfliPCDDHgolliTbryL7UqRRSq32UdxfAXGisSRrzq82zVNTdwljA0Mxw==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 8fed6b9d..00000000 --- a/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "@fontsource/open-sans": "^5.2.7", - "@fontsource/ramaraja": "^5.2.8" - } -} diff --git a/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json b/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json deleted file mode 100644 index 67d59804..00000000 --- a/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "version": "2.0.0", - "description": "desc", - "inputDefinition": { - "properties": { - "custom": { - "properties": { - "testinput": {} - }, - "type": "object", - "required": [ - "testinput" - ] - } - }, - "type": "object", - "required": [ - "custom" - ] - }, - "parameterDefinitions": [ - { - "label": "P 1", - "key": "p1", - "type": "string", - "required": true - } - ], - "dmnModel": "\n\n \n \n \n \n \n \n \n \n \n \n \n \n true\n \n \n \n \n \n \n \n 300\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", - "isArchived": false, - "datePublished": 1775619072497, - "module": "testchecks", - "name": "test", - "id": "P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0", - "_id": "P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0" -} \ No newline at end of file diff --git a/seed-data/example-screener/firestore/system/config.json b/seed-data/example-screener/firestore/system/config.json deleted file mode 100644 index bed52047..00000000 --- a/seed-data/example-screener/firestore/system/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "latestJsonStoragePath": "LibraryApiSchemaExports/export_2026-04-08_03-28-10.json", - "updatedAt": { - "_type": "timestamp", - "value": "2026-04-08T03:28:11.430000+00:00" - }, - "_id": "config" -} \ No newline at end of file diff --git a/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json b/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json deleted file mode 100644 index 8d0cca35..00000000 --- a/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "version": "2.0.0", - "description": "desc", - "inputDefinition": { - "properties": { - "custom": { - "type": "object", - "required": [ - "testinput" - ] - } - }, - "type": "object", - "required": [ - "custom" - ] - }, - "parameterDefinitions": [ - { - "label": "P 1", - "key": "p1", - "type": "string", - "required": true - } - ], - "dmnModel": "\n\n \n \n \n \n \n \n \n \n \n \n \n \n true\n \n \n \n \n \n \n \n 300\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", - "isArchived": false, - "module": "testchecks", - "name": "test", - "id": "W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test", - "_id": "W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test" -} \ No newline at end of file diff --git a/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json b/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json deleted file mode 100644 index fead8af3..00000000 --- a/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "screenerName": "Example - Philly Property Tax Relief", - "benefits": [ - { - "description": "desc", - "name": "OOPA", - "id": "1c09392c-913c-4b2b-9870-a1951534c3fb" - }, - { - "description": "If you own your primary residence, you are eligible for the Homestead Exemption on your Real Estate Tax. The Homestead Exemption reduces the taxable portion of your property’s assessed value.\n\nWith this exemption, the property’s assessed value is reduced by $100,000. Most homeowners will save about $1,399 a year on their Real Estate Tax bill starting in 2025.\n\nmore info:\nhttps://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/get-the-homestead-exemption/", - "name": "Homestead Exemption", - "id": "fdd4405a-1a00-4650-8005-9595f16e3788" - } - ], - "_id": "hEStvPeFmEte58GQTC7Y" -} \ No newline at end of file diff --git a/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json b/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json deleted file mode 100644 index 2052c809..00000000 --- a/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "description": "desc", - "name": "OOPA", - "checks": [ - { - "evaluationUrl": "/api/v1/checks/residence/tax-delinquent", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "taxDelinquent": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "c4a9f028-9617-4c3a-8617-1ffdae6b3ebe", - "parameters": {}, - "checkName": "tax-delinquent", - "checkModule": "residence", - "sourceCheckId": "L-residence-tax-delinquent-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/lives-in-philadelphia-pa", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "livesInPhiladelphiaPa": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "2b330906-4c65-4209-acf8-8b404b1614e7", - "parameters": {}, - "checkName": "lives-in-philadelphia-pa", - "checkModule": "residence", - "sourceCheckId": "L-residence-lives-in-philadelphia-pa-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/owner-occupant", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "ownerOccupant": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "daff8d3a-02e1-42d1-a8cc-173acc24c4fb", - "parameters": {}, - "checkName": "owner-occupant", - "checkModule": "residence", - "sourceCheckId": "L-residence-owner-occupant-0.7.1" - }, - { - "checkVersion": "2.0.0", - "parameterDefinitions": [ - { - "label": "P 1", - "key": "p1", - "type": "string", - "required": true - } - ], - "checkId": "751cc016-6e24-4d0c-b4f3-83b6ca9afe3f", - "inputDefinition": { - "properties": { - "custom": { - "properties": { - "testinput": {} - }, - "type": "object", - "required": [ - "testinput" - ] - } - }, - "type": "object", - "required": [ - "custom" - ] - }, - "checkName": "test", - "checkModule": "testchecks", - "parameters": { - "p1": "blah" - }, - "sourceCheckId": "P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0" - } - ], - "id": "1c09392c-913c-4b2b-9870-a1951534c3fb", - "_id": "1c09392c-913c-4b2b-9870-a1951534c3fb" -} \ No newline at end of file diff --git a/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json b/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json deleted file mode 100644 index 0a50fdcd..00000000 --- a/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "description": "If you own your primary residence, you are eligible for the Homestead Exemption on your Real Estate Tax. The Homestead Exemption reduces the taxable portion of your property’s assessed value.\n\nWith this exemption, the property’s assessed value is reduced by $100,000. Most homeowners will save about $1,399 a year on their Real Estate Tax bill starting in 2025.\n\nmore info:\nhttps://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/get-the-homestead-exemption/", - "name": "Homestead Exemption", - "checks": [ - { - "evaluationUrl": "/api/v1/checks/enrollment/person-not-enrolled-in-benefit", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "enrollments": { - "items": { - "properties": { - "personId": { - "type": "string" - }, - "benefit": { - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - } - }, - "type": "object" - }, - "checkId": "7621168e-e456-4690-a8bc-f081753c9535", - "parameterDefinitions": [ - { - "label": "personId", - "key": "personId", - "type": "string", - "required": false - }, - { - "label": "benefit", - "key": "benefit", - "type": "string", - "required": false - } - ], - "checkName": "person-not-enrolled-in-benefit", - "checkModule": "enrollment", - "parameters": { - "personId": "client", - "benefit": "PhlHomesteadExemption" - }, - "sourceCheckId": "L-enrollment-person-not-enrolled-in-benefit-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/owner-occupant", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "ownerOccupant": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "d6376311-49d2-44d8-bed3-4cb619df68f0", - "parameters": {}, - "checkName": "owner-occupant", - "checkModule": "residence", - "sourceCheckId": "L-residence-owner-occupant-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/lives-in-philadelphia-pa", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "livesInPhiladelphiaPa": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "352e984a-5dfc-46fe-9518-0bf61a0defeb", - "parameters": {}, - "checkName": "lives-in-philadelphia-pa", - "checkModule": "residence", - "sourceCheckId": "L-residence-lives-in-philadelphia-pa-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/no-ten-year-tax-abatement", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "tenYearTaxAbatement": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "658fc9f2-4789-45f6-a2d2-e851e0d85c58", - "parameters": {}, - "checkName": "no-ten-year-tax-abatement", - "checkModule": "residence", - "sourceCheckId": "L-residence-no-ten-year-tax-abatement-0.7.1" - } - ], - "id": "fdd4405a-1a00-4650-8005-9595f16e3788", - "_id": "fdd4405a-1a00-4650-8005-9595f16e3788" -} \ No newline at end of file diff --git a/seed-data/example-screener/manifest.json b/seed-data/example-screener/manifest.json deleted file mode 100644 index 35ad8ed1..00000000 --- a/seed-data/example-screener/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "exportedAt": "2026-04-08T03:42:19.210460+00:00Z", - "source": "firebase-emulators", - "projectId": "demo-bdt-dev", - "storageBucket": "demo-bdt-dev.appspot.com", - "firestoreDocuments": 4, - "storageFiles": 3 -} \ No newline at end of file