Skip to content

DC-153 - Implement “Request replacement card” flow in self-service portal#108

Open
necampanini wants to merge 58 commits intomainfrom
feat/DC-153-request-replacement-card-flow
Open

DC-153 - Implement “Request replacement card” flow in self-service portal#108
necampanini wants to merge 58 commits intomainfrom
feat/DC-153-request-replacement-card-flow

Conversation

@necampanini
Copy link
Copy Markdown
Contributor

@necampanini necampanini commented Mar 25, 2026

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_replacement feature flag.

Affected Areas

  • Dashboard (child card accordions, replacement link, success/warning alerts)
  • Standalone card replacement flow (confirm address, confirm request)
  • Address update flow (replacement prompt, card selection, confirm request)
  • Co-loaded card info page (DC only)

Test Cases

Happy Path — Standalone Flow (from Dashboard)

  • 1. Sign in and navigate to the dashboard. Expand a child's card accordion. Verify a "Request a replacement card" link appears below the card details.
  • 2. Click the replacement link. Verify the Confirm Address page shows the mailing address on file with the question "Do you want the new card mailed to this address?" and Yes/No radio options.
  • 3. Select "Yes" and click Continue. Verify the Confirm Request page shows information about card deactivation, 7-10 day delivery, and balance rollover, followed by a card order summary with the child's name and mailing address.
  • 4. Click "Order card." Verify you are redirected to the dashboard with a green success alert reading "Your replacement card request has been recorded" with delivery timeframe information.
  • 5. On the Confirm Address page, select "No" and click Continue. Verify you are taken to the address change page for this card.

Happy Path — Address Flow

  • 6. Navigate to the address update page (Profile > Address). Fill in a new address and submit.
  • 7. On the Replacement Card Prompt page, verify the new address is displayed and the question asks whether you want replacement cards sent to this address.
  • 8. Select "Yes" and click Continue. Verify the Card Selection page shows checkboxes for each child's card.
  • 9. Select one or more cards and click Continue. Verify the Confirm Request page shows the selected cards and the new mailing address.
  • 10. Click "Order card." Verify you are redirected to the dashboard with a green success alert reading "Address update and card replacement recorded."
  • 11. On the Replacement Card Prompt, select "No" and click Continue. Verify you are redirected to the dashboard with a green alert reading "Address update recorded" (no card replacement).

DC-Specific: Co-Loaded Cards

  • 12. (DC build only) Sign in with a user that has a SNAP or TANF co-loaded card. On the dashboard, verify the replacement link for that card points to a Card Replacement Information page, not the standard replacement flow.
  • 13. On the information page, verify it shows instructions to call Fidelity Information Services (FIS) at (888) 304-9167, your mailing address, and a note to keep your card for next year.
  • 14. Verify the "Tap to call" link opens a phone dialer.
  • 15. Verify the Back and Continue buttons both return to the dashboard.

CO-Specific

  • 16. (CO build only) Verify the Confirm Address page shows the child's name (not last 4 digits) in the subtitle.
  • 17. On the Card Selection page, verify each card shows the last 4 digits below the child's name.

14-Day Cooldown

  • 18. After requesting a replacement card, return to the dashboard. Verify the "Request a replacement card" link is no longer visible for that child's card.
  • 19. In the address flow card selection page, verify any card that was recently replaced (within 14 days) does not appear as a selectable option.
  • 20. If all cards are within the 14-day cooldown, verify the card selection page shows an info message "All cards were recently replaced. Please try again later." instead of checkboxes.

Sibling Auto-Select

  • 21. For a household with multiple children on the same application, select one child's card on the Card Selection page. Verify all siblings on that application are automatically checked with a note "These children share a card."
  • 22. Verify the auto-selected sibling checkboxes are disabled (cannot be individually unchecked).

Edge Cases

  • 23. On the Confirm Address page, submit without selecting Yes or No. Verify a validation error appears.
  • 24. On the Card Selection page, click Continue without selecting any cards. Verify a validation error "Please select at least one card" appears.
  • 25. Navigate directly to the card replacement confirm page without going through the flow (e.g., type the URL manually without the "app" parameter). Verify you are redirected to the dashboard.
  • 26. Navigate directly to the address flow's replacement cards page without completing the address form. Verify you are redirected back to the address form.

Feature Flag

  • 27. With the enable_card_replacement feature flag disabled, verify no "Request a replacement card" links appear on the dashboard for any child.

Regression Checks

  • 28. Verify the dashboard still loads correctly with all child card accordions expanding and collapsing.
  • 29. Verify the address update flow still works end-to-end without requesting replacement cards (select "No" at the prompt).
  • 30. Verify dashboard alerts for other scenarios (address update failed, contact update failed) still display correctly.
  • 31. Verify card status timeline and card status display still show correct information in child card accordions.
  • 32. Verify the SEBT ID and card number rows in the child card accordion still respect their respective feature flags (show_case_number, show_card_last4).

Environment Notes

  • Test on both DC and CO builds. DC and CO have different card subtitle formats and co-loaded card behavior.
  • The replacement request is recorded but does not yet call the state benefits system (backend stub). The success alert reflects this.
  • Cooldown testing requires a card that was "requested" within the last 14 days. Check if mock/seed data provides this scenario, or manually trigger a replacement request and then verify the cooldown.

Nick Campanini added 24 commits March 23, 2026 21:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/select this will resolve to /profile/address/replacement-cards/select/select/confirm, but the confirm page route is /profile/address/replacement-cards/select/confirm. Use router.push('confirm?apps=...'), router.push('./confirm?...'), or an absolute path to avoid the duplicated select/ segment.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Nick Campanini added 4 commits April 1, 2026 13:20
# 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
@necampanini necampanini requested a review from adbergen April 1, 2026 21:01
@necampanini necampanini requested a review from noxmwalsh April 6, 2026 16:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 119 to 129
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]
};
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 59 to 69
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)

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +67
const groups = buildApplicationGroups(data.applications)

if (groups.length === 0) {
return (
<Alert variant="info">
{t('cardSelectionNoChildren', 'No children found in your household.')}
</Alert>
)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +15
/**
* 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.
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants