- Given I have
assessments.createand an active class - When I enter a name, type, exam date, and pick a centre score template or define components with weights and scales
- Then the assessment is
draft; the roster is snapshotted from the class on the exam date; an empty grid opens at once; weights not summing to 100 are blocked; for level scope, a parent assessment gets one child grid per class, each reviewed and published separately.
PRD 13 · Exam scores and learning results
Centre-level assessments per class or per level, a skill-based score structure with weights and scales, grid entry for a whole class or per-student entry with teacher comments and attachments, a draft then review then publish flow, guardian notification through the contact book, score history for re-grading, grading labels, report cards and certificates. FLYER online exams already exist and attach to an assessment as a score source.
Key facts, origin and limits
Drafted by an AI assistant in a single session on 16/09/2026, extending the PRD set under the original request quoted verbatim on the hub page. A proposal for team discussion, not a decision of the product team.
Sources
- Public competitor research (PSE ONE, pse4.one), the
center-crm-frontendcodebase and the staging/v3OpenAPI specification (182 paths), as of 10/09/2026. - The existing / extend / new tags on this page come from a comparison with the staging
/v3specification at that date. - A second scan of PSE ONE's public sources on 16/09/2026 (features page, pricing, user-guide articles).
- Competitor reference: PSE ONE (pse4.one, public scan of 16/09/2026) has a "Quản lý điểm thi" module with two entry modes, whole class and per student, video, image and document attachments, and guardian notification through a mobile application and email; certificates are only mentioned in marketing copy. The specification below is Trường Số's own design, adding a review step, score history and the FLYER score source.
Limits
- Verification pending in-app survey, because automated login to the reference product was not authorised in the drafting session:
- The competitor's score structure: a single column or several per skill, weighted or not.
- Whether academic staff review before scores reach guardians or teachers send directly; whether post-send edits are logged.
- What the parent app shows under "kết quả học tập": score, comment, label, ranking.
- Attachment limits; whether Excel import or grid shortcuts exist.
- Whether "chứng chỉ" is a real product feature or marketing copy; if real, whether it has templates, serials and verification.
Proposed design
An assessment list by status, the whole-class score grid as the central screen, the student drawer and the review modal layered over the grid, a Results tab on the student record, and two mobile screens for guardians and teachers. The key point: scores pass through draft, pending review and published before they reach a guardian, and every cell pulled from FLYER is marked as such.








1Purpose
The evidence of progress a guardian trusts most is the score of a test the centre itself ran, with the class teacher's comment next to it. Today most centres keep scores in each teacher's spreadsheet and post a photo of the sheet into a Zalo group; academic staff have no way to check before scores reach guardians. This module turns an assessment into an operational record: a predefined score structure, a whole-class entry grid, a review step, automatic notification and a history of every correction. FLYER online exam results attach to an assessment as a score source instead of being retyped.
In scope
- Assessments for one class or one level across several classes: periodic (mid-course, end-of-course), regular tests, Cambridge or IELTS mock exams.
- Score structure by component (Listening, Speaking, Reading, Writing or custom) with weights and scales of 10, 100, Cambridge shields, IELTS band; centre-level score templates.
- Whole-class grid entry (keyboard, paste from Excel) and per-student entry; teacher comments; image, document and video attachments.
- Draft, pending review and published states; guardian notification on publish through PRD 07; post-publish corrections with a reason and audit.
- Attaching a FLYER exam or placement test result as a score source; a Results tab on the student record and the parent portal; class xlsx export and student PDF report cards.
- Phase 2: grading-label rules; course completion certificates with a template, serial number and QR verification.
Out of scope
- Authoring, running and marking online tests (the FLYER platform).
- Daily homework and practice scores on FLYER (Học tập tab, PRD 02).
- Periodic progress reports (PRD 07); this PRD supplies the assessment-results block for them.
- Public ranking between students or classes.
2Current state
The FLYER platform already covers the full life cycle of an online exam: exam creation, sessions, candidates, a four-skill score report, notification. Placement test results exist since PRD 01. What is missing is the centre-level assessment record where a paper test, a live speaking test and a FLYER exam sit side by side, get reviewed and get published to guardians.
| Existing capability | Endpoint | Keep? |
|---|---|---|
| FLYER exams, sessions, candidate list | GET/POST /v3/teacher/exams; GET/PUT/DELETE .../{id}; POST .../{exam_id}/sessions; GET .../students | existing the source to pick when attaching to an assessment; matched against the class roster |
| Skill score report (overall, four skills, shields, CEFR, band) | GET /v3/teacher/exams/{exam_id}/score-report; .../analytics/summary | existing read when pulling scores from FLYER |
| Exam notification; learner-side exams | POST .../exams/{exam_id}/notify; GET /v3/learner/exams | existing kept for exam schedules; result notification goes through PRD 07 |
| Placement test with four-skill scores | GET/POST /v3/crm/placement-tests; POST .../{id}/submit-result | existing used as the entry score |
| Learner report for guardians | GET /v3/parent/learner/{id}/report | existing the parent portal adds an assessment results section |
FLYER scores on the student record (StudentScoreEntry) | Học tập tab, PRD 02 | extend add a Results tab |
| Assessment, score structure, entry grid, review, publish, history, grading labels, report cards, certificates | none | new |
3User stories and acceptance criteria
- Given the assessment is
draftorreturnedand I am assigned to the class - When I type into cells, use Enter to move down, Tab to move across, arrow keys to navigate, or paste a range from Excel
- Then each cell is validated instantly against the component scale (0–10 with one decimal, 0–100 integer, shields 0–5, band 0–9 in steps of 0.5); invalid cells get a red border; the total recalculates live; the draft autosaves 10 seconds after the last keystroke; pasted data is matched by row order or by a Student code column, with a dialog stating how many cells will be overwritten.
- Given the assessment grid
- When I click a student's name to open the detail drawer, enter each component, mark Vắng thi or Miễn if needed, write a comment (up to 1,000 characters, with phrase suggestions by score band) and attach up to 5 files
- Then the data saves into the same score record; files go through the existing upload service; the grid row shows a comment icon and file count; Previous and Next move through students.
- Given the assessment has skill components and the school has a completed FLYER exam
- When I choose "Lấy điểm từ FLYER", pick the exam from
GET /v3/teacher/exams, confirm the skill-to-component mapping and the conversion scale - Then the system reads the
score-report, matches candidates to students through their FLYER accounts, fills cells with sourceflyer_exam; students without a test and candidates outside the class are listed; manually entered cells are overwritten only when "Ghi đè" is chosen explicitly.
- Given every student has scores or is marked Vắng thi or Miễn, and comments are complete per configuration
- When the teacher chooses "Gửi duyệt"; academic staff review the grid with anomalies highlighted (zero, maximum, a gap above 3 points between components, empty comment) and choose "Công bố" or "Trả lại" with a note
- Then submit moves to
pending_review, locks the grid for the teacher and notifies holders ofassessments.publish. Publish moves topublished, locks scores and emitsassessment.published; PRD 07 sends one message to the primary contact of each scored student (parent app, Zalo OA, email; ZNS templateassessment_published) with the student name, assessment name, total and a link to the result page showing component scores, comment, files and grading label, with no ranking; delivery status is shown on the assessment. Return moves toreturned, the teacher gets the note and the grid reopens.
- Given the assessment is
publishedand I haveassessments.regrade - When I edit a score cell or comment, enter a reason (required, 10–300 characters) and choose whether to send an adjustment message
- Then a history row stores before, after, actor, time and reason; total and label recalculate; the parent portal shows "Đã điều chỉnh ngày dd/MM"; the adjustment message goes only to the affected guardian; a teacher without this permission sees "Đề nghị sửa điểm", which creates a task for academic staff.
- Given a student with at least one published assessment
- When I open the Results tab on the student record
- Then I see a table of assessments (date, class, type, each component, total, label), a line chart per component, the entry score from the placement test, and "Xuất bảng điểm", which produces a one-page PDF with the centre logo, the score table and the comment; on the assessment screen, "Xuất Excel" produces a class xlsx with a scores sheet and a comments sheet.
- Given I have
assessments.settingsand a published end-of-course assessment - When I define label bands per scale (for a 10-point scale: Xuất sắc from 9.0; Giỏi from 8.0; Khá from 6.5; Trung bình from 5.0; Cần cố gắng below 5.0), then choose "Cấp chứng nhận", pick a template and confirm the list of eligible students (minimum label and attendance rate)
- Then assessments published after the rule is saved show labels, earlier ones keep their old label unless "Áp dụng lại" is chosen; bands must cover the scale without overlap. Each certificate gets a unique serial (for example
CN-2026-000123), a PDF rendered from the template with a QR code leading to a public verification page, and is sent to the guardian through PRD 07; revocation requires a reason and the verification page shows "Đã thu hồi".
4Screens
Tabs: Nháp (N) · Chờ duyệt (N) · Đã công bố · Tất cả. Filters: branch, class, level, type, teacher, exam date range. Columns: assessment name, class or level, exam date, teacher, entry progress (11/12), source (Nhập tay · FLYER · Xếp lớp), status (Nháp · Chờ duyệt · Đã công bố · Trả lại), notifications sent / total, kebab Mở · Nhân bản · Lưu trữ. Empty state: "Chưa có kỳ đánh giá. Tạo kỳ đánh giá đầu tiên từ một lớp đang học hoặc lấy điểm từ bài kiểm tra FLYER." Teachers only see their assigned classes.
Fields: Name (required, 3–120 characters); Type (midterm / final / regular / mock_cambridge / mock_ielts); Scope (one class / one level with class selection); Exam date (required); Score template (prefills the component table); Component table (name, scale, maximum, weight %; 1–10 rows, total = 100); Total scale (ten / hundred / shields / band); Comment required toggle; Reviewer (optional).
Header: assessment name, exam date, status, autosave time; buttons Lấy điểm từ FLYER · Dán từ Excel · Xuất Excel · Gửi duyệt. Table: one row per student, one column per component (header shows the weight), a Total column, a label column (P2), an NX column; footer row: entered / total, mean, highest, lowest. The student column is pinned on horizontal scroll. Enter moves down, Tab right, arrow keys navigate, Escape cancels a cell edit, Ctrl+Z undoes. FLYER-sourced cells carry an F mark. Typing "v" marks Vắng thi, "m" marks Miễn. The NX column opens the student drawer. States: loading skeleton; empty roster "Lớp chưa có học viên tại ngày thi"; conflict "Lưới đã được [tên] cập nhật lúc HH:mm" with a reload button; banner "Chờ duyệt: chỉ đọc" or "Đã công bố: sửa điểm cần lý do".
Drawer: name, student code, total and label; one field per component; Vắng thi / Miễn toggle; comment (1,000 characters, "Gợi ý" inserts a phrase by score band); attachments (drag and drop, image preview, video playback); score history; Trước · Sau · Lưu. Review modal: student count, scored, absent, exempt, missing comments, anomalies linked to their cells; a preview of the guardian message with a sample student, the number of reachable guardians and the number without a channel; toggles "Gửi thông báo ngay" and "Hiển thị xếp loại" (P2); Công bố · Trả lại with note · Huỷ.
Student record: table of published assessments (date, class, type, each component, total, label, comment, files), a line chart per component over up to 12 assessments, an "Điểm đầu vào" block from the placement test, certificates (P2), a PDF report card button. Parent portal: a "Kết quả kỳ đánh giá" section on the PRD 07 results page, one card per assessment opening a detail page; no ranking. Settings: score templates (system templates: four skills on 10, Cambridge YLE shields, IELTS band, single-column regular test), comment phrases, grading rules (P2), certificate templates and serial format (P2).
5Business rules
- An assessment belongs to one branch and one class (or one level with child grids per class); the roster is snapshotted from active enrollments on the exam date; academic staff may add students manually before publishing, audited.
- Weights sum to 100. Total = sum (component score converted to the total scale × weight) / 100; rounded to one decimal on the 10-point scale, an integer on 100, steps of 0.5 for band. An absent component counts 0; an exempt component leaves the denominator; a fully absent student has no total and no label.
- States:
draft→pending_review→published;pending_review→returned→pending_review;published→archived. Any other transition returnsINVALID_TRANSITION. - Submit requires every student scored or marked absent or exempt, and comments complete when "Nhận xét bắt buộc" is on. The reviewer must differ from the submitter when the centre enables "Tách vai trò duyệt" (default on for branches with at least 2 publish holders).
- After publishing, scores and comments change only through
assessments.regradewith a reason; every change is written to history and never overwrites the old value; a published assessment cannot be deleted, only archived. - Publish notification sends one message to each scored student's primary contact through PRD 07 with type
assessment_published; quiet hours 21:30–07:00 apply; an adult student with a parent-portal role for themselves receives it directly. - Values pulled from FLYER keep the
exam_idand read time; refresh is allowed only in draft and returned; manually edited cells carrymanual_overrideand are not overwritten by a refresh unless chosen explicitly. - Fixed conversions: 100 to 10 divides by 10; Cambridge shields to 10 multiplies by 2; IELTS band keeps its value with the note "quy đổi tham khảo"; no reverse conversion from 10 to band.
- Labels (P2) are computed at publish time and stored with the rule version. Certificates (P2) are issued only from a published end-of-course assessment; serials are unique per tenant and never reused after revocation; the public verification page shows only the student name, course, issue date, branch and status.
- Attachments: up to 5 per student per assessment; images, PDF, DOCX up to 10 MB; video up to 100 MB or a link; retained 24 months after publishing.
- Assessments are branch-level data (
school_id); score templates, grading rules, certificate templates and comment phrases are centre-level data (center_id).
6Data model
| Entity | Fields | Notes |
|---|---|---|
| assessment_template | id; center_id; name; components jsonb [{key, name, scale, max, weight}]; total_scale enum(ten, hundred, shields, band); comment_required; is_system | centre level; new |
| assessment | id uuid; center_id; school_id; class_id null; level_id null; parent_assessment_id null; name; type enum(midterm, final, regular, mock_cambridge, mock_ielts); exam_date; components jsonb (snapshot from template); total_scale; comment_required; source enum(manual, flyer_exam, placement_test, mixed); flyer_exam_id null; flyer_synced_at; status enum(draft, pending_review, returned, published, archived); reviewer_staff_id null; published_by; published_at; return_note; show_grade; grading_rule_version; version int; created_by; updated_at | branch level; new; a level assessment is a parent record with child records per class |
| assessment_score | id uuid; assessment_id; student_id; enrollment_id null; roster_type enum(enrolled, added); component_scores jsonb {key: {value, source, manual_override}}; total null; grade_label null; attendance_status enum(taken, absent, exempt); exempt_components[]; flyer_learner_id null; updated_by; updated_at; version int | unique (assessment_id, student_id); new |
| assessment_comment | id; assessment_score_id (unique); body text (≤ 1,000 characters); template_phrase_id null; author_staff_id; updated_at | new; separate table for full-text search |
| assessment_attachment | id; assessment_score_id; kind enum(image, document, video, video_link); file_id null; url null; name; size_bytes; uploaded_by; created_at | new; max 5 per score |
| assessment_score_history | id; assessment_score_id; field (component key | total | comment | attendance_status); from_value; to_value; reason; notify_sent; actor_id; created_at | audit detail; new |
| grading_rule (P2), comment_phrase | grading_rule: id; center_id; scale; version; bands jsonb [{label, min, max}]; min_component null; effective_from. comment_phrase: id; center_id; min_total; max_total; text; lang | centre level; each rule edit creates a new version |
| report_card | id; school_id; student_id; scope enum(assessment, course, period); assessment_id null; period_from; period_to; file_id; generated_by; generated_at; sent_at | new; rendered PDF, immutable |
| certificate_template, certificate (P2) | template: id; center_id; name; background_file_id; fields jsonb (positions of name, course, date, serial, QR); serial_format (CN-{yyyy}-{seq:6}); signer_name; signature_file_id; seal_file_id. certificate: id; center_id; school_id; student_id; assessment_id; template_id; serial (unique per center); course_name; grade_label; issued_by; issued_at; file_id; qr_token (unique); status enum(active, revoked); revoked_reason | template centre level; certificate branch level with centre-level serial; new |
| message_template (PRD 07) | type adds assessment_published, assessment_adjusted, certificate_issued | extend |
7API contract
| Method | Path | Purpose / request / response | Status |
|---|---|---|---|
| GET/POST | /v3/teacher/exams; GET/PUT/DELETE /v3/teacher/exams/{id}; GET .../{exam_id}/students, /score-report, /analytics/summary | FLYER exams, candidates and skill scores; read by the backend when attaching a FLYER source | existing |
| POST / GET | /v3/teacher/exams/{exam_id}/sessions, /notify; /v3/learner/exams; /v3/parent/learner/{id}/report; /v3/crm/placement-tests, /{id}/submit-result | unchanged | existing |
| GET/POST | /v3/crm/assessments | filters school_id, status, class_id, level_id, type, teacher_id, from, to → ListResponse; POST {school_id, name, type, exam_date, class_id | level_id + class_ids[], template_id | components[], total_scale, comment_required, reviewer_staff_id?} → assessment (children[] for level scope) | new |
| GET/PUT/DELETE | /v3/crm/assessments/{id} | detail with counts; PUT only in draft/returned; DELETE only in draft; 409 STALE_VERSION | new |
| GET / PUT | /v3/crm/assessments/{id}/grid; PUT /{id}/scores | grid → {assessment, components, rows: [{student, roster_type, score, comment_present, attachment_count, flags}], stats}; scores {rows: [{student_id, component_scores?, attendance_status?, exempt_components?}], mode: grid|paste, overwrite, version}; validated in full before writing; 422 with the list of invalid cells; Idempotency-Key | new |
| GET/PUT | /v3/crm/assessments/{id}/students/{student_id}; POST/DELETE .../attachments[/{attachment_id}]; GET .../history | one student's scores, comment, files; PUT {component_scores, attendance_status, comment, reason? (required when published), notify_adjustment?}; score history | new |
| POST | /v3/crm/assessments/{id}/roster; /link-flyer-exam; /submit; /return; /publish; /archive | roster {add[], remove[], reason} before publishing only; link-flyer-exam {exam_id, mapping, overwrite} → {filled, unmatched_students[]}; /return {note}; /publish {notify, show_grade} emits assessment.published; INVALID_TRANSITION, VALIDATION_INCOMPLETE | new |
| GET / POST | /v3/crm/assessments/{id}/export?format=xlsx; POST /{id}/report-cards | class xlsx; report-cards {student_ids[] | all} → {job_id} | new |
| GET | /v3/crm/students/{id}/results | published assessments (staff also see drafts, flagged), entry score, certificates, per-component series | new |
| GET/POST/PUT/DELETE | /v3/crm/assessment-templates[/{id}]; /v3/crm/comment-phrases[/{id}]; GET/POST /v3/crm/grading-rules; /v3/crm/certificate-templates | centre-level catalogues; deleting a template in use is blocked; versioned grading rules (P2) | new |
| GET/POST | /v3/crm/certificates; POST /{id}/revoke; GET /{id}/file; GET /v3/crm/certificates/verify/{qr_token} | {assessment_id, template_id, student_ids[]} → certificates with serials; revoke {reason}; verify is public, no login (P2) | new |
| GET | /v3/parent/learner/{id}/results; /results/{assessment_id} | published results for the parent portal; no other student's data | new |
| GET/PUT | /v3/teacher/school-config | assessment_policy block: comment_required_default, separate_reviewer, notify_absent, show_grade_default, attachment_limits | extend |
8Permissions
| Code | owner | branch_manager | admissions | academic_admin | accountant | teacher | parent |
|---|---|---|---|---|---|---|---|
assessments.view | all | branch | no | branch | no | assigned classes | linked children (published only) |
assessments.create, .enter_scores, .submit, .link_flyer, .export | yes | branch | no | branch | no | assigned classes | own child's report card only |
assessments.publish, assessments.roster_edit | yes | branch | no | branch | no | no | no |
assessments.regrade | yes | branch | no | branch | no | request only | no |
certificates.issue, certificates.revoke (P2) | yes | branch | no | issue; revoke needs branch_manager | no | no | no |
assessments.settings (templates, labels, certificate templates, phrases) | yes | no | no | comment phrases only | no | no | no |
9Analytics events
| Event | Properties |
|---|---|
| assessment_created | type, scope (class|level), from_template, components_count, total_scale |
| assessment_scores_saved | mode (grid|single|paste|flyer_import), cells_written, cells_invalid, seconds_in_grid, by_role, unmatched (flyer) |
| assessment_submitted / assessment_returned | students, absent_count, comments_missing, hours_from_exam_date, anomalies_flagged |
| assessment_published | students, notified, skipped_no_channel, show_grade, hours_in_review |
| assessment_notification_sent | channel, outcome, template (published|adjusted|certificate) |
| assessment_score_regraded | field, days_after_publish, by_role, notify |
| assessment_exported | format (xlsx|pdf), scope (class|student) |
| parent_result_viewed | seconds_after_notification, has_attachment, has_grade |
| certificate_issued / certificate_revoked / certificate_verified | template_id, count, reason, source (qr|link) |
10Edge cases
- Student transfers class between the exam date and publishing: stays on the old class's roster; the record shows the class name at exam time.
- Student enrolled after the exam date but sat the test: academic staff add them manually (
roster_type = added) with a reason before publishing. - Main teacher and assistant enter the grid at the same time: the save with a stale
versiongets 409 with the latest grid and differing cells highlighted. - Excel paste with extra rows or unknown student codes: those rows are skipped and listed, with no row shift.
- FLYER candidate not linked to a student in Trường Số: listed as "Chưa khớp" with name suggestions; academic staff match once and the link is saved on the student record (PRD 02).
- Changing components or scale after scores exist: draft only, with a warning of how many cells will be cleared; removing a component with scores requires typing its name to confirm.
- Guardian without any consented channel: message skipped with reason "Chưa có kênh liên lạc"; the assessment shows the skipped count and the desk sees it in the follow-up list (PRD 07).
- Publishing during quiet hours: results appear on the portal immediately, messages are held until 07:00.
- A re-grade makes an issued certificate ineligible: not revoked automatically; the assessment shows a warning for academic staff to decide.
- Level assessment with one class unfinished: other classes still publish separately; the level report states how many classes are published.
11Open questions
- Should students see results inside the FLYER learning app? Proposed: phase 1 guardians only; phase 2 consider students aged 15 and up.
- Should comments be required by default for every assessment type? Proposed: required for mid-course and end-of-course, optional for regular tests.
- Do certificates need a legally recognised digital signature? Legal opinion needed before P2.
- Should regular test scores roll into the end-of-course total by weight? Proposed: phase 2, as a composite assessment reading from component assessments.
- Does the PRD 07 progress report replace the PDF report card? Proposed: both; the report card covers one assessment, the progress report a period.
Trường Số (truongso.net) · FLYER internal