From f8a05ba6af8b0aa8446f9594c06342aeef01b9e4 Mon Sep 17 00:00:00 2001 From: Daniel Joo Date: Wed, 25 Feb 2026 16:40:44 -0500 Subject: [PATCH 1/2] applications can be annotated to pull from worker and org --- src/main/Config/AppConfig.java | 2 +- src/main/PDF/PdfControllerV2.java | 23 ++- .../V2Services/GetQuestionsPDFServiceV2.java | 151 +++++++++++++++--- .../PDFV2Test/SetMatchedFieldsUnitTests.java | 87 +++++++++- 4 files changed, 242 insertions(+), 21 deletions(-) diff --git a/src/main/Config/AppConfig.java b/src/main/Config/AppConfig.java index bcf35246..09690068 100644 --- a/src/main/Config/AppConfig.java +++ b/src/main/Config/AppConfig.java @@ -97,7 +97,7 @@ public static Javalin appFactory(DeploymentLevel deploymentLevel) { new MailController(mailDao, fileDao, encryptionController, deploymentLevel); FileBackfillController backfillController = new FileBackfillController(db, fileDao, userDao); PdfControllerV2 pdfControllerV2 = - new PdfControllerV2(fileDao, formDao, activityDao, userDao, encryptionController); + new PdfControllerV2(fileDao, formDao, activityDao, orgDao, userDao, encryptionController); // try { do not recommend this block of code, this will delete and regenerate our encryption // key // System.out.println("generating keyset"); diff --git a/src/main/PDF/PdfControllerV2.java b/src/main/PDF/PdfControllerV2.java index cd40a73e..1e093c90 100644 --- a/src/main/PDF/PdfControllerV2.java +++ b/src/main/PDF/PdfControllerV2.java @@ -6,6 +6,7 @@ import Database.Activity.ActivityDao; import Database.File.FileDao; import Database.Form.FormDao; +import Database.Organization.OrgDao; import Database.User.UserDao; import File.IdCategoryType; import PDF.Services.V2Services.*; @@ -29,6 +30,7 @@ public class PdfControllerV2 { private FileDao fileDao; private ActivityDao activityDao; private UserDao userDao; + private OrgDao orgDao; // Needed for EncryptionController private EncryptionController encryptionController; @@ -37,11 +39,13 @@ public PdfControllerV2( FileDao fileDao, FormDao formDao, ActivityDao activityDao, + OrgDao orgDao, UserDao userDao, EncryptionController encryptionController) { this.fileDao = fileDao; this.formDao = formDao; this.activityDao = activityDao; + this.orgDao = orgDao; this.userDao = userDao; this.encryptionController = encryptionController; } @@ -185,7 +189,7 @@ public PdfControllerV2( userParams.setUserParamsGetApplicationQuestions(ctx, req); fileParams.setFileParamsGetApplicationQuestions(req); GetQuestionsPDFServiceV2 getQuestionsPDFServiceV2 = - new GetQuestionsPDFServiceV2(formDao, userDao, userParams, fileParams); + new GetQuestionsPDFServiceV2(formDao, userDao, orgDao, userParams, fileParams); Message response = getQuestionsPDFServiceV2.executeAndGetResponse(); if (response != PdfMessage.SUCCESS) { ctx.result(response.toResponseString()); @@ -218,6 +222,7 @@ public PdfControllerV2( public static class UserParams { private String username; + private String workerUsername; private String organizationName; private UserType privilegeLevel; @@ -225,6 +230,7 @@ public UserParams() {} public UserParams(String username, String organizationName, UserType privilegeLevel) { this.username = username; + this.workerUsername = username; this.organizationName = organizationName; this.privilegeLevel = privilegeLevel; } @@ -265,6 +271,7 @@ public Message setUserParamsFillAndUploadSignedPDF(Context ctx) { String clientUsernameParameter = Objects.requireNonNull(ctx.formParam("clientUsername")); this.username = clientUsernameParameter.equals("") ? sessionUsername : clientUsernameParameter; + this.workerUsername = sessionUsername; } catch (Exception e) { return PdfMessage.INVALID_PARAMETER; } @@ -275,6 +282,7 @@ public Message setUserParamsFillAndUploadSignedPDF(Context ctx) { public void setUserParamsUploadAnnotatedPDF(Context ctx) { this.username = ctx.sessionAttribute("username"); + this.workerUsername = ctx.sessionAttribute("username"); this.organizationName = ctx.sessionAttribute("orgName"); this.privilegeLevel = UserType.Developer; } @@ -284,6 +292,8 @@ public void setUserParamsGetApplicationQuestions(Context ctx, JSONObject req) { String clientUsernameParameter = req.getString("clientUsername"); this.username = clientUsernameParameter.equals("") ? sessionUsername : clientUsernameParameter; + this.workerUsername = sessionUsername; + this.organizationName = ctx.sessionAttribute("orgName"); this.privilegeLevel = ctx.sessionAttribute("privilegeLevel"); } @@ -328,12 +338,14 @@ public Message setUserParamsFromTargetUser(User targetUser, Context ctx) { } this.username = targetUser.getUsername(); this.organizationName = targetUserOrg; + this.workerUsername = ctx.sessionAttribute("username"); this.privilegeLevel = targetUser.getUserType(); return null; } public Message setUserParamsFromSessionUser(Context ctx) { this.username = ctx.sessionAttribute("username"); + this.workerUsername = this.username; this.organizationName = ctx.sessionAttribute("orgName"); this.privilegeLevel = ctx.sessionAttribute("privilegeLevel"); return null; @@ -348,6 +360,15 @@ public UserParams setUsername(String username) { return this; } + public String getWorkerUsername() { + return workerUsername; + } + + public UserParams setWorkerUsername(String workerUsername) { + this.workerUsername = workerUsername; + return this; + } + public String getOrganizationName() { return organizationName; } diff --git a/src/main/PDF/Services/V2Services/GetQuestionsPDFServiceV2.java b/src/main/PDF/Services/V2Services/GetQuestionsPDFServiceV2.java index 92c900f8..b6fe6527 100644 --- a/src/main/PDF/Services/V2Services/GetQuestionsPDFServiceV2.java +++ b/src/main/PDF/Services/V2Services/GetQuestionsPDFServiceV2.java @@ -3,18 +3,22 @@ import Config.Message; import Config.Service; import Database.Form.FormDao; +import Database.Organization.OrgDao; import Database.User.UserDao; import Form.FieldType; import Form.Form; import Form.FormQuestion; import Form.FormSection; +import Organization.Organization; import PDF.PdfControllerV2.FileParams; import PDF.PdfControllerV2.UserParams; import PDF.PdfMessage; +import User.Address; import User.Services.GetUserInfoService; import User.UserMessage; import User.UserType; import Validation.ValidationUtils; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -30,12 +34,17 @@ public class GetQuestionsPDFServiceV2 implements Service { private FormDao formDao; private UserDao userDao; - private String username; + private OrgDao orgDao; + private String clientUsername; + private String workerUsername; + private String orgName; private UserType privilegeLevel; private String fileId; private JSONObject applicationInformation; private Form form; - private Map flattenedFieldMap; + private Map clientFieldMap; + private Map workerFieldMap; + private Map orgFieldMap; private FormQuestion currentFormQuestion; /** Alias map: common alternative field names -> canonical flattened map keys. */ @@ -58,12 +67,23 @@ public class GetQuestionsPDFServiceV2 implements Service { public GetQuestionsPDFServiceV2( FormDao formDao, UserDao userDao, UserParams userParams, FileParams fileParams) { + this(formDao, userDao, null, userParams, fileParams); + } + + public GetQuestionsPDFServiceV2( + FormDao formDao, UserDao userDao, OrgDao orgDao, UserParams userParams, FileParams fileParams) { this.formDao = formDao; this.userDao = userDao; - this.username = userParams.getUsername(); + this.orgDao = orgDao; + this.clientUsername = userParams.getUsername(); + this.workerUsername = userParams.getWorkerUsername(); + this.orgName = userParams.getOrganizationName(); this.privilegeLevel = userParams.getPrivilegeLevel(); this.fileId = fileParams.getFileId(); this.applicationInformation = new JSONObject(); + this.clientFieldMap = new HashMap<>(); + this.workerFieldMap = new HashMap<>(); + this.orgFieldMap = new HashMap<>(); } public JSONObject getApplicationInformation() { @@ -76,12 +96,34 @@ public Message executeAndGetResponse() { if (getQuestionsConditionsErrorMessage != null) { return getQuestionsConditionsErrorMessage; } - GetUserInfoService getUserInfoService = new GetUserInfoService(userDao, username); + GetUserInfoService getUserInfoService = new GetUserInfoService(userDao, clientUsername); Message getUserInfoServiceResponse = getUserInfoService.executeAndGetResponse(); if (getUserInfoServiceResponse != UserMessage.SUCCESS) { return getUserInfoServiceResponse; } - this.flattenedFieldMap = getUserInfoService.getFlattenedFieldMap(); + this.clientFieldMap = getUserInfoService.getFlattenedFieldMap(); + + if (workerUsername == null || workerUsername.isBlank() || workerUsername.equals(clientUsername)) { + this.workerFieldMap = this.clientFieldMap; + } else { + GetUserInfoService workerInfoService = new GetUserInfoService(userDao, workerUsername); + Message workerInfoResponse = workerInfoService.executeAndGetResponse(); + if (workerInfoResponse == UserMessage.SUCCESS) { + this.workerFieldMap = workerInfoService.getFlattenedFieldMap(); + } else { + log.warn( + "Could not load worker profile '{}' for getQuestions; leaving worker.* unmatched", + workerUsername); + this.workerFieldMap = new HashMap<>(); + } + } + + this.orgFieldMap = new HashMap<>(); + if (orgDao != null && orgName != null && !orgName.isBlank()) { + Optional orgOptional = orgDao.get(orgName); + orgOptional.ifPresent(organization -> this.orgFieldMap = flattenOrganization(organization)); + } + return getQuestions(); } @@ -183,24 +225,48 @@ private static String humanizeFieldName(String fieldName) { * (graceful degradation -- never errors). */ private void matchFieldFromFlattenedMap(FormQuestion fq, String directive) { - if (this.flattenedFieldMap == null) { - log.warn("Flattened field map is null; cannot match directive '{}'", directive); - return; + String source = "client"; + String field = directive; + + int dotIndex = directive.indexOf('.'); + if (dotIndex > 0) { + String prefix = directive.substring(0, dotIndex); + if (prefix.equals("client") || prefix.equals("worker") || prefix.equals("org")) { + source = prefix; + field = directive.substring(dotIndex + 1); + } + } + + Map targetMap; + switch (source) { + case "worker": + targetMap = this.workerFieldMap; + break; + case "org": + targetMap = this.orgFieldMap; + break; + default: + targetMap = this.clientFieldMap; + break; + } + + if (targetMap == null) { + targetMap = Collections.emptyMap(); } // 1. Exact key match - String value = this.flattenedFieldMap.get(directive); + String value = targetMap.get(field); // 2. Alias lookup - if (value == null && FIELD_ALIASES.containsKey(directive)) { - String aliasedKey = FIELD_ALIASES.get(directive); - value = this.flattenedFieldMap.get(aliasedKey); + if (value == null && FIELD_ALIASES.containsKey(field)) { + String aliasedKey = FIELD_ALIASES.get(field); + value = targetMap.get(aliasedKey); } // 3. Case-insensitive full-key match if (value == null) { - for (Map.Entry entry : this.flattenedFieldMap.entrySet()) { - if (entry.getKey().equalsIgnoreCase(directive)) { + for (Map.Entry entry : targetMap.entrySet()) { + if (entry.getKey().equalsIgnoreCase(field)) { value = entry.getValue(); break; } @@ -209,11 +275,11 @@ private void matchFieldFromFlattenedMap(FormQuestion fq, String directive) { // 4. Case-insensitive leaf-key match (e.g. "firstname" matches "optionalInformation.person.firstName") if (value == null) { - for (Map.Entry entry : this.flattenedFieldMap.entrySet()) { + for (Map.Entry entry : targetMap.entrySet()) { String key = entry.getKey(); int lastDot = key.lastIndexOf('.'); String leafKey = lastDot >= 0 ? key.substring(lastDot + 1) : key; - if (leafKey.equalsIgnoreCase(directive)) { + if (leafKey.equalsIgnoreCase(field)) { value = entry.getValue(); break; } @@ -225,14 +291,63 @@ private void matchFieldFromFlattenedMap(FormQuestion fq, String directive) { fq.setDefaultValue(value); } else { log.debug( - "Field directive '{}' not found in user profile for user '{}'; skipping autofill", + "Field directive '{}' not found in source '{}' for client '{}' / worker '{}' / org '{}'; skipping autofill", directive, - this.username); + source, + this.clientUsername, + this.workerUsername, + this.orgName); fq.setMatched(false); fq.setDefaultValue(""); } } + private Map flattenOrganization(Organization org) { + Map map = new HashMap<>(); + if (org.getOrgName() != null) { + map.put("name", org.getOrgName()); + } + if (org.getOrgPhoneNumber() != null) { + map.put("phone", org.getOrgPhoneNumber()); + } + if (org.getOrgEmail() != null) { + map.put("email", org.getOrgEmail()); + } + if (org.getOrgWebsite() != null) { + map.put("website", org.getOrgWebsite()); + } + if (org.getOrgEIN() != null) { + map.put("ein", org.getOrgEIN()); + } + + Address addr = org.getOrgAddress(); + if (addr != null) { + if (addr.getLine1() != null) { + map.put("address.line1", addr.getLine1()); + map.put("address", addr.getLine1()); + } + if (addr.getLine2() != null) { + map.put("address.line2", addr.getLine2()); + } + if (addr.getCity() != null) { + map.put("address.city", addr.getCity()); + map.put("city", addr.getCity()); + } + if (addr.getState() != null) { + map.put("address.state", addr.getState()); + map.put("state", addr.getState()); + } + if (addr.getZip() != null) { + map.put("address.zip", addr.getZip()); + map.put("zip", addr.getZip()); + } + if (addr.getCounty() != null) { + map.put("address.county", addr.getCounty()); + } + } + return map; + } + public Message getQuestions() { FormSection formBody = form.getBody(); applicationInformation.put("title", formBody.getTitle()); diff --git a/src/test/PDFTest/PDFV2Test/SetMatchedFieldsUnitTests.java b/src/test/PDFTest/PDFV2Test/SetMatchedFieldsUnitTests.java index ff7d0a8e..aa834e61 100644 --- a/src/test/PDFTest/PDFV2Test/SetMatchedFieldsUnitTests.java +++ b/src/test/PDFTest/PDFV2Test/SetMatchedFieldsUnitTests.java @@ -10,6 +10,8 @@ import Database.File.FileDaoFactory; import Database.Form.FormDao; import Database.Form.FormDaoFactory; +import Database.Organization.OrgDao; +import Database.Organization.OrgDaoFactory; import Database.User.UserDao; import Database.User.UserDaoFactory; import Form.FieldType; @@ -41,6 +43,7 @@ public class SetMatchedFieldsUnitTests { private FileDao fileDao; private FormDao formDao; + private OrgDao orgDao; private UserDao userDao; private MongoDatabase db; private EncryptionController encryptionController; @@ -60,6 +63,7 @@ public void initialize() throws InterruptedException { Thread.sleep(1000); this.fileDao = FileDaoFactory.create(DeploymentLevel.TEST); this.formDao = FormDaoFactory.create(DeploymentLevel.TEST); + this.orgDao = OrgDaoFactory.create(DeploymentLevel.TEST); this.userDao = UserDaoFactory.create(DeploymentLevel.TEST); this.db = MongoConfig.getDatabase(DeploymentLevel.TEST); @@ -94,9 +98,38 @@ public void initialize() throws InterruptedException { new Address("456 Oak Ave", null, "Pittsburgh", "PA", "15213", null)); userDao.save(clientUser); + User workerUser = + EntityFactory.createUser() + .withUsername("worker1") + .withFirstName("Wendy") + .withLastName("Worker") + .withEmail("worker@example.com") + .withPhoneNumber("4445556666") + .withAddress("987 Worker St") + .withCity("Camden") + .withState("NJ") + .withZipcode("08102") + .withUserType(UserType.Worker) + .withOrgName("org2") + .build(); + userDao.save(workerUser); + + EntityFactory.createOrganization() + .withOrgName("org2") + .withAddress("311 Broad Street") + .withCity("Philadelphia") + .withState("PA") + .withZipcode("19107") + .withPhoneNumber("1234567890") + .withEmail("org@example.com") + .withWebsite("https://www.example.org") + .withEIN("123456789") + .buildAndPersist(orgDao); + this.clientUserParams = new UserParams() .setUsername("matchclient") + .setWorkerUsername("worker1") .setOrganizationName("org2") .setPrivilegeLevel(UserType.Client); @@ -121,7 +154,8 @@ public void initialize() throws InterruptedException { FileParams getQuestionsFileParams = new FileParams().setFileId(uploadedFileId.toString()); this.service = - new GetQuestionsPDFServiceV2(formDao, userDao, clientUserParams, getQuestionsFileParams); + new GetQuestionsPDFServiceV2( + formDao, userDao, orgDao, clientUserParams, getQuestionsFileParams); Message initResponse = service.executeAndGetResponse(); assertEquals(PdfMessage.SUCCESS, initResponse); } @@ -130,6 +164,7 @@ public void initialize() throws InterruptedException { public void reset() { fileDao.clear(); formDao.clear(); + orgDao.clear(); userDao.clear(); } @@ -251,6 +286,56 @@ public void nestedCurrentNameFirst() { assertEquals("John", fq.getDefaultValue()); } + // ===================================================================== + // Multi-source directives: org.* and worker.* + // ===================================================================== + + @Test + public void orgPrefixMatchName() { + FormQuestion fq = makeQuestion("Agency Name:org.name"); + Message result = service.setMatchedFields(fq); + assertNull(result); + assertTrue(fq.isMatched()); + assertEquals("org2", fq.getDefaultValue()); + } + + @Test + public void orgPrefixMatchAddressAlias() { + FormQuestion fq = makeQuestion("Agency Address:org.address"); + Message result = service.setMatchedFields(fq); + assertNull(result); + assertTrue(fq.isMatched()); + assertEquals("311 Broad Street", fq.getDefaultValue()); + } + + @Test + public void workerPrefixMatchCanonicalField() { + FormQuestion fq = makeQuestion("Completed By:worker.currentName.first"); + Message result = service.setMatchedFields(fq); + assertNull(result); + assertTrue(fq.isMatched()); + assertEquals("Wendy", fq.getDefaultValue()); + } + + @Test + public void mixedSourceFormUsesClientOrgAndWorkerMaps() { + FormQuestion clientField = makeQuestion("Client First Name:currentName.first"); + FormQuestion orgField = makeQuestion("Agency City:org.address.city"); + FormQuestion workerField = makeQuestion("Worker Email:worker.email"); + + assertNull(service.setMatchedFields(clientField)); + assertNull(service.setMatchedFields(orgField)); + assertNull(service.setMatchedFields(workerField)); + + assertTrue(clientField.isMatched()); + assertTrue(orgField.isMatched()); + assertTrue(workerField.isMatched()); + + assertEquals("John", clientField.getDefaultValue()); + assertEquals("Philadelphia", orgField.getDefaultValue()); + assertEquals("worker@example.com", workerField.getDefaultValue()); + } + // ===================================================================== // Unmatched field -- graceful degradation (no error) // ===================================================================== From f76fa51c9358886c5dfa88776e862df2d0298d88 Mon Sep 17 00:00:00 2001 From: Daniel Joo Date: Wed, 25 Feb 2026 16:51:57 -0500 Subject: [PATCH 2/2] form upload now updates profile --- src/main/PDF/PdfControllerV2.java | 2 +- .../V2Services/UploadSignedPDFServiceV2.java | 36 ++ .../UpdateProfileFromFormService.java | 331 ++++++++++++++++++ .../UpdateProfileFromFormServiceTest.java | 132 +++++++ 4 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 src/main/User/Services/UpdateProfileFromFormService.java create mode 100644 src/test/UserTest/UpdateProfileFromFormServiceTest.java diff --git a/src/main/PDF/PdfControllerV2.java b/src/main/PDF/PdfControllerV2.java index 1e093c90..21f6561b 100644 --- a/src/main/PDF/PdfControllerV2.java +++ b/src/main/PDF/PdfControllerV2.java @@ -176,7 +176,7 @@ public PdfControllerV2( } UploadSignedPDFServiceV2 uploadSignedPDFServiceV2 = new UploadSignedPDFServiceV2( - fileDao, formDao, activityDao, userParams, fileParams, encryptionController); + fileDao, formDao, activityDao, userDao, userParams, fileParams, encryptionController); ctx.result(uploadSignedPDFServiceV2.executeAndGetResponse().toResponseString()); }; diff --git a/src/main/PDF/Services/V2Services/UploadSignedPDFServiceV2.java b/src/main/PDF/Services/V2Services/UploadSignedPDFServiceV2.java index b9e84c96..28bbb11f 100644 --- a/src/main/PDF/Services/V2Services/UploadSignedPDFServiceV2.java +++ b/src/main/PDF/Services/V2Services/UploadSignedPDFServiceV2.java @@ -6,21 +6,27 @@ import Database.Activity.ActivityDao; import Database.File.FileDao; import Database.Form.FormDao; +import Database.User.UserDao; import File.File; import Form.Form; import PDF.PdfControllerV2.FileParams; import PDF.PdfControllerV2.UserParams; import PDF.PdfMessage; import Security.EncryptionController; +import User.Services.UpdateProfileFromFormService; +import User.UserMessage; import User.UserType; import Validation.ValidationUtils; import java.io.InputStream; +import lombok.extern.slf4j.Slf4j; import org.bson.types.ObjectId; import org.json.JSONObject; +@Slf4j public class UploadSignedPDFServiceV2 implements Service { private FileDao fileDao; private FormDao formDao; + private UserDao userDao; ActivityDao activityDao; private String username; private String organizationName; @@ -41,9 +47,21 @@ public UploadSignedPDFServiceV2( UserParams userParams, FileParams fileParams, EncryptionController encryptionController) { + this(fileDao, formDao, activityDao, null, userParams, fileParams, encryptionController); + } + + public UploadSignedPDFServiceV2( + FileDao fileDao, + FormDao formDao, + ActivityDao activityDao, + UserDao userDao, + UserParams userParams, + FileParams fileParams, + EncryptionController encryptionController) { this.fileDao = fileDao; this.formDao = formDao; this.activityDao = activityDao; + this.userDao = userDao; this.userParams = userParams; this.username = userParams.getUsername(); this.organizationName = userParams.getOrganizationName(); @@ -98,10 +116,28 @@ public Message checkUploadConditions() { public Message upload() { this.fileDao.save(this.filledFile); this.formDao.save(this.filledForm); + updateClientProfileFromFormAnswers(); recordSubmitApplicationActivity(); return PdfMessage.SUCCESS; } + private void updateClientProfileFromFormAnswers() { + if (this.userDao == null) { + return; + } + UpdateProfileFromFormService updateProfileFromFormService = + new UpdateProfileFromFormService(this.userDao, this.username, this.formAnswers); + Message updateResponse = updateProfileFromFormService.executeAndGetResponse(); + if (updateResponse != UserMessage.SUCCESS) { + // Do not block application submission on profile backfill errors. + // Profile update is a best-effort post-submit enhancement. + log.warn( + "UpdateProfileFromFormService failed for user '{}': {}", + this.username, + updateResponse); + } + } + private void recordSubmitApplicationActivity() { SubmitApplicationActivity activity = new SubmitApplicationActivity(username, username, fileId, filledFile.getFilename()); diff --git a/src/main/User/Services/UpdateProfileFromFormService.java b/src/main/User/Services/UpdateProfileFromFormService.java new file mode 100644 index 00000000..b534edc5 --- /dev/null +++ b/src/main/User/Services/UpdateProfileFromFormService.java @@ -0,0 +1,331 @@ +package User.Services; + +import Config.Message; +import Config.Service; +import Database.User.UserDao; +import User.Address; +import User.Name; +import User.User; +import User.UserMessage; +import Validation.ValidationUtils; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONObject; + +@Slf4j +public class UpdateProfileFromFormService implements Service { + private static final Map FIELD_ALIASES = new HashMap<>(); + private static final Pattern NAME_HISTORY_PATTERN = + Pattern.compile("^nameHistory\\.(\\d+)\\.(first|middle|last|suffix|maiden)$"); + + static { + FIELD_ALIASES.put("firstName", "currentName.first"); + FIELD_ALIASES.put("lastName", "currentName.last"); + FIELD_ALIASES.put("middleName", "currentName.middle"); + FIELD_ALIASES.put("phone", "phoneBook.0.phoneNumber"); + FIELD_ALIASES.put("phoneNumber", "phoneBook.0.phoneNumber"); + FIELD_ALIASES.put("address", "personalAddress.line1"); + FIELD_ALIASES.put("streetAddress", "personalAddress.line1"); + FIELD_ALIASES.put("city", "personalAddress.city"); + FIELD_ALIASES.put("state", "personalAddress.state"); + FIELD_ALIASES.put("zipcode", "personalAddress.zip"); + FIELD_ALIASES.put("genderAssignedAtBirth", "sex"); + FIELD_ALIASES.put("emailAddress", "email"); + } + + private final UserDao userDao; + private final String username; + private final JSONObject formAnswers; + + public UpdateProfileFromFormService(UserDao userDao, String username, JSONObject formAnswers) { + this.userDao = userDao; + this.username = username; + this.formAnswers = formAnswers; + } + + @Override + public Message executeAndGetResponse() { + if (userDao == null || username == null || formAnswers == null) { + return UserMessage.INVALID_PARAMETER; + } + + Optional userOptional = userDao.get(username); + if (userOptional.isEmpty()) { + return UserMessage.USER_NOT_FOUND; + } + + User user = userOptional.get(); + boolean changed = false; + + for (String fieldName : formAnswers.keySet()) { + String answer = getNonEmptyAnswer(formAnswers.opt(fieldName)); + if (answer == null) { + continue; + } + + String directive = normalizeDirective(fieldName); + if (directive == null) { + continue; + } + + changed = applyDirective(user, directive, answer) || changed; + } + + if (changed) { + userDao.update(user); + } + return UserMessage.SUCCESS; + } + + private String getNonEmptyAnswer(Object rawValue) { + if (rawValue == null || JSONObject.NULL.equals(rawValue)) { + return null; + } + String answer = String.valueOf(rawValue).trim(); + if (answer.isEmpty() || answer.equalsIgnoreCase("null")) { + return null; + } + return answer; + } + + private String normalizeDirective(String fieldName) { + String directive = fieldName; + int lastColon = fieldName.lastIndexOf(':'); + if (lastColon >= 0 && lastColon + 1 < fieldName.length()) { + directive = fieldName.substring(lastColon + 1); + } + + if (directive.isEmpty() + || directive.equals("currentDate") + || directive.equals("anyDate") + || directive.equals("signature") + || directive.startsWith("+") + || directive.startsWith("-")) { + return null; + } + + if (directive.startsWith("org.") || directive.startsWith("worker.")) { + return null; + } + if (directive.startsWith("client.")) { + directive = directive.substring("client.".length()); + } + + return FIELD_ALIASES.getOrDefault(directive, directive); + } + + private boolean applyDirective(User user, String directive, String value) { + switch (directive) { + case "currentName.first": + case "currentName.middle": + case "currentName.last": + case "currentName.suffix": + case "currentName.maiden": + return applyNameField(ensureCurrentName(user), directive.substring("currentName.".length()), value); + case "motherName.first": + case "motherName.middle": + case "motherName.last": + case "motherName.suffix": + case "motherName.maiden": + return applyNameField(ensureMotherName(user), directive.substring("motherName.".length()), value); + case "fatherName.first": + case "fatherName.middle": + case "fatherName.last": + case "fatherName.suffix": + case "fatherName.maiden": + return applyNameField(ensureFatherName(user), directive.substring("fatherName.".length()), value); + case "birthDate": + return applyBirthDate(user, value); + case "sex": + user.setSex(value); + return true; + case "email": + if (ValidationUtils.isValidEmail(value)) { + user.setEmail(value.toLowerCase()); + return true; + } + return false; + case "phoneBook.0.phoneNumber": + if (ValidationUtils.isValidPhoneNumber(value)) { + user.setPhone(value); + return true; + } + return false; + case "personalAddress.line1": + case "personalAddress.line2": + case "personalAddress.city": + case "personalAddress.state": + case "personalAddress.zip": + case "personalAddress.county": + return applyAddressField( + ensurePersonalAddress(user), + directive.substring("personalAddress.".length()), + value); + case "mailAddress.line1": + case "mailAddress.line2": + case "mailAddress.city": + case "mailAddress.state": + case "mailAddress.zip": + case "mailAddress.county": + return applyAddressField( + ensureMailAddress(user), + directive.substring("mailAddress.".length()), + value); + default: + return applyNameHistoryField(user, directive, value); + } + } + + private boolean applyBirthDate(User user, String value) { + String normalized = normalizeBirthDate(value); + if (normalized == null || !ValidationUtils.isValidBirthDate(normalized)) { + return false; + } + user.setBirthDate(normalized); + return true; + } + + private String normalizeBirthDate(String input) { + List formatters = + List.of( + DateTimeFormatter.ofPattern("MM-dd-uuuu").withResolverStyle(ResolverStyle.STRICT), + DateTimeFormatter.ofPattern("MM/dd/uuuu").withResolverStyle(ResolverStyle.STRICT), + DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT)); + for (DateTimeFormatter formatter : formatters) { + try { + LocalDate parsed = LocalDate.parse(input, formatter); + return parsed.format(DateTimeFormatter.ofPattern("MM-dd-uuuu")); + } catch (DateTimeParseException ignored) { + // Try next format. + } + } + return input; + } + + private boolean applyNameField(Name name, String part, String value) { + switch (part) { + case "first": + name.setFirst(value); + return true; + case "middle": + name.setMiddle(value); + return true; + case "last": + name.setLast(value); + return true; + case "suffix": + name.setSuffix(value); + return true; + case "maiden": + name.setMaiden(value); + return true; + default: + return false; + } + } + + private boolean applyAddressField(Address address, String part, String value) { + switch (part) { + case "line1": + if (!ValidationUtils.isValidAddress(value)) { + return false; + } + address.setLine1(value); + return true; + case "line2": + address.setLine2(value); + return true; + case "city": + if (!ValidationUtils.isValidCity(value)) { + return false; + } + address.setCity(value); + return true; + case "state": + if (!ValidationUtils.isValidUSState(value)) { + return false; + } + address.setState(value); + return true; + case "zip": + if (!ValidationUtils.isValidZipCode(value)) { + return false; + } + address.setZip(value); + return true; + case "county": + address.setCounty(value); + return true; + default: + return false; + } + } + + private boolean applyNameHistoryField(User user, String directive, String value) { + Matcher matcher = NAME_HISTORY_PATTERN.matcher(directive); + if (!matcher.matches()) { + return false; + } + int index = Integer.parseInt(matcher.group(1)); + String part = matcher.group(2); + Name historyName = ensureNameHistoryEntry(user, index); + return applyNameField(historyName, part, value); + } + + private Name ensureCurrentName(User user) { + if (user.getCurrentName() == null) { + user.setCurrentName(new Name()); + } + return user.getCurrentName(); + } + + private Name ensureMotherName(User user) { + if (user.getMotherName() == null) { + user.setMotherName(new Name()); + } + return user.getMotherName(); + } + + private Name ensureFatherName(User user) { + if (user.getFatherName() == null) { + user.setFatherName(new Name()); + } + return user.getFatherName(); + } + + private Address ensurePersonalAddress(User user) { + if (user.getPersonalAddress() == null) { + user.setPersonalAddress(new Address()); + } + return user.getPersonalAddress(); + } + + private Address ensureMailAddress(User user) { + if (user.getMailAddress() == null) { + user.setMailAddress(new Address()); + } + return user.getMailAddress(); + } + + private Name ensureNameHistoryEntry(User user, int index) { + List nameHistory = user.getNameHistory(); + if (nameHistory == null) { + nameHistory = new ArrayList<>(); + user.setNameHistory(nameHistory); + } + while (nameHistory.size() <= index) { + nameHistory.add(new Name()); + } + return nameHistory.get(index); + } +} diff --git a/src/test/UserTest/UpdateProfileFromFormServiceTest.java b/src/test/UserTest/UpdateProfileFromFormServiceTest.java new file mode 100644 index 00000000..b1116a6a --- /dev/null +++ b/src/test/UserTest/UpdateProfileFromFormServiceTest.java @@ -0,0 +1,132 @@ +package UserTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import Config.DeploymentLevel; +import Config.Message; +import Database.User.UserDao; +import Database.User.UserDaoFactory; +import TestUtils.EntityFactory; +import User.Address; +import User.Services.UpdateProfileFromFormService; +import User.User; +import User.UserMessage; +import User.UserType; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Test; + +public class UpdateProfileFromFormServiceTest { + private final UserDao userDao = UserDaoFactory.create(DeploymentLevel.IN_MEMORY); + + @After + public void reset() { + userDao.clear(); + } + + @Test + public void updatesClientProfileFromAnnotatedFormAnswers() { + EntityFactory.createUser() + .withUsername("client1") + .withFirstName("Old") + .withLastName("Name") + .withEmail("old@example.com") + .withPhoneNumber("1112223333") + .withAddress("100 Old St") + .withCity("Old City") + .withState("PA") + .withZipcode("19104") + .withUserType(UserType.Client) + .buildAndPersist(userDao); + + JSONObject formAnswers = new JSONObject(); + formAnswers.put("First Name:firstName", "Jane"); + formAnswers.put("Last Name:currentName.last", "Doe"); + formAnswers.put("DOB:birthDate", "01/25/1990"); + formAnswers.put("Phone:phone", "2155551212"); + formAnswers.put("Street:personalAddress.line1", "456 New St"); + formAnswers.put("City:city", "Philadelphia"); + formAnswers.put("State:state", "PA"); + formAnswers.put("Zip:zipcode", "19107"); + + UpdateProfileFromFormService service = + new UpdateProfileFromFormService(userDao, "client1", formAnswers); + Message response = service.executeAndGetResponse(); + + assertEquals(UserMessage.SUCCESS, response); + User updated = userDao.get("client1").orElse(null); + assertNotNull(updated); + assertEquals("Jane", updated.getCurrentName().getFirst()); + assertEquals("Doe", updated.getCurrentName().getLast()); + assertEquals("01-25-1990", updated.getBirthDate()); + assertEquals("2155551212", updated.getPhone()); + assertEquals("456 New St", updated.getPersonalAddress().getLine1()); + assertEquals("Philadelphia", updated.getPersonalAddress().getCity()); + assertEquals("PA", updated.getPersonalAddress().getState()); + assertEquals("19107", updated.getPersonalAddress().getZip()); + } + + @Test + public void ignoresWorkerOrgAndEmptyValues() { + EntityFactory.createUser() + .withUsername("client1") + .withFirstName("Existing") + .withEmail("existing@example.com") + .withAddress("100 Keep St") + .withCity("Keep City") + .withState("PA") + .withZipcode("19104") + .withUserType(UserType.Client) + .buildAndPersist(userDao); + + JSONObject formAnswers = new JSONObject(); + formAnswers.put("Worker Email:worker.email", "worker@example.com"); + formAnswers.put("Agency Name:org.name", "My Org"); + formAnswers.put("Email:email", ""); + formAnswers.put("City:personalAddress.city", ""); // Empty should preserve existing. + formAnswers.put("Client First Name:client.currentName.first", "Updated"); + + UpdateProfileFromFormService service = + new UpdateProfileFromFormService(userDao, "client1", formAnswers); + Message response = service.executeAndGetResponse(); + + assertEquals(UserMessage.SUCCESS, response); + User updated = userDao.get("client1").orElse(null); + assertNotNull(updated); + assertEquals("Updated", updated.getCurrentName().getFirst()); + assertEquals("existing@example.com", updated.getEmail()); + assertEquals("Keep City", updated.getPersonalAddress().getCity()); + } + + @Test + public void updatesNestedNameAndMailingFields() { + User seeded = + EntityFactory.createUser() + .withUsername("client1") + .withFirstName("Alpha") + .withLastName("Beta") + .withUserType(UserType.Client) + .buildAndPersist(userDao); + seeded.setMailAddress(new Address("10 Old Mail", null, "Oldtown", "PA", "19000", null)); + userDao.update(seeded); + + JSONObject formAnswers = new JSONObject(); + formAnswers.put("Mother Maiden:motherName.maiden", "Smith"); + formAnswers.put("Father First:fatherName.first", "Robert"); + formAnswers.put("Mail City:mailAddress.city", "Newtown"); + formAnswers.put("Former Name:nameHistory.0.first", "Legacy"); + + UpdateProfileFromFormService service = + new UpdateProfileFromFormService(userDao, "client1", formAnswers); + Message response = service.executeAndGetResponse(); + + assertEquals(UserMessage.SUCCESS, response); + User updated = userDao.get("client1").orElse(null); + assertNotNull(updated); + assertEquals("Smith", updated.getMotherName().getMaiden()); + assertEquals("Robert", updated.getFatherName().getFirst()); + assertEquals("Newtown", updated.getMailAddress().getCity()); + assertEquals("Legacy", updated.getNameHistory().get(0).getFirst()); + } +}