Files
baya-monorepo/archive/docs/flows/care-circle-patients.md
T
2026-08-02 20:01:31 +03:30

11 KiB

Flow — care-circle-patients

Last verified: 2026-08-02 against commit c841bde

Actor(s): customer · Status: partial Client: partial · Server: real Business source: product/business/01-actors-and-onboarding.md · data model: product/data-model/10-reviews-and-records.md Integration: docs/integration/domains/patients.md · patient-records.md

What it does

A customer keeps a list of the people they arrange care for — «حلقهٔ مراقبت», renamed from «بیماران» in ui-phase-9. Each one is a first-class row, not the account holder: the payer is usually an adult child and the care recipient an elderly parent or a newborn. Tapping a person opens their care record — the family's own medication/routine/task plan, plus the read-only history of visit notes nurses wrote for them.

The seam splits mid-flow. The list screen is real end to end and was probed live. The record screen is real UI on a mock service (USE_PATIENT_RECORDS_MOCK = true, patientRecords/constants.ts:13) — even though all five server endpoints exist and return 200. Nothing you see on /record comes from the database.

Screens

Step Route Component / notes
E1 — the care circle /fa/patients patients/page.tsx · PatientCard (uses InitialsAvatar) · add/edit in FormDialogShell + PatientForm · archive behind ConfirmDialog
— home teaser /fa HomeScreen.tsx:87 calls the same usePatients()
E2 — care record /fa/patients/[id]/record record/page.tsx · 4 tabs داروها / روتین / سوابق / وظایف · PatientHeader, VisitNoteCard, per-item bottom-sheet edit · EmptyState access-denied card gated before any clinical fetch
— nurse counterpart /fa/nurse/visits/[id] NurseVisitNotesPanel.tsx:38-44 — same domain, append-only

Age is derived client-side by patients/age.ts; the wire carries birthDate.

API

Call Endpoint Notes
usePatientspatientsClientApi.list GET /api/v1/patients/list real · clientApi.ts:46PatientsController.List
usePatient GET /patients/get/{id} real
useCreatePatient / useUpdatePatient POST /patients/create · /update/{id} real · relation+conditions round-trip (REQ-005)
useArchivePatient POST /patients/archive/{id} real · archive, never delete
useRecordAccess GET /patients/{id}/record_access server real, client mocked
usePatientCareRecord GET /patients/{id}/care_record server real, client mocked
useUpdateCareRecord PUT /patients/{id}/care_record server real, client mocked — the only PUT in the API
usePatientHistory GET /patients/{id}/care_records server real, client mocked
useCreateVisitNote (nurse) POST /patients/{id}/care_records server real, client mocked

