DC-153 - Implement “Request replacement card” flow in self-service portal#108
DC-153 - Implement “Request replacement card” flow in self-service portal#108necampanini wants to merge 58 commits intomainfrom
Conversation
…cooldown instability
There was a problem hiding this comment.
Pull request overview
Implements the DC-153 “Request replacement card” flow end-to-end across the self-service portal, including a new backend endpoint with cooldown enforcement, new standalone and address-flow UI routes, and stabilized mock/E2E data to support testing while persistence is stubbed (pending DC-160).
Changes:
- Added backend command + API endpoint for requesting replacement cards (14-day cooldown enforcement).
- Added standalone and address-flow integrated UI for confirming address, selecting cards, and submitting the replacement request.
- Added/updated unit tests, MSW handlers, and Playwright E2E fixtures/specs to cover the flow.
Reviewed changes
Copilot reviewed 54 out of 54 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/SEBT.Portal.Tests/Unit/UseCases/Household/RequestCardReplacementCommandHandlerTests.cs | Unit coverage for validation/authorization/cooldown behavior in the new handler |
| test/SEBT.Portal.Tests/Unit/UseCases/DependenciesTests.cs | Ensures DI registers the new command handler |
| test/SEBT.Portal.Tests/Unit/Repositories/MockHouseholdRepositoryTests.cs | Verifies stable mock application numbers + new seeded fields |
| test/SEBT.Portal.Tests/Unit/Controllers/HouseholdControllerTests.cs | Controller tests for the new POST endpoint mapping/result handling |
| src/SEBT.Portal.Api/Controllers/Household/HouseholdController.cs | Adds POST api/household/cards/replace endpoint |
| src/SEBT.Portal.Api/Models/Household/RequestCardReplacementRequest.cs | Adds request DTO w/ DataAnnotations validation |
| src/SEBT.Portal.UseCases/Household/RequestCardReplacement/RequestCardReplacementCommand.cs | Adds command model for replacement requests |
| src/SEBT.Portal.UseCases/Household/RequestCardReplacement/RequestCardReplacementCommandHandler.cs | Implements cooldown enforcement + household lookup (persistence stubbed) |
| src/SEBT.Portal.UseCases/Household/RequestCardReplacement/RequestCardReplacementCommandValidator.cs | Adds validator wrapper consistent with other use cases |
| src/SEBT.Portal.UseCases/Dependencies.cs | Registers the new command handler |
| src/SEBT.Portal.Infrastructure/Dependencies.cs | Changes mock repository lifetime (singleton) for stable seeded data |
| src/SEBT.Portal.Infrastructure/Repositories/MockHouseholdRepository.cs | Seeds issuance type/last4/cardRequestedAt + stabilizes application numbers |
| src/SEBT.Portal.TestUtilities/Helpers/HouseholdFactory.cs | Makes generated application numbers deterministic |
| src/SEBT.Portal.Web/src/mocks/handlers.ts | Adds MSW stub for POST /api/household/cards/replace |
| src/SEBT.Portal.Web/src/features/household/components/DashboardAlerts/DashboardAlerts.tsx | Adds flash-based “card replaced” success alert (PII-safe) |
| src/SEBT.Portal.Web/src/features/household/components/DashboardAlerts/DashboardAlerts.test.tsx | Tests for new flash=card_replaced alert behavior |
| src/SEBT.Portal.Web/src/features/household/components/ChildCard/ChildCard.tsx | Adds replacement link gating (cooldown + co-loaded behavior) + case number flag |
| src/SEBT.Portal.Web/src/features/household/components/ChildCard/ChildCard.test.tsx | Tests for SEBT ID rendering + feature flag gating |
| src/SEBT.Portal.Web/src/features/household/components/CardStatusTimeline/CardStatusTimeline.tsx | USWDS class update (counters-sm removal) |
| src/SEBT.Portal.Web/src/features/household/components/ActionButtons/ActionButtons.tsx | Updates replacement CTA link target |
| src/SEBT.Portal.Web/src/features/household/components/ActionButtons/ActionButtons.test.tsx | Updates CTA link expectation |
| src/SEBT.Portal.Web/src/features/household/api/schema.ts | Fixes CardStatus integer→string mapping (aligns with backend enum) |
| src/SEBT.Portal.Web/src/features/household/api/schema.test.ts | Adds tests to lock enum mapping behavior |
| src/SEBT.Portal.Web/src/features/cards/utils/cooldown.ts | Adds shared frontend 14-day cooldown utility |
| src/SEBT.Portal.Web/src/features/cards/utils/cooldown.test.ts | Unit tests for cooldown boundary behavior |
| src/SEBT.Portal.Web/src/features/cards/api/schema.ts | Adds Zod schema for replacement request payload |
| src/SEBT.Portal.Web/src/features/cards/api/schema.test.ts | Tests for replacement request schema validation |
| src/SEBT.Portal.Web/src/features/cards/api/client.ts | Adds React Query mutation useRequestCardReplacement() |
| src/SEBT.Portal.Web/src/features/cards/components/ConfirmAddress/ConfirmAddress.tsx | New address confirmation step (radio validation + navigation) |
| src/SEBT.Portal.Web/src/features/cards/components/ConfirmAddress/ConfirmAddress.test.tsx | Tests for selection validation + routing |
| src/SEBT.Portal.Web/src/features/cards/components/ConfirmRequest/ConfirmRequest.tsx | New confirmation + submit step (POST mutation + error handling) |
| src/SEBT.Portal.Web/src/features/cards/components/ConfirmRequest/ConfirmRequest.test.tsx | Tests success redirect, error message, pending state |
| src/SEBT.Portal.Web/src/features/cards/components/CardSelection/CardSelection.tsx | Card selection UI, sibling grouping, cooldown filtering |
| src/SEBT.Portal.Web/src/features/cards/components/CardSelection/CardSelection.test.tsx | Tests sibling auto-select, filtering, and navigation |
| src/SEBT.Portal.Web/src/features/address/components/CoLoadedInfo/CoLoadedInfo.tsx | Updates co-loaded DC guidance content + optional continue button |
| src/SEBT.Portal.Web/src/features/address/components/CoLoadedInfo/CoLoadedInfo.test.tsx | Updates tests for new content + conditional continue button |
| src/SEBT.Portal.Web/src/features/address/components/CardSelection/index.ts | Re-export points to canonical CardSelection implementation |
| src/SEBT.Portal.Web/src/features/address/components/AddressForm/AddressForm.tsx | Adds redirectPath override for flow integration |
| src/SEBT.Portal.Web/src/app/(authenticated)/cards/replace/layout.tsx | Guard for missing app query param in standalone flow |
| src/SEBT.Portal.Web/src/app/(authenticated)/cards/replace/page.tsx | Standalone confirm-address entry point (/cards/replace) |
| src/SEBT.Portal.Web/src/app/(authenticated)/cards/replace/confirm/page.tsx | Standalone confirm-request step (/cards/replace/confirm) |
| src/SEBT.Portal.Web/src/app/(authenticated)/cards/replace/address/page.tsx | Standalone address update step (/cards/replace/address) |
| src/SEBT.Portal.Web/src/app/(authenticated)/cards/info/page.tsx | DC-only co-loaded info page + redirect guard for non-DC |
| src/SEBT.Portal.Web/src/app/(authenticated)/profile/address/(flow)/replacement-cards/select/confirm/page.tsx | Address-flow confirm step loads selected apps + uses AddressFlowContext |
| src/SEBT.Portal.Web/e2e/fixtures/household-data.ts | Adds E2E household fixture factory + enum integer fixtures |
| src/SEBT.Portal.Web/e2e/fixtures/auth.ts | Adds E2E auth token injection helper |
| src/SEBT.Portal.Web/e2e/fixtures/api-routes.ts | Adds Playwright route intercepts for card replace + address update |
| src/SEBT.Portal.Web/e2e/card-replacement/standalone-flow.spec.ts | E2E coverage for standalone replacement flow |
| src/SEBT.Portal.Web/e2e/card-replacement/address-flow.spec.ts | E2E coverage for address-integrated replacement flow + guards |
| src/SEBT.Portal.Web/e2e/card-replacement/dashboard-alerts.spec.ts | E2E coverage for dashboard flash alerts + URL cleanup |
| src/SEBT.Portal.Web/e2e/card-replacement/child-card.spec.ts | E2E coverage for replacement link visibility + co-loaded behavior |
Comments suppressed due to low confidence (1)
src/SEBT.Portal.Web/src/features/cards/components/CardSelection/CardSelection.tsx:93
router.push(select/confirm?...)is a relative navigation. From/profile/address/replacement-cards/selectthis will resolve to/profile/address/replacement-cards/select/select/confirm, but the confirm page route is/profile/address/replacement-cards/select/confirm. Userouter.push('confirm?apps=...'),router.push('./confirm?...'), or an absolute path to avoid the duplicatedselect/segment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...EBT.Portal.UseCases/Household/RequestCardReplacement/RequestCardReplacementCommandHandler.cs
Outdated
Show resolved
Hide resolved
src/SEBT.Portal.Api/Controllers/Household/HouseholdController.cs
Outdated
Show resolved
Hide resolved
...Web/src/app/(authenticated)/profile/address/(flow)/replacement-cards/select/confirm/page.tsx
Outdated
Show resolved
Hide resolved
… 404 to OpenAPI docs
# Conflicts: # packages/design-system/content/states/co.csv # packages/design-system/content/states/dc.csv # src/SEBT.Portal.Infrastructure/Repositories/MockHouseholdRepository.cs # src/SEBT.Portal.Web/next.config.ts # src/SEBT.Portal.Web/src/features/address/api/schema.ts
…ve dead validator
…EbtCases + CO IAL
# Conflicts: # src/SEBT.Portal.Infrastructure/Repositories/MockHouseholdRepository.cs # src/SEBT.Portal.Web/src/features/household/api/schema.test.ts
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 60 out of 60 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var validationParams = new TokenValidationParameters | ||
| { | ||
| ValidateIssuer = false, | ||
| ValidateAudience = false, | ||
| ValidateLifetime = true, | ||
| ClockSkew = TimeSpan.FromMinutes(1), | ||
| IssuerSigningKey = key | ||
| // Use resolver instead of IssuerSigningKey to bypass kid-matching; | ||
| // jose (Next.js) signs without a kid header, which causes IDX10517 | ||
| // when JwtSecurityTokenHandler tries to match by kid. | ||
| IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => [key] | ||
| }; |
There was a problem hiding this comment.
TokenValidationParameters for validating the callbackToken does not explicitly set ValidateIssuerSigningKey=true. In the rest of the API (e.g., Program.cs JWT bearer setup) signature validation is enabled explicitly; doing the same here avoids relying on framework defaults and prevents accepting unsigned/invalidly signed callback tokens if defaults change.
| const { t, i18n } = useTranslation('dashboard') | ||
| const showCaseNumber = useFeatureFlag('show_case_number') | ||
| const showCardLast4 = useFeatureFlag('show_card_last4') | ||
| const [isExpanded, setIsExpanded] = useState(defaultExpanded) | ||
| const childName = `${child.firstName} ${child.lastName}` | ||
|
|
||
| const { benefitIssueDate, benefitExpirationDate, last4DigitsOfCard, issuanceType } = application | ||
| const { caseNumber, benefitIssueDate, benefitExpirationDate, last4DigitsOfCard, issuanceType } = | ||
| application | ||
| const cardTypeKey = issuanceType ? (CARD_TYPE_KEYS[issuanceType] ?? null) : null | ||
| const replacementLink = getReplacementLink(application) | ||
|
|
There was a problem hiding this comment.
The replacement link is rendered whenever getReplacementLink() returns a URL, but it isn’t gated behind the enable_card_replacement feature flag (even though other fields in this component are flag-gated and the MSW default has enable_card_replacement=false). Consider checking useFeatureFlag('enable_card_replacement') before computing/rendering the link so the feature can be safely toggled off.
| const groups = buildApplicationGroups(data.applications) | ||
|
|
||
| if (groups.length === 0) { | ||
| return ( | ||
| <Alert variant="info"> | ||
| {t('cardSelectionNoChildren', 'No children found in your household.')} | ||
| </Alert> | ||
| ) |
There was a problem hiding this comment.
buildApplicationGroups() filters out applications within the cooldown window, so groups.length===0 can mean "no eligible cards to replace" rather than "no children found". The current info alert message is misleading in that case; consider updating the copy (or separating the empty states) to reflect cooldown-based ineligibility.
| /** | ||
| * Displays success and warning alerts on the dashboard triggered by URL search params. | ||
| * Captures alert state on first read, then cleans the params from the URL. | ||
| * The alert persists because rendering is driven by captured state, not live params. | ||
| * Extensible: add new param checks for future alert types (e.g., DC-153 card ordering). | ||
| * Card replacement success (flash=card_replaced) sources dynamic details from the | ||
| * household data cache rather than URL params to avoid PII in URLs. |
There was a problem hiding this comment.
The header comment says the card replacement success alert "sources dynamic details" from the household data cache, but the implementation only branches on whether addressOnFile exists and doesn’t actually render any dynamic details. Consider adjusting the comment to match the current behavior (or include the intended dynamic details).
…y states, add explicit signing key validation
# Conflicts: # src/SEBT.Portal.Web/src/features/household/api/schema.test.ts
QA Summary — DC-153: Request Replacement Card Flow
What Changed
This feature adds the ability for parents to request replacement Summer EBT cards through the self-service portal. There are two entry points: a "Request a replacement card" link on each child's card on the dashboard, and a prompt after updating a mailing address. Cards requested within the last 14 days are restricted from re-requesting. DC users with SNAP/TANF co-loaded cards are routed to an informational page instead of the replacement flow. The feature is controlled by the
enable_card_replacementfeature flag.Affected Areas
Test Cases
Happy Path — Standalone Flow (from Dashboard)
Happy Path — Address Flow
DC-Specific: Co-Loaded Cards
CO-Specific
14-Day Cooldown
Sibling Auto-Select
Edge Cases
Feature Flag
enable_card_replacementfeature flag disabled, verify no "Request a replacement card" links appear on the dashboard for any child.Regression Checks
show_case_number,show_card_last4).Environment Notes