- Given the student list
- When I type a name, phone (student or guardian), code or username
- Then results filter server-side within 300 ms, accents ignored, and the guardian phone match is highlighted.
PRD 02 · Students (SIS)
The student record as the center of operations: profile, guardians, status lifecycle, enrollment history, session balance, documents, notes and learning data from FLYER. Extends the existing school student and parent objects.
Key facts, origin and limits
Drafted by an AI assistant in a single session on 10/09/2026, from the original request quoted verbatim on the hub page; the Proposed design section was added on 16/09/2026. 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.
Limits
- Verification pending in-app survey, because automated login to the reference product was not authorised in the drafting session:
- Competitor's student statuses and how "bảo lưu, nghỉ học" affect packages and reminders.
- Whether they store several guardians and which one receives messages.
- Student code format and whether it is per branch.
- How "báo cáo học sinh sắp hết hạn" is defined (sessions, days, or amount).
- Which documents they attach to a student and whether a photo is required.
Proposed design
A student list with every column and status chip, a two-column student profile with a fixed right rail (primary guardian, session balance, debt, FLYER account) and three status-change dialogs that always require a reason; the key point is that every manual transition (pause, withdraw, transfer branch) previews its effect on enrollments, session packages and finance before confirmation.








1Purpose
Every downstream module (classes, attendance, tuition, communication) needs one authoritative student record per branch with a clear status, verified guardian contacts and a history of what the student bought and attended. The FLYER backend already has the learner profile (StudentProfile, int64 id), a single StudentParent, finance stats and score entries. This PRD adds the operational wrapper: status lifecycle, several guardians with consent, enrollment history, documents, notes, and the session balance derived from PRD 05 packages and PRD 04 attendance.
In scope
- Student list and profile for staff.
- Status lifecycle and transitions with reasons.
- Guardians (many per student, one primary payer, one primary contact).
- Enrollment history, transfers between classes and branches, pause and withdrawal.
- Documents and notes, consent records.
- Learning data panel from FLYER (scores, assignments, study time).
Out of scope
- Student self-service (adult learners use the parent portal role, PRD 07).
- Academic grading beyond what FLYER already records; report cards are PRD 07 progress reports.
2What exists today
| Object / endpoint | Fields | Use |
|---|---|---|
GET /v3/teacher/school-finance/students (SchoolStudent) | id, first_name, last_name, display_name, avatar_url, phone_number, email, user_name, student_name, class {id, name, code, class_type} | Base list, filter by class_ids and q |
GET /v3/teacher/school-finance/students/{id} (StudentDetail) | user {phone, email, user_name}, parent {id, display_name}, courses[] | Base detail |
.../students/{id}/stats (StudentStats) | current_class_name, total_amount, paid_amount, unpaid_amount, payment_count | Finance summary |
.../students/{id}/tuitions, /courses | Tuition[], Course[] | Finance tab |
StudentScoreEntry via exam score report | overall, four skills, Cambridge shields, CEFR, IELTS band | Learning tab |
GET /v3/parent/learner/{id}/report, /weekly-report | assignments, self-study, learning time, skills | Learning tab and parent report |
CustomerLearner, GET /v3/crm/customers/learners/search | lead to learner link | Conversion (PRD 01) |
GET /v3/teacher/students/quick-search | Pickers |
Missing: a student status, several guardians, enrollment as an object (today a student is simply "in a class"), documents, notes, consent, branch transfer, session balance.
3Status lifecycle
| Status | VI label | Meaning | Enters by | Leaves to |
|---|---|---|---|---|
prospect | Tiềm năng | Not a student yet; lead record only (PRD 01). Not stored in student table. | trial, waiting, active | |
trial | Học thử | Has a trial booking, no enrollment | trial booking from lead | waiting, active, dropped |
waiting | Chờ xếp lớp | Converted, no class yet | conversion without class | active |
active | Đang học | At least one active enrollment | enrollment start | paused, completed, withdrawn |
paused | Bảo lưu | Enrollment suspended with an expected return date; package sessions frozen | staff action with reason and dates | active, withdrawn |
completed | Hoàn thành | All enrollments ended by course completion | class end | active (re-enroll), alumni |
withdrawn | Đã nghỉ | Left before completion; reason recorded | staff action | active (re-enroll) |
dropped | Không nhập học | Trial or waiting student who never enrolled | staff action or 60 days idle | active |
Status is stored on the student record and recomputed by the system when enrollments change; manual transitions are limited to paused, withdrawn and dropped and always require a reason. A student who is Active in one branch and transfers keeps one record per branch (school); the profile shows both.
4User stories and acceptance criteria
- Given permission
students.create - When I submit the student form with at least name, birthday, branch, one guardian with phone
- Then a learner profile is created on FLYER (username generated, credentials shown once and sent to the guardian on request), the student record is Waiting, and a lead is not created.
- Given a student profile
- When I add a guardian with name, relationship, phone, email, Zalo consent, flags "Liên hệ chính" and "Người thanh toán"
- Then the guardian is saved; exactly one guardian is primary contact and one is payer (may be the same); a guardian shared by siblings is one record linked to several students; changing the phone writes an audit entry and re-verifies Zalo reachability (PRD 07).
- Given a Waiting or Active student
- When I choose class, start date and (optionally) fee plan
- Then an enrollment is created (PRD 03) if capacity allows; the student appears in the class roster from the start date; sessions before the start date are not counted as absences; status becomes Active.
- Given an Active student
- When I set pause from date, expected return, reason
- Then status Paused; the roster hides the student from sessions in the pause window; the session package expiry (PRD 05) is extended by the pause length; a reminder is created 7 days before expected return; resume re-activates and asks for the class (same or new).
- Given an Active or Paused student
- When I withdraw with date and reason (select: chi phí, chuyển nhà, không hài lòng, hoàn thành mục tiêu, khác + free text)
- Then all enrollments end on that date; unused sessions are shown with the refund estimate from PRD 05 and a link "Tạo phiếu hoàn"; the churn report in PRD 09 records the reason.
- Given a student at branch A
- When I transfer to branch B with date and target class (optional)
- Then the A enrollment ends, a B student record is created or reused with the same learner profile, guardians are shared, package balance moves to B (finance entry at A "chuyển đi", at B "chuyển đến"), and the profile shows a timeline entry.
- Given a linked learner profile
- When I open the Học tập tab
- Then I see study time this month, assignments assigned/completed, latest scores per skill, placement result and latest exam entries, all from existing endpoints, with an empty state "Chưa có dữ liệu học tập" when the learner has not used FLYER.
- Given permission
students.edit - When I upload a file with type (birth certificate, ID, agreement, photo consent, medical, other) or write a note with visibility (staff, teachers too)
- Then the file is stored privately with signed URLs; notes show author and time; notes marked "Cần lưu ý" appear as a chip on the roster for teachers (e.g. allergy).
- Given permission
students.anonymiseand a Withdrawn or Dropped student - When I confirm with the request reference
- Then personal fields are replaced (PRD 12), documents deleted, finance records retained with the anonymised name, and the action logged.
5Screens
Header: h1 Học viên, buttons Nhập từ Excel (PRD 11), + Thêm học viên. Tabs with counts: Tất cả · Đang học · Chờ xếp lớp · Học thử · Bảo lưu · Sắp hết buổi · Nợ học phí · Đã nghỉ. Filters: search, branch, class, level, guardian consent, start date range. Bulk: Gửi thông báo (PRD 07), Xuất Excel, Chuyển lớp.
| Column | Source | Notes |
|---|---|---|
| Mã HV | student.code | HV-000123 per tenant |
| Họ tên | display_name + avatar | link; age in small text "8 tuổi" |
| Lớp | active enrollments | chips; dashed "Chưa xếp lớp" |
| Trạng thái | status | chip per lifecycle table |
| Phụ huynh | primary guardian name + phone | phone click-to-copy |
| Buổi còn lại | session balance (PRD 05) | red when ≤ 3; "-" for monthly plans |
| Công nợ | unpaid_amount (StudentStats) | red when > 0 and overdue |
| Điểm danh 30 ngày | attendance rate | percent; late/absent counts on hover |
| Chi nhánh | school name | only when "Tất cả chi nhánh" |
| Ngày nhập học | first enrollment start | |
| Thao tác | Xếp lớp · Thu học phí · Gửi tin |
Empty state: "Chưa có học viên. Chuyển đổi từ khách hàng hoặc thêm trực tiếp." Rows 44 px; 50 per page; column visibility saved via view-configs (view key student_list).
Header: avatar, name, code, age, status chip, branch, class chips, "Cần lưu ý" chip if any flagged note. Buttons: Sửa · Xếp lớp · Thu học phí · Gửi tin · kebab (Bảo lưu, Nghỉ học, Chuyển chi nhánh, Ẩn danh hoá). Right rail: Phụ huynh chính (name, relationship, phone, Zalo status), Số dư buổi, Công nợ, Tài khoản FLYER (username, last active).
Tabs: Tổng quan · Phụ huynh · Lớp & lịch sử · Điểm danh · Học phí · Học tập · Tài liệu · Ghi chú · Nhật ký.
- Tổng quan: personal info (birthday, gender, school, address, level, allergies), consent summary, timeline of key events (created, converted, enrolled, paused, transferred).
- Phụ huynh: cards per guardian with flags; + Thêm phụ huynh; "Gửi lại thông tin đăng nhập".
- Lớp & lịch sử: table of enrollments (class, level, teacher, start, end, status, sessions attended / total) and actions Chuyển lớp, Kết thúc.
- Điểm danh: month view with chips per session, rate, absences list with excuses (PRD 04).
- Học phí: package balance, tuition list with status, receipts, "Tạo hoá đơn" (PRD 05).
- Học tập: FLYER data (STU-08).
- Tài liệu, Ghi chú, Nhật ký (audit).
| Field | Type | Validation |
|---|---|---|
| Họ và tên | text | required 2–80 |
| Ngày sinh | date | required; age 3–80 |
| Giới tính | Nam/Nữ/Khác | optional |
| Chi nhánh | select | required |
| Trường đang học, Lớp (phổ thông) | text | optional |
| Trình độ hiện tại | select CEFR/Cambridge level list (PRD 03 catalog) | optional |
| Địa chỉ | text | optional |
| Lưu ý sức khoẻ | text | optional; flagged note |
| Phụ huynh 1 | name, relationship, phone, email, consent | phone required; existing guardian suggested by phone |
| Tài khoản FLYER | radio: tạo mới / liên kết có sẵn (search by username or phone) | required |
| Ảnh | upload | optional, 5 MB |
6Business rules
- One student record per (school, learner_profile). The same learner at two branches is two records sharing guardians and learning data.
- Student code is tenant-wide sequential and never reused.
- A student must have at least one guardian with a phone unless age ≥ 18 (adult learner, self as contact).
- Status is derived from enrollments except for manual Paused, Withdrawn, Dropped; manual transitions need a reason.
- Pause maximum 90 days by default (tenant setting); longer pauses require branch manager approval.
- Withdrawal does not delete anything; re-enrollment reuses the record.
- Guardian consent is versioned; a message channel is used only when consent for that channel exists (PRD 07).
- Notes flagged "Cần lưu ý" are visible to teachers of the student's classes; other notes are staff-only.
7Data model
| Entity | Fields | Notes |
|---|---|---|
| student (new; wraps learner profile) | id uuid; tenant_id (center_id); school_id int64; learner_profile_id int64; code; display_name; birthday; gender; school_name; grade; level_code; address; health_note; status enum (lifecycle); status_reason; status_changed_at; first_enrolled_at; avatar_file_id; created_by; created_at; updated_at | unique (school_id, learner_profile_id) |
| guardian (new; supersedes single StudentParent for operations) | id uuid; tenant_id; full_name; phone (E.164); email; zalo_user_id null; address; parent_user_id int64 null (FLYER parent account); created_at | unique (tenant_id, phone) |
| student_guardian | student_id; guardian_id; relationship enum(father, mother, grandparent, sibling, self, other); is_primary_contact; is_payer; created_at | PK (student_id, guardian_id) |
| consent | id; guardian_id; student_id null; type enum(service, zalo, email, marketing, photo); version; granted_at; revoked_at; channel enum(web, portal, paper); actor_id | |
| student_status_history | id; student_id; from_status; to_status; reason; effective_date; expected_return_date null; actor_id; created_at | |
| student_document | id; student_id; type enum(birth_cert, id, agreement, photo_consent, medical, other); file_id; name; uploaded_by; created_at | |
| student_note | id; student_id; body; is_flagged; visibility enum(staff, teachers); author_id; created_at | |
| enrollment (PRD 03 owns) | see PRD 03 | |
| student_balance (maintained summary) | student_id; sessions_purchased; sessions_used; sessions_remaining; sessions_expire_on; unpaid_amount; overdue_amount; attendance_rate_30d; updated_at | recomputed on package, attendance, tuition writes |
8API contract
| Method | Path | Request / response | Status |
|---|---|---|---|
| GET | /v3/school/students | q, school_id, status[], class_ids[], level, has_debt, low_balance, consent_zalo, enrolled_from, enrolled_to, sort, page, limit → StudentRow[] (student + primary guardian + balance) | new (wraps existing school-finance/students) |
| POST | /v3/school/students | {school_id, display_name, birthday, gender, school_name, grade, level_code, address, health_note, learner: {learner_profile_id} | {create: true}, guardians: [{guardian_id | {full_name, phone, email}, relationship, is_primary_contact, is_payer, consents: [type]}]} → Student | new |
| GET | /v3/school/students/{id} | Student + guardians + enrollments + balance + flags | new |
| PATCH | /v3/school/students/{id} | partial profile | new |
| POST | /v3/school/students/{id}/status | {to: paused|withdrawn|dropped|active, reason, effective_date, expected_return_date?, class_id?} → Student; 409 INVALID_TRANSITION | new |
| POST | /v3/school/students/{id}/transfer | {to_school_id, effective_date, class_id?, note} → {new_student_id} | new |
| GET/POST | /v3/school/students/{id}/guardians | add or link guardian; PATCH /v3/school/guardians/{id}; DELETE link | new |
| GET/POST | /v3/school/guardians?phone= | lookup by phone for reuse | new |
| POST | /v3/school/consents | {guardian_id, student_id?, type, version, channel} ; DELETE to revoke | new |
| GET/POST/DELETE | /v3/school/students/{id}/documents | {type, file_id, name} | new |
| GET/POST/PATCH/DELETE | /v3/school/students/{id}/notes | {body, is_flagged, visibility} | new |
| GET | /v3/school/students/{id}/balance | student_balance | new |
| POST | /v3/school/students/{id}/anonymise | {request_ref} | new |
| GET | /v3/teacher/school-finance/students/{id}/stats | /tuitions | /courses | finance tab | existing |
| GET | /v3/parent/learner/{id}/report, /weekly-report | learning tab (staff call with school auth) | existing (needs staff access grant) |
| GET | /v3/teacher/students/quick-search | pickers | existing |
| POST | /v3/school/students/{id}/credentials/resend | sends FLYER login to guardian channel | new |
9Permissions
| Code | owner | branch_manager | academic_admin | admissions | accountant | teacher |
|---|---|---|---|---|---|---|
students.view | all | branch | branch | read (name, class, guardian) | branch read | own classes |
students.create | yes | yes | yes | via convert | no | no |
students.edit | yes | yes | yes | no | no | notes only |
students.status (pause, withdraw, drop) | yes | yes | yes | no | no | no |
students.transfer | yes | yes | no | no | no | no |
students.guardians | yes | yes | yes | no | payer flag | no |
students.documents | yes | yes | yes | no | no | no |
students.learning.view | yes | yes | yes | no | no | own classes |
students.anonymise | yes | no | no | no | no | no |
students.export | yes | yes | yes | no | yes | no |
10Analytics events
| Event | Properties |
|---|---|
| student_created | via (convert|form|import), age_bucket, with_class |
| guardian_added | relationship, consents[] |
| student_status_changed | from, to, reason_code, manual (bool) |
| student_transferred | from_school, to_school |
| student_profile_viewed | tab, role |
| student_note_flagged | |
| student_anonymised |
11Edge cases
- Guardian with two phones: second phone is a second guardian record marked relationship "other" and note; or the phone is updated. No multi-phone field in v1.
- Twins with the same guardian and birthday: allowed; duplicate warning shows both, admin confirms.
- Learner profile deleted on FLYER side: student stays with a "Tài khoản đã xoá" chip; learning tab empty.
- Pause overlapping a package expiry: expiry is extended by pause days, computed at resume.
- Student age turns 18 during study: no automatic change; adult flag editable.
- Import creates students without guardians: allowed with a warning chip "Thiếu phụ huynh" and a list tab filter.
12Open questions
- Should the FLYER learner account be mandatory at creation (D4) or allowed later for centers that do not use FLYER content?
- Is "Hoàn thành" a status centers actually use, or do they treat end of course as withdrawal?
- Do we need a photo-consent flag at student level for social media posts?
Trường Số (truongso.net) · FLYER internal