Reports
The term report card — remarks and attendance around the gradebook, one per enrollment per term, published to families with a shareable slug.
A report is the term report card: the remarks and attendance summary wrapped around a student's grades for one term. One exists per (enrollmentId, termId) — a DB unique constraint — and its lifecycle is a single boolean: isPublished (+ publishedAt). No status enum, no approval chain.
The entity — and what it is not
report carries enrollmentId, termId, campusAcademicYearId, a unique shareable slug, four remark fields (overallRemarks, conductRemarks, classTeacherRemarks, headTeacherRemarks), an attendance jsonb ({ present, absent, total }), isPublished/publishedAt, and generatedBy.
The report is not an aggregator
create persists exactly what the caller sends — it does not pull grades or compute attendance. Grades stay in the grade module and are surfaced beside the report by the frontend; attendance arrives pre-summed in the DTO. There's also no PDF renderer here — the output is JSON, and the slug is the shareable identifier.
Scoping note: the entity is not @TenantScoped — isolation comes from the required campusAcademicYearId plus CASL permission scope on every access (the same belt-and-suspenders as campus_term).
Creating and publishing
create runs the familiar year-scoped guard chain: the enrollment must exist and its year must equal the payload's, then assertNotLocked and assertTermMatchesYear, then a unique slug is minted and the report saves unpublished.
publish/unpublishtoggle visibility to families ("visible to guardians/students") and stamp/clearpublishedAt.- Every mutation — update, publish, unpublish, remove — re-loads the enrollment and gates on the report's own year being unlocked; a locked year freezes its report cards along with everything else.
- All routes check
Resource.ACADEMIC_REPORTper action (CREATE/READ/UPDATE/DELETE), and list reads merge the caller's CASL scope plus the requiredcampusAcademicYearIdfilter.
Audit snapshots exclude the content
reportSnapshot records metadata only — id, enrollment, term, slug, publish state. Remarks, attendance, and anything grade-shaped never enter the audit trail, matching the lesson-planning allowlist pattern.
Where to go next
Templates
Admin-authored Tiptap document templates for lesson planning — school-owned or platform built-ins, validated against a single editor schema.
Admissions
The admission application — a guest-owned draft through a strict state machine, with fee-gated submission, emailed decisions, and worker-rendered PDFs.