Lesson Planning
Schemes of work and weekly lesson plans — Tiptap documents scoped from the parent scheme, draft/publish only, teaching-relationship access.
Teachers author a scheme of work per subject-stream-term — the term's roadmap — then hang weekly lesson plans off it. Both are rich-text Tiptap documents (Tiptap is the JSON-based editor format; see Templates for the schema) stored twice on the row: the editable content jsonb and a server-rendered contentHtml. Two statuses only, draft and published — there is no approval workflow, just a visibility switch.
Creating a scheme
SchemeOfWorkService.create walks a chain that's mostly about deriving scope rather than trusting it:
- Author gate —
assertCanAuthor: the caller must be a subject-teacher of that subject in that stream. Teaching is authorization; there's no admin bypass on writes. - Scope derivation — the
termIdresolves the campus (term → cay → campusId), then the standard year gates:assertNotLocked+assertTermMatchesYear. - Uniqueness — a partial-unique index allows one live scheme per (subject, stream, term); a second create is a
409. ("Live" = not soft-deleted, so deleting genuinely frees the slot.) - Content source — the request supplies either
templateIdorcontent, never both. The template path callstemplateService.resolveForInstantiation(templateId, TemplateType.SCHEME_OF_WORK)— published and type-matched or it throws — then copiescontent+contentHtmland stampssourceTemplateIdfor provenance. The direct path validates the doc against the editor allowlist and renders the HTML. Neither given →EMPTY_TIPTAP_DOC.
Every scheme is born draft.
Lesson plans are scheme-first
A lesson plan is created under a scheme, and its scope — subject, stream, term, campus, campusAcademicYearId — is stamped from the parent scheme, never read from the client; a request whose campusAcademicYearId disagrees with the scheme's is rejected. What the client actually owns: weekNumber, title, optional plannedDate, and the document (same template-or-content rule). This is the same "derive, don't trust" move CBT makes with campusId — the parent is the source of truth for where a child lives.
Invariants that hold everywhere
contentHtmlis regenerated on every content write — the server is the single writer of the HTML projection, so it can never drift from the JSON.- All mutations gate on the scheme's year being current and unlocked (
assertSchemeYearIsCurrentviagetOwnedScheme) — last year's plans are read-only history once the year locks. - Publish/unpublish are idempotent — they set/clear
publishedAt, and re-publishing a published scheme is a no-op, not an error. - A scheme with lesson plans attached can't be deleted (
409) — remove the plans first; deletes are soft. duplicatecopies a scheme into another stream (same subject + term) that the caller teaches; the target slot must be empty, and the copy starts asdraft— a term's scheme fans out to sibling streams without re-typing.
Who sees what
Routes carry only @RequireUserType(STAFF) — no @RequirePermissions. LessonPlanningAccessService does the work, and the read side is a three-branch union:
| Reader | Sees |
|---|---|
| The authoring subject-teacher | Their own subject-stream pairs, any status |
| A class-teacher of the stream | That stream's documents, published only |
Anyone with scoped SCHEME_OF_WORK / LESSON_PLAN read permission | Published only — the ability is built on demand (hasScopedRead), since the routes themselves carry no permission decorator |
List queries compose the same branches — and a "drafts only" filter quietly drops the two published-only branches, so drafts are visible strictly to their authors.
Audit snapshots exclude the document
lesson-planning.snapshots.ts allowlists scalar fields and records only contentBytes (the JSON's byte length) — full Tiptap docs never enter the audit trail, keeping audit rows small and the trail readable. Fed to @Audit via afterFrom + loadBefore over each service's findForAudit. Follow the same pattern for any new document-bearing resource.
Endpoints
| Controller | Routes |
|---|---|
schemes-of-work | POST /, GET / (cursor-paginated, searches title), GET /:id, PATCH /:id, POST /:id/publish, POST /:id/unpublish, POST /:id/duplicate, DELETE /:id |
lesson-plans | Same shape minus duplicate |
Where to go next
Assessments & Scores
Per-stream assessments with a group weight budget, the lazy score-entry matrix, and the guarded upsert that feeds the compute engine.
Templates
Admin-authored Tiptap document templates for lesson planning — school-owned or platform built-ins, validated against a single editor schema.