Skip to main content

GGBv2 -Complete Implementation Guide

Project Overview

Grade Book application for schools. Teachers create classes, enroll students, enter grades, and generate report cards. Supports multiple schools (multi-tenancy) with two grading models: TERM_BASED (primary) and YEAR_BASED (secondary).

ComponentTechnology
BackendNestJS
FrontendNext.js
DatabasePostgreSQL (Supabase-managed)
AuthSupabase OTP (email-based, no passwords)
StorageSupabase Storage (PDF report cards)
Package managerBun

PART A -Database

1. Schemas

5 custom schemas + auth (Supabase-managed):

SchemaPurpose
publicShared reference data: school, user_profile, academic_year, student_group, term, subject
studentStudent data: student, student_group_enrollment, student_subject_profile, parent_student_link
gradingGrade entry: assessment, grade
reportingReport cards: report_book, report_book_entry, report_book_pdf, class_report_file
staffTeacher access: teacher_group_assignment, teacher_subject_assignment
authSupabase-managed: auth.users

2. Enums

All lowercase values in the database.

EnumValuesUsed in
schooltypeprimary, secondaryschool.school_type
roleadmin, teacheruser_profile.role
gradingmodelterm_based, year_basedacademic_year.grading_model
term_namemichaelmas, hilary, trinityterm.name
gendermale, femalestudent.gender
assessment_typeexam, courseworkassessment.assessment_type
report_book_statusdraft, published, sent_to_ministryreport_book.status
report_book_typeterm, year_endreport_book.report_type
relationship_typemother, father, guardianparent_student_link

3. Tables

public.school

ColumnTypeNotes
iduuid PK
namevarchar
codevarcharShort code
school_typeenum schooltypeprimary or secondary
addressvarchar
phonevarchar
emailvarchar
logo_urlvarchar nullable
is_activeboolean default true
created_attimestamptz
updated_attimestamptz

public.user_profile

ColumnTypeNotes
iduuid PKSame as auth.users.id
school_iduuid FK → schoolNULL until onboarding
emailvarchar
first_namevarchar
last_namevarchar
roleenum roleadmin or teacher
avatar_urlvarchar nullable
is_activeboolean default true
created_attimestamptz
updated_attimestamptz

public.academic_year

ColumnTypeNotes
iduuid PK
school_iduuid FK → school
namevarchare.g. "2025/2026"
start_datedate
end_datedate
is_activebooleanOnly one per school
grading_modelenum gradingmodelterm_based or year_based
year_exam_weightinteger nullableOnly for year_based
year_coursework_weightinteger nullableOnly for year_based
created_attimestamptz
updated_attimestamptz

CHECK: year_exam_weight + year_coursework_weight = 100 when year_based.

public.student_group

ColumnTypeNotes
iduuid PK
namevarchare.g. "Class 3A"
academic_year_iduuid FK → academic_year
created_byuuid FK → user_profileTeacher who created it
created_attimestamptz
updated_attimestamptz

public.term

ColumnTypeNotes
iduuid PK
academic_year_iduuid FK → academic_year
nameenum term_namemichaelmas, hilary, trinity
start_datedate
end_datedate
exam_weightintegere.g. 60
coursework_weightintegere.g. 40
is_ministry_reportingboolean default falseTrue for the term sent to ministry
sort_orderinteger1, 2, 3
created_attimestamptz
updated_attimestamptz

CHECK: exam_weight + coursework_weight = 100 UNIQUE: (academic_year_id, name)

public.subject

ColumnTypeNotes
iduuid PK
school_iduuid FK → school
namevarchare.g. "Mathematics"
codevarchar nullablee.g. "MATH"
is_gradedboolean default trueFalse for PE, Assembly
sort_orderinteger default 0Display order
created_attimestamptz
updated_attimestamptz

UNIQUE: (school_id, name)

student.student

ColumnTypeNotes
iduuid PK
school_iduuid FK → school
first_namevarchar
last_namevarchar
registration_numbervarchare.g. "STU-2025-001"
genderenum gender
date_of_birthdate nullable
addressvarchar nullable
avatar_urlvarchar nullable
is_activeboolean default true
created_attimestamptz
updated_attimestamptz

UNIQUE: (school_id, registration_number)

student.student_group_enrollment

