Report Cards¶
Report cards are the official end-of-period record of each student. Management defines reusable templates, opens cycles over chosen grades, and fills or delegates the hand-entered values. Each student's card is then generated into a frozen, versioned JSON document and published grade by grade to referents and students. Structure (subjects, criteria, class, teacher) and attendance are derived; only the values are typed in: subject values, the template's custom evaluations and its custom texts. The backend never renders a PDF and sends no notification at any point.
Canonical implementation: src/report-cards/ (one folded module, no dominant resource):
| Folder | Owns |
|---|---|
shared/ |
Qualified authority (report-cards.access.ts), reference dates, the fixed Effort list (used by a template without an Effort scale), the report-card default values (default-values.ts), versioned JSON contracts and decoders (snapshots.ts), cycle/card locks and stamps (queries.ts), live logo minting |
structure/ |
Card structure rules (card-structure.ts), the batched structure resolver, the population resolver and card-evaluation.resolver.ts (structure + entries + status for a cycle) |
status/ |
Pure requirement and status computation (card-status.ts, no I/O) |
attendance/ |
Pure attendance tally and the adapter over the attendance reader |
templates/, cycles/, cards/, worksheets/, family/ |
One sub-resource each: controller, service, queries.ts, swagger, dto/ |
1. Model¶
| Table | Row | Notes |
|---|---|---|
report_card_templates |
Tenant-wide, reusable across years | Title, a grade scale (evaluationScaleId, preset or own), includeEffort with an optional Effort scale (effortScaleId, preset or own; NULL is the fixed Effort list), includeCriteria (default true), includeCustomTexts / includeAttendance, ordered custom text items (customTextItems, versioned JSON with a stable key per item and an optional presetText, kept while texts are off) |
report_card_template_evaluations |
One custom evaluation of a template | The row id is the evaluation's stable key; name, ordinalPosition, its own evaluation scale (preset or own), ordered criteria (JSON, each with a stable key). Deleted with the template |
report_card_cycles |
One evaluation period of one academic year | Name, start/end dates, informative deadline, state OPEN / CLOSED / PUBLISHED |
report_card_cycle_grades |
One grade inside one cycle | templateSnapshot (the template frozen when the grade was added or re-applied), layoutRevision, publication stamp |
report_cards |
One student in one cycle grade | Created on demand; rosteredOn, value revision, generation stamp, document, attendance columns, publishedAt |
report_card_entries |
One value slot of one card | slotKey = S:<subjectId> / C:<criterionId> / T:<textKey> / E:<evaluationKey> / EC:<evaluationCriterionKey>; soft references to curriculum rows and to the snapshot's evaluations (evaluationKey, evaluationCriterionKey); notEvaluated, grade, effort, text; last-editor provenance |
- All six tables are tenant-bearing with FORCE RLS (Class S) and a
Restricttenant FK; see Multitenancy. - Curriculum subjects and criteria are referenced softly: curriculum edits may delete them, and an entry whose subject, criterion or text key is no longer in the card's structure is an orphan — kept, never required, never printed. Evaluation keys reference the grade's snapshot, not the live template; an entry whose evaluation or criterion key is not in the snapshot is an orphan too.
- The partial unique index
report_card_cycle_grades_one_unpublishedallows one unpublished cycle grade per grade. Named CHECKs pin date order, publication coherence, the generation stamp (document, revisions and attendance columns set together), a template's Effort scale only while Effort is on (report_card_templates_effort_scale_requires_effort) and the entry kind/slot-key shape (report_card_entries_kind_coherent: each kind sets exactly its own reference columns, an evaluation criterion slot also carries its evaluation's key,notEvaluatedonly on subject and evaluation slots, and a kind without a branch is refused). - The template's two scales (grade and Effort) and each custom evaluation's scale are real FKs (
Restrict), each registered inSCALE_FK_REFERENCES(reportCardTemplates,reportCardTemplateEfforts,reportCardTemplateEvaluations): a scale used by a template, as either scale, or by a custom evaluation is in use, so it cannot be deleted and its values cannot be edited (409EVALUATION_SCALE_IN_USE), and a tenant template on a platform preset locks that preset (Grades). Because Effort off clears the Effort scale, only a template with Effort on holds one. - Template settings. The Effort scale is the scale of every Effort value on the template's cards (subjects, curriculum criteria, custom evaluations and their criteria): any scale visible to the school, whatever the curriculum, possibly the grade scale itself. The stored value follows the resulting
includeEffort(resolveEffortScaleId): with Effort off nothing is stored and a sent id is ignored unvalidated; with Effort on a sent value wins (nullreturns to the fixed list) and an unsent one keeps the stored scale. A grade or Effort scale id that a write would newly store must be visible to the school (isScaleVisible), else 422REPORT_CARD_TEMPLATE_SCALE_NOT_FOUNDnaming the field (§12).includeCriteriagoverns the curriculum criteria only (§3); custom evaluations keep their own. Duplicate copies both settings. Like every template setting, both are frozen into the cycle grade's snapshot, so a template edit reaches a cycle grade only when it is re-applied (§5).
2. Roster and anchor dates¶
- The reference date of a cycle grade is
schoolTodayclamped into the grade department's calendar (referenceDateFor). - While the cycle is
OPENand the grade unpublished, the roster is live: the year's students placed in the grade on the reference date, with the live participation fence when that date is today and the history fence otherwise. Nothing is written to follow enrolment. Joiners appear; leavers and grade movers disappear, and their card rows become dormant (kept, invisible, uncounted). - Close materialises the roster of every unpublished grade: it upserts a card row for each live-roster student with
rosteredOn = reference dateand nullsrosteredOnon the other rows of those grades. ACLOSEDorPUBLISHEDcycle, or a published grade, reads the rows withrosteredOnset. Reopen writes nothing. - Card rows are created on demand by the first entry write or generation (
ensureCardRows, insert-on-conflict-nothing in id order, before the row lock). A student without a row has an empty entry set. Values never follow a student to another grade of the same cycle. - A student on the rosters of two grades of one cycle (a grade move after the first grade was frozen) has two cards. Single-card routes and entry writes locate the card that is not published (
locateCards); bulk generation and publication consider every card. - The anchor date of a card — the date on which plan, homeroom, course and teachers are resolved — is the reference date for a live roster and
rosteredOnfor a frozen one. Dated reads use the end-of-dayeffectiveOnview.
3. Card structure¶
For a student, cycle grade and anchor date, buildCardStructure derives:
- Study plan. The selection effective on the anchor date. No selection adds one blocking requirement,
STUDY_PLAN, owned by management; an incomplete selection is not blocking and is flaggedstudyPlanIncompleteon the monitoring row. - Subjects. Top-level subjects of the curriculum that have an hours cell for the grade, are covered for the student's track and option choices (
resolveTargetCoverage), and are taught during the cycle (taughtAllYear, or a teaching window that overlaps the cycle dates). Order: the common section first, then the track section; mandatory subjects by ordinal, then option blocks by ordinal with the chosen subjects by ordinal; ties by id. - Criteria.
pickCriteriaForGrade(a per-grade override replaces the defaults), sorted by name. A subject without units uses its own criteria (SUBJECT, orNONEwhen empty); a subject with units uses one list forcriteriaLevel = SUBJECT, or one group per unit forUNIT(units without criteria are omitted; no criteria at all isNONE). When the grade's snapshot excludes criteria (criteria: false, frozen from the template'sincludeCriteria), every subject isNONEwith no criteria and no units; the subject list, its order and the responsibilities are unchanged.evaluateCyclepasses the flag (withCriteria) to the structure resolver, so the card detail, worksheets, monitoring, the outstanding list, the preview and generation all follow. Custom evaluations keep their own criteria. The template preview structure does not go throughbuildCardStructureand always lists the curriculum's criteria. - Subject responsibility. The student's own course for the subject on the anchor date, with every teacher of that course effective on the date (
loadSubjectGroupTeachersOn, any ordinal, in teaching order). No course, or a course without teachers, isUNASSIGNED. Lesson-assistance teachers and the teachers of sibling courses in a combined class are never responsible. - Class responsibility (the custom texts and the custom evaluations), resolved when the grade's snapshot has a custom text without a preset text or at least one custom evaluation (
hasClassItems); first non-empty tier: the homeroom's currenthomeroomTeacherId(HOMEROOM_TEACHER); then active holders of a CURRICULUM-parametric role with the student's curriculum (COORDINATOR); then Director-function holders (DIRECTOR); then admins (ADMIN). Tiers 2–4 count only assignments whose role grantsreport_cards.entriesWRITE and whose user is active (loadClassFallbackHolders), so a read-only Director-function role never becomes responsible. - Header. Class = the homeroom on the anchor date, else the grade; teacher = the homeroom teacher or
null; school =School.legalName.
People. Every person in a responsibility, in the computed header (stored documents: §8) and in the outstanding list carries firstName, lastName and name (formatPersonName, first name first), so clients can render either order. Holder lists (COORDINATOR, DIRECTOR, ADMIN) sort by last name, then first name, then id; course teachers keep their teaching order. The outstanding list sorts by missing count (descending), then last name, first name, role and id.
Required values (status/card-status.ts), in card order: STUDY_PLAN; for every subject not marked Not evaluated, the subject grade and every criterion grade of its structure (none when the snapshot excludes criteria), plus the matching Effort values when the snapshot has Effort; the same for every custom evaluation of the snapshot not marked Not evaluated (its grade, every criterion grade, Effort when on), with or without a study plan; every custom text item without a preset text when the snapshot has texts. An Effort value counts once it is stored: writes accept only the snapshot's levels and the default values (§7), and re-apply clears the values outside them (§5). A default value (-) counts as given like any other.
Preset texts. A custom text item with a presetText is fixed content: management writes it once in the template and every card prints it under the item's title. It is not a class item, requires no value, is left out of the class worksheet, shows read-only on the card detail (presetText, text = the preset text, editable: false, missing: false), and a text write on it is refused (TEXT_ITEM_PRESET). The snapshot freezes it like the rest of the item, so editing the template changes cards only after a re-apply.
Owner of a missing value (missingValueOwner). Evaluation values reuse the kinds GRADE and EFFORT and carry evaluationKey (and evaluationCriterionKey) instead of subjectId, so the kind alone does not say whose a value is. STUDY_PLAN is management's; TEXT and every value with an evaluationKey are the class responsibility's; every other value is its subject's course. The outstanding list, the worksheet counts and the card detail's per-field missing all dispatch through this predicate.
4. Status and staleness¶
stale = document != null AND (generatedRevision != revision OR generatedLayoutRevision != cycleGrade.layoutRevision)
status = publishedAt != null → PUBLISHED (stale exposed separately: "Published · Stale")
document && !stale → GENERATED
document && stale → STALE
no missing value → READY
otherwise → INCOMPLETE
- Status is computed on every read and never stored; monitoring filters and paginates in memory.
revisionincrements once per committed batch that changes a stored value of the card; a no-op write does not.layoutRevisionincrements on a template change or re-apply for that grade, and on a change of the cycle's name, start or end date for every unpublished grade.- Structural drift after generation (curriculum, homeroom, teachers) and register edits do not make a card stale: the document froze those facts.
missingCountstays live.
5. Lifecycle¶
open ─→ OPEN ──close──→ CLOSED ──publish(grades)──→ CLOSED (some grades published)
↑ │ │
└────reopen──────┘ └─publish(last grade)─→ PUBLISHED (terminal)
| Command | Allowed when | Effect |
|---|---|---|
| Open | Active year; ≥1 grade; grade rules pass | One cycle grade per grade with a frozen snapshot |
| Edit name / dates | Not PUBLISHED and no grade published |
Grade rules re-checked; layoutRevision++ on unpublished grades |
| Edit deadline | Not PUBLISHED |
Informative only |
| Add grade, change or re-apply template | OPEN, grade unpublished |
Re-snapshot and layoutRevision++; on an existing grade, the values the new snapshot cannot hold are cleared (below) |
| Remove grade | OPEN, grade unpublished |
Deletes the cycle grade, its cards and entries; when only published grades remain, the cycle becomes PUBLISHED |
| Close | OPEN |
Materialises the roster of unpublished grades |
| Reopen | CLOSED |
Unpublished grades are live again; published grades keep their roster |
| Publish grades | CLOSED; every rostered card of each grade GENERATED (not stale) |
Stamps the grades and their cards; the last grade makes the cycle PUBLISHED |
| Delete | OPEN, no grade published |
Cascade delete |
Re-apply clearing (pruneIncompatibleEntries, in the transaction that re-snapshots the grade). Changing or re-applying the template of an existing grade clears, on the grade's cards:
- subject and curriculum criterion grades outside the new scale, and evaluation and evaluation criterion grades outside that evaluation's new scale; a default value is in every scale, so it is never cleared;
- Effort values, matched by code against the new snapshot's Effort levels on subject, criterion, evaluation and evaluation criterion slots: a code in both lists (default values included) is kept and takes the new label, any other is cleared, and with Effort off every Effort value is cleared, a default value too. Moving between the fixed list and a scale, or between two scales, therefore clears the values the new list lacks;
- by deleting the slot: texts whose key is gone (every text when texts are off; a text on an item that became preset is kept, unread, and shows again if the preset text is removed and the template re-applied), evaluation slots whose evaluation is gone, evaluation criterion slots whose criterion is gone from their evaluation, and every curriculum criterion slot when the new snapshot excludes criteria. Turning criteria back on does not restore them.
Each card where a cleared or deleted slot held a grade, an Effort value, a text or a Not evaluated mark gets one revision++, and the grade_template_changed audit lists those cards (clearedCardIds). Every generated card of the grade turns stale in any case, through layoutRevision++.
Grade rules (open, add grade, date change): a grade may not have an unpublished cycle grade in another cycle, its published cycles may not overlap the dates, and the dates must fit its department calendar.
Concurrency. Open, add grade, date changes, template update and template delete take the per-tenant advisory lock report-cards:structure:<tenantId> before the cycle row lock. A template update takes it first and then reads the template, validates the request's keys and scales, replaces the custom evaluations and writes the audit in the same transaction, so a snapshot never sees a half-replaced evaluation list; template create needs no lock. Lifecycle commands lock the cycle row FOR UPDATE; entry writes and generation lock it FOR SHARE and then the affected card rows FOR UPDATE in id order. Generation stamps compare-and-set on the revision it read under the lock. Close and bulk generation run under the REPORT_CARD_HEAVY transaction budget.
6. Authority¶
Every decision goes through ReportCardsAccess over PermissionsService.resolveAuthorizedRoleSlice: only assignments whose own grants satisfy the requirement contribute their role keys and parameters. School-wide means a platform admin, admin, or a Director-function role in the slice. Route gates use the singular @RequireScope; the service adds reach.
| Operation | Slice | Reach |
|---|---|---|
| Template create / edit / delete | report_card_templates.configuration WRITE (+ action) |
School-wide only |
| Cycle open / edit / grades / close / reopen / delete | report_cards.cycles WRITE (+ create / delete) |
School-wide, or every grade of the cycle in the slice's departments |
| Publish | cycles WRITE + publish |
Each requested grade's department |
| Value write, unpublished card | report_cards.entries WRITE |
Management: school-wide, the grade's department, or the student's curriculum; writes while OPEN or CLOSED. Teacher, OPEN only, per slot (ownsSlot): subject slots of a course they teach on the anchor date; custom text and custom evaluation slots when they hold the class responsibility as HOMEROOM_TEACHER |
| Generate, unpublished card | entries WRITE + generate |
Management reach (no teacher branch) |
| Write or regenerate a published card | entries WRITE + correct |
Management reach of the correct slice; otherwise 409 REPORT_CARD_PUBLISHED |
| Read cards, previews, documents, worksheets | entries READ |
Tenant-wide for admin, Director/Front Office functions, SEN Manager and any DEPARTMENT/CURRICULUM-parametric role; a teacher-only slice sees only cards with one of its responsibilities; anything else fails closed |
| Read templates, cycles, monitoring | the scope's READ | Tenant-wide |
| Family list / detail | report_cards.published READ |
ReportCardsFamilyPolicy (referent, student) |
Presets: admin holds everything (correct included); the Director manages everything except correct; the department head manages cycles and values of its departments; the curriculum coordinator writes and generates values of its curricula; Front Office, Communications Manager, Administrative Assistant and SEN Manager read; teachers write their own values; referents and students read published cards; HR and staff hold nothing. SEN Manager's template and cycle reads come from its extra-read list. See Permissions.
Hidden targets return the resource's 404 (a teacher-only reader outside its responsibilities gets REPORT_CARD_STUDENT_NOT_IN_CYCLE); visible but non-writable slots return 403 REPORT_CARD_ENTRY_NOT_AUTHORIZED listing each slot with NOT_RESPONSIBLE or CYCLE_NOT_OPEN.
7. Values¶
PATCH /report-card-cycles/:cycleId/entries takes up to 500 slot writes for any number of students and is all-or-nothing. The body lists subjects[] (with criteria[]), evaluations[] (with criteria[], keyed by the snapshot's evaluation and criterion keys) and texts[]; every subject, criterion, evaluation, evaluation criterion and text write counts towards the 500. One student can carry up to 420 evaluation slots, so a whole class does not always fit one request.
- A present field sets the value,
nullclears it, an absent field is untouched.notEvaluated(subjects and evaluations) keeps the stored values, so turning it off restores them. Texts are trimmed; a blank text clears the slot. - Failure order: a student off the roster or hidden (404), validation against the card's live structure and the grade's snapshot (422
REPORT_CARD_ENTRY_INVALID, reasonsSUBJECT_NOT_IN_PLAN,CRITERION_NOT_IN_SUBJECT,VALUE_NOT_IN_SCALE,EFFORT_DISABLED,EFFORT_INVALID,CRITERIA_DISABLED,TEXTS_DISABLED,TEXT_ITEM_UNKNOWN,TEXT_ITEM_PRESET,TEXT_TOO_LONG,EVALUATION_UNKNOWN,EVALUATION_CRITERION_UNKNOWN), a published card withoutcorrect(409), slots outside authority (403). A subject grade is checked against the snapshot's scale, an evaluation grade against that evaluation's own scale. - Default values. Every grade scale and every Effort list of a card also accepts the report-card default values (
REPORT_CARD_DEFAULT_VALUES:-, a value that cannot be given), on subject, curriculum criterion, evaluation and evaluation criterion slots. They are not frozen into the snapshot or the frozen scale (a format shared with the gradebook):withDefaultValues/withDefaultLevelsappend them wherever a card's scale is used — entry validation, re-apply, document labels, and thevalues/effortLevelsthe card detail and worksheets expose, where a default value hasnumericEq,minandmaxnullon every scale type. A scale's own value with the same code wins. Open cycles therefore accept them without a re-apply, and evaluation scales and presets carry no such value. The list is add-only: removing a value would orphan the values stored with it. Not evaluated is a different thing: it removes a subject or evaluation from the card, while a default value is printed. - An Effort value, on any valued kind, is the
codeof one of the snapshot's Effort levels — a value of the template's Effort scale, or a fixed-list code when it has none — or a default value. A value longer than 50 characters, the length of a scale value, fails request validation (400VALIDATION_FAILED) before the rules run; any other value that is not a level code isEFFORT_INVALID, and a value on a card without Effort isEFFORT_DISABLED. - A curriculum criterion write on a card whose snapshot excludes criteria is
CRITERIA_DISABLEDatsubjects[i].criteria[j], checked before the subject and criterion lookups, asTEXTS_DISABLEDis for texts; the subject's own values in the same item are validated as usual. Custom evaluation criteria are unaffected. - The response returns each named student's status, stale flag and missing count after the write.
8. Generation and the document¶
- Generation requires a complete card (else 422
REPORT_CARD_INCOMPLETEwith the missing values). Single generation also refreshes an up-to-date card. Bulk generation (POST …/generate) generates completeREADYandSTALEcards in the caller's reach, skips up-to-date cards unlessrefresh: true, and never touches published cards; cards outside the caller's reach are left out of every count. - Structure and attendance are computed before the card locks; entries are re-read under the lock.
- The document is
ReportCardDocumentV1(v: 1): values with their labels, names, header, the scale and Effort tables, the subjects (each with itsteachers),customEvaluations, custom texts and the attendance block, rendered in that order. A custom text with a preset text prints that text on every card, whatever per-student text is stored. Not evaluated subjects and evaluations are omitted. Each custom evaluation carries its own scale as its legend and labels its grades by it. The Effort table iseffort: { levels: { code, label }[], scale: { id, name } | null } | null(nullwithout Effort): the snapshot's levels as the legend, and the Effort scale they come from,nullfor the fixed list; every Effort value carries its level's{ code, label }. A default value prints with its own label; the scale and Effort legends list only the scale's own values. When the snapshot excludes criteria, every subject hascriteriaMode: 'NONE'and empty criteria and units. Clients render byv. customEvaluationsis always present on a decoded document: av: 1document generated before custom evaluations existed decodes with[]. Likewiseeffort.scaleis always present when Effort is on: av: 1document without it decodes withscale: null.subjects[].teachersis{ firstName, lastName, name }[]: the subject's responsibility (§3 rule 4) frozen without ids, every teacher of the student's own course on the anchor date in teaching order,[]for an unassigned subject. Custom evaluations carry no teachers (the class responsibility's teacher is the header teacher). It is always present on a decoded document: av: 1subject without the key decodes with[], and a key that is present must be an array of objects whose three fields are strings or the whole document is corrupt. Regenerating the card writes the list.- The header teacher is
{ firstName, lastName, name } | null. The decoder requiresnameand accepts the parts only as strings; av: 1document without them stays valid, and clients rendernamefor it. Regenerating the card writes the parts. - Stored JSON (template items, snapshots, documents) is written through
reportCardJsonand read only through the hand-written decoders inshared/snapshots.ts; a payload that fails to decode is corrupt and is never coerced. A shape change bumpsvand adds a decoder branch — frozen documents are never rewritten. The template snapshot's additive keys decode to defaults when absent, so its consumers never handle absence:customEvaluationsas[],criteriaastrue(criteria included) and the Effort section'sscaleasnull(the fixed list); a key that is present but malformed makes the snapshot corrupt. - The school logo is not frozen: previews, stored documents, the template preview structure and family responses mint the current
School.logoFileat read time. The school name is frozen. - The preview (
GET …/students/:studentId/preview) computes the document now without storing it; missing values render as empty strings so the V1 shape holds.
9. Attendance measure¶
Owned by attendance/attendance-tally.ts and report-card-attendance.adapter.ts, over the attendance reader of Reports and the register.
- Window:
from = cycle start,to = min(cycle end, schoolToday − 1)— completed days only. A card generated before the end date ispartial; monitoring flags and filters it (attendancePartial). - A school day for a student is a date the register expects them (
expectedCount = 1: placed, in the calendar, outside closing periods, a timetable version applies and they are on a slot roster) and on which someone in the reader row's department has a register cell (loadRegisterTakenDates). Joiners and leavers count only their placed days. - Measures, each in days and a one-decimal percentage of school days (
nullwithout school days): absences (every cell absent), late entries, early exits (one day can be both). Trip days count as present; suspension days count in no measure and win over a trip; a PERIOD day with only some absent periods is in no measure. - Days not recorded (school days without any cell of the student) stay in the denominator, count in no measure, are listed with their dates, and never block generation or publication. Management completes them in the register (
POST /attendance/records) and regenerates; bulkrefresh: truerefreshes generated cards. - The figures are a snapshot. Register edits never make a card stale; the preview always shows live figures. On a published card, refreshing attendance is a correction (
correctholder, single generation). - Generation copies
attendanceNotRecordedDaysandattendanceThroughonto the card row so monitoring filters without decoding documents. - Each command (preview, single or bulk generation) computes attendance in one reader pass narrowed to its students, then one taken-dates read. The reader resolves each timetable version's schedule once for the window and loads the students' facts per 93-day horizon and their cells per 28-day window (Reports §5), so the cost grows with the horizons, not with the dates.
10. Worksheets and family¶
- Worksheets (
/report-cards/worksheets) are the caller's own: one per course whose teachers include the caller's Teacher row (kindCOURSE), and one class worksheet (kindCLASS) per class whose class responsibility the caller holds — homeroom teacher, or a tier 2–4 user whose session reads cards tenant-wide, so a narrowed teacher view keeps only its own. A class worksheet exists when the grade's snapshot has class items (hasClassItems); it carries the text items without a preset text ([]when texts are off), the evaluations with their own scales and criteria, and each student's texts and evaluation values. Both kinds, like the card detail (template.effortLevels), carry the snapshot's Effort levels aseffortLevels: the Effort scale's values, the fixed list without one, followed by the default values;nullwithout Effort. Every scale they expose likewise ends with the default values (§7). A course worksheet takes its criteria from the card structure, so it has no criterion rows (criteriaMode: 'NONE') when the snapshot excludes criteria. OnlyOPENandCLOSEDcycles are listed; a published grade stays listed read-only until the whole cycle is published.remainingCountcounts students whose card still needs values (INCOMPLETE or STALE) from the worksheet: a subject value for a course worksheet, a text or evaluation value for a class worksheet (classMissingCount). Writes go through the entries route. - Family (
/report-cards/family) usesReportCardsFamilyPolicy(referent link, or the student's own row), evaluated at the Student root. The list returns the published cards of every accessible Student row of the same person (personUuid), so past years appear while access to those rows persists, newest publication first.beingUpdatedis the stale flag of a corrected, not yet regenerated card; the stored document is served until then. Unpublished, inaccessible and unknown targets are one 404REPORT_CARD_NOT_FOUND.
11. REST contract¶
| Method | Path | Gate |
|---|---|---|
GET |
/report-card-templates, /report-card-templates/:id, /report-card-templates/preview-structure |
configuration READ |
POST |
/report-card-templates, /report-card-templates/:id/duplicate |
create |
PATCH / DELETE |
/report-card-templates/:id |
configuration WRITE / delete |
GET |
/report-card-cycles, /report-card-cycles/:id, …/:id/students, …/:id/outstanding |
cycles READ |
POST |
/report-card-cycles |
create |
PATCH |
/report-card-cycles/:id |
cycles WRITE |
PUT / DELETE |
/report-card-cycles/:id/grades/:gradeId |
cycles WRITE |
POST |
…/:id/close, …/:id/reopen |
cycles WRITE |
POST |
…/:id/publish |
publish |
DELETE |
/report-card-cycles/:id |
delete |
POST |
…/:id/generate (bulk), …/:cycleId/students/:studentId/generate |
generate |
GET |
…/:cycleId/students/:studentId, …/preview, …/document |
entries READ |
PATCH |
/report-card-cycles/:cycleId/entries |
entries WRITE |
GET |
/report-cards/worksheets, …/:cycleId/courses/:subjectGroupId, …/:cycleId/class |
entries READ |
GET |
/report-cards/family/students/:studentId/cards, /report-cards/family/cards/:cardId |
published READ + family policy |
Command POSTs answer 200; creates answer 201. Both entities are flat (FLAT_DTO_ENTITIES); no route uses @AggregateResponse(), and no top-level response key is named after a scope.
12. Errors¶
The REPORT_CARD_* family lives in src/common/constants/error-codes.ts with bilingual messages and Swagger examples. Structured payloads reach the wire as passthrough collections (COLLECTION_ENRICHMENTS): each leaf carries its references plus a localized messages.
| Code | HTTP | data |
|---|---|---|
REPORT_CARD_TEMPLATE_IN_USE |
409 | cycles[] { cycleId, cycleName, academicYearId, state } |
REPORT_CARD_TEMPLATE_EVALUATIONS_INVALID |
422 | items[] { index, path, reason } (NAME_TAKEN / CRITERION_NAME_TAKEN / SCALE_NOT_FOUND), one per problem of the request's custom evaluations |
REPORT_CARD_CYCLE_DATES_INVALID |
422 | grades[] { gradeId, calendarStartDate, calendarEndDate } for OUTSIDE_CALENDAR |
REPORT_CARD_CYCLE_GRADE_CONFLICT |
409 | conflicts[] { gradeId, cycleId, cycleName, reason } (UNPUBLISHED_ELSEWHERE / DATES_OVERLAP) |
REPORT_CARD_PUBLISH_BLOCKED |
409 | students[] { studentId, firstName, lastName, gradeId, status } |
REPORT_CARD_ENTRY_INVALID |
422 | items[] { index, path, reason } |
REPORT_CARD_ENTRY_NOT_AUTHORIZED |
403 | items[] { index, path, studentId, reason } |
REPORT_CARD_INCOMPLETE |
422 | missing[] { kind, subjectId, criterionId, textKey, evaluationKey, evaluationCriterionKey } |
The other codes carry no data: template not found / name taken / scale not found (with the top-level field, evaluationScaleId or effortScaleId, naming the template field whose scale the school cannot see) / texts required; cycle not found / name taken / invalid state / grade published / fields locked / has published grades / not authorized; student not in cycle; published; not generated; not found (family); preview curriculum invalid. Unknown grades, subject groups and homerooms use the generic NOT_FOUND; template writes or generation outside the qualifying reach use the generic FORBIDDEN. A template write whose custom text, evaluation or criterion key repeats, or is not one of the template's current keys at its level, is a client error (400 VALIDATION_FAILED). See Error Handling.
13. Audit¶
resolveActor once per transaction, one record per logical mutation (Audit Log):
| Entity type | Actions |
|---|---|
report_card_template |
created (duplicates carry sourceTemplateId), updated, deleted; each template snapshot carries its custom evaluations (key, name, evaluationScaleId, criteria) |
report_card_cycle |
opened, updated, grade_added, grade_template_changed, grade_removed, closed, reopened, grades_published, deleted |
report_card |
generated (bulk uses recordMany; wasPublished marks a correction), corrected (before/after of the changed slots) |
Value writes on unpublished cards are not audited; each entry keeps its last editor. Presentation is label-only.
14. Extending¶
- New document field: add it to
ReportCardDocumentV1only if older documents decode without it; otherwise introducev: 2with its own decoder branch and keep V1 rendering. Never rewrite stored documents. - New template switch or template item list: add the column (or a child table when items reference other rows, as custom evaluations reference their scale), freeze it in
ReportCardTemplateSnapshotV1with a decoder that fills its default when old snapshots lack it (bump the snapshot version otherwise), derive requirements from the snapshot, never from the live template, and decide inpruneIncompatibleEntries(EntryPruneRule) what a re-apply clears. Template writes that replace a list run under the structure lock. A setting without a foreign key may be kept while its switch is off (custom text items are); a setting that holds a scale FK is cleared in the same write that turns its switch off, with a CHECK that backs the rule (the Effort scale,report_card_templates_effort_scale_requires_effort), so a hidden selection never keeps a scale in use. A new scale FK is a realRestrictFK with its ownSCALE_FK_REFERENCESrow. - New value kind: a new
ReportCardEntryKind, slot-key prefix and kind CHECK branch. The CHECK ends withELSE FALSE, so the enum value lands in one migration and the branch that admits it in the next (PostgreSQL cannot use a value in the transaction that adds it); no row of the new kind can exist before its branch. Decide its owner inmissingValueOwnerand its authority inownsSlot; requirement and status logic stays instatus/(pure). - New responsibility tier or role: change the structure rules and
loadClassFallbackHolders, keeping tiers grant-aware; authority stays inReportCardsAccessover qualified slices. - Report cards import other modules only through their barrels (attendance reader and taken dates, grades scale helpers, curriculum coverage, subject-group teachers, student placement fences); no owner module imports report cards.