Skip to content

fix(membership): surface emergency-contact rejection as a 400, not a 500#288

Open
dkaygithub wants to merge 3 commits into
mainfrom
fix/membership-intake-error-transparency
Open

fix(membership): surface emergency-contact rejection as a 400, not a 500#288
dkaygithub wants to merge 3 commits into
mainfrom
fix/membership-intake-error-transparency

Conversation

@dkaygithub

Copy link
Copy Markdown
Collaborator

Problem

Submitting the membership intake while the emergency contact collides with a household member (shared name, email, or phone) returned a raw 500 Internal Server Error instead of a clean, field-highlighted validation message.

Found while testing the join flow: a household of one (the applicant) with an emergency contact that reused the same mock phone number as the applicant. The not-a-household-member check (emergencyContacts/identity.ts) matches on any one of phone/email/name, so it correctly decided the contact was the applicant — but the rejection surfaced as a 500.

Root cause

saveIntake → upsertPrimaryContact throws an EmergencyContactError. The intake routes' shared intakeErrorResponse only translated IntakeError, so EmergencyContactError fell through to the generic 500 branch. (The message text only appeared because dev instances echo detail; prod would have shown a blank "Internal Server Error".) Every other emergency-contact route already catches this class and returns a 400 — the intake route was the only gap.

Fix

  • lib/membership/intakeResponse.ts — map EmergencyContactError to a 400 carrying its code and fields: ["emergencyContact"], consistent with the other routes and with the form's field-highlighting contract.
  • app/membership/page.tsx — the submit flow's PATCH-failure branch now highlights returned fields (via mapServerFields), matching what its submit-POST branch already did.

Result: a clean inline error ("…can't be its emergency contact…") with the field highlighted, on prod as well as dev — no 500.

Behavior unchanged

The identity-matching rule itself is intentional (phone OR email OR name) and is left as-is. This PR is purely about error transparency.

Testing

  • tsc --noEmit clean
  • test:ci (mocked): 177 passed
  • Pre-commit hooks (lint + full suite) passed

🤖 Generated with Claude Code

dkaygithub and others added 3 commits June 14, 2026 22:14
Starting/submitting a membership application could fail with a bare
"Internal Server Error" and no indication of what was wrong. This makes
the intake errors transparent:

- Field-level validation: submit() highlights missing required inputs
  (home address, emergency contact name/phone, primary name) in red with
  inline messages before any network call — instant feedback on every
  environment, prod included.
- Server-driven highlighting: IntakeError now carries `fields`, and
  submitIntake tags each missing requirement. The submit route returns
  those keys so the client can highlight cases it can't detect locally
  (e.g. an emergency contact who is a household member).
- Unexpected 500s: error->response mapping is centralized in
  intakeResponse.ts and shared by all three membership routes. The real
  error message is echoed as `detail` only on dev/local instances (prod
  stays generic — no DB/stack leak); the full error is always logged.

Existing intake integration test still holds (asserts 400 + code
'incomplete', both preserved; `fields` is additive).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts in the membership intake flow:
- IntakeError: keep the new fields? param and add main's lead_limit code
- intakeResponse.ts STATUS_FOR: add lead_limit so the shared map stays exhaustive
- intake/submit/route + membership/route: keep the shared intakeErrorResponse
  helper, dropping main's now-redundant inline STATUS_FOR/handleError
- membership/page.tsx: keep both fieldErrors and warnings state; combine
  apiError()/mapServerFields() error handling with main's saveWarnings flow;
  drop a duplicate saveRes.json() read

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saveIntake -> upsertPrimaryContact throws EmergencyContactError when the
intake's emergency contact matches a household member (shared name, email,
or phone). The intake routes' shared intakeErrorResponse only translated
IntakeError, so EmergencyContactError fell through to a generic 500
Internal Server Error — the message only surfaced at all because dev
instances echo `detail`.

Map EmergencyContactError to a 400 carrying its code and
fields: ["emergencyContact"], matching how every other emergency-contact
route already handles it. Client: the submit flow's PATCH-failure branch
now highlights the returned fields, like its submit-POST branch already did.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant