Skip to content

Commit c2b08f9

Browse files
authored
Merge branch 'main' into feat/DC-150-step-up
2 parents c03f211 + c6095e2 commit c2b08f9

25 files changed

Lines changed: 1277 additions & 31 deletions

src/SEBT.Portal.Api/Controllers/Household/HouseholdController.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,15 @@ public async Task<IActionResult> GetHouseholdData(
5858
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
5959
/// <returns>No content on success; otherwise, BadRequest or Unauthorized.</returns>
6060
/// <response code="204">Address updated successfully.</response>
61-
/// <response code="400">Validation failed (missing fields or invalid format).</response>
61+
/// <response code="400">Validation failed (missing fields, invalid format, or address could not be verified).</response>
6262
/// <response code="403">User is not authorized or no household identifier could be resolved from token.</response>
63+
/// <response code="502">Address verification provider error or timeout.</response>
6364
[HttpPut("address")]
6465
[Authorize]
6566
[ProducesResponseType(StatusCodes.Status204NoContent)]
6667
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
6768
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
69+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
6870
public async Task<IActionResult> UpdateAddress(
6971
[FromBody] UpdateAddressRequest request,
7072
[FromServices] ICommandHandler<UpdateAddressCommand> commandHandler,

src/SEBT.Portal.Api/appsettings.co.example.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
{
2+
"AddressValidationPolicy": {
3+
"AllowGeneralDelivery": true
4+
},
5+
"Smarty": {
6+
"Enabled": false,
7+
"AuthId": "",
8+
"AuthToken": ""
9+
},
210
"Socure": {
311
"Enabled": false
412
},

src/SEBT.Portal.Api/appsettings.dc.example.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
{
2+
"AddressValidationPolicy": {
3+
"AllowGeneralDelivery": true
4+
},
5+
"Smarty": {
6+
"Enabled": false,
7+
"AuthId": "",
8+
"AuthToken": ""
9+
},
210
"Socure": {
311
"Enabled": true,
412
"UseStub": true,

src/SEBT.Portal.Api/appsettings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@
9191
"ProfileId": ""
9292
}
9393
},
94+
"Smarty": {
95+
"Enabled": false,
96+
"AuthId": "",
97+
"AuthToken": "",
98+
"BaseUrl": "https://us-street.api.smartystreets.com",
99+
"TimeoutSeconds": 20
100+
},
101+
"AddressValidationPolicy": {
102+
"AllowGeneralDelivery": true
103+
},
94104
"FeatureManagement": {
95105
"email_dob_opt_in": false,
96106
"show_application_number": true,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace SEBT.Portal.Core.AppSettings;
2+
3+
/// <summary>
4+
/// Per-state policy for address updates. Override in <c>appsettings.{state}.json</c> as needed.
5+
/// </summary>
6+
public sealed class AddressValidationPolicySettings
7+
{
8+
public const string SectionName = "AddressValidationPolicy";
9+
10+
/// <summary>
11+
/// When true, USPS General Delivery addresses validated via Smarty are accepted.
12+
/// When false, General Delivery is rejected with a structured validation error.
13+
/// </summary>
14+
public bool AllowGeneralDelivery { get; set; } = true;
15+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace SEBT.Portal.Core.AppSettings;
4+
5+
/// <summary>
6+
/// Configuration for Smarty US Street Address API.
7+
/// </summary>
8+
public sealed class SmartySettings
9+
{
10+
public const string SectionName = "Smarty";
11+
12+
/// <summary>
13+
/// When false, the portal uses pass-through normalization only (no HTTP calls to Smarty).
14+
/// </summary>
15+
public bool Enabled { get; set; }
16+
17+
/// <summary>Smarty embedded key auth-id.</summary>
18+
public string? AuthId { get; set; }
19+
20+
/// <summary>Smarty embedded key auth-token (secret).</summary>
21+
public string? AuthToken { get; set; }
22+
23+
/// <summary>API host without trailing slash (e.g. https://us-street.api.smartystreets.com).</summary>
24+
public string BaseUrl { get; set; } = "https://us-street.api.smartystreets.com";
25+
26+
/// <summary>HTTP timeout for Smarty requests.</summary>
27+
[Range(1, 120)]
28+
public int TimeoutSeconds { get; set; } = 20;
29+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using SEBT.Portal.Core.Models.Household;
2+
3+
namespace SEBT.Portal.Core.Models.AddressUpdate;
4+
5+
/// <summary>
6+
/// Canonical input for shared address validation and normalization (portal handlers and state connectors).
7+
/// Carries optional identifiers and metadata so callers can correlate requests without coupling to HTTP.
8+
/// </summary>
9+
public sealed record AddressUpdateOperationRequest
10+
{
11+
/// <summary>Line-one street address or USPS-style General Delivery line.</summary>
12+
public required string StreetAddress1 { get; init; }
13+
14+
/// <summary>Optional secondary line (unit, suite, etc.).</summary>
15+
public string? StreetAddress2 { get; init; }
16+
17+
/// <summary>City or USPS post office location for General Delivery.</summary>
18+
public required string City { get; init; }
19+
20+
/// <summary>US state name or two-letter abbreviation.</summary>
21+
public required string State { get; init; }
22+
23+
/// <summary>US ZIP or ZIP+4.</summary>
24+
public required string PostalCode { get; init; }
25+
26+
/// <summary>Optional end-to-end correlation id (logging, support).</summary>
27+
public string? CorrelationId { get; init; }
28+
29+
/// <summary>Optional state case or eligibility system identifier for the household.</summary>
30+
public string? HouseholdExternalId { get; init; }
31+
32+
/// <summary>Optional opaque key/value metadata (connector-specific; not sent to Smarty).</summary>
33+
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
34+
35+
/// <summary>
36+
/// Builds a request from the domain <see cref="Household.Address"/> model.
37+
/// </summary>
38+
public static AddressUpdateOperationRequest FromHouseholdAddress(Address address) =>
39+
new()
40+
{
41+
StreetAddress1 = address.StreetAddress1 ?? string.Empty,
42+
StreetAddress2 = address.StreetAddress2,
43+
City = address.City ?? string.Empty,
44+
State = address.State ?? string.Empty,
45+
PostalCode = address.PostalCode ?? string.Empty
46+
};
47+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using SEBT.Portal.Core.Models.Household;
2+
3+
namespace SEBT.Portal.Core.Models.AddressUpdate;
4+
5+
/// <summary>
6+
/// Successful outcome of <see cref="SEBT.Portal.Core.Services.IAddressUpdateService"/>.
7+
/// </summary>
8+
public sealed record AddressUpdateSuccess
9+
{
10+
/// <summary>USPS-style normalized mailing address.</summary>
11+
public required Address NormalizedAddress { get; init; }
12+
13+
/// <summary>True when Smarty indicates USPS General Delivery (record type G or equivalent).</summary>
14+
public bool IsGeneralDelivery { get; init; }
15+
16+
/// <summary>True when normalized fields differ from the submitted input (casing, abbreviations, ZIP+4).</summary>
17+
public bool WasCorrected { get; init; }
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using SEBT.Portal.Core.Models.AddressUpdate;
2+
using SEBT.Portal.Kernel;
3+
using SEBT.Portal.Kernel.Results;
4+
5+
namespace SEBT.Portal.Core.Services;
6+
7+
/// <summary>
8+
/// Validates and normalizes mailing addresses using Smarty (when enabled) and state policy from configuration.
9+
/// Intended for use by portal use cases and state connectors so validation rules stay consistent.
10+
/// </summary>
11+
public interface IAddressUpdateService
12+
{
13+
/// <summary>
14+
/// Validates the address with Smarty (or pass-through when disabled), applies per-state policy
15+
/// (e.g. General Delivery), and returns a normalized address or structured validation errors.
16+
/// </summary>
17+
Task<Result<AddressUpdateSuccess>> ValidateAndNormalizeAsync(
18+
AddressUpdateOperationRequest request,
19+
CancellationToken cancellationToken = default);
20+
}

src/SEBT.Portal.Core/Services/IAddressValidationService.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
namespace SEBT.Portal.Core.Services;
44

55
/// <summary>
6-
/// Validates a mailing address against an external service (e.g., Smarty).
7-
/// Implementations may autocomplete, suggest alternatives, or reject invalid addresses.
6+
/// Legacy façade over <see cref="IAddressUpdateService"/> for simple valid/invalid checks.
7+
/// Prefer <see cref="IAddressUpdateService"/> for structured errors, identifiers, and metadata.
88
/// </summary>
99
public interface IAddressValidationService
1010
{
@@ -19,14 +19,17 @@ public interface IAddressValidationService
1919
/// Result of an address validation call.
2020
/// </summary>
2121
/// <param name="IsValid">Whether the address passed validation.</param>
22+
/// <param name="NormalizedAddress">USPS-normalized address when valid, if available.</param>
2223
/// <param name="SuggestedAddress">An alternative address suggested by the validation service, if any.</param>
2324
/// <param name="ErrorMessage">A user-facing error message if validation failed.</param>
2425
public record AddressValidationResult(
2526
bool IsValid,
27+
Address? NormalizedAddress = null,
2628
Address? SuggestedAddress = null,
2729
string? ErrorMessage = null)
2830
{
29-
public static AddressValidationResult Valid() => new(true);
31+
public static AddressValidationResult Valid(Address? normalizedAddress = null) =>
32+
new(true, NormalizedAddress: normalizedAddress);
3033

3134
public static AddressValidationResult Invalid(string errorMessage) => new(false, ErrorMessage: errorMessage);
3235

0 commit comments

Comments
 (0)