Skip to content

feat(consultations): consultation note sign-off with append-only addenda - #144

Open
Oyinkans0la12 wants to merge 9 commits into
DocForum:mainfrom
Oyinkans0la12:feat/consultation-note-sign-off-with-append-only
Open

Oyinkans0la12 wants to merge 9 commits into
DocForum:mainfrom
Oyinkans0la12:feat/consultation-note-sign-off-with-append-only

Conversation

@Oyinkans0la12

Copy link
Copy Markdown
Contributor

What changed

Implements consultation note sign-off from #64: Consultation.signedAt +
doctorProfileId, a ConsultationAddendum model, and
POST /consultations, GET /consultations/:id, PATCH /consultations/:id,
POST /consultations/:id/sign-off, POST /consultations/:id/addenda, plus an
auto-sign-off job for drafts left editable too long.

Closes #64

Why

Clinical notes are legal records. Editing a note in place after sign-off
destroys the record of what the doctor knew at the time — the same reason
prescriptions are append-only. Doctors still need to correct mistakes, which is
what addenda are for.

Implementation notes

  • Immutability is enforced twice, on purpose. Repository writes are scoped
    WHERE id = ? AND signedAt IS NULL, so a signed note simply doesn't match —
    but that only binds callers who go through the repository. The migration adds
    a BEFORE UPDATE trigger on Consultation that rejects any change to
    notes, outcome, or signedAt once the row is signed off. The integration
    test does exactly what the issue's review section asks: it updates a signed
    note through Prisma directly and expects a failure.
  • signedAt is in the trigger's guard too, so a signed note can't be
    un-signed back into mutability.
  • Addenda are append-only by construction — a separate table with inserts
    only, read back oldest-first. Adding one before sign-off is a 409: while the
    note is still a draft, an addendum would just be a confusing second draft.
  • Only the authoring doctor can read, edit, sign off, or add an addendum.
    Another doctor's consultation id is a 404 (not 403) so an id can't be used
    to confirm whose care thread it is.
  • Sign-off is a conditional update (WHERE signedAt IS NULL), so a second
    sign-off — or a concurrent one — is a 0-row update and a 409, never an
    overwrite of the original timestamp.
  • Auto sign-off runs as a plain interval from src/server.ts (src/jobs,
    per the "no queue until a real job needs one" rule), and is exposed as a
    function so tests drive it deterministically instead of waiting on a timer.

Schema change (proposed)

  • Consultation: + doctorProfileId (FK -> DoctorProfile), + signedAt
    (nullable) — ARCHITECTURE.md §4.3.
  • ConsultationAddendum: id, consultationId, authorDoctorProfileId, body, createdAt — §4.3.

Migration: backend/prisma/migrations/20261015182926_consultation_note_sign_off/
— generated with scripts/gen-migration.mjs (real Postgres diff), with the
trigger SQL appended by hand and commented in place.

New hard rule recorded in ARCHITECTURE_ESSENTIALS.md (rule 7): never edit a
consultation note/outcome once signed off; corrections are addenda.

Decisions the issue left open (documented, not guessed silently)

  • Can a referral be created before sign-off? No. A referral snapshots the
    consultation's notes at creation (hard rule 3), so allowing it on a draft
    would freeze a moving note — or let two referrals capture different contents.
    The rule is: a referral may only be created from a signed-off consultation.
    Recorded in ARCHITECTURE.md §4.3 and §4.4. It is not enforced in code yet
    because referral creation doesn't exist yet (Phase 3, [Phase 3] Referral model + creation from consultation outcome #5) — flagged as the
    thing [Phase 3] Referral model + creation from consultation outcome #5 must implement rather than pretended done here.
  • The auto-sign-off window is measured from createdAt, not from appointment
    completion.
    Consultation.appointmentId doesn't exist (issue [Phase 2] Consultation record + outcome #4 owns it),
    so "N hours after the appointment completes" isn't literally implementable
    yet. Flagged in the service, ARCHITECTURE.md §4.3 and
    ARCHITECTURE_ESSENTIALS.md known gaps.
  • Patient access to their own consultation waits on that same appointment
    link; today the routes are doctor-only and author-only. Flagged, not invented.

Docs updated in the same PR

ARCHITECTURE.md (§1 principle 4, §4.3 with the addendum model and design note,
§4.4 referral/sign-off rule), ARCHITECTURE_ESSENTIALS.md (hard rule 7, data
model map, three new known gaps), docs/api/README.md (all five endpoints),
ROADMAP.md (Phase 2 item + changelog). backend/src/jobs/README.md documents
the first real job.

Tests

npm run typecheck and npm run build clean. npm run test → 21 passed.
npm run test:integration → 12 passed (1 existing booking + 11 new).

