-
Notifications
You must be signed in to change notification settings - Fork 0
DC-152 - Update Mailing Address flow #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
36a599f
Add backend address update endpoint with stubbed handler and service …
eb688a1
Add address feature scaffold with types, mutation hook, flow context,…
e0c3d23
Add address form component with state-specific defaults, validation, …
b1dddf4
Add dashboard success alerts for address update and card request flows
29c2b4f
Add replacement card prompt with address display, SNAP/TANF callout, …
1ac435a
Add card selection page with children checkboxes, CO card numbers, an…
f3a38e6
Add co-loaded info stub page and card-flow entry TODO comments
a0fb411
Register address handler and validation service in DI, add integratio…
0c6a897
Merge branch 'main' into feat/DC-152-update-mailing-address
2c8a857
fix dashboard alert disappearing after URL param cleanup
38d23ec
Merge branch 'main' into feat/DC-152-update-mailing-address
6671cc4
Address walkthrough findings: flow guard loading state, program name …
7ce64fc
Pre-populate address form with addressOnFile from household data
e9c3cdd
Use stub alert copy to signal pending state system integration
02c327e
add state abbreviation map, stub alert copy, and shared MSW handler
96bfc17
Wire up address update flow with form pre-population, card selection…
810c151
Wire up CO visual walkthrough fixes: button colors, heading styles, l…
3e21d81
Merge branch 'main' into feat/DC-152-update-mailing-address
f757a05
Address update: harden validation, fix ZIP inputMode, satisfy CodeQL…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
src/SEBT.Portal.Api/Models/Household/UpdateAddressRequest.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| using System.ComponentModel.DataAnnotations; | ||
|
|
||
| namespace SEBT.Portal.Api.Models.Household; | ||
|
|
||
| /// <summary> | ||
| /// Request model for updating a household's mailing address. | ||
| /// </summary> | ||
| public record UpdateAddressRequest | ||
| { | ||
| /// <summary>Street address line 1 (e.g., "123 Main St NW").</summary> | ||
| [Required(ErrorMessage = "Street address is required.")] | ||
| public required string StreetAddress1 { get; init; } | ||
|
|
||
| /// <summary>Street address line 2 (e.g., apartment, suite). Optional.</summary> | ||
| public string? StreetAddress2 { get; init; } | ||
|
|
||
| /// <summary>City name.</summary> | ||
| [Required(ErrorMessage = "City is required.")] | ||
| public required string City { get; init; } | ||
|
|
||
| /// <summary>State or territory name.</summary> | ||
| [Required(ErrorMessage = "State is required.")] | ||
| public required string State { get; init; } | ||
|
|
||
| /// <summary>5- or 9-digit ZIP code (e.g., "20001" or "20001-1234").</summary> | ||
| [Required(ErrorMessage = "Postal code is required.")] | ||
| [RegularExpression(@"^\d{5}(-\d{4})?$", ErrorMessage = "Postal code must be a valid 5- or 9-digit ZIP code.")] | ||
| public required string PostalCode { get; init; } | ||
| } |
34 changes: 34 additions & 0 deletions
34
src/SEBT.Portal.Core/Services/IAddressValidationService.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| using SEBT.Portal.Core.Models.Household; | ||
|
|
||
| namespace SEBT.Portal.Core.Services; | ||
|
|
||
| /// <summary> | ||
| /// Validates a mailing address against an external service (e.g., Smarty). | ||
| /// Implementations may autocomplete, suggest alternatives, or reject invalid addresses. | ||
| /// </summary> | ||
| public interface IAddressValidationService | ||
| { | ||
| /// <summary> | ||
| /// Validates the given address and returns a result indicating whether the address is valid, | ||
| /// invalid, or has a suggested alternative. | ||
| /// </summary> | ||
| Task<AddressValidationResult> ValidateAsync(Address address, CancellationToken cancellationToken = default); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Result of an address validation call. | ||
| /// </summary> | ||
| /// <param name="IsValid">Whether the address passed validation.</param> | ||
| /// <param name="SuggestedAddress">An alternative address suggested by the validation service, if any.</param> | ||
| /// <param name="ErrorMessage">A user-facing error message if validation failed.</param> | ||
| public record AddressValidationResult( | ||
| bool IsValid, | ||
| Address? SuggestedAddress = null, | ||
| string? ErrorMessage = null) | ||
| { | ||
| public static AddressValidationResult Valid() => new(true); | ||
|
|
||
| public static AddressValidationResult Invalid(string errorMessage) => new(false, ErrorMessage: errorMessage); | ||
|
|
||
| public static AddressValidationResult Suggestion(Address suggested) => new(false, SuggestedAddress: suggested); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
src/SEBT.Portal.Infrastructure/Services/AlwaysValidAddressValidator.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| using SEBT.Portal.Core.Models.Household; | ||
| using SEBT.Portal.Core.Services; | ||
|
|
||
| namespace SEBT.Portal.Infrastructure.Services; | ||
|
|
||
| /// <summary> | ||
| /// Stub address validation service that always returns valid. | ||
| /// Replace with a real Smarty integration when DC-160 is implemented. | ||
| /// </summary> | ||
| public class AlwaysValidAddressValidator : IAddressValidationService | ||
| { | ||
| public Task<AddressValidationResult> ValidateAsync(Address address, CancellationToken cancellationToken = default) | ||
| { | ||
| return Task.FromResult(AddressValidationResult.Valid()); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
src/SEBT.Portal.UseCases/Household/UpdateAddress/UpdateAddressCommand.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| using System.ComponentModel.DataAnnotations; | ||
| using System.Security.Claims; | ||
| using SEBT.Portal.Kernel; | ||
|
|
||
| namespace SEBT.Portal.UseCases.Household; | ||
|
|
||
| /// <summary> | ||
| /// Command to update the mailing address for an authenticated user's household. | ||
| /// </summary> | ||
| public class UpdateAddressCommand : ICommand | ||
| { | ||
| /// <summary> | ||
| /// The authenticated user's claims principal, used to resolve household identity. | ||
| /// </summary> | ||
| [Required] | ||
| public required ClaimsPrincipal User { get; init; } | ||
|
|
||
| [Required(ErrorMessage = "Street address is required.")] | ||
| [RegularExpression(@"\S.*", ErrorMessage = "Street address cannot be whitespace only.")] | ||
| public required string StreetAddress1 { get; init; } | ||
|
|
||
| public string? StreetAddress2 { get; init; } | ||
|
|
||
| [Required(ErrorMessage = "City is required.")] | ||
| [RegularExpression(@"\S.*", ErrorMessage = "City cannot be whitespace only.")] | ||
| public required string City { get; init; } | ||
|
|
||
| [Required(ErrorMessage = "State is required.")] | ||
| [RegularExpression(@"\S.*", ErrorMessage = "State cannot be whitespace only.")] | ||
| public required string State { get; init; } | ||
|
|
||
| [Required(ErrorMessage = "Postal code is required.")] | ||
| [RegularExpression(@"^\d{5}(-\d{4})?$", ErrorMessage = "Postal code must be a valid 5- or 9-digit ZIP code.")] | ||
| public required string PostalCode { get; init; } | ||
|
necampanini marked this conversation as resolved.
|
||
| } | ||
54 changes: 54 additions & 0 deletions
54
src/SEBT.Portal.UseCases/Household/UpdateAddress/UpdateAddressCommandHandler.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| using Microsoft.Extensions.Logging; | ||
| using SEBT.Portal.Core.Services; | ||
| using SEBT.Portal.Kernel; | ||
| using SEBT.Portal.Kernel.Results; | ||
|
|
||
| namespace SEBT.Portal.UseCases.Household; | ||
|
|
||
| /// <summary> | ||
| /// Handles mailing address updates for an authenticated user's household. | ||
| /// Validates input, resolves household identity, and returns success. | ||
| /// State connector call is stubbed — actual address persistence is a future integration. | ||
| /// </summary> | ||
| public class UpdateAddressCommandHandler( | ||
| IValidator<UpdateAddressCommand> validator, | ||
| IHouseholdIdentifierResolver resolver, | ||
| ILogger<UpdateAddressCommandHandler> logger) | ||
| : ICommandHandler<UpdateAddressCommand> | ||
| { | ||
| public async Task<Result> Handle(UpdateAddressCommand command, CancellationToken cancellationToken = default) | ||
| { | ||
| var validationResult = await validator.Validate(command, cancellationToken); | ||
| if (validationResult is ValidationFailedResult validationFailed) | ||
| { | ||
| logger.LogWarning("Address update validation failed"); | ||
| return Result.ValidationFailed(validationFailed.Errors); | ||
| } | ||
|
|
||
| var identifier = await resolver.ResolveAsync(command.User, cancellationToken); | ||
| if (identifier == null) | ||
| { | ||
| logger.LogWarning("Address update attempted but no household identifier could be resolved from claims"); | ||
| return Result.Unauthorized("Unable to identify user from token."); | ||
| } | ||
|
|
||
| // Never log raw address fields — PII policy. | ||
| // Extract enum name to a local to break CodeQL taint chain (identifier is tainted via .Value). | ||
| var identifierKind = identifier.Type.ToString(); | ||
|
|
||
| logger.LogInformation( | ||
| "Address update received for household identifier kind {Kind}", | ||
| identifierKind); | ||
|
|
||
| // TODO: Call state connector to persist address update. | ||
| // This is stubbed — the handler returns success without writing to the state system. | ||
| // When DC-160 / state connector work lands, wire up IAddressValidationService and | ||
| // the state connector write method here. | ||
|
|
||
| logger.LogInformation( | ||
| "Address update completed for household identifier kind {Kind}", | ||
| identifierKind); | ||
|
|
||
| return Result.Success(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { readFileSync } from 'node:fs' | ||
| import { resolve } from 'node:path' | ||
| import { describe, expect, it } from 'vitest' | ||
|
|
||
| import { primaryFont } from './fonts' | ||
|
|
||
| /** | ||
| * Contract test: the SCSS font override must reference the same CSS variable | ||
| * that the font generator produces. A mismatch causes a FOUT (flash of unstyled | ||
| * text) because the SCSS !important override resolves to an undefined variable, | ||
| * falling back to the browser's default serif font. | ||
| * | ||
| * This caught a real bug: generate-fonts.js was updated in DC-143 to produce | ||
| * --font-primary, but _uswds-theme-custom-styles.scss still referenced | ||
| * --font-urbanist. | ||
| */ | ||
| describe('font variable contract', () => { | ||
| const scssPath = resolve(__dirname, 'sass/_uswds-theme-custom-styles.scss') | ||
| const scssContent = readFileSync(scssPath, 'utf-8') | ||
|
|
||
| it('primaryFont exports a CSS variable name', () => { | ||
| expect(primaryFont.variable).toBeDefined() | ||
| expect(primaryFont.variable).toMatch(/^--font-/) | ||
| }) | ||
|
|
||
| it('SCSS font override references the same variable as primaryFont', () => { | ||
| const cssVariable = primaryFont.variable | ||
| expect(scssContent).toContain(`var(${cssVariable})`) | ||
| }) | ||
|
|
||
| it('SCSS font override does not reference stale font variable names', () => { | ||
| // Guard against the specific bug: old variable names left behind after | ||
| // the font generator is updated to produce a different variable. | ||
| const fontVarPattern = /var\(--font-([^)]+)\)/g | ||
| const referencedVars = [...scssContent.matchAll(fontVarPattern)].map((m) => `--font-${m[1]}`) | ||
|
|
||
| for (const varName of referencedVars) { | ||
| expect(varName).toBe(primaryFont.variable) | ||
| } | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.