AllCare / Conformance Audit / Patient Canvas

Canvas to PatientsV2 - Field Mapping

An authoritative field-by-field audit of where the new patient canvas saves data versus where the legacy "Ask AI" path saved it, scored against live current code. Status colors are locked across the whole page.

Section 1 / Verdict

Substantially conformant. Every field the old path saved now lands in the same table and column.

Three follow-ups merged today (#510, #483, #482). Two by-design product decisions remain, neither blocking parity. What shipped, how the data flows, and the per-field detail are below.

Section 2 / What shipped and how it works

The PRs, the data model, and the save flow.

Start here: the pull requests that delivered the mapping, then two diagrams of where patient data lives and how an onboarding save reaches it. The per-field detail is in Section 3.

One master record, many satellites

Patient demographics live once, in PatientsV2. Every other service keeps only its own slice, keyed by ID. No service copies the demographics.

GlobalPatient - master identity
PatientsV2 · GlobalPatients table · one per real person
Patient - facility-scoped record
PatientsV2 · Patients table · DefaultFacilityId = the facility you select (place of service) · unique per (GlobalPatientId, DefaultFacilityId)
↓   PatientCreated event   ↓
Facility enrollment
CurentaFacilityCoreAPI · PatientEnrollmentStatus · keyed GlobalPatientId + FacilityId
Pharmacy claim
CurentaPharmacy · PharmacyPatientClaim · keyed PatientId
Clinic records
clinic-service · tasks / prechart · keyed patient_id

The facility you pick at onboarding is stored as Patients.DefaultFacilityId. Satellites hold relationship and status data only, reconciled by the PatientCreated event, never a demographic copy.

How an onboarding save flows

The canvas creates the patient synchronously and gets a patientId back; the PatientCreated event then fans out to the satellites. CareTeam, enrollment, and eligibility are created reactively, not inline.

1 · Canvas (frontend)
POST /Patient/Create with demographics + the selected facility (ln)
2 · CreatePatientCommandHandler
GetOrCreateGlobalPatient → Patient.Create (raises PatientCreated) → SetFacilityContext (sets DefaultFacilityId, patches the event) → SaveChanges
↓   returns patientId   ·   publishes PatientCreated   ↓
CareTeam entry
PatientCreatedCareTeamConsumer
Facility enrollment
PatientCreatedConsumer
Stedi eligibility
PatientCreatedEligibilityConsumer
3 · Canvas saves each section
POST update-personal-info · medications · AddContact, against the returned patientId

This is why folding the create path into the CareTeam admission flow would double-write: the event already creates the CareTeam entry, and the admission flow would clone a second facility patient into the same unique key and throw.

Per-domain status, plain.

Status is measured against where the old platform saved each field. By-design notes mark fields neither system ever stored.

DemographicsMatchName, DOB, sex, SSN, MRN, room, facility land in the same columns. Language / marital status were never stored by either system; ADL / Vitals / IADL removed.
AddressMatchDiscrete fields plus Mapbox context to the Address table; the parse-failure that dropped whole addresses is gone.
DiagnosesMatchAll rows save as text to Main / SecondaryDiagnosis. ICD-10 stays display-only in both systems (no column).
AllergiesMatchDescription matches. Severity / reaction were never columns in either system.
MedicationsMatchName, route, qty, dose-form, dates, schedule, refill all reach PatientMedication + RXInfo + schedule children.
InsuranceMatchReads Stedi GlobalPatientInsurance; the orphaned PatientInsuranceInfo writer was deleted.
Patient Circle / ContactsMatchContact, NPI, specialty, care team. Email-only POA now persists and activates the patient (#483).
AttachmentsMatchFace sheet uploads to /Patient/add-document as a patient Document.
Removed sectionsRemovedADL / Vitals / IADL intentionally dropped from the canvas.
Match = saves where the old path saved it Partial = lands but lossy Mismatch / Open = wrong place or unresolved Removed = intentionally dropped
Section 3 / Field-by-field matrix

Old destination vs today's DB destination, per field.

The two mono columns are the comparison surface. file:line citations sit as secondary footnotes under the note.

Demographics

14 fields / 9 match / 2 open / 3 removed
Field Old platform destination Today's DB destination Status Note
First name Patients.BasicInfo_FirstName Patients.FirstName (owned PatientBasicInfo) + mirrored GlobalPatient demographics Match Canvas firstName to PATCH /Patient/Demographics.PatientBasicInfo.cs:10
Last name Patients.BasicInfo_LastName Patients.LastName (owned) + GlobalPatient Match PatientBasicInfo.cs:11
DOB Patients.BasicInfo_DateOfBirth Patients.DateOfBirth (owned, DateOnly) + GlobalPatient Match FE normalizes to ISO; unparseable to null (no fabrication).PatientBasicInfo.cs:20
Sex / gender Patients.BasicInfo_Gender Patients.Gender (enum, owned) + GlobalPatient Match FE M/F/X to 1/2/3; UI offers only F/M/X.PatientBasicInfo.cs:21
SSN Patients.BasicInfo_SSN Patients.SSN (owned) + GlobalPatient Match Same role-based masking gate old and new.PatientBasicInfo.cs:25
Member ID / MBI Not in old demographics tool GlobalPatients demographics .MBI via GlobalPatient.SetMbi() Match New field, no old equivalent; FE PUT /Patient/MemberId, MBI normalized to 11-char.CreatePatientCommandHandler.cs:151-156
MedicaidId Not in old GlobalPatients demographics .MedicaidId via SetMedicaidId() Match New, no old equivalent.CreatePatientCommandHandler.cs:158-163
MRN / Medical Record # PatientPersonalInformation.MR (old free-text "MR" field) No canvas input; echoed read-only as mr (seeded from Apollo) to avoid clobber; no edit route Removed Dead PUT /Patient/Mrn 404 removed (#506). No structured MRN column exists in Domain.
Room Patients.Room (owned FacilityInfo) Patients facility-context Room via SetFacilityContext Match FE PUT /Patient/{id}/room-wing; WingId/WingName not sent by canvas.CreatePatientCommandHandler.cs:103-112
Language Not captured by old path Ignored - no column anywhere in Domain Removed No language input in canvas; no DB column. Parity with old (also absent).
Marital status Not captured by old path Ignored - no column anywhere in Domain Removed No input; no DB column. Parity with old.
Phone Patients.BasicInfo_PhoneNumber Patients owned PatientBasicInfo.PhoneNumber + GlobalPatient Match FE PUT /Patient/PhoneNumber, normalized to raw digits.PatientBasicInfo.cs:16-19
Email Patients.BasicInfo_Email Patients owned PatientBasicInfo.Email + GlobalPatient Match FE in PERSONAL_INFO_FIELDS to PATCH /Patient/Demographics.
Facility / Place of Service Facility scoping via GlobalPatient / tenant context (no demographics column) Patients.DefaultFacilityId set at create via SetFacilityContext; canvas-edit placeOfServiceId scalar routes to local_only and is dropped on flush Open Create-time facility scoping works; the canvas Place-of-Service edit scalar is captured-but-unsent with a "coming soon" warning - no scalar PUT contract.CreatePatientCommandHandler.cs:78-112 ; canvas-draft-store.ts:247,263-267,295-300
Doctor / primary physician Patients.PrimaryPhysicianId + PatientProviders Handled via contacts / care-team path (see Patient Circle) Match Old resolved doctor_name to provider; current threads provider via Contact / CareTeam.

Address

1 field (multi-row) / 1 match
Field Old platform destination Today's DB destination Status Note
Address rows (Home / Billing / Place-of-Service / Other) Address table (FK PatientId): AddressLine1, City, State, PostalCode, Latitude, Longitude, Type, BuildingNumber Addresses.* (Address.Create): AddressLine1, City, State, PostalCode, AddressLine2, BuildingNumber, CountryCode, FullAddress, Description, Type, Lat/Long, UseForDelivery/Billing/Mailing, IsDefault Match FE POST /PatientAddress/Add (one POST per row); type to server enum home 1 / billing 5 / place-of-service 3 / other 5. Brittle parseUsAddress regex deleted (#506/#509); discrete fields backfilled from Mapbox context. Row skipped (not 400) if geocode null or city/state unresolved; OCR auto-fill only on exact confidence.CreatePatientCommandHandler.cs:200-242 ; Address.cs:9-32

Diagnoses

3 fields / 2 match / 1 open
Field Old platform destination Today's DB destination Status Note
Primary diagnosis (text) PatientPersonalInformation.MainDiagnosis PatientPersonalInformation.MainDiagnosis (comma-joined free-text) Match Row 0 to main; FE POST /Patient/update-personal-info. Section serializer preferred over dirty-only builder, so all rows persist.PatientPersonalInformation.cs:10
Secondary diagnosis (text, incl. 3rd+) PatientPersonalInformation.SecondaryDiagnosis (extras comma-joined) PatientPersonalInformation.SecondaryDiagnosis (rows 1..n comma-joined) Match More-than-2-rows-dropped bug FIXED (#506).PatientPersonalInformation.cs:11
ICD-10 code None - no ICD column (string FDB-coded server-side) No dedicated ICD column; codes live only as display text inside the free-text strings; Patient.cs:300-336 GetDiagnosisList emits CodedDto{Code==Description==text} Open Captured in UI (per-row, OCR-seeded) but explicitly display-only, not serialized. Decided-by-design: matches old (old also had no column). Open only if product later wants stored ICD-10.clinical-snapshot-section.tsx:304-309

Allergies

4 fields / 2 match / 2 open (by-design)
Field Old platform destination Today's DB destination Status Note
Allergen / description Owned allergy child table, AllergyDescription PatientAllergiesInfo.Description (owned collection) Match FE allergies:[{description, changeStateType:'Add'}] to update-personal-info; empty rows filtered.PatientAllergiesInfo.cs:16-17
AllergyId (FDB code) Owned allergy table AllergyId (legacy DEFECT: Guid.NewGuid() fabricated on FDB miss) PatientAllergiesInfo.AllergyId set when FDB-resolvable, null when not (flag-not-fabricate) Match Improved over old: FE omits AllergyId, BE no longer fabricates.UpdatePatientPersonalInformationCommandHandler.cs:117-142
Severity None - no column Ignored - no Severity property on entity/model Open Captured per-row + OCR-seeded in UI, never on DTO. By-design parity with old (neither stored it).PatientAllergiesInfo.cs:16-19
Reaction None - no column Ignored - no Reaction property Open Same as severity - captured, never persisted. By-design parity with old.
NKA confirmed n/a allergies:[] sent (empty array) when nkaConfirmedRef true Match New affordance; serializer sends empty array.clinical-snapshot-section.tsx:494-495

Medications

10 fields / 10 match

All land on PatientMedications (owned MedicationInfo + RXInfo) via the NEW canvas route POST /api/Patient/medications to CreatePatientMedicationCommand (PatientController.cs:918-922), NOT legacy /PatientMedicaion/Add.

Field Old platform destination Today's DB destination Status Note
Name MedicationInfo.MedName PatientMedications.MedName (owned MedicationInfo.MedName) Match CreatePatientMedicationCommandHandler.cs:98
Strength MedicationInfo.MedStrength MedicationInfo.MedStrength Match FE splits strength out of name.
Dose form RXInfo.DoseFormDesc / DoseFormId RXInfo.DoseFormDesc Match FIXED #506 (FE send) + #259 (extractor packs dose_form).
Route RXInfo.Route RXInfo.Route Match FIXED #506/#259 (was dropped).
Directions RXInfo.Directions RXInfo.Directions Match
PRN RXInfo.Isprn RXInfo.Isprn Match
Qty / total units RXInfo.Quantity RXInfo.Quantity Match FIXED #506/#259 (qty parsing added).
Start / End date RXInfo.StartDate / RXInfo.EndDate RXInfo.StartDate / RXInfo.EndDate Match FIXED #506 (was dropped).
Refill date RXInfo.NextRefillDate RXInfo.NextRefillDate Match FIXED #479 (threaded RefillDate to NextRefillDate).
Schedule / frequency PatientMedicationSchedule + PatientMedicationAdminHour child tables Same child tables via PatientMedicationScheduleRequest{AdminHours} to IMedicationAdminHoursCalculator.GenerateSchedules Match FIXED #479; validator rejects unparseable times. Minor open nit: Repeated not set, defaults can produce a weekly-on-sync-day schedule instead of Daily (unfixed Greptile nit).CreatePatientMedicationCommandHandler.cs:193-200 ; :196-199

Insurance

3 fields / 3 match (resolved-by-design)
Field Old platform destination Today's DB destination Status Note
Payer / member id (manual) PatientInsuranceInfo owned collection (via POST /{patientId}/insurance / AddInsuranceInfo) Canvas does NOT write it; dead FE writer (buildInsuranceDtos / saveInsurance) deleted (#506). Legacy BE endpoint still exists but unused by canvas. Match Orphaned split-brain writer removed - no save-read disagreement remains.ac640850
Eligibility (Stedi 271) n/a (old had no eligibility) GlobalPatientInsurance (read path: hooks.ts / queries.ts); priority set upstream by RunEligibility UpsertProgramRow Match Canvas insurance is read-only from Stedi GlobalPatientInsurance; empty = eligibility not yet run, by design. Eligibility-drives-insurance shipped.7b7674e5
GlobalPatientInsurance vs PatientInsuranceInfo Old wrote PatientInsuranceInfo Authoritative ranking on GlobalPatientInsurance (CoverageStatus / Source); PatientInsuranceInfo has no ordering and is no longer canvas-written Match Resolved-by-design (was the "split-brain"). patientInsurance value-object is unordered; ranking is on GlobalPatientInsurance.

Patient Circle / Contacts

6 fields / 6 match (poa_phone gate relaxed to phone-or-email, #483)
Field Old platform destination Today's DB destination Status Note
Contact / POA name Provider / contact resolution Contact (name required) Match POA flag = role-label OR extracted is_power_of_attorney (#506).571f0d7e
POA / contact phone Contact phone Contact phone Match Persists, and activation gate relaxed to phone-OR-email (#483) - PatientStateMachine.cs now hasPoaContact = phone || email; email-only POA activates.679fd40e
POA / contact email Contact email Contact email Match BE relaxed to "at least one of phone/email" (#479); FE now sends an email-only POA standalone (PR #510 merged). Email-only POA persists.7ed2f87b
NPI Provider record Contact NPI Match Threaded into Contact (#506).571f0d7e
Specialty Provider record Contact Specialty Match Threaded into Contact (#506).
Care team member PatientProviders / provider resolution CareTeam_Member resolver pre-fills extracted clinicians Match Resolver added #506; role-derived relationship bot-fix.037d2763

Attachments

1 field / 1 match
Field Old platform destination Today's DB destination Status Note
Face sheet Not persisted in old "Ask AI" path Patient Document (DocumentType=FaceSheet) via POST /Patient/add-document Match FIXED #506 (FE-only via blob objectUrl); idempotency re-keyed by patientId:artifactId (#509).

Removed sections

3 fields / 3 removed
Field Old platform destination Today's DB destination Status Note
ADL Existed in canvas pre-fix Removed from canvas Removed Deleted #506.47906c57
Vitals Existed pre-fix Removed Removed Deleted #506.
IADL Existed pre-fix Removed Removed Deleted #506.
Section 4 / What's left

Open items, ordered.

Each item tagged for whether it blocks a Match, and whether it is ship-ready or needs a product decision. Items 01, 02, 04 (PRs #510, #483, #482) are now merged and shown for context; two remain (03 and 05, both product decisions, neither blocks parity).

01

PR #510 - FE email-only POA send

Merged 2026-06-01

Backend accepts a phone-less, email-bearing POA (#479); the FE now sends an email-only contact standalone, so an email-only POA persists end-to-end. Done.

merge 7ed2f87b
Match-blocker: No Done (merged)
02

D-51 - poa_phone activation gate relaxed

Merged 2026-06-01

The activation gate now accepts phone OR email: PatientStateMachine.cs uses hasPoaContact = phone || email (the "poa_phone" missing-key string is preserved so the task creator and FE still parse it). An email-only POA now persists AND activates the patient end-to-end. Done.

PR #483 ; merge 679fd40e
Match-blocker: No Done (merged)
03

Place-of-Service edit scalar - captured but unsent

Create-time facility scoping works. The canvas Place-of-Service edit scalar (placeOfServiceId) routes to local_only and is dropped on flush, surfaced with a "coming soon" warning, because there is no scalar PUT contract yet.

canvas-draft-store.ts:247,263-267,295-300
Match-blocker: No Needs decision (no PUT contract)
04

Med-schedule Repeated.Daily made explicit

Merged 2026-06-01

The canvas schedule request now sets Repeated = Repeated.Daily explicitly (it already defaulted to Daily via the enum zero-value, so no behavior change - this aligns the one site that relied on the implicit default and guards against future enum reordering). Done.

PR #482 ; merge 38abf516
Match-blocker: No Done (merged)
05

By-design absent columns - ICD-10, allergy severity/reaction, language, marital status

Captured in the UI but never serialized because no column exists in Domain (and none existed in the old path either). These are Open only if product later wants them stored. Not divergence from the old platform.

Match-blocker: No Needs decision (product scope)
Section 5 / Bottom line

The mapping holds. The remainder is policy, not divergence.

The canvas-to-PatientsV2 mapping is substantially conformant: every field the old "Ask AI" path persisted now lands in the same table and column, and the orphaned insurance and dead-route writers were deleted. With the poa_phone activation gate now relaxed (#483) and the med-schedule default made explicit (#482), the only things that do not round-trip were never columns in either system, or are deliberately read-only.

No item on the open list blocks a Match where the old platform actually stored data. The medications, insurance, and contacts domains are fully resolved; demographics, diagnoses, and allergies carry only by-design gaps.

Is there anything left?
Almost nothing, and nothing that breaks parity. Three follow-ups merged today: email-only POA send (#510), the poa_phone activation gate relaxed to phone-or-email (#483), and the med-schedule Repeated.Daily default (#482). Two remain, both product decisions: wire a Place-of-Service edit PUT contract, and decide whether to ever store the by-design-absent columns (ICD-10, allergy severity/reaction, language, marital status). Neither blocks where the old platform stored data.
Substantially conformant - #510/#483/#482 merged, two by-design items remain