Files
baya-monorepo/docs/flows/patient-care-records.md
T
2026-08-02 17:18:36 +03:30

12 KiB

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. This is the atlas's largest product-doc hole. Integration: docs/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 — 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 mounts the panel under the EVV surface
E3 nurse visit note same route 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.tsmockApi, because USE_PATIENT_RECORDS_MOCK = true (constants.ts:13). The table is the real half (clientApi.ts) that is compiled but not selected. Shapes: docs/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
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: one controller, 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), plus constants.ts:6-8 and types.ts:11-13, 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 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 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) VIOLATED — see gaps
Nurse view never wires plan editing 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.
  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 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_record200 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) vs client Medication { id: string; doseAmount; doseUnit; frequencyCode; frequencyText; timeOfDay: TimeOfDayCode[] } (types.ts:56-66). 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 PUTs 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 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<TaskResultDto> 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 patientGET /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/ is now only an index.)