ColumnTypeNotes
iduuid PK
student_iduuid FK → student
student_group_iduuid FK → student_group
enrolled_attimestamptz
created_attimestamptz

UNIQUE: (student_id, student_group_id)

student.student_subject_profile

ColumnTypeNotes
iduuid PK
student_iduuid FK → student
subject_iduuid FK → subject
academic_year_iduuid FK → academic_year
created_attimestamptz

UNIQUE: (student_id, subject_id, academic_year_id)

ColumnTypeNotes
iduuid PK
user_profile_iduuid FK → user_profileParent's profile
student_iduuid FK → student
relationship_typeenum relationship_typemother, father, guardian
created_attimestamptz

grading.assessment

ColumnTypeNotes
iduuid PK
term_iduuid FK → term
subject_iduuid FK → subject
student_group_iduuid FK → student_group
titlevarchare.g. "Mid-term Exam"
assessment_typeenum assessment_typeexam or coursework
assessment_datedate nullable
max_scorenumerice.g. 100
weightnumeric default 1Relative weight within same type
sort_orderinteger default 0
is_excludedboolean default falseExclude entire assessment
exclusion_reasonvarchar nullable
created_attimestamptz
updated_attimestamptz

grading.grade

ColumnTypeNotes
iduuid PK
assessment_iduuid FK → assessment
student_iduuid FK → student
scorenumeric nullableNull = not yet graded
letter_gradevarchar nullable
remarkstext nullable
is_excludedboolean default falseExclude this student's grade
exclusion_reasonvarchar nullable
created_byuuid FK → user_profile
updated_byuuid FK → user_profile
created_attimestamptz
updated_attimestamptz

UNIQUE: (assessment_id, student_id)

reporting.report_book

ColumnTypeNotes
iduuid PK
student_iduuid FK → student
academic_year_iduuid FK → academic_year
term_iduuid FK → term
student_group_iduuid FK → student_group
report_typeenum report_book_typeterm or year_end
statusenum report_book_statusdraft → published → sent_to_ministry
overall_averagenumeric nullable
positioninteger nullableRank in class
total_studentsinteger nullableClass size
class_teacher_remarktext nullable
conductvarchar nullablee.g. "Excellent"
attendance_percentagenumeric nullable0–100
created_attimestamptz
updated_attimestamptz

UNIQUE: (student_id, term_id, report_type)

reporting.class_report_file

ColumnTypeNotes
iduuid PK
student_group_iduuid FK → student_group
term_iduuid FK → term
report_typetextterm or year_end
file_typetextpdf, csv, or xlsx
file_pathtextPath in Supabase Storage
file_sizeintegerSize in bytes
generated_byuuid FK → user_profile nullable
generated_attimestamptz

reporting.report_book_entry

ColumnTypeNotes
iduuid PK
report_book_iduuid FK → report_book
subject_iduuid FK → subject
coursework_averagenumeric nullable
exam_averagenumeric nullable
term_compositenumeric nullable
year_gradenumeric nullableOnly for year_end reports
letter_gradevarchar nullable
teacher_remarktext nullableSubject teacher's comment
sort_orderinteger default 0
created_attimestamptz
updated_attimestamptz

reporting.report_book_pdf

ColumnTypeNotes
iduuid PK
report_book_iduuid FK → report_book
file_pathtextPath in Supabase Storage
file_sizeint4Size in bytes
generated_byuuid FK → user_profile
generated_attimestamptz

staff.teacher_group_assignment

ColumnTypeNotes
iduuid PK
user_profile_iduuid FK → user_profile
student_group_iduuid FK → student_group
academic_year_iduuid FK → academic_year
is_class_teacherbooleanDrives entire permission model
created_attimestamptz

staff.teacher_subject_assignment

ColumnTypeNotes
iduuid PK
user_profile_iduuid FK → user_profile
subject_iduuid FK → subject
student_group_iduuid FK → student_group
academic_year_iduuid FK → academic_year
created_attimestamptz

PART B -Security

4. Security Architecture

Three layers:

LayerWhat it doesCount
RLS school isolationEvery table scoped by school_id. Prevents cross-school data leakage.16 policies
RLS assignment scopingTeachers see only assigned groups/subjects. Class teacher reads all, writes own.12 policies
NestJS guardsWorkflow permissions: class teacher write ops, ministry lock, role checks.3 guards

5. RLS Helper Functions

-- Returns current user's school_id
get_user_school_id() RETURNS uuid

-- Returns true if role = 'admin' and is_active = true
is_admin() RETURNS boolean

-- Returns true if teacher has any assignment to the group
is_assigned_to_group(p_group_id uuid) RETURNS boolean

-- Returns true if teacher teaches the subject in the group
is_assigned_to_subject_in_group(p_subject_id uuid, p_group_id uuid) RETURNS boolean

All use SECURITY DEFINER STABLE.

6. RLS Policies -School Isolation (16 policies)

Every table gets one school_isolation policy (FOR ALL):

TableHow it resolves school_id
public.schoolid = get_user_school_id()
public.user_profileschool_id = get_user_school_id()
public.academic_yearschool_id = get_user_school_id()
public.subjectschool_id = get_user_school_id()
student.studentschool_id = get_user_school_id()
public.student_groupacademic_year.school_id
public.termacademic_year.school_id
staff.teacher_group_assignmentacademic_year.school_id
staff.teacher_subject_assignmentacademic_year.school_id
student.student_subject_profileacademic_year.school_id
student.student_group_enrollmentstudent_group → academic_year.school_id
student.parent_student_linkstudent.school_id
grading.assessmentterm → academic_year.school_id
grading.gradeassessment → term → academic_year.school_id
reporting.report_bookacademic_year.school_id
reporting.report_book_entryreport_book → academic_year.school_id

7. RLS Policies -Assignment Scoping (12 policies)

Student tables (4 policies, FOR SELECT)

Teachers see only students in their assigned groups. Admin sees all.

TableCheck
student.studentis_admin() OR is_assigned_to_group(enrollment.student_group_id)
student.student_group_enrollmentis_admin() OR is_assigned_to_group(student_group_id)
student.student_subject_profileis_admin() OR is_assigned_to_group(enrollment.student_group_id)
student.parent_student_linkis_admin() OR self OR is_assigned_to_group(enrollment.student_group_id)

Grading tables -split READ / WRITE (6 policies)

This is the most critical part. Subject teacher sees/edits own subject. Class teacher reads ALL subjects (for reports) but cannot edit other teachers' grades.

grading.assessment:

PolicyOperationWho
assignment_readSELECTAdmin OR subject teacher (via teacher_subject_assignment) OR class teacher (via is_class_teacher = true)
assignment_writeINSERTAdmin OR subject teacher only
assignment_updateUPDATEAdmin OR subject teacher only

grading.grade:

PolicyOperationWho
assignment_readSELECTAdmin OR subject teacher OR class teacher (sees ALL grades for students in their group)
assignment_writeINSERTAdmin OR subject teacher only
assignment_updateUPDATEAdmin OR subject teacher only

The class teacher READ path on grade:

OR EXISTS (
SELECT 1 FROM student.student_group_enrollment sge
JOIN staff.teacher_group_assignment tga
ON tga.student_group_id = sge.student_group_id
AND tga.is_class_teacher = true
WHERE tga.user_profile_id = auth.uid()
AND sge.student_id = grade.student_id
)

Reporting tables (2 policies, FOR SELECT)

TableCheck
reporting.report_bookis_admin() OR is_assigned_to_group(enrollment.student_group_id)
reporting.report_book_entryis_admin() OR is_assigned_to_group(enrollment.student_group_id)

8. NestJS Guards

GuardWhat it checksUsed on
AuthGuardValidates Supabase JWT. Attaches request.user = { id, email }All protected endpoints
ClassTeacherGuardteacher_group_assignment.is_class_teacher = true. Admin bypasses.Enrollment, reports, teacher management, class updates
ReportGuardreport_book.status !== 'sent_to_ministry'. Blocks edits on locked reports.Report updates, entry edits, regenerate, publish

9. Supabase Clients

ClientWhen to useRLS
serviceClient (service role key)Auth operations, class creation, enrollment, report generation, guard queriesBYPASSES
createUserClient (user JWT)Grade entry/read, assessment CRUD, report reading -any query where RLS should enforce subject+groupRESPECTS

Rule: If the endpoint involves grading data, use the user client. For everything else, use serviceClient with NestJS guards.

10. Schema Grants

Custom schemas need explicit grants (Supabase only auto-grants public):