Handlers: PatientsController.cs (Features/Identity/{Commands,Queries}) and PatientCareRecordsController.csFeatures/PatientCareRecords/**. Request/response shapes live in the integration docs linked above — not here.

Rules that must hold

Rule Source
A patient is a first-class row distinct from the customer; self still creates its own row. Customer→patient is 1:N business/01
gender is load-bearing — it drives same-gender caregiver matching. Never defaulted or inferred integration/patients.md
Archive, never delete. A patient referenced by a booking must stay resolvable integration/patients.md
The plan is family-owned; visit records are nurse-owned and append-only. No edit, no delete endpoint exists and none should integration/patient-records.md
Two-stage clinical disclosure — clinical bodies encrypted at rest, decrypted only after the access check; nurse read is scoped by an active booking INV-6, server/CLAUDE.md rule 18
Tenancy mismatch is a 404, never a 403 api-contract.md
initialMedicalNotes is encrypted PII and is not the care record integration/patients.md

How to test

  1. Log in as 09120000010 (سارا محمدی, customer) — see testing-setup.md.

  2. Open /fa/patients. Expect: two cards — «فاطمه محمدی» and «حسن محمدی». Header reads «حلقهٔ مراقبت». Verified live: GET /patients/list?page=1&pageSize=10total: 2. Note: both seeded rows carry relation: null and conditions: [], so the relation chip and the condition chips render nothing. That is seed data, not a UI bug.

  3. Tap «افزودن» and save a new person. Expect: the card appears immediately (the list is invalidated on mutation) and a green «ذخیره شد». A new GET /patients/list now returns total: 3 — this writes to the shared remote DB.

  4. Tap a card's archive action and confirm. Expect: the card disappears, toast «بایگانی شد». The row is still resolvable server-side (isActive: false), so any booking that references it keeps working.

  5. Tap a card body to open /fa/patients/1/record. Expect: the ownership banner «این پرونده متعلق به خانواده است…», then four tabs. This screen is mock-served. What you see under داروها / روتین / وظایف is patientRecords/apis/mockApi.ts fixtures, and سوابق shows mock notes — not the two real seeded care records. Every edit you make is lost on reload (the mock store is module-level).

  6. To see the real data the screen is hiding, call it directly: curl --noproxy '*' "http://localhost:5002/api/v1/patients/1/care_records?page=1&pageSize=20" -H "Authorization: Bearer $T_09120000010". Expect: total: 2, two Persian notes by «زهرا عزیزی» dated 2026-07-16 and 2026-07-25 — and taskResults entries whose label is null (see gaps).

  7. Access control, verified live and only observable via curl today:

    Caller record_access for patient 1 Result
    09120000010 (owner) canView: true, canEdit: true, canAppendNote: false
    09120000001 (nurse 1, has bookings for patient 1) canView: true, canEdit: false, canAppendNote: true
    09120000011 (other customer) canView: false, deniedReason: "not_authorized" denies; GET care_record for the same caller returns 403

    In the browser the mock's denial path is only reachable via MOCK_FOREIGN_PATIENT_ID = 8888/fa/patients/8888/record.

The seeded world still supports every step of this flow — it does not depend on booking-request freshness.

Known gaps

  • USE_PATIENT_RECORDS_MOCK = true (client/src/services/patientRecords/constants.ts:13) while all five server endpoints are live and return 200. The customer's care record and the nurse's visit-note panel both render fixtures instead of the database. The single highest-value flag flip in this flow.
  • Flipping that flag today breaks the record screen: getFamilyRecord (patientRecords/apis/clientApi.ts:68-69) is a bare unwrap() with no mapper, and the wire shape does not match. Server MedicationDto(long Id, string Name, string? Dosage, string Frequency, string? TimingNote) vs client Medication { id: string, doseAmount, doseUnit, frequencyCode, frequencyText, timeOfDay: TimeOfDayCode[], timingNote } — dose/frequency/time-of-day all render blank.
  • Same flip crashes the روتین tab: server RoutineItemDto.TimeOfDay is a single string?; the client types it TimeOfDayCode[] and calls .map() on it (record/page.tsx:616).
  • Care-plan id type mismatch (hardening H-17, confirmed): server long, client string. updateFamilyRecord PUTs the whole record including client temp ids like new-1754… (record/page.tsx:277), so the write is unsafe against the real endpoint.
  • Server defect — taskResults labels are lost. GetPatientHistoryQuery.Handler.cs:78 deserializes TaskResultsJson with default (case-sensitive) JsonSerializer options, but DemoLifecycleSeeder.Social.cs:324 seeds camelCase {"label":…,"done":…}. Live result for patient 1: three entries of {"label": null, "done": false}. Any externally-written JSON in that column silently degrades to blank labels.
  • Client discards a field the server does serve. toVisitNote (patientRecords/apis/clientApi.ts:37) hardcodes taskResults: [] although CareRecordDto.taskResults is on the wire. The «X از Y» task-summary chip on VisitNoteCard can never render on the real path.
  • deniedReason enum mismatch: the server returns "not_authorized" (PatientAccess.cs:20), while patientRecords/types.ts:117 and docs/integration/domains/patient-records.md both declare no_access | not_found. Latent today (the UI renders a generic card), a bug the moment anyone branches on it.
  • Stale code comments assert the opposite of reality. patientRecords/types.ts:11-13, constants.ts:1-12 and apis/clientApi.ts:62-64 all say the family record and access check have "no backend" / are REQ-027 gaps. All three routes exist, are [Authorize]d, and were probed 200. The integration doc is right and the code comments are wrong.
  • patients/constants.ts:1-7 still explains the domain "demos behind the mock" because REQ-005 is missing — REQ-005 is delivered and the flag is already false.
  • Editing a patient destroys their birth date. PatientForm.tsx:94 always submits ageToBirthDate(age) = YYYY-01-01. Saving seeded patient 1 (birthDate: 1948-03-15) with no changes rewrites it to 1948-01-01. Age display survives; the date does not.
  • Seeded care plans are emptyGET /patients/1/care_record returns { medications: [], routine: [], tasks: [] }, so three of the four record tabs would be blank the moment the flag flips. DemoLifecycleSeeder never seeds a PatientCarePlan.
  • Seeded patients have relation: null and conditions: [], so the relation/condition chips the UI is built around never appear on demo data.
  • REQ-057 open: PatientDto has no lastVisitAt/visitCount and BookingListItemDto has no patientId, so no booking card can show a care teaser.
  • No product/business/ file covers patient care records at all — no documented rule for the append-only constraint, the encryption, or the clinical-access gate. Only product/data-model/10-reviews-and-records.md mentions the table. Largest product-doc hole touching this flow.
  • Archive is optimistic with no undo; a cross-tenant/stale id 404s and the card silently reappears with a generic «در دسترس نیست» toast (patients/page.tsx:65-70).