Integration (tests/integration/consultations.test.mts, real Postgres):
draft is editable; empty PATCH is a 400; another doctor gets 404 for read,
sign-off and addendum (and the note is demonstrably untouched by those
attempts); addendum before sign-off is 409; sign-off works, a second one is
409, editing afterwards is 409; direct Prisma updates of notes,
outcome and signedAt on a signed row all throw, and the row is verified
unchanged afterwards
; addenda after sign-off append in order and leave the
note itself alone; auto sign-off picks up a backdated draft and makes it
genuinely immutable; and a fresh draft is left alone by the same job.

Test Files  2 passed (2)
     Tests  12 passed (12)   # npm run test:integration
Test Files  4 passed (4)
     Tests  21 passed (21)   # npm run test

Cross-PR note

ARCHITECTURE.md §4 and docs/api/README.md are also touched by the open
password-reset (#140), 2FA (#142) and session-management (#143) PRs — none of
them touches §4.3, so this one should rebase cleanly, but the §4 subsection
numbering may need a line moved.

Clinical notes are legal records: a consultation is a draft until it is
signed off, and immutable afterwards. Corrections are
ConsultationAddendum rows, not edits -- the same reasoning as hard rule 1
for prescriptions and lab orders.

Adds Consultation.signedAt + doctorProfileId, the ConsultationAddendum
model, and POST /consultations, GET/PATCH /consultations/:id,
POST /consultations/:id/sign-off and POST /consultations/:id/addenda.

Immutability is enforced twice on purpose: repository writes are scoped to
signedAt IS NULL (a signed note simply doesn't match), and a BEFORE UPDATE
trigger in the migration rejects any change to notes/outcome/signedAt on a
signed row -- including a direct Prisma update(), which the integration
test does exactly as the issue's reviewer would. signedAt is part of the
trigger's guard so a signed note can't be un-signed back into mutability.
Only the authoring doctor can read, edit, sign off or add an addendum.

An interval job (src/jobs, a plain interval per the no-queue-yet rule)
auto-signs-off drafts older than CONSULTATION_AUTO_SIGNOFF_HOURS.

Flagged rather than invented: the auto-sign-off window is measured from
createdAt because the appointment link (DocForum#4) isn't modelled yet, and the
referral-snapshot rule -- referrals may only be created from a signed-off
consultation -- is documented for Phase 3 to implement.

Docs updated in the same change: ARCHITECTURE.md §1/§4.3/§4.4, a new hard
rule 7 in ARCHITECTURE_ESSENTIALS.md, docs/api/README.md, ROADMAP.md.

Refs DocForum#64
@drips-wave

drips-wave Bot commented Oct 15, 2026

Copy link
Copy Markdown

@Oyinkans0la12 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! 🚀

Learn more about application limits

@afeezorobsco-cyber

afeezorobsco-cyber commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

@Oyinkans0la12 pls fix CI

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

CI is failing on this PR. main itself is green, so the cause is in this branch:

Typecheck fails: src/jobs/consultation-auto-sign-off.ts(24,29): Property 'consultations' does not exist on the config type. The job reads config.consultations.autoSignoffCheckIntervalMinutes, but src/config/index.ts has no consultations section. Add it (with an env var and a default) and document it in .env.example. This branch is also 3 commits behind main and now has merge conflicts, so please rebase.

Please push a fix and CI will re-run. Before pushing, run npm run prisma:generate && npm run typecheck && npm test && npm run test:integration in backend/.

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Oyinkans0la12

This PR has merge conflicts with main, which has moved on since this branch was created. Please rebase onto the latest main (or merge main into your branch), resolve the conflicts, and push. CI will then re-run, and the PR can be merged once it's conflict-free, CI is green and it's been reviewed. Thanks!

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Oyinkans0la12 Thanks for the PR. It can't be merged yet. To unblock it:

CI is failing. Here's what broke:

Backend (typecheck + test + build):

src/jobs/consultation-auto-sign-off.ts(24,29): error TS2339: Property 'consultations' does not exist on config
src/server.ts(9,1): error TS2304: Cannot find name 'startConsultationAutoSignOffJob'.

Please fix these errors and run the same steps locally before pushing (npm run typecheck && npm test && npm run build, or cargo test for contracts).

Merge conflicts: this branch conflicts with main. Please rebase onto the latest main (git fetch upstream && git rebase upstream/main), resolve the conflicts (often ROADMAP.md, ARCHITECTURE*.md or prisma/schema.prisma), and force-push.

Push to this same branch and CI will re-run. Once it's green and conflict-free, it'll be reviewed for merge.

@github-actions github-actions Bot added the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 20, 2026
@github-actions

Copy link
Copy Markdown

@Oyinkans0la12 This PR now has merge conflicts with main, most likely because #153 (feat(referrals): Phase 3 referral core — creation, accept/decline, cascade, loop guard, admin overrides) was just merged, so it can't be merged until it's rebased:

git fetch upstream            # upstream = https://github.com/DocForum/docforum-core.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-lease

The usual culprits are shared files like ROADMAP.md and ARCHITECTURE*.md. CI re-runs when you push, and the needs-rebase label is removed automatically once the conflicts are resolved.

Automated message. It won't repeat unless the PR conflicts again after your next push.

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Oyinkans0la12 CI still fails at typecheck. The Prisma models this PR uses aren't in the schema, and the config section is missing:

src/jobs/consultation-auto-sign-off.ts(24,29): error TS2339: Property 'consultations' does not exist on type '{ readonly port: number; ... }'
src/modules/consultations/repositories/index.ts(23,16): error TS2353: 'addenda' does not exist in type 'ConsultationInclude'
src/modules/consultations/repositories/index.ts(49,17): error TS2339: Property 'consultationAddendum' does not exist on type 'PrismaClient'

Add the ConsultationAddendum model (and the addenda relation on Consultation) to backend/prisma/schema.prisma with a migration, add the consultations block to src/config/index.ts, then run npm run prisma:generate && npm run typecheck && npm test.

Push to this branch and CI will re-run. Once it's green it can be merged.

The main merge resolution dropped the Consultation doctorProfileId/signedAt
fields, the ConsultationAddendum model, the config block, the
/consultations router mount and the auto-sign-off job startup, so Prisma
could not generate and the controllers were unreachable.

Consultations are now created against the appointment they close (the FK
added in DocForum#170): creation resolves it through the appointments service so
only the authoring doctor can open a note, and orders/fixtures seed it.
@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Oyinkans0la12 Thanks for rebasing. The malware scan passed, but the backend CI now fails at prisma generate. The conflict resolution in backend/prisma/schema.prisma kept both copies of the Consultation model's fields and dropped the new model:

error P1012: Field "outcome" is already defined on model "Consultation".
error P1012: Field "notes" is already defined on model "Consultation".
error P1012: Field "createdAt" is already defined on model "Consultation".
error P1012: Type "ConsultationAddendum" is neither a built-in type, nor refers to another model, composite type, or enum.
  1. In model Consultation, keep one copy each of outcome, notes and createdAt (the main versions plus your new sign-off fields), and add back model ConsultationAddendum { ... }.
  2. Port collision, the same one as on your feat(auth): password reset flow with single-use, hashed, expiring tokens #140/feat(auth): TOTP two-factor authentication for doctor, facility and admin roles #142: tests/integration/consultations.test.mts uses const PORT = 5439, which admin-lifecycle.test.mts already uses, so one file's beforeAll will time out. Use a free port such as 5450. Ports 5448 and 5449 are suggested for feat(auth): TOTP two-factor authentication for doctor, facility and admin roles #142 and feat(auth): password reset flow with single-use, hashed, expiring tokens #140.

Then run npm run prisma:generate && npm run typecheck && npm run test:integration in backend/ and push.

@github-actions github-actions Bot removed the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 20, 2026
afeezorobsco-cyber and others added 4 commits October 20, 2026 14:40
…off routes

The merge with main duplicated outcome/notes/createdAt on Consultation and
left ConsultationAddendum undefined, which fails `prisma generate` (P1012)
and therefore CI. Also drop the duplicated consultationsRouter import/mount
in app.ts left by the same merge.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
…with-append-only' into feat/consultation-note-sign-off-with-append-only
@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Oyinkans0la12 CI is failing on this branch, so it can't be merged yet. The errors from the latest run:

Backend (typecheck + test + build)

Error: Prisma schema validation - (get-dmmf wasm)
error: Field "outcome" is already defined on model "Consultation".
error: Field "notes" is already defined on model "Consultation".
error: Field "createdAt" is already defined on model "Consultation".
error: Type "ConsultationAddendum" is neither a built-in type, nor refers to another model, composite type, or enum.

prisma/schema.prisma still defines outcome, notes and createdAt twice on Consultation, and references a ConsultationAddendum model that isn't defined.

@github-actions github-actions Bot added the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 20, 2026
@github-actions

Copy link
Copy Markdown

@Oyinkans0la12 This PR now has merge conflicts with main, most likely because #140 (feat(auth): password reset flow with single-use, hashed, expiring tokens) was just merged, so it can't be merged until it's rebased:

git fetch upstream            # upstream = https://github.com/DocForum/docforum-core.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-lease

The usual culprits are shared files like ROADMAP.md and ARCHITECTURE*.md. CI re-runs when you push, and the needs-rebase label is removed automatically once the conflicts are resolved.

Automated message. It won't repeat unless the PR conflicts again after your next push.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase Has merge conflicts with main; the author needs to rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 2] Consultation note sign-off with append-only addenda

2 participants