# Flow — patient-care-records > Last verified: 2026-08-02 against commit `c841bde` **Actor(s):** customer (owns the care plan) · nurse (appends visit notes) · admin (read-only) · **Status:** mocked **Client:** mock · **Server:** real **Business source:** none — **no `product/business/` area covers clinical records.** The only product source is [product/data-model/10-reviews-and-records.md](../../product/data-model/10-reviews-and-records.md). This is the atlas's largest product-doc hole. **Integration:** [docs/integration/domains/patient-records.md](../integration/domains/patient-records.md) ## What it does A family keeps a **care plan** for a patient — medications, daily routine, a task checklist — that outlives any one booking. A nurse on a confirmed booking reads that plan, ticks the checklist during a visit, and **appends** one visit note. Nobody edits or deletes a note; the next nurse reads the whole patient history for continuity. > ⚠ **Two endpoints one character apart.** `care_record` (singular) is the **family-owned plan** > (`GET`/`PUT`). `care_records` (plural) is the **append-only visit history** (`GET`/`POST`). Different > resources, different owners, different write verbs. Misreading the `s` silently targets the wrong resource. ## Screens | Step | Route | Component / notes | | --- | --- | --- | | Customer opens the care circle | `/fa/patients` | E1 list → row links to the record | | **E2 care record** | `/fa/patients/[id]/record` | [`record/page.tsx`](../../client/src/app/%5Blocale%5D/%28private-routes%29/%28customer%29/patients/%5Bid%5D/record/page.tsx) — 4 tabs داروها / روتین / سوابق / وظایف; per-item bottom-sheet editing; `canView === false` → non-leaking access-denied card **before** any clinical fetch | | Nurse opens today's visit | `/fa/nurse/visits/[id]` | [`page.tsx:24`](../../client/src/app/%5Blocale%5D/%28private-routes%29/nurse/visits/%5Bid%5D/page.tsx) mounts the panel under the EVV surface | | **E3 nurse visit note** | same route | [`NurseVisitNotesPanel.tsx`](../../client/src/app/%5Blocale%5D/%28private-routes%29/nurse/visits/%5Bid%5D/NurseVisitNotesPanel.tsx) — checklist + free-text composer + read-only history. Panel renders only when the booking is confirmed-or-beyond (`:36`); composer hides unless `canAppendNote` (`:72`) | ## API All five ops go hook → [`services/patientRecords/apis/index.ts`](../../client/src/services/patientRecords/apis/index.ts) → **`mockApi`**, because `USE_PATIENT_RECORDS_MOCK = true` ([`constants.ts:13`](../../client/src/services/patientRecords/constants.ts)). The table is the **real half** ([`clientApi.ts`](../../client/src/services/patientRecords/apis/clientApi.ts)) that is compiled but not selected. Shapes: [docs/integration/domains/patient-records.md](../integration/domains/patient-records.md). | Call | Endpoint | Live probe (2026-08-02) | | --- | --- | --- | | `getRecordAccess` | `GET /patients/{id}/record_access` | **200** — owner `{canView:t, canEdit:t, canAppendNote:f}`; nurse 1 `{t, f, t}`; patient 9999 `{f,f,f, deniedReason:"not_found"}` | | `getFamilyRecord` | `GET /patients/{id}/care_record` | **200** — but patient 1 returns `{medications:[],routine:[],tasks:[]}` (nothing seeded) | | `updateFamilyRecord` | `PUT /patients/{id}/care_record` | not probed (write). Owner-only guard at [`UpsertCarePlanCommand.Handler.cs:24`](../../server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/UpsertCarePlan/UpsertCarePlanCommand.Handler.cs) | | `getPatientHistory` | `GET /patients/{id}/care_records?page&pageSize` | **200** — 2 notes for patient 1, newest-first, Persian bodies decrypted | | `createVisitNote` | `POST /patients/{id}/care_records` | not probed (write). Nurse + qualifying-booking guard at [`WritePatientCareRecordCommand.Handler.cs:39-43`](../../server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/WritePatientCareRecord/WritePatientCareRecordCommand.Handler.cs) | Server: one controller, [`PatientCareRecordsController.cs`](../../server/src/API/Baya.Web.Api/Controllers/V1/PatientCareRecordsController.cs), five actions, all `[Authorize]`, all five handlers present. **REQ-027 is delivered.** Every client comment claiming "no wire endpoint exists" ([`clientApi.ts:61-62`, `:67`, `:71`, `:87`](../../client/src/services/patientRecords/apis/clientApi.ts)), plus [`constants.ts:6-8`](../../client/src/services/patientRecords/constants.ts) and [`types.ts:11-13`](../../client/src/services/patientRecords/types.ts), is **stale** — contradicted by a live 200. ## Rules that must hold | Rule | Where enforced | Verified | | --- | --- | --- | | **Append-only.** No edit, no delete of a visit note — there is no such endpoint and none may be added | controller has only `POST` + `GET` on `care_records` | ✅ read | | **Encrypted at rest.** Bodies leave the repo as ciphertext (`CareRecordCipherRow.BodyEncrypted`) and are decrypted only after the access check | [`GetPatientHistoryQuery.Handler.cs:56-65`](../../server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetPatientHistory/GetPatientHistoryQuery.Handler.cs) | ✅ read | | **Patient-scoped, not booking-scoped** — a new nurse reads the whole history | `GetPatientHistoryAsync(patientId, …)` | ✅ live (2 notes span bookings 3 and 4) | | **One access resolver** — edit = owning customer, append = nurse with a confirmed booking, view = either or admin | [`PatientAccess.cs:24-50`](../../server/src/Core/Baya.Application/Features/PatientCareRecords/PatientAccess.cs) | ✅ live | | `record_access` **never 403s** — always 200 with non-leaking flags (deliberate INV-7 exception) | same | ✅ live (patient 9999 → 200) | | **Tenancy mismatch is a 404, never a 403** ([server/CLAUDE.md hard rule 20](../../server/CLAUDE.md)) | — | ❌ **VIOLATED — see gaps** | | Nurse view never wires plan editing | [`NurseVisitNotesPanel.tsx`](../../client/src/app/%5Blocale%5D/%28private-routes%29/nurse/visits/%5Bid%5D/NurseVisitNotesPanel.tsx) imports no `useUpdateCareRecord` | ✅ read | | Client hard rule 19 — never leak clinical data; gate access **before** any clinical fetch | `useRecordAccess` gates `enabled:` on the other two queries (`:42-43`) | ✅ read | Note counts are not load-bearing; there are no money or config numbers in this flow. ## How to test 1. Log in as `09120000010` (سارا محمدی, customer) — see [testing-setup.md](testing-setup.md). 2. Go to `/fa/patients`, open a patient, tap پروندهٔ مراقبت (`/fa/patients/1/record`). **Expect (today, mocked):** all four tabs populated — «متفورمین ۵۰۰», a routine list, a task list, and a month-grouped سوابق timeline. **None of it is on the server**; it is [`mockApi.ts:47`](../../client/src/services/patientRecords/apis/mockApi.ts) `defaultFamilyRecord`, and it **resets on every page reload**. 3. Navigate to `/fa/patients/8888/record`. **Expect:** the access-denied card. `8888` is `MOCK_FOREIGN_PATIENT_ID` (`constants.ts:36`) — a mock-only sentinel with no real-path equivalent. 4. To see the **truth**, bypass the client: `curl -s --noproxy '*' "http://localhost:5002/api/v1/patients/1/care_records?page=1&pageSize=10" -H "Authorization: Bearer $TOKEN"`. **Expect:** `200`, `total: 2`, two Persian bodies by «زهرا عزیزی» — and `taskResults` entries with **`label: null, done: false`** (a seed defect, see gaps). `GET /patients/1/care_record` → `200` with **three empty arrays** — the seeded world has no care plan at all. 5. Log in as `09120000001` (زهرا عزیزی, nurse), open `/fa/nurse/visits/3`, scroll past EVV. **Expect:** the checklist + composer + continuity history — again all mock. `GET /patients/1/record_access` with the nurse token returns `canAppendNote: true`, so the real path would also permit the append. **Seeded-world limits:** no care plan row exists for any patient, so the real path's medications/routine/tasks tabs and the nurse's checklist would all render empty. Only patients 1 and (nurse 2 / customer 011's infant) have visit notes. Creating a plan requires a real `PUT /patients/1/care_record`. ## Known gaps - `USE_PATIENT_RECORDS_MOCK = true` (`patientRecords/constants.ts:13`) while **all five server endpoints are live** — the entire clinical surface both actors see is in-browser fiction that resets on reload. - **Care-plan shape mismatch blocks the flip.** Server `MedicationDto(long Id, string Name, string? Dosage, string Frequency, string? TimingNote)` ([`CarePlanDtos.cs:11`](../../server/src/Core/Baya.Application/Models/Patients/CarePlanDtos.cs)) vs client `Medication { id: string; doseAmount; doseUnit; frequencyCode; frequencyText; timeOfDay: TimeOfDayCode[] }` ([`types.ts:56-66`](../../client/src/services/patientRecords/types.ts)). Only `name`/`timingNote` survive. The ui-phase-9 structured addendum was never delivered server-side. - **H-17 ID-TYPE MISMATCH — CONFIRMED, not fixed.** Client `Medication.id` / `RoutineItem.id` / `CareTask.id` are `string` (`types.ts:57,70,78`, mock-seeded `'m1'`/`'r1'`/`'t1'`); the wire is `long Id`. `updateFamilyRecord` `PUT`s the record whole, ids included, so a flip sends `"m1"` where the server binds `long` — the write is unsafe, not just lossy. - **`RoutineItem.timeOfDay` is an array client-side, a single `string?` server-side** (`RoutineItemDto`, `CarePlanDtos.cs:13`) — multi-slot routine items cannot round-trip. - **`deniedReason` vocabulary disagrees.** Server emits `"not_authorized"` (`PatientAccess.cs:20`); the client union is `'no_access' | 'not_found'` (`types.ts:117`) and [the integration doc](../integration/domains/patient-records.md) documents `no_access`. A real denial would land on an unmapped code. - **Structured `taskResults` are discarded in both directions on the real path.** The server accepts them on write (`WriteCareRecordBody.TaskResults`, controller `:63`) and returns them on read (`CareRecordDto.TaskResults`), but `clientApi.ts:37` hardcodes `taskResults: []` and `clientApi.ts:100-103` posts only `{bookingId, body}`, folding the checklist into free text via `composeVisitNoteBody`. The `:36`/`:45` comments saying the wire has no structured field are wrong. - **Seeded visit notes return `label: null, done: false`.** `DemoLifecycleSeeder.Social.cs:323,330,337` writes camelCase JSON (`[{"label":…,"done":true}]`) while `GetPatientHistoryQuery.Handler.cs:78` deserializes `List` with default (PascalCase) options. Live-confirmed: 3 result rows, all null/false. API-written notes round-trip fine; only the demo data is broken. - **Tenancy leak: a foreign patient returns 403, not 404.** Live-confirmed — customer `09120000011` on `GET /patients/1/care_records` got `403 "You do not have clinical access to this patient's care records."`, confirming patient 1 exists. `GetPatientHistoryQuery.Handler.cs:56-58`, `GetCarePlanQuery.Handler.cs:23-24`, `UpsertCarePlanCommand.Handler.cs:24-25` all return `ForbiddenResult`. Violates server/CLAUDE.md hard rule 20 and INV-7. - **No care plan is seeded for any patient** — `GET /patients/1/care_record` returns three empty arrays, so on the real path the nurse's task checklist would always be empty and the E2 plan tabs blank. - **No `product/business/` file covers clinical records** — no owning business area for encryption, the append-only rule, or the clinical access gate. Everything above is derived from code, not from a product decision. - **`MOCK_FOREIGN_PATIENT_ID = 8888`** (`constants.ts:36`) is the only way to demo access-denied; the real path has no such sentinel, so that screen state is untested against the server. - Four stale doc-blocks assert "no wire endpoint exists" / "REQ-027 gap": `clientApi.ts:61-62`, `constants.ts:6-11`, `types.ts:11-16`, `apis/index.ts:7-8`. All four are wrong; the integration domain file is right. (`docs/status/mocks-registry.md`, which recorded the correct verdict, no longer exists — [docs/status/](../status/index.md) is now only an index.)