- Given Settings → Tích hợp → Zalo
- When I complete the OAuth flow (or paste app id, secret, OA id, access and refresh tokens as the current Zalo flow requires) and the connection test passes
- Then the OA appears as connected with name and avatar; the ZNS template registry lists FLYER default templates (attendance absent, late, invoice, receipt, reminder, schedule change, credentials, welcome) with their Zalo template ids to be registered per OA; each template row shows status (Chờ duyệt / Đã duyệt / Từ chối) and parameters.
PRD 07 · Parent communication
The contact book (sổ liên lạc): transactional messages for attendance, fees and schedule changes, announcements to classes and branches, progress reports built from FLYER learning data, Zalo OA and ZNS configuration, and a mobile-first parent portal with OTP login.
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 Zalo configuration fields (app id, secret, OA token) and which events send ZNS.
- The list of their ZNS template types and parameters (their guide references a spreadsheet).
- What the parent app shows (sổ liên lạc điện tử): scores, attendance, fees, chat.
- Whether teachers can message parents directly.
- How score entry ("nhập điểm thi") reaches parents (app, email) and whether attachments are supported.
Proposed design
The layout pairs a staff contact-book page (five tabs, KPI tiles and a message log by channel, status and cost) with a mobile-first parent portal; the most important thing to see is that every message shows its channel, delivery outcome and ZNS cost.








1Purpose
Parents judge a center by how quickly and clearly it communicates. The contact book turns operational events into messages a guardian receives where they already are (Zalo), gives staff one place to send announcements and progress reports, and offers a portal for timetable, attendance, invoices with QR payment and learning reports. The backend already sends tuition and lesson reminders through app, email and Zalo with per-channel outcomes, and exposes parent-facing tuition and learning report endpoints. This PRD adds the messaging layer, templates, consent-aware routing, announcements, progress reports and the portal.
In scope
- Message routing: Zalo ZNS (templated, paid), Zalo OA follow-up messages (free within 48 h of user interaction), in-app push (FLYER parent app), email; SMS not built.
- Transactional messages: attendance, fee created, receipt, reminder, low balance, schedule change, session cancelled, trial confirmation, enrollment welcome, credentials.
- Announcements to a branch, class, or selected students; scheduling; read receipts where the channel supports it.
- Progress reports: periodic (monthly or end of course) with attendance, FLYER scores, teacher comment; PDF and portal.
- Parent portal: OTP login, children switcher, timetable, attendance, invoices with QR, receipts, reports, announcements, excuse request, make-up request, data correction request, consent management.
- Zalo OA configuration per tenant, ZNS template registry, cost visibility.
Out of scope
- Two-way chat inbox (P3; Pancake and Zalo OA inbox remain the tools).
- Native parent app beyond the existing FLYER app push; the portal is a PWA.
2What exists today
| Capability | Endpoint / schema | Note |
|---|---|---|
| Tuition notify with channels app, email, zalo and per-channel outcome with skip reason | POST .../tuitions/{id}/notify, /tuitions/notify, TuitionNotifyOutcome, ChannelOutcome, SkipReason | Reuse the channel abstraction |
| Class lesson reminder | POST /v3/teacher/class-schedules/{id}/remind, RemindLessonResult | FLYER homework reminder |
| Notification config per feature | NotificationConfig {feature, notify_email, notify_app, notify_zalo} in school config | Extend with new features |
| Parent tuition views | GET /v3/parent/tuitions, /{id}, /overview | Portal invoices |
| Learner report and weekly report | GET /v3/parent/learner/{id}/report, /weekly-report (assignments, self-study, learning time, skills, activity) | Portal learning tab and progress report source |
| Exam notify | POST /v3/teacher/exams/{exam_id}/notify | keep |
| Parent user with FLYER app (push) | StudentParent, parent app | in-app channel |
3Channel policy
| Channel | Requires | Cost | Used for | Fallback order |
|---|---|---|---|---|
| Zalo ZNS | Guardian phone with Zalo; tenant Zalo OA verified; approved template; consent type zalo | Per message (about 200–800 VND by template type); shown in settings | Transactional: attendance absent/late, invoice, receipt, reminder, schedule change, credentials | 1 |
| Zalo OA message | Guardian followed the OA and interacted within 48 h | Free | Announcements, replies, progress report link | 2 for non-transactional |
| In-app push | Guardian has FLYER parent app linked to the learner | Free | All | 2 for transactional |
| Guardian email | Free | Receipts, reports, announcements | 3 | |
| Portal inbox | Portal account | Free | Everything; always written | always |
Every message is written to the portal inbox and then delivered through the first available channel according to the tenant policy per message type; a message type may be configured to send on all available channels (e.g. receipt). Quiet hours 21:30–07:00 hold non-urgent messages. Do-not-call on a lead (PRD 01) does not apply to a guardian of an enrolled student; consent does.
4User stories and acceptance criteria
- Given an event (attendance saved with absent, payment recorded, session cancelled ...)
- When the event is published
- Then a message per guardian with the relevant flag (primary contact for attendance and schedule; payer for money) is created, rendered from the template with parameters, delivered per channel policy, and its status (queued, sent, delivered, failed, skipped with reason) is visible on the source record and in the message log; retries 3 times over 15 minutes on transient failures.
- Given permission
communication.announce - When I compose title, body (plain text with placeholders {ten_hoc_vien}, {lop}), attachments (images, PDF), audience (branch, classes, students, guardians with a flag), channels, send now or schedule
- Then recipients are previewed with counts per channel and unreachable guardians listed; the announcement is delivered, and a read/delivered count updates; announcements appear in the portal and in the class page.
- Given the attendance sheet after save
- When I write a short class summary and choose "Gửi phụ huynh"
- Then the summary goes as an OA message or push to guardians of present and absent students with the student's status line; teachers cannot send free-text ZNS (paid templates are admin-only).
- Given a class and a period
- When I generate reports
- Then one draft per student is built from attendance (rate, absences), FLYER data (assignments completed, accuracy per skill, study time, latest exam scores), package balance and a teacher comment field; the teacher fills comments in a list view; the admin publishes; guardians receive a link (portal) and PDF; published reports are immutable, corrections create a new version.
- Given my phone is on a student's guardian record
- When I open the portal link from a Zalo message and enter the OTP
- Then I see all children linked to my phone across branches of the tenant, with tabs Lịch học, Điểm danh, Học phí, Kết quả, Thông báo, Hồ sơ; sessions persist 30 days on the device.
- Given an unpaid invoice
- When I open it
- Then I see lines, total, due date and a bank QR (existing payment QR) with the payment code; after the transfer is matched the status updates and the receipt PDF appears.
- Given upcoming sessions
- When I submit "Xin phép nghỉ" with session and reason, or "Đăng ký học bù" for an excused absence
- Then a request is created for the desk (today plan and Điểm danh → Yêu cầu), the excuse is applied when accepted (PRD 04), and I receive confirmation.
- Given Hồ sơ in the portal
- When I toggle consents (Zalo, email, marketing, photo) or request a correction
- Then consent records are updated with timestamp and channel (PRD 12), a correction request becomes a task for academic admin, and a revoked channel is not used from the next message.
5Screens
Tabs: Thông báo (announcements) · Tin đã gửi (message log) · Báo cáo học tập (progress reports) · Yêu cầu từ PH (requests) · Mẫu tin (templates). KPI tiles: sent today, failed today, unreachable guardians, ZNS spend this month.
Message log columns
| Column | Notes |
|---|---|
| Thời gian | created_at |
| Loại | message type chip |
| Học viên · Phụ huynh | links |
| Kênh | Zalo ZNS / OA / App / Email / Portal |
| Trạng thái | Đã gửi (ok) · Đã nhận (ok) · Thất bại (abs) · Bỏ qua: lý do (grey) · Chờ (late) |
| Nội dung | rendered preview |
| Thao tác | Gửi lại · Xem |
Filters: date, type, channel, status, class, branch. Empty: "Chưa có tin nào được gửi."
Fields: Tiêu đề (required, 120), Nội dung (required, 2,000, placeholders), Tệp đính kèm (up to 5, 10 MB), Đối tượng (branch, classes multi, students multi, guardian flag: all / primary contact / payer), Kênh (checkboxes with reach preview), Thời điểm (now / schedule date-time), "Cho phép phản hồi" toggle (OA only). Preview panel shows the message as the guardian sees it. Validation: at least one recipient reachable, else warning with list.
List of report batches (class, period, students, drafted, commented, published). Batch view: table of students with attendance rate, assignments done, avg accuracy, study time, package balance, teacher comment (inline edit), status; buttons Gửi GV nhận xét, Xuất bản, Xuất PDF. Report page template: header with center logo, student, class, period; sections Chuyên cần, Học tập trên FLYER, Nhận xét của giáo viên, Học phí và số dư, Lịch tháng tới.
Table: message type, channel, Zalo template id, status, parameters, last edited. Editor: subject (email), body with placeholder chips ({ten_hoc_vien}, {lop}, {ngay}, {gio}, {so_tien}, {ma_thanh_toan}, {link}), test send to my phone.
/parent/login: phone, OTP; error states (phone not found: "Số điện thoại chưa được đăng ký với trung tâm. Vui lòng liên hệ ..."), resend cooldown 60 s./parent: child switcher (avatar chips), next session card, unpaid invoices card, latest report card, announcements list./parent/schedule: week list of sessions with room and teacher; changes highlighted./parent/attendance: month calendar with chips; tap for note; buttons Xin phép nghỉ, Đăng ký học bù./parent/tuition: invoices (existing parent tuitions), package balance, QR, receipts./parent/results: FLYER report (existing), progress reports list./parent/inbox: all messages; unread badge./parent/profile: children info, guardians, consents, correction request, FLYER login credentials resend.
Design: Cloud tokens, 16 px base on mobile, bottom tab bar, no sidebar; works inside the Zalo in-app browser.
6Business rules
- Transactional messages go to guardians by flag: attendance and schedule to primary contact; money to payer; credentials to primary contact; announcements to the audience selection.
- A channel is used only with the matching consent; the portal inbox needs no consent (service consent covers it).
- ZNS templates are per message type and must be approved by Zalo before use; until approved, the fallback channel is used and the settings page shows the gap.
- ZNS spend is tracked per tenant and shown monthly; an optional monthly cap pauses ZNS and falls back.
- Announcements to more than 500 recipients require
communication.announce_bulk. - Teachers may send OA or push messages to their own classes; only staff with
communication.announceuse ZNS templates beyond attendance. - Progress reports are published per student; a report cannot be published without a teacher comment unless the admin overrides.
- Portal sessions are per device, 30 days, revocable from the guardian's profile by staff.
- All outbound messages are logged with rendered content for 12 months.
7Data model
| Entity | Fields | Notes |
|---|---|---|
| message_template | id; tenant_id; type enum(attendance_absent, attendance_late, attendance_present, invoice_created, receipt, fee_reminder, low_balance, schedule_change, session_cancelled, trial_confirm, welcome, credentials, report_published, announcement, custom); channel enum(zns, oa, push, email); zalo_template_id; body; subject; params jsonb; status enum(draft, pending, approved, rejected); updated_at | new |
| message | id uuid; tenant_id; school_id; type; guardian_id; student_id null; source_type, source_id (attendance_record, tuition, payment, session, announcement, report); channel; rendered_body; status enum(queued, sent, delivered, read, failed, skipped); skip_reason; provider_msg_id; cost_vnd; attempts; created_at; sent_at; delivered_at; read_at | new; index (guardian_id, created_at), (source_type, source_id) |
| announcement | id; tenant_id; school_id null; title; body; attachments file_id[]; audience jsonb {school_ids, class_ids, student_ids, guardian_flag}; channels[]; allow_reply; scheduled_at; sent_at; created_by; stats jsonb {recipients, sent, delivered, read, failed} | new |
| progress_report_batch, progress_report | batch: class_id, period_start, period_end, status, created_by; report: batch_id, student_id, data jsonb (attendance, flyer, balance), teacher_comment, status enum(draft, commented, published), version, pdf_file_id, published_at | new |
| parent_request | id; guardian_id; student_id; type enum(excuse, makeup, correction, other); payload jsonb; status enum(open, accepted, rejected); handled_by; note; created_at | new |
| zalo_oa_connection | tenant_id; oa_id; oa_name; app_id; tokens (encrypted); connected_at; status; last_error | new |
| portal_session | id; guardian_id; device; created_at; expires_at; revoked_at | new |
| notification_policy (school config) | per type: channels[], quiet_hours_apply bool; zns_monthly_cap | extend NotificationConfig |
8API contract
| Method | Path | Request / response | Status |
|---|---|---|---|
| GET/PUT | /v3/teacher/school-config | notification policy block extended per type | extend |
| POST | /v3/teacher/school-finance/tuitions/{id}/notify, /notify | reuse; routed through the message layer | existing |
| GET/POST/PATCH | /v3/school/message-templates[/{id}]; POST /{id}/test | {to_phone} | new |
| GET | /v3/school/messages | from, to, type, channel, status, student_id, class_id → Message[] | new |
| POST | /v3/school/messages/{id}/resend | new | |
| POST | /v3/school/messages/send | {type: custom, guardian_ids | student_ids, body, channels} ad-hoc (teacher class note) | new |
| GET/POST | /v3/school/announcements; POST /{id}/preview; POST /{id}/send; POST /{id}/cancel | audience resolution returns counts and unreachable list | new |
| POST | /v3/school/progress-reports/batches | {class_id, period_start, period_end} → batch with drafts | new |
| GET/PATCH | /v3/school/progress-reports/{id} | {teacher_comment} | new |
| POST | /v3/school/progress-reports/batches/{id}/publish | {student_ids?} → messages | new |
| GET | /v3/school/progress-reports/{id}.pdf | new | |
| GET/POST/PATCH | /v3/school/parent-requests[/{id}] | accept/reject with note | new |
| GET/POST/DELETE | /v3/school/integrations/zalo | connect, status, disconnect; GET /zalo/templates syncs statuses | new |
| POST | /v3/parent/auth/otp, /v3/parent/auth/verify | {phone} / {phone, code} → session token | new |
| GET | /v3/parent/me | guardian + children[] across schools | new |
| GET | /v3/parent/learner/{id}/schedule, /attendance, /reports, /announcements | new | |
| GET | /v3/parent/tuitions, /{id}, /overview; /v3/parent/learner/{id}/report, /weekly-report | unchanged; add receipts and packages | existing / extend |
| GET/POST | /v3/parent/inbox; POST /{id}/read | new | |
| POST | /v3/parent/requests | {student_id, type, payload} | new |
| GET/PUT | /v3/parent/consents | new | |
| POST | /v3/webhooks/zalo | delivery and read events, OA user messages (stored for P3 inbox) | new |
9Permissions
| Code | owner | branch_manager | academic_admin | accountant | admissions | teacher | parent |
|---|---|---|---|---|---|---|---|
communication.view_log | all | branch | branch | finance types | own leads | own classes | own inbox |
communication.announce | yes | yes | yes | no | no | own classes (OA/push) | no |
communication.announce_bulk | yes | yes | no | no | no | no | no |
communication.templates | yes | no | no | no | no | no | no |
communication.reports (progress) | yes | yes | yes | no | no | comment own classes | read own |
communication.requests | yes | yes | yes | no | no | no | create |
integrations.zalo | yes | no | no | no | no | no | no |
10Analytics events
| Event | Properties |
|---|---|
| message_sent / message_failed / message_skipped | type, channel, reason, cost |
| announcement_sent | audience_size, channels, scheduled |
| progress_report_published | class_level, students, with_comment_rate |
| parent_login | via (zalo_link|direct), children_count |
| parent_invoice_viewed / parent_qr_shown | |
| parent_request_created | type |
| consent_changed | type, granted |
| zalo_connected / zalo_template_approved |
11Edge cases
- Guardian phone shared by two families (grandparent): portal shows all children linked to the phone; consent is per guardian record.
- Zalo token expiry: refresh job; on failure, owner notified and channel falls back.
- ZNS rejected for a template after being approved (policy change): status synced; messages fall back.
- Message with a placeholder missing data (no room): placeholder renders as empty and the template avoids dangling text ("tại phòng {phong}" becomes "tại {chi_nhanh}").
- Announcement scheduled then class list changes: audience resolved at send time.
- Parent requests correction of birthday: task created; the change itself is done by staff and audited.
- Guardian revokes Zalo consent mid-month: no more ZNS; portal inbox continues; desk sees "Chỉ liên hệ qua điện thoại".
12Open questions
- Who pays ZNS: included in the Trường Số subscription with a quota, or billed per tenant at cost?
- Should the FLYER parent app absorb the portal instead of a separate PWA in P2?
- Is a teacher-to-parent direct message (one-to-one) needed before the P3 inbox?
Trường Số (truongso.net) · FLYER internal