Repository navigation
feat(referrals): Phase 3 referral core — creation, accept/decline, cascade, loop guard, admin overrides - #153
Conversation
|
@GdAyo19 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
@GdAyo19 fix conflicts |
|
CI is failing on this PR.
Please push a fix and CI will re-run. Before pushing, run |
|
CI fails at |
|
@GdAyo19 Thanks for the PR. It can't be merged yet. To unblock it: CI is failing. Here's what broke:
Please fix these errors and run the same steps locally before pushing ( Merge conflicts: this branch conflicts with Push to this same branch and CI will re-run. Once it's green and conflict-free, it'll be reviewed for merge. |
|
i have fixed the conflicts please merge @afeezorobsco-cyber |
…scade, loop guard, admin overrides A referral was still a Phase 0 sketch, and the two bug classes it was most likely to get wrong — a double-booked acceptance and a patient stuck bouncing between specialists — had no mechanism behind them at all. Closes issues DocForum#5, DocForum#7 and DocForum#65–DocForum#68 (plus the minimal DocForum#3/DocForum#4 slices they need: appointment status transitions and a one-per-appointment Consultation record). `Referral` now carries an immutable copy of the consultation notes, the lineage parent that loop detection walks, and the no-show/loop/admin bookkeeping; `Appointment.availabilitySlotId` became nullable so a cancellation can release its slot without erasing history; and `AuditEvent` is a new append-only table for the admin reassign/cancel actions. Accept books a real appointment through the same claimOpenSlot conditional UPDATE as a direct booking — proven by 8 genuinely concurrent accepts, exactly one of which wins — and appointment status transitions cascade back into the referral inside the same transaction, so a referral can never be left accepted behind a dead appointment. Repeat doctors or specialties anywhere in a chain are flagged for the admin queue rather than silently allowed; chains over the hop limit are hard rejections. Two deliberate re-scopes for review, not silent defaults: a concrete verified target_doctor_id is required, so the routing gap (DocForum#6) is closed for v1 by making unassigned referrals impossible — which also makes PRD §8.2.2's distinct "waiting for a specialist" state unreachable rather than implemented, and the ADR is still owed; and snapshotIntakeId has no column because IntakeForm doesn't exist yet (DocForum#1), so the intake half of the snapshot rule is noted as unenforceable rather than faked. Time-based referral expiry (DocForum#8) is likewise still open — only the no-show path reaches expired. Docs reconciled in the same change (ARCHITECTURE §4.2–§4.4/§4.10/§5.2/§6, ARCHITECTURE_ESSENTIALS, PRD §8.1.2–§8.1.4 and §8.2.1–§8.2.4, ROADMAP Phase 2/3 + changelog, docs/api/README.md, README, docs site). Tests: 42 unit + 55 integration against real embedded Postgres.
… PR closes-keywords ready decision, which the implementation had already made (independent lifecycles, atomic all-or-none creation, grouped only for the patient view, no bundle table) but never written down. Adds docs/adr/0006-multiple-referrals-per-consultation.md and points ARCHITECTURE §5.2, PRD §8.2.4, ROADMAP and docs/api at it. Also draws out the referral status machine in ARCHITECTURE §4.4 — DocForum#67's state-diagram DoD — with the conditional-UPDATE rule that makes the cascade idempotent.
The Phase 3 referral branch added its own `AuditEvent` model (referral-shaped: actorRole as a string, a referralId FK, a free-text reason column). Upstream's issue DocForum#43 work added the canonical, append-only `AuditEvent` (entityType / entityId / fromState / toState / requestId, DB triggers rejecting mutation). Having both in one schema is invalid Prisma (P1012: duplicate model) and would have meant two audit trails with different guarantees. Resolution: keep upstream's single AuditEvent table and record the referral admin actions in it as `entityType: 'Referral'` rows, written through the audit module's `record()` inside the transaction of the referral change — so the append-only triggers cover them too, and `GET /referrals/admin/:id/audit-events` reads the same rows `GET /admin/audit-events` serves. - schema: drop the duplicate model and the `Referral.auditEvents` relation. - migration: the referral migration no longer creates the table (upstream's 20261017212335_add_audit_event owns it) and no longer adds `Consultation.appointmentId` (upstream's 20261020000000_order_payment_intents owns that column, its unique index and its FK — adding it twice made `prisma migrate deploy` fail on a duplicate column). - referrals service: admin reassign/cancel now call `auditService.record()` and the trail is read through the audit module's service, so the referrals module never touches the audit table directly (module-boundary hard rule). - reason: bounded to 200 chars (the audit metadata string limit) and carried in `metadata.reason` — the shared table has no free-text reason column. - appointments: the export section tolerates the now-nullable `availabilitySlotId` (a cancelled appointment releases its slot). Co-Authored-By: OpenCode <opencode@opencode.ai>
…th main - ARCHITECTURE.md §3/§4.4/§4.10 + ARCHITECTURE_ESSENTIALS.md: one audit trail — the referral admin actions are 'Referral'-typed rows in the Phase 8 AuditEvent table, not a second table. - ARCHITECTURE.md §4 intro: the referral migration owns the Referral columns; the AuditEvent table is upstream's migration. - docs/api/README.md: drop the duplicated 'Audit trail' section left by the merge, document the referral audit rows and the recorded actions, and fix a mojibake range in the escrow-events section. - ROADMAP.md: the audit-wiring status, the referral branch's entry, and a note on the rebase. - integration tests: the referral suites move to ports 5445-5447 — main already uses 5433-5442, and vitest runs the files in parallel, so the old 5440-5442 collided with payments-bigint, audit and patient-export. Co-Authored-By: OpenCode <opencode@opencode.ai>
6b96dde to
8b534d3
Compare
…produces The referral accept path created an Appointment through bookForReferral without writing an audit row, so it was the one way to create an appointment that left no trail at all — and the existing `appointment.booked` event's `origin` metadata (always `direct` until now) was clearly built to tell the two booking paths apart. - appointments: `bookForReferral` now writes the same `appointment.booked` event a direct booking writes, in the caller's transaction, with `origin: 'referral'` and the referral it came from. - referrals: the patient's own state change is recorded as `referral.accepted` / `referral.declined`, in the same transaction as the accept — so the decision and the appointment it produced are recorded together or not at all. - `declineReferral` now runs in a transaction; it previously used the root client, which `auditService.record()` refuses by design. - The controller builds the actor with `auditActor(req)`, like the admin referral routes already did. Still unaudited, and recorded as an open gap in ARCHITECTURE.md §4.10 rather than silently dropped: the referral's creation (`null → pending`) and the two transitions the appointment cascade drives (`accepted → completed`, `accepted → expired`) — the cascade fires from inside the appointment's status transition, which has no actor of its own to attribute. Both are owed when Phase 4/6 wire the remaining state changes. Tests: two new integration cases asserting the audit rows against the real AuditEvent table — the accept's two events (the referral transition and the appointment it booked) and the decline's one. Co-Authored-By: OpenCode <opencode@opencode.ai>
afeezorobsco-cyber
left a comment
There was a problem hiding this comment.
Reviewed: no malicious code found, CI is green. Merging.
#153 added a repo-relative link (docs/adr/0006-...md) to ROADMAP.md. The docs sync copies ROADMAP.md into the VitePress site verbatim, where that path has no page, so vitepress build failed its dead-link check and the docs deploy on main broke. Remote files already get their relative links rewritten to GitHub URLs; apply the same rewrite to local files. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #65
Closes #66
Closes #67
Closes #68
What this does
Referralis real (migration20261016214929_referral_phase3): immutable notes snapshot, required verified target doctor, lineageparentReferralId, no-show counter + referring-doctor flag, loop flags,cancelledstatus + reason.POST /referrals, gates in one transaction (own consultation, no self-referral, target verified [+ in specialty], no duplicate target per consultation).POST /referrals/from-consultation/:consultationId/POST /consultations/:id/referrals), one snapshot copy per target, independent acceptance afterwards.pending → accepted(one winner under concurrency), then a real appointment booked through the sameclaimOpenSlotconditional UPDATE as a direct booking —origin: 'referral', patient picks the slot, no re-intake.pendingwith the slot released; no-show → count++, pending, thenexpired+referringDoctorNotifiedAtat threshold 2. Every write is conditional, so a doubled hook no-ops.REFERRAL_MAX_HOPS(default 4) are hard 422s.AuditEventtable.Two re-scopes a reviewer should check (flagged, not absorbed)
target_doctor_idis required — rather than ship a nullable column with no assignment logic, v1 makes unassigned referrals impossible. Consequence: PRD §8.2.2's distinct "waiting for a specialist" state is unreachable by construction rather than implemented. The routing ADR ([Phase 3] ADR: referral routing rule when no target doctor is specified #6) is still owed and the gap stays open.Referral.snapshotIntakeIdhas no column —IntakeFormdoesn't exist yet ([Phase 2] IntakeForm model + structured symptom entry #1). The notes half of the snapshot rule is real and enforced; the intake half is noted as unenforceable rather than faked.Still open, deliberately: #8 (time-based referral expiry — only the no-show cascade reaches
expiredtoday).Testing
npm run typecheckclean; 42 unit + 55 integration tests pass (integration suites are self-contained: each spins up its own embedded Postgres, no pre-existing DB needed).booking.test.mts).Docs (same change, per AGENTS.md)
ARCHITECTURE.md§3/§4.2–§4.4/§4.10/§5.2/§6.1.2/§6.2.2/§6.2.4,ARCHITECTURE_ESSENTIALS.md(data model map + known gaps),PRD.md§8.1.2–§8.1.4 and §8.2.1–§8.2.4,ROADMAP.md(Phase 2/3 status + changelog),docs/api/README.md(all new endpoints; also corrected its "not yet implemented" list, which still claimedpaymentshad no routes),README.md,docs/site/repos/core.md.