- Given the create drawer
- When I enter the guardian name and phone, and under "Học viên dự kiến" enter the child's name, birth year and current level
- Then the customer is created with
guardianandrelationshipas today, and a newprospect_learnersJSON block stores child name, birth year, school, level; the duplicate check runs on phone as today and, additionally, on child name + birth year within the center (soft warning only).
PRD 01 · Admissions CRM
Extends the live lead pipeline: sources and channels, call log, trial bookings, placement tests, and the conversion of a won lead into an enrolled student with a guardian, a class and a first receipt. Most of the backend exists; this PRD closes the gaps between the pipeline and the school.
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 lead fields and "phân loại khách hàng" categories; whether guardian and child are separate objects.
- Their "nhật ký chăm sóc" activity types and whether call results are a fixed list.
- How trial ("học thử") is booked: from the lead or from the class, and whether it counts in attendance.
- Whether entrance test ("kiểm tra đầu vào") stores per-skill scores and maps to a class level automatically.
- Their Facebook lead integration behaviour (page inbox vs lead form).
Proposed design
Every admissions screen sits in the School OS shell with the Admissions group in the sidebar; the thing to see is the chain from callback queue, trial and placement test to the four-step conversion wizard, where a won lead becomes a student, a guardian, an enrollment and a first invoice in one flow.










1Purpose
The admissions consultant already works in b2c.truongso.net: leads arrive by import or manual entry, move through pipeline stages on a Kanban or table, receive calls and notes, and are marked won or lost. What is missing is the last mile. A won lead today stays a customer record with a linked learner; the school side (student record, class seat, tuition) is entered elsewhere. This PRD adds trial bookings, a callback queue, do-not-call handling that the UI already expects, and a conversion wizard that creates the student, guardian, enrollment and first invoice in one flow.
In scope
- Pipeline and stages (existing), sources and channels (existing API; settings pages switched from mocks to the API).
- Call log and quick actions (existing), callback queue (new view over existing data).
- Trial class bookings (new), placement tests (existing, labels fixed).
- Conversion wizard lead to student (new).
- Lead retention and anonymisation rule (PRD 12).
Out of scope
- Marketing automation, ad-platform sync, chat inbox (Pancake and Zalo OA inbox remain external).
- Sales commission calculation (PRD 06 pay rules may reference won leads in P3).
2What exists today
| Capability | Frontend | API |
|---|---|---|
| Lead list: Kanban per stage with cursor pagination and drag to stage; table with 17 columns plus custom fields; filters stage, q, status[], priority[], call_status[], assigned_to[], tag_ids[], source[], channel[]; column visibility saved per view | features/crm/customers/list/* | GET /v3/crm/customers, /customers/kanban, PATCH /customers/{id}/kanban-position, /view-configs/customer_list |
| Create and edit drawer with duplicate check by phone; inline create of source, channel, stage, status | customer-form.tsx, duplicate-check-modal.tsx | POST /customers, /customers/check-duplicate(-multi), POST /sources|channels|stages|statuses |
| Detail shell: value, deal info, contact, marketing, learner account groups; tabs Hoạt động, Sản phẩm, Thảo luận, Tài liệu, Phân công, Học viên liên kết; Won, Lost, Archive, Star, Delete, Assign | customer-detail-shell.tsx | GET/PATCH /customers/{id}, /activities, /audit-logs, /packages, /discussions, /customer-files, /assignments, /learners |
| Interactions: call, meeting, task, note with statuses done/cancelled/missed and call outcomes | features/crm/calls/*, timeline tab | /interactions, /quick-actions/log-call, /quick-actions/log-note |
| Placement tests: list, create, submit result | features/crm/placement-tests/* | /placement-tests, /{id}/submit-result, /v3/teacher/placement-test/send |
| Bulk: archive, unarchive, reassign, import with dedupe strategy, export xlsx | table bulk bar, import wizard | /customers/bulk, /assignments/bulk-reassign, /imports/* |
| Today plan, notifications (WebSocket), universal search, mentions | widgets | /me/today-plan, /me/notifications, /search, /me/mentions |
| Dashboard: sale summary, manager summary, funnel, trend, top sales, drill-down | features/crm/dashboard | 5 dashboard endpoints |
Referenced in the frontend but absent from the API and therefore specified here as NEW: POST /customers/{id}/convert, POST /customers/{id}/do-not-call, POST /customers/{id}/transfer-school. Not carried forward: tasks, meetings and saved-views endpoints; tasks and meetings are Interactions, saved views are view-configs.
3Personas and roles
| Persona | Role | Scope |
|---|---|---|
| Admissions consultant | admissions | own leads; team leads if scope team |
| Admissions lead / branch manager | branch_manager | all leads targeting the branch; reassign; approve discounts at conversion |
| Academic admin | academic_admin | read leads; schedules trials and placement tests; receives converted students |
| Owner | owner | all |
4User stories and acceptance criteria
- Given calls logged with
call_status = callback_requestedandrequest_callback_at - When I open Lịch hẹn & cuộc gọi → tab Gọi lại
- Then I see rows overdue first (red), then today, then upcoming, each with a one-click "Gọi" that opens the quick-log dialog with the customer preselected; logging any outcome removes the row.
- Given a lead
- When I choose Đánh dấu không gọi with a reason
- Then
do_not_call_at/by/reasonare set; the quick-log-call dialog shows the existing DNC guard; bulk notifications in PRD 07 skip the phone; removing the mark requirescustomers.force_call.
- Given classes with upcoming sessions (PRD 03) that have
allow_trial = trueand free trial seats - When I pick a class and session in the Học thử dialog on the lead
- Then a
trial_bookingis created with statusbooked, the session roster shows the trial student with a Học thử chip, the guardian receives a ZNS confirmation (PRD 07) if consent exists, and the lead moves to the stage configured as "trial stage" for the pipeline (if set). - Given the session has been held
- When the teacher marks attendance (PRD 04)
- Then the trial booking status becomes
attendedorno_show, an activity is written on the lead, and the consultant receives a notification "Học thử xong: Nguyễn An (Movers 2A)" with the teacher's note.
- Given a lead with or without a linked learner
- When I create a placement test with level and scheduled time
- Then status is
not_started; if a linked learner exists the FLYER test is sent (/v3/teacher/placement-test/send) and the result auto-fills on completion; otherwise the consultant submits scores manually; the result block shows overall score, four skills and a suggested class level (mapping table in PRD 03). - Given the i18n mismatch
- When the page renders statuses
- Then labels are Chưa bắt đầu / Đang làm / Hoàn thành / Đã huỷ, matching the API enum.
- Given a lead in a terminal won status or any open lead with permission
customers.convert - When I press Chuyển đổi
- Then a 4-step wizard opens: (1) Học viên: choose linked learner or create new learner (name, birthday, gender) and pick target branch; (2) Phụ huynh: confirm guardian name, phone, relationship, Zalo consent; (3) Lớp & gói: choose class (filtered by branch and level suggestion) or "Chờ xếp lớp", choose fee plan or package from PRD 05, start date, discount (approval if above the consultant's limit); (4) Xác nhận: summary and "Tạo hoá đơn ngay" toggle.
- When I confirm
- Then in one transaction: learner profile created or reused and linked (
CustomerLearner), student record created (PRD 02) with status Active or Waiting, guardian record created, enrollment created (PRD 03) if a class was chosen, tuition created (PRD 05) if requested, the lead status set to won withwon_at, an activity "Đã chuyển thành học viên" written, and the wizard shows links to the student and invoice. - Given a failure in any step
- Then nothing is persisted and the error names the step.
- Given a lead with
school_idA - When I choose Chuyển chi nhánh and pick B and optionally a new assignee
- Then
school_idbecomes B, assignments at A are ended with reason "transfer", the new assignee is notified, and an audit entry is written.
- Given Settings → Nguồn or Kênh
- When I add, rename, recolour, reorder or deactivate
- Then the page reads and writes
/v3/crm/sourcesand/v3/crm/channels(replacing the localStorage mock), deactivated items stay on historical leads but disappear from pickers.
5Screens
Keep the current Kanban and table. Adjustments: add columns Học viên dự kiến (from prospect_learners), Học thử (next or last trial with chip), Xếp lớp (placement level); add filter "Có gọi lại" and "Không gọi"; add tab-like quick filters above the table: Tất cả · Của tôi · Gọi lại hôm nay · Học thử tuần này · Thắng tháng này. Bulk bar gains "Gửi ZNS" (PRD 07) for leads with consent.
Add a "Học viên dự kiến" group in the sidebar (child name, birth year, level, school) and a "Học thử & xếp lớp" tab listing trial bookings and placement tests with actions Đặt lịch học thử, Tạo bài kiểm tra. The Chuyển đổi button becomes primary once status is won; it is enabled for open leads too when the user has customers.convert. Remove the feature flag.
Tabs: Hôm nay (today plan items) · Gọi lại (callback queue) · Cuộc hẹn (meetings) · Học thử (trial bookings). Table for Gọi lại: Khách hàng, SĐT, Giờ hẹn gọi, Quá hạn (chip), Người phụ trách, Lần gọi gần nhất, action Gọi. Filters: assignee, date range. Empty: "Không có yêu cầu gọi lại."
| Column | Notes |
|---|---|
| Học viên | prospect name; link to lead |
| Phụ huynh / SĐT | |
| Lớp | class name + level chip |
| Buổi | session date and time |
| Trạng thái | Đã đặt (exc) · Đã học (ok) · Vắng (abs) · Đã huỷ (grey) |
| Nhận xét GV | teacher note, truncated |
| Người phụ trách | |
| Thao tác | Đổi buổi · Huỷ · Chuyển đổi |
Trial dialog fields: class (search, shows level, teacher, free trial seats), session (next 5 sessions), note for teacher, "Gửi xác nhận Zalo" toggle. Validation: session in the future; trial seats > 0; same prospect cannot hold two bookings on the same day.
Columns: Khách hàng, Học viên, Trình độ, Thời gian, Trạng thái, Điểm, Đề xuất lớp, Thao tác. All form labels move to i18n: Trình độ, Thời gian kiểm tra, Hạn hoàn thành, Giám khảo, Mã đề FLYER, Ghi chú. Result modal: Điểm tổng, Nghe, Nói, Đọc, Viết, Link kết quả, Ghi chú; suggested level computed client-side from the PRD 03 mapping and shown read-only.
| Step | Fields | Validation |
|---|---|---|
| 1 Học viên | Linked learner radio list (from /customers/{id}/learners) or "Tạo học viên mới": Họ tên, Ngày sinh, Giới tính, Trường đang học; Chi nhánh (select from accessible schools, default lead's school_id) | name required; birthday required for under-18; branch required |
| 2 Phụ huynh | Họ tên, SĐT (prefilled from lead phone), Quan hệ (Bố/Mẹ/Ông bà/Khác), Zalo: "Đồng ý nhận thông báo" checkbox with consent text version, Email | phone required and valid; consent optional but recorded |
| 3 Lớp & gói | Lớp (search; filtered by branch; shows seats free / capacity, schedule, level; "Chờ xếp lớp" option), Ngày bắt đầu, Gói học phí (from PRD 05 fee plans of the course), Giảm giá (percent or amount, reason), Mã voucher | class capacity check; discount above limit shows "Cần duyệt" and creates approval request |
| 4 Xác nhận | Summary; "Tạo hoá đơn ngay" toggle (default on); "Gửi thông báo nhập học" toggle |
Result screen: three link cards Học viên · Lớp · Hoá đơn, and "Chuyển đổi tiếp" for siblings.
6Business rules
- One phone number is one lead per center; the duplicate check is mandatory on create and import (existing). Siblings are additional
prospect_learnersentries on the same lead, and conversion can run once per prospect. - A lead may be converted at most once per prospect learner; the wizard hides prospects already converted and shows the student link.
- Trial bookings require a class session in the future and free trial seats (class.capacity_trial minus booked). Default trial seats per class = 2.
- Do-not-call blocks quick-log-call (except force), bulk ZNS and reminders; it does not block transactional messages to the guardian of an enrolled student.
- Lead retention: leads not won and without activity for 24 months are anonymised by a nightly job (PRD 12); a warning chip appears 30 days before.
- Won requires either a conversion or an explicit "Thắng không chuyển đổi" reason (e.g. enrolled at another branch outside the system).
- Stage automation (optional per pipeline): trial booked moves to "trial stage"; conversion moves to the terminal won status; no other automatic moves.
7Data model changes
| Entity | Change | Fields |
|---|---|---|
| customer (Customer) | extend | prospect_learners jsonb [{key, name, birth_year, gender, school, level, converted_student_id}], marketing_consent_at, target_school_id (nullable; defaults to school_id) |
| trial_booking | new | id uuid; center_id; school_id; customer_id; prospect_key; class_id int64; session_id uuid (PRD 03); status enum(booked, attended, no_show, cancelled); teacher_note; consultant_id (sale_profile_id); notified_at; created_at; updated_at |
| placement_test (PlacementTest) | extend | suggested_level text (computed), prospect_key |
| pipeline (PipelineItem) | extend | trial_stage_id nullable, won_status_id (terminal), lost_status_id |
| conversion | new | id; customer_id; prospect_key; student_id (PRD 02); guardian_id; enrollment_id null; tuition_id null; converted_by; created_at |
8API contract
| Method | Path | Request / response | Status |
|---|---|---|---|
| GET/POST/PATCH/DELETE | /v3/crm/customers[...] | as today; prospect_learners, target_school_id accepted on create and update | extend |
| GET | /v3/crm/interactions?interaction_type=call&call_status=callback_requested&status= | callback queue; add query callback_from, callback_to, overdue | extend |
| POST | /v3/crm/customers/{id}/do-not-call | {reason} → Customer; DELETE to clear (requires customers.force_call) | new |
| POST | /v3/crm/customers/{id}/transfer-school | {school_id, assignee_id?, note} → Customer | new |
| GET | /v3/crm/trial-bookings | status, class_id, from, to, assignee_id, page, limit → TrialBooking[] with customer, class, session | new |
| POST | /v3/crm/trial-bookings | {customer_id, prospect_key, class_id, session_id, note, notify} → TrialBooking; 409 NO_TRIAL_SEATS / DUPLICATE_TRIAL | new |
| PATCH | /v3/crm/trial-bookings/{id} | {session_id?, status?, teacher_note?} | new |
| GET/POST/PATCH | /v3/crm/placement-tests[...] | as today; response adds suggested_level | extend |
| POST | /v3/crm/customers/{id}/convert | {prospect_key, learner: {learner_profile_id} | {display_name, birthday, gender, school_name}, school_id, guardian: {name, phone, relationship, email?, zalo_consent: bool, consent_version}, enrollment?: {class_id, start_date}, tuition?: {fee_plan_id, discount_pct?, discount_amount?, discount_reason?, voucher_code?, due_date}, notify: bool} → {student_id, guardian_id, enrollment_id?, tuition_id?, approval_request_id?} | new |
| PATCH | /v3/crm/pipelines/{id} | add trial_stage_id, won_status_id, lost_status_id | extend |
| GET/POST/PATCH/DELETE | /v3/crm/sources, /v3/crm/channels (+reorder) | used by settings pages | existing |
9Permissions
| Code | owner | branch_manager | admissions | academic_admin |
|---|---|---|---|---|
customers.view (scope own/branch/all) | all | branch | own or team | branch read |
customers.create, customers.edit | yes | yes | own | no |
customers.delete | yes | yes | no | no |
customers.force_call | yes | yes | no | no |
customers.transfer | yes | yes | no | no |
customers.convert | yes | yes | yes | yes |
trials.manage | yes | yes | yes | yes |
crm.placement_tests.create (existing code) | yes | yes | yes | yes |
pipeline.manage, config.manage (sources, channels, tags) | yes | no | no | no |
finance.discount.approve (used by wizard) | yes | yes | no | no |
10Analytics events
| Event | Properties |
|---|---|
| lead_created | source, channel, has_prospect, via (form|import|api) |
| call_logged | call_type, call_result, duration_bucket, from (quick|timeline|queue) |
| callback_queue_viewed | overdue_count |
| trial_booked / trial_attended / trial_no_show | class_level, days_from_lead_created |
| placement_test_created / placement_result_submitted | auto (bool), suggested_level |
| lead_converted | with_class, with_tuition, discount_pct_bucket, days_from_lead_created, trial_before (bool) |
| lead_won_without_conversion | reason |
| lead_transferred | from_school, to_school |
11Edge cases
- Guardian phone already belongs to an enrolled student's guardian (returning family): the create drawer shows "Phụ huynh đã có học viên" with a link and lets the consultant continue; conversion step 2 reuses the guardian record.
- Two prospects on one lead converted on different days: each conversion is independent; the lead stays won after the first.
- Trial session cancelled by the school: bookings are set to cancelled and consultants notified; the dialog to rebook opens from the notification.
- Conversion with "Chờ xếp lớp": the student is Waiting; the class-fill report in PRD 09 lists waiting students by level.
- Learner already linked to another center's student: allowed (FLYER accounts are global); the student record is per school.
- Placement result submitted twice: last write wins, with audit.
12Open questions
- Should the pipeline Kanban remain default, or should table be default for new tenants (D3 says lists first)?
- Is a "trial stage" per pipeline enough, or do centers want a stage per trial outcome?
- Consultant discount limit: fixed percent per role, or per user in settings?
Trường Số (truongso.net) · FLYER internal