GRANT USAGE ON SCHEMA student TO service_role, authenticated, anon;
GRANT USAGE ON SCHEMA grading TO service_role, authenticated, anon;
GRANT USAGE ON SCHEMA reporting TO service_role, authenticated, anon;
GRANT USAGE ON SCHEMA staff TO service_role, authenticated, anon;

GRANT ALL ON ALL TABLES IN SCHEMA student TO service_role, authenticated, anon;
GRANT ALL ON ALL TABLES IN SCHEMA grading TO service_role, authenticated, anon;
GRANT ALL ON ALL TABLES IN SCHEMA reporting TO service_role, authenticated, anon;
GRANT ALL ON ALL TABLES IN SCHEMA staff TO service_role, authenticated, anon;

ALTER DEFAULT PRIVILEGES IN SCHEMA student GRANT ALL ON TABLES TO service_role, authenticated, anon;
ALTER DEFAULT PRIVILEGES IN SCHEMA grading GRANT ALL ON TABLES TO service_role, authenticated, anon;
ALTER DEFAULT PRIVILEGES IN SCHEMA reporting GRANT ALL ON TABLES TO service_role, authenticated, anon;
ALTER DEFAULT PRIVILEGES IN SCHEMA staff GRANT ALL ON TABLES TO service_role, authenticated, anon;

PART C -Backend Modules

11. Build Order

1. Auth -OTP login, JWT validation
2. Academic Year -create, activate, grading model
3. Subject -school-level CRUD
4. Term -3 terms per year, weights
5. Student -school-level CRUD
6. Class -student groups, teacher assignment
7. Enrollment -enroll students, assign subjects
8. Grading -assessments, grades, bulk entry, exclusions
9. Calculation -weighted averages, rankings
10. Reporting -report books, remarks, PDF, status flow
11. Cache -pluggable caching (memory or Redis)

12. Project Structure

backend/src/
├── main.ts
├── app.module.ts
├── supabase/
│ └── supabase.service.ts
├── types/
│ ├── database.types.ts ← auto-generated
│ └── helpers.ts ← Row/Insert/Update/Enum types
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── auth.guard.ts
│ └── dto/
│ ├── send-otp.dto.ts
│ ├── verify-otp.dto.ts
│ └── refresh-token.dto.ts
├── academic-year/
│ ├── academic-year.module.ts
│ ├── academic-year.controller.ts
│ ├── academic-year.service.ts
│ └── dto/
│ ├── create-academic-year.dto.ts
│ └── update-academic-year.dto.ts
├── subject/
│ ├── subject.module.ts
│ ├── subject.controller.ts
│ ├── subject.service.ts
│ └── dto/
│ ├── create-subject.dto.ts
│ ├── update-subject.dto.ts
│ └── reorder-subjects.dto.ts
├── term/
│ ├── term.module.ts
│ ├── term.controller.ts
│ ├── term.service.ts
│ └── dto/
│ ├── create-term.dto.ts
│ └── update-term.dto.ts
├── student/
│ ├── student.module.ts
│ ├── student.controller.ts
│ ├── student.service.ts
│ └── dto/
│ ├── create-student.dto.ts
│ └── update-student.dto.ts
├── class/
│ ├── class.module.ts
│ ├── class.controller.ts
│ ├── class.service.ts
│ ├── class-teacher.guard.ts
│ └── dto/
│ ├── create-class.dto.ts
│ ├── update-class.dto.ts
│ └── add-teacher.dto.ts
├── enrollment/
│ ├── enrollment.module.ts
│ ├── enrollment.controller.ts
│ ├── enrollment.service.ts
│ └── dto/
│ ├── enroll-student.dto.ts
│ ├── bulk-enroll.dto.ts
│ ├── assign-subjects.dto.ts
│ └── bulk-assign-subjects.dto.ts
├── grading/
│ ├── grading.module.ts
│ ├── assessment.controller.ts
│ ├── grade.controller.ts
│ ├── assessment.service.ts
│ ├── grade.service.ts
│ └── dto/
│ ├── create-assessment.dto.ts
│ ├── update-assessment.dto.ts
│ ├── create-grade.dto.ts
│ ├── update-grade.dto.ts
│ ├── bulk-grade.dto.ts
│ └── exclude.dto.ts
├── calculation/
│ ├── calculation.module.ts
│ ├── calculation.controller.ts
│ ├── calculation.service.ts
│ └── interfaces/
│ └── calculation.interfaces.ts
├── reporting/
│ ├── reporting.module.ts
│ ├── report.controller.ts
│ ├── report.service.ts
│ ├── report.guard.ts
│ └── dto/
│ ├── generate-report.dto.ts
│ ├── update-report.dto.ts
│ ├── update-report-entry.dto.ts
│ └── save-pdf.dto.ts
└── cache/
├── cache.module.ts
└── cache.service.ts

PART D -API Routes

13. Auth

MethodRouteGuardsDescription
POST/auth/otp/sendPublicSend OTP to email. shouldCreateUser: true
POST/auth/otp/verifyPublicVerify OTP → JWT session + user profile
GET/auth/meAuthGet current user profile with school
POST/auth/refreshPublicRefresh expired access token
POST/auth/logoutAuthSign out

New teacher: OTP → auth.users created → handle_new_user() trigger creates user_profile (school_id = null) → frontend redirects to onboarding.

14. Academic Year

MethodRouteGuardsDescription
GET/academic-yearsAuthList years for teacher's school
GET/academic-years/activeAuthGet currently active year
GET/academic-years/:idAuthGet year details
POST/academic-yearsAuthCreate year (any teacher)
PATCH/academic-years/:idAuthUpdate year
PATCH/academic-years/:id/activateAuthDeactivate others, activate this one

15. Subject

MethodRouteGuardsDescription
GET/subjectsAuthList subjects for teacher's school
GET/subjects/:idAuthGet subject details
POST/subjectsAuthCreate subject (any teacher)
PATCH/subjects/reorderAuthBatch update sort_order for multiple subjects
PATCH/subjects/:idAuthUpdate subject
DELETE/subjects/:idAuthDelete (blocked if grades exist)

16. Term

MethodRouteGuardsDescription
GET/terms?yearId=AuthList terms for a year
GET/terms/:idAuthGet term details
POST/termsAuthCreate term (auto sort_order)
PATCH/terms/:idAuthUpdate weights/dates
DELETE/terms/:idAuthDelete (blocked if assessments exist)

17. Student

MethodRouteGuardsDescription
GET/students?search=AuthList students, optional search
GET/students/:idAuthGet student details
POST/studentsAuthCreate student (school_id from profile)
PATCH/students/:idAuthUpdate student

18. Class (Student Group)

MethodRouteGuardsDescription
GET/classesAuthList teacher's assigned classes
POST/classesAuthCreate class (auto-assigns is_class_teacher = true)
GET/classes/:classIdAuthGet class details
PATCH/classes/:classIdAuth + CTUpdate class name
DELETE/classes/:classIdAuth + CTDelete class
GET/classes/:classId/teachersAuthList teachers + subjects
POST/classes/:classId/teachersAuth + CTAdd teacher (is_class_teacher = false)
DELETE/classes/:classId/teachers/:teacherIdAuth + CTRemove teacher

Creating a class auto-assigns:

  1. teacher_group_assignment with is_class_teacher = true
  2. teacher_subject_assignment × N subjects (if provided)

19. Enrollment

All endpoints: Auth + ClassTeacherGuard.

MethodRouteDescription
GET/classes/:classId/studentsList enrolled students
POST/classes/:classId/enrollEnroll one student
POST/classes/:classId/enroll/bulkBulk enroll
DELETE/classes/:classId/enroll/:studentIdUnenroll (removes subject assignments too)
GET/classes/:classId/students/:studentId/subjectsList student's subjects
POST/classes/:classId/subjectsAssign subjects to one student
POST/classes/:classId/subjects/bulkBulk assign subjects to multiple students
DELETE/classes/:classId/students/:studentId/subjects/:subjectIdRemove subject

20. Grading

All endpoints: Auth only. RLS handles subject+group enforcement via user client.

Assessments

MethodRouteDescription
GET/assessments?termId=&subjectId=&studentGroupId=List assessments
GET/assessments/:idGet assessment
POST/assessmentsCreate (RLS checks subject assignment)
PATCH/assessments/:idUpdate
PATCH/assessments/:id/excludeExclude entire assessment
DELETE/assessments/:idDelete + cascade grades

Grades

MethodRouteDescription
GET/grades?assessmentId=Grades for one assessment
GET/grades/by-term?termId=&subjectId=&studentGroupId=All grades for subject in term
POST/gradesEnter a grade
POST/grades/bulkBulk upsert (update existing, insert new)
PATCH/grades/:idUpdate score/remarks
PATCH/grades/:id/excludeExclude single grade

21. Calculation

MethodRouteGuardsDescription
GET/calculations/student-term?studentId=&termId=&studentGroupId=AuthOne student, one term
GET/calculations/student-year?studentId=&academicYearId=&studentGroupId=AuthOne student, full year
GET/calculations/class-term?termId=&studentGroupId=AuthAll students + rankings
GET/calculations/class-year?academicYearId=&studentGroupId=AuthAll students year results
GET/calculations/class-summary?termId=&studentGroupId=AuthStudent × subject grid

22. Reporting

MethodRouteGuardsDescription
POST/reports/generateAuth + CTGenerate reports for class
GET/reports?studentGroupId=&termId=&reportType=Auth + CTList reports
GET/reports/student?studentId=&termId=&reportType=Auth + CTSpecific student's report
GET/reports/:idAuth + CTFull report with entries + PDFs
PATCH/reports/:idAuth + CT + RGUpdate remark, conduct, attendance
PATCH/reports/:id/regenerateAuth + CT + RGRecalculate, keep remarks
PATCH/reports/:id/publishAuth + CT + RGdraft → published
PATCH/reports/:id/send-to-ministryAuth + CTpublished → sent_to_ministry
PATCH/report-entries/:entryIdAuth + RGSubject teacher adds remark/letter grade
POST/reports/:id/pdfAuth + CT + RGSave PDF record
POST/reports/:id/pdf/uploadAuth + CT + RGUpload PDF to Supabase Storage
GET/reports/:id/pdfsAuth + CTPDF version history
GET/reports/:id/pdf/latestAuth + CTMost recent PDF
GET/reports/:id/pdf/:pdfId/downloadAuth + CTDownload PDF file
GET/reports/class-summary?studentGroupId=&termId=&reportType=Auth + CTClass summary from persisted reports
POST/reports/class-summary/uploadAuth + CTUpload class summary file (PDF/CSV/XLSX)
GET/reports/class-summary/download?studentGroupId=&termId=&reportType=&fileType=Auth + CTDownload stored class summary file
GET/reports/class-summary/files?studentGroupId=&termId=&reportType=Auth + CTList stored class summary files

CT = ClassTeacherGuard, RG = ReportGuard


PART E -Grading Calculation

23. Term Grade Calculation

For each student, for each subject, for each term:

Step 1: Collect coursework grades (non-excluded)
Normalize to percentages: (score / max_score) × 100
Calculate weighted average using assessment weights

Step 2: Collect exam grades (non-excluded)
Same normalization and weighting

Step 3: Apply term weights
term_composite = (coursework_avg × coursework_weight%) + (exam_avg × exam_weight%)

Step 4: Overall average
average of all graded subjects' term_composites

Worked example

Term: Michaelmas (exam_weight: 60, coursework_weight: 40)

Maths coursework:
Homework 1: 35/50 (70%) weight 1
Homework 2: 40/50 (80%) weight 1
Class Test: 28/40 (70%) weight 2
coursework_avg = (70×1 + 80×1 + 70×2) / (1+1+2) = 72.5%

Maths exam:
Mid-term: 78/100 (78%) weight 1
exam_avg = 78.0%

term_composite = (72.5 × 40%) + (78.0 × 60%) = 29.0 + 46.8 = 75.8%

24. Year-End Calculation -TERM_BASED

Year grade = simple average of 3 term composites

Maths:
Michaelmas: 75.8
Hilary: 80.2
Trinity: 72.0
year_grade = (75.8 + 80.2 + 72.0) / 3 = 76.0

Ministry receives: Trinity term report (is_ministry_reporting = true)

25. Year-End Calculation -YEAR_BASED

Year grade = terms_avg × year_coursework_weight% + yr_exam × year_exam_weight%

Where:
terms_avg = (T1 composite + T2 composite + T3 composite) / 3
yr_exam = Trinity term's EXAM average (end of year exam)

Example (year_coursework_weight: 40, year_exam_weight: 60):

Maths:
terms_avg = (75.8 + 80.2 + 72.0) / 3 = 76.0
yr_exam = Trinity exam_average = 82.0

year_grade = (76.0 × 40%) + (82.0 × 60%) = 30.4 + 49.2 = 79.6

Ministry receives: Year-end report

26. Handling Edge Cases

CaseBehaviour
Assessment excluded (is_excluded = true)All students' grades for that assessment skipped
Grade excluded (grade.is_excluded = true)Only that student's grade skipped
Only coursework, no examsterm_composite = coursework_avg (exam weight ignored)
Only exams, no courseworkterm_composite = exam_avg
No grades at all for a subjectterm_composite = null, not included in overall
Weight is relative, not absoluteWeights don't need to sum to 100. Weighted average formula handles it.

PART F -Reporting

27. Report Status Flow

draft → published → sent_to_ministry

draft: Teachers adding remarks. Can edit, regenerate.
published: Parents can view. Teachers can still edit remarks.
sent_to_ministry: LOCKED FOREVER. No edits. No regenerate.

28. Which Report Goes to Ministry

Grading ModelReport SentWhy
TERM_BASEDTrinity term report (type: 'term')Primary schools report per term
YEAR_BASEDYear-end report (type: 'year_end')Secondary schools report annually

29. Report Card PDF -Term

Portrait A4. One per student.

[School Logo]
SCHOOL NAME
School Address

STUDENT REPORT CARD -Michaelmas Term 2025/2026

Name: James Thompson Reg: STU-2025-001
Class: 3A Gender: Male
DOB: 15/03/2015 Position: 3rd out of 30

┌────────────────┬──────┬──────┬───────┬───────┬─────────────┐
│ Subject │ C/W │ Exam │ Total │ Grade │ Remark │
├────────────────┼──────┼──────┼───────┼───────┼─────────────┤
│ Mathematics │ 72.5 │ 78.0 │ 75.8 │ B+ │ Good effort │
│ English Lang. │ 80.0 │ 85.0 │ 83.0 │ A- │ Excellent │
│ PE │ - │ - │ - │ - │ Very active │
├────────────────┼──────┼──────┼───────┼───────┼─────────────┤
│ OVERALL │ │ │ 75.1 │ B+ │ │
└────────────────┴──────┴──────┴───────┴───────┴─────────────┘

CLASS TEACHER'S REMARK:
James has shown consistent effort...

Class Teacher: Mrs. Mary Johnson Date: 15 Dec 2025
Signature: ___________________

30. Report Card PDF -Year-End TERM_BASED

┌────────────────┬───────┬───────┬───────┬───────┬───────┐
│ Subject │ T1 │ T2 │ T3 │ Year │ Grade │
├────────────────┼───────┼───────┼───────┼───────┼───────┤
│ Mathematics │ 75.8 │ 80.2 │ 72.0 │ 76.0 │ B+ │
│ English Lang. │ 83.0 │ 79.5 │ 85.0 │ 82.5 │ A- │
└────────────────┴───────┴───────┴───────┴───────┴───────┘

Year = (T1 + T2 + T3) / 3

31. Report Card PDF -Year-End YEAR_BASED

Different table -two extra columns for Terms Avg and Yr Exam:

┌────────────────┬──────┬──────┬──────┬───────────┬──────────┬───────┬───────┐
│ Subject │ T1 │ T2 │ T3 │ Terms Avg │ Yr Exam │ Year │ Grade │
│ │ │ │ │ (40%) │ (60%) │ │ │
├────────────────┼──────┼──────┼──────┼───────────┼──────────┼───────┼───────┤
│ Mathematics │ 75.8 │ 80.2 │ 72.0 │ 76.0 │ 82.0 │ 79.6 │ B+ │
│ English Lang. │ 83.0 │ 79.5 │ 85.0 │ 82.5 │ 88.0 │ 85.8 │ A- │
└────────────────┴──────┴──────┴──────┴───────────┴──────────┴───────┴───────┘

Terms Avg = (T1 + T2 + T3) / 3
Yr Exam = Trinity term exam average
Year = Terms Avg × 40% + Yr Exam × 60%

The frontend checks academic_year.grading_model to pick the right layout.

32. Class Summary Sheet

Landscape A4. All students × all graded subjects:

CLASS SUMMARY -Class 3A -Michaelmas Term 2025/2026

┌────┬──────────────────┬───────┬───────┬───────┬───────┬─────┐
│ # │ Student │ Maths │ Eng │ Sci │ Art │ Avg │
├────┼──────────────────┼───────┼───────┼───────┼───────┼─────┤
│ 1 │ Davis, Emily │ 90.0 │ 88.0 │ 85.0 │ 90.0 │83.3│
│ 2 │ Thompson, James │ 75.8 │ 83.0 │ 68.0 │ 88.0 │75.1│
│ 3 │ Williams, Sarah │ 82.0 │ 76.5 │ 90.5 │ 75.0 │79.3│
│ ...│ ... │ ... │ ... │ ... │ ... │ ... │
├────┼──────────────────┼───────┼───────┼───────┼───────┼─────┤
│ │ CLASS AVERAGE │ 74.2 │ 76.8 │ 72.1 │ 81.2 │74.0│
│ │ HIGHEST │ 90.0 │ 88.0 │ 90.5 │ 90.0 │83.3│
│ │ LOWEST │ 55.0 │ 60.0 │ 58.0 │ 70.0 │59.2│
└────┴──────────────────┴───────┴───────┴───────┴───────┴─────┘

Ranked by overall average. Non-graded subjects (PE) excluded from grid.

33. Grading Scale

ScoreGradeDescription
90 -100AExcellent
85 -89A-Very Good
80 -84B+Good
75 -79BAbove Average
70 -74B-Average
65 -69C+Below Average
60 -64CFair
55 -59C-Needs Improvement
50 -54DPoor
0 -49FFail

Use teacher's manual letter_grade if entered. Otherwise compute from score.

34. PDF Generation Flow

Frontend generates PDF → uploads to Supabase Storage → saves record via API

1. GET /reports/:id → fetch all report data
2. Render PDF (jsPDF or @react-pdf) → client-side
3. supabase.storage.upload(path, blob) → upload to Storage
4. POST /reports/:id/pdf { filePath, size } → save record in report_book_pdf

File naming:

report-cards/{year}/{term}/{student-reg}.pdf
report-cards/2025-2026/michaelmas/STU-2025-001.pdf
report-cards/2025-2026/year-end/class-3a-summary.pdf

PART G -Teacher Workflow

35. Class Teacher Flow

1. Create class "Class 3A" for 2025/2026
→ auto-assigns as class teacher + subject assignments

2. Enroll 30 students
→ POST /classes/:id/enroll/bulk

3. Bulk assign core subjects to all students
→ POST /classes/:id/subjects/bulk

4. Add subject teachers (optional)
→ POST /classes/:id/teachers { teacherId, subjectIds }
→ Added teacher gets is_class_teacher = false

5. Enter grades throughout the term
→ POST /grades/bulk (for their own subjects)

6. Generate reports at end of term
→ POST /reports/generate

7. Add class teacher remark per student
→ PATCH /reports/:id

8. Publish reports
→ PATCH /reports/:id/publish

9. Generate PDFs
→ Frontend renders + uploads

10. Send to ministry (Trinity term or year-end)
→ PATCH /reports/:id/send-to-ministry

36. Subject Teacher Flow

1. Gets added to a class by the class teacher
→ is_class_teacher = false

2. Can see students enrolled in that class
→ RLS assignment_isolation filters

3. Creates assessments for their subject
→ POST /assessments

4. Enters grades for their subject only
→ POST /grades/bulk
→ RLS blocks if wrong subject

5. Adds teacher remark on report entries for their subject
→ PATCH /report-entries/:id

6. Cannot: enroll students, generate reports, add teachers,
edit other teachers' grades, change class name

37. Permission Matrix

ActionClass TeacherSubject TeacherAdmin
Create classYes (auto-assigns)No (added by CT)Yes
Enroll studentsYesNoYes
Assign subjectsYesNoYes
Add teachersYesNoYes
Read all grades in groupYes (RLS)Own subject only (RLS)Yes
Write gradesOwn subjects only (RLS)Own subject only (RLS)Yes
Create assessmentsOwn subjects only (RLS)Own subject only (RLS)Yes
Generate reportsYes (guard)No (guard blocks)Yes
Add class teacher remarkYes (guard)No (guard blocks)Yes
Add subject remarkYes (any subject)Own subject only (RLS)Yes
Publish reportsYes (guard)No (guard blocks)Yes
Send to ministryYes (guard)No (guard blocks)Yes