Skip to main content

34. Facility onboarding intake survey

Date: 2026-06-14

Status

Accepted

Context

New clients are onboarded via a "New Client Intake Form" (a 40-question Google Form: facility profile, every named contact — Administrator, DON, MDS coordinators, IT/EMR — service request, EMR, access, compliance/BAA, billing recipient, and an authorized representative). That data lived only in a spreadsheet. We want the survey in-app so onboarding produces a real client record, and we want to seed the existing responses. Two of the three current responses already exist as facilities (Huntington Hills as a prospect, Westchester Center as active), so seeding must not duplicate.

Decision

  • Dedicated facility_intakes table (migration 0033), one row per submission, capturing all 40 questions verbatim. Multi-selects (facility types, services, access requirements, policies) are text[] (each option is atomic). Dirty single values (e.g. target start "3/6/0026", phone extensions, the "SAA" email placeholder) are kept as text — only the legal name is required, so fidelity is preserved over premature validation. Company-scoped admin RLS + set_updated_at / audit_row triggers, like other PII-bearing tables. Linked to the facility via facility_id.
  • Intake → operational records. Submitting the survey (submitFacilityIntake) creates the client or matches an existing facility by name (DBA, else legal) and re-activates it (sets status='active'), records the full intake, and seeds facility_contacts for the three operational roles: Administrator → admin, DON + MDS coordinators → clinical, invoice recipient → billing (only contacts with a real name + email; de-duped by email). The IT/EMR contact and authorized representative don't map to those three roles, so they live only on the intake record (avoids widening the facility_contacts role check). The name→facility match keeps the survey idempotent and prevents duplicate clients. Mapping (address parse, name resolution, contact derivation) is a pure, unit-tested lib (src/lib/facility/intake.ts).
  • Admin-filled survey at /admin/facilities/new — the "Add a facility" flow (FacilityIntakeForm) — with visible labels and checkbox groups for the multi-selects (ux §Forms), organised into tabs (Facility · Key contacts · Service request · Compliance · Billing & notes) to limit scrolling. Panels stay mounted (hidden via the hidden utility) so all fields submit together; on a validation error the form opens the tab holding the offending field (no native required, since a hidden control can't be focused). The facility detail page shows the latest intake read-only.
  • Seed script scripts/seed-facility-intakes.mjs transcribes the three responses and applies the same match-by-name / re-activate / insert-intake / seed-contacts logic over Supabase REST (service key from .env.local), DRY-RUN by default and idempotent (skips a facility's intake if one with the same legal name already exists).

Consequences

  • The three responses seed cleanly: Huntington Hills flips prospect → active, Westchester stays active, Pelham Parkway is created active — no duplicates.
  • facility_intakes is the immutable submission record; facilities + facility_contacts are the editable operational projection. Re-submitting for the same facility adds a new intake row (history) and re-activates the facility; the detail page shows the latest.
  • The survey is admin-filled (the app has no facility-user role). A public/self-service intake link is a possible follow-up. Converting a won CRM lead straight into this intake is another.
  • Like every admin service-role write in the app (other than the payroll-close DB function), these inserts run through the service client with no JWT, so the audit_row trigger records a null actor. The trigger tolerates this; capturing admin.userId via an app.actor_id GUC for all service-role actions is a known, codebase-wide follow-up rather than something done only here.