Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 116 additions & 25 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions ARCHITECTURE_ESSENTIALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ Quick-reference only. Full reasoning lives in `ARCHITECTURE.md`. Full product sc
## Data model map (see ARCHITECTURE.md §4 for full field lists)
`User` → `PatientProfile | DoctorProfile | FacilityProfile`; `User` → `AdminInvite` (Phase 8, issue #39 — hashed/expiring/single-use; `User.deactivatedAt` soft-deactivates an account, refusing login/refresh/access tokens without deleting anything)
`DoctorProfile` —< `DoctorSpecialty` >— `Specialty`
`AvailabilitySlot` (doctor's calendar) → `Appointment` → `IntakeForm`, `Consultation`
`Consultation` → (outcome=referred) → `Referral` (snapshot!) → new `Appointment`
`AvailabilitySlot` (doctor's calendar) → `Appointment` (`availability_slot_id` nullable — a cancelled appointment releases its slot; booking safety is `claimOpenSlot`, not that column) → `IntakeForm`, `Consultation`
`Consultation` → (outcome=referred) → `Referral` (snapshot! required `target_doctor_id`, self-FK lineage `parent_referral_id`, no-show/loop/admin flags) → new `Appointment`
`Appointment` → `Consultation` → immutable `Prescription`(+`PrescriptionItem`) / `LabOrder`(+`LabResult`)
`FulfillmentRecord` (facility-owned) references an order but doesn't mutate it.
`User` → `EmailVerificationToken` (Phase 8, issue #34 — hashed/expiring, single-use; `User.emailVerifiedAt` gates booking + payment mutations, never login/browsing).
`User` → `EmailVerificationToken` (Phase 8, issue #34 — hashed/expiring, single-use; `User.emailVerifiedAt` gates booking, consultation/referral mutations and payment mutations, never login/browsing).
`User` → `AccessLog` (Phase 8, issue #45 — PHI read-access log: who viewed which clinical record, when; fire-and-forget writes, fail-open).
`LoginAttempt` (Phase 8, issue #35 — Postgres-backed login throttle buckets, one per normalised email and per client IP; counters move only via a conditional upsert).
`PatientProfile` → `PatientExport` → `PatientExportChunk` (Phase 8, issue #40 — right-of-access export jobs + their chunked JSON bundle; each module contributes a section via its own service, registered in `src/export-sections.ts` — see ARCHITECTURE.md §3.1).
Expand All @@ -40,10 +40,12 @@ Quick-reference only. Full reasoning lives in `ARCHITECTURE.md`. Full product sc
- The actual Stellar contract calls go through `@docforum/escrow-sdk` (`backend/src/modules/payments/services/escrow-sdk-loader.ts` — dynamic `import()`, the SDK is ESM-only and this backend is CommonJS). Never reimplement contract-calling logic in this repo.

## Known unresolved design gaps (do not silently "fix" without flagging in PR description)
- No routing rule yet for a referral with no specific target doctor (which specialist gets it?).
- No expiry/TTL job for held slots or pending referrals — currently just an enum value with no worker.
- No routing rule yet for a referral with no specific target doctor (which specialist gets it?). **v1 re-scope (2026-10-17): `target_doctor_id` is required, so unassigned referrals can't exist — but that makes PRD §8.2.2's "waiting for a specialist" state unreachable rather than implemented. ADR still owed (issue #6).**
- No time-based expiry/TTL job for held slots or pending referrals — the referral *no-show* cascade (#67) now reaches `expired`, but nothing expires a referral (or a held slot) purely by age. Issue #8 / #19.
- `AuditEvent` exists (§4.10 of ARCHITECTURE.md) and records booking (both booking paths), doctor verification, payment fund/release/refund, the patient's referral accept/decline and the referral admin actions — auth (the admin login-throttle clear), the referral's creation + cascade transitions, and the remaining Phase 4 order transitions are still unaudited. PRD §7 is not satisfied yet.
- `Referral.snapshotIntakeId` has no column — `IntakeForm` doesn't exist yet (issue #1); the notes half of the snapshot rule is real and enforced.
- Polymorphic `FulfillmentRecord.order_id` is a sketch, not final — may need splitting into two tables.
- `AuditEvent` foundation exists (Phase 8, issue #43 — ARCHITECTURE.md §4.10): append-only (DB triggers), written by `auditService.record(tx, …)` **inside the same transaction** as the change. Wired for booking, doctor verification and payment fund/release/refund only — **Phase 3/4 transitions must call it when they're built.** Never put PHI in `metadata`.
- `AuditEvent` foundation exists (Phase 8, issue #43 — ARCHITECTURE.md §4.10): append-only (DB triggers), written by `auditService.record(tx, …)` **inside the same transaction** as the change. Wired for booking (direct and referral-origin), doctor verification, payment fund/release/refund, referral accept/decline and referral admin actions (`entity_type: 'Referral'`) — **Phase 4 transitions must call it when they're built.** There is one audit table: referral actions go into it, not into a second one. Never put PHI in `metadata`.
- No email/SMS provider integrated yet (Phase 6, issue #17) — email verification (issue #34) therefore can't deliver its link on a hosted deployment; `backend/scripts/mark-email-verified.mjs` is the interim operator path.
- One doctor = one calendar assumed; multi-facility doctors not modeled.

Expand Down
14 changes: 7 additions & 7 deletions PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,17 @@ The core value proposition: **collapse a multi-visit, multi-queue hospital journ

### 8.1 What do we think would break?
1. **Double-booking under concurrency** — two patients booking the same slot simultaneously without a locking/transaction strategy at the DB level. This is the single most likely v1 bug class. **Resolved 2026-09-14:** `availability/repositories.ts`'s `claimOpenSlot` (a conditional `UPDATE ... WHERE status = 'open'`) makes this a DB-level guarantee, not an app-logic one. Proven by `backend/tests/integration/booking.test.mts` — 8 genuinely concurrent booking requests on the same slot, exactly 1 succeeds, verified against a real Postgres instance every run.
2. **Referral loops** — Doctor A refers to specialty B, specialist in B refers back to A or sideways to C, with no cycle detection or max-hop limit → patient stuck in a referral loop with no human-visible "this isn't working" signal.
3. **Orphaned orders** — a prescription/lab order issued, but the patient never selects a fulfilling facility, and there is no reminder/expiry logic → orders silently rot.
4. **Referral context loss** — if referral doesn't strictly carry forward intake + notes (FR-7/FR-8 not enforced at the data layer), the whole value proposition (no re-explaining) quietly breaks and nobody notices until a patient complains.
2. **Referral loops** — Doctor A refers to specialty B, specialist in B refers back to A or sideways to C, with no cycle detection or max-hop limit → patient stuck in a referral loop with no human-visible "this isn't working" signal. **Resolved 2026-10-17 (Phase 3, issue #65):** every referral carries a lineage (`parent_referral_id`, derived consultation → appointment → referral), the creation transaction walks that chain with a bounded CTE (default `REFERRAL_MAX_HOPS = 4`), and a target doctor or specialty that already appears **anywhere** in the chain — including the original referring GP — is *flagged* (`loop_flagged_at`/`loop_flag_reason`) and surfaced in the admin queue rather than silently allowed or hard-blocked (a human decides whether the repeat is clinically legitimate). Self-referral and hop-limit overflow are hard `422` rejections. Unit-tested (`tests/unit/referral-loop.test.ts`) and integration-tested end-to-end (`tests/integration/referral-loops.test.mts`).
3. **Orphaned orders** — a prescription/lab order issued, but the patient never selects a fulfilling facility, and there is no reminder/expiry logic → orders silently rot. **Referral half partly addressed 2026-10-17 (issue #67):** repeated no-shows on a referral-origin appointment now expire the referral and flag the referring doctor. **Still open:** time-based expiry (a pending referral nobody accepts, a held slot nobody books, and orders generally) has no job yet — issues #8 and #19; orders themselves stay Phase 4.
4. **Referral context loss** — if referral doesn't strictly carry forward intake + notes (FR-7/FR-8 not enforced at the data layer), the whole value proposition (no re-explaining) quietly breaks and nobody notices until a patient complains. **Notes half resolved 2026-10-17 (issue #5):** `Referral.snapshotNotes` is a copy written in the creation transaction and there is no consultation-editing path that could reach it; acceptance re-books through the real slot-claim path carrying the same snapshot, so no re-intake happens. The **intake** half is not enforceable yet — `IntakeForm` doesn't exist (issue #1) and `Referral.snapshotIntakeId` is deliberately absent rather than faked. Flagged, not dropped.
5. **Specialty mismatch at signup** — doctors self-select specialty with no verification → patients get referred into a specialty the doctor doesn't actually practice.
6. **Facility fulfillment integrity** — nothing currently stops a facility from marking an order "fulfilled" without actually doing the work, or a patient walking into an unqualified/non-partnered facility with a screenshot of an order.

### 8.2 What edge cases are we missing?
1. Patient has **no matching specialist available** (none online / none accepting new patients) after a referral — what happens?
2. Doctor **refers to a specialty with zero verified doctors on the platform** — dead end.
3. Patient **cancels or no-shows** after a referral was already created downstream.
4. **Multiple simultaneous referrals** — can a single consultation spawn more than one referral (e.g., "see both cardiology and endocrinology")?
1. Patient has **no matching specialist available** (none online / none accepting new patients) after a referral — what happens? **Still open, narrowed 2026-10-17:** with v1 requiring a concrete verified `target_doctor_id`, the dead end is now "the named doctor never offers a slot" rather than "nobody exists in this specialty" — and that case still has no time-based expiry (issue #8), so the referral sits `pending` forever until a human in the admin queue finds it. Not resolved; re-read with 8.2.2 and issue #6.
2. Doctor **refers to a specialty with zero verified doctors on the platform** — dead end. **Re-scoped 2026-10-17, not implemented as specified (Phase 3, issue #5 / #6):** this state is now **unreachable by construction** — creation rejects a target doctor who isn't verified (and isn't verified in the claimed specialty when one is claimed), so no referral can point at a dead end. The "waiting for a specialist" distinct patient-facing state issue #5's DoD asked for is therefore *not* built; it only becomes possible once the specialty-only routing ADR (issue #6) lands. Deliberate scope reduction — flagged so it isn't mistaken for a completed requirement.
3. Patient **cancels or no-shows** after a referral was already created downstream. **Resolved 2026-10-17 (issue #67):** the appointment status transition and the referral state change happen in one transaction, so a referral can never be left `accepted` behind a dead appointment. Cancelled → referral returns to `pending` (slot released, re-bookable, same snapshot); first no-show → `pending` with `noShowCount = 1`; second no-show → `expired` with `referring_doctor_notified_at` set (threshold is a deliberately boring default, not a tuned one). Every write is conditional on the referral still pointing at that appointment, so a doubled hook is a no-op — integration-tested in `tests/integration/referral-cascade.test.mts`.
4. **Multiple simultaneous referrals** — can a single consultation spawn more than one referral (e.g., "see both cardiology and endocrinology")? **Resolved 2026-10-17 (issue #66, decision recorded in `docs/adr/0006-multiple-referrals-per-consultation.md`):** yes — an atomic all-or-none batch (`POST /referrals/from-consultation/:consultationId`, equivalently `POST /consultations/:id/referrals`) whose referrals then live **independently** (accept/decline each on its own; no bundle lifecycle). Duplicate doctor *or* specialty within a batch, or against a referral already made from that consultation, fails the whole batch. Each target gets its own snapshot copy, and `GET /referrals/mine` groups them by source consultation for the patient's "one visit, several referrals" view.
5. **Minor/dependent patients** — booking on behalf of a child or dependent; consent and account ownership model undefined.
6. **Doctor moonlighting across facilities** — same doctor, multiple hospitals/clinics, different availability per location — v1 assumes one doctor = one availability calendar, which may be wrong.
7. **Lab result requiring urgent follow-up** — abnormal result with no automatic escalation path back to a doctor.
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# docforum-core

![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)
![Status](https://img.shields.io/badge/status-Phase%201-yellow.svg)
![Status](https://img.shields.io/badge/status-Phase%203%20in%20progress-yellow.svg)
![Stack](https://img.shields.io/badge/stack-Node.js%20%2B%20TypeScript-339933.svg)

**The backend of DocForum** — a platform that collapses a multi-visit,
Expand Down Expand Up @@ -91,6 +91,7 @@ User ─┬─ PatientProfile

AvailabilitySlot (doctor's calendar) → Appointment → IntakeForm, Consultation
Consultation ──(outcome = referred)──▶ Referral (immutable snapshot!) → new Appointment
Referral ──▶ AuditEvent (append-only admin actions)
Consultation → Prescription (+ PrescriptionItem) / LabOrder (+ LabResult)
FulfillmentRecord (facility-owned) references an order but never mutates it
PaymentIntent / WalletLink — relational metadata only; contract logic lives in docforum-escrow
Expand Down Expand Up @@ -183,8 +184,15 @@ the safe-booking foundation): built and tested** — auth, doctor
verification, availability/slot generation, and the booking-concurrency
guarantee are real, not aspirational (see Getting started above). One
item is unverified: the Docker Compose file for local dev exists but
wasn't exercisable in this environment. Phase 2 (intake, appointments
lifecycle beyond booking, consultations) is next.
wasn't exercisable in this environment. **Phase 3 (referrals): the core
is built and tested** — referral model with an immutable notes snapshot,
patient accept/decline producing a real pre-populated appointment through
the same slot-claim path, no-show/cancellation cascade, loop detection
with a hop limit, multi-referral bundles from one consultation, and an
admin queue with audited reassign/cancel. Still open within Phase 3: the
routing ADR for specialty-only referrals (issue #6) and a time-based
expiry job (issue #8). Phase 2's remaining pieces (intake, doctor's
today view, specialty discovery) come next.

Full phase breakdown: [`ROADMAP.md`](ROADMAP.md).

Expand Down
Loading
Loading