Trường Số PRD/19 Service plans
Trường Số · Product Requirements · 09/2026

PRD 19 · Service plans and subscription

The Trường Số plan catalogue with per-plan limits (active students, branches, staff users, storage, pooled ZNS messages, modules), the tenant subscription lifecycle from trial to termination, a shared limit-enforcement point, upgrades recalculated on a 12-month cycle, subscription invoices collected by bank transfer with SePay matching, and FLYER's internal console. No plan or subscription entity exists at tenant level in the backend today.

Key facts, origin and limits
Status
Draft v1
Owning module
Nền tảng
Backend
EXISTING centers/onboarding · EXTEND create-center, accessible-schools, SePay webhook · NEW plan, subscription, usage_counter, subscription_invoice, credit
Phase
P1 (plans, limits, trial, upgrade) / P2 (self-service payment, recurring invoices, storage add-ons)
Depends on
00 Overview, 10 Settings, 12 Non-functional, 14 Curriculum, 18 E-learning

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-frontend codebase and the staging /v3 OpenAPI specification (182 paths), as of 10/09/2026.
  • The existing / extend / new tags on this page come from a comparison with the staging /v3 specification 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's public price list (pse4.one, surveyed 16/09/2026) has Basic at 199,000 VND/month (billed yearly, up to 100 students, 1 GB), Pro at 400,000 VND/month (300 students, 2 GB), Business at 700,000 VND/month (500 students, 4 GB) and Enterprise at 1,000,000 VND/month (unlimited students, 5 GB, adds online curriculum, courses, test prep, student accounts, games); extra storage costs 2,500,000 VND/year per 40 GB. Every tier includes the full operations suite (mobile app, HR, leads, trial, schedule, attendance, finance, inventory, reports, daily backup, 24/7 support). Its published upgrade rule, verbatim: "hợp đồng sẽ được tính lại 12 tháng mới theo gói đã chọn", "phần chi phí còn lại của gói cũ sẽ được khấu trừ trực tiếp vào gói mới", "Hợp đồng cũ chấm dứt ngay tại thời điểm nâng cấp"; published example: Basic used 7 of 12 months, remaining value 199,000 × 5 = 995,000 VND, upgrade to Pro at 4,800,000 VND, amount due 3,805,000 VND. Downgrade, trial length, renewal and data retention are not published. These figures are for positioning only; Trường Số prices are a business decision.

Limits

  • Verification pending in-app survey, because automated login to the reference product was not authorised in the drafting session:
    • The competitor's trial length, whether payment details are required, and what happens when the trial ends.
    • Behaviour at plan expiry: full lock, read-only or warning only; whether a grace period exists and how long.
    • Downgrade rules and whether the difference is refunded.
    • Whether "students" in the plan limit means active students or all records; whether withdrawn records count.
    • Which file types count toward storage, whether the tenant can see it; whether extra storage is prorated or always a full year.
    • Whether a self-service page exists to upgrade and see invoices in-app, or sales must be contacted.

Proposed design

Eight screens in the FLYER Cloud design system: the Plan page in Settings with usage bars that change colour from 90% and a plan comparison with prices left as "To be decided", the 12-month upgrade quote, subscription invoices, the banner and the 402 block dialog on the student list, and FLYER's internal console; the key point is that at every limit touchpoint the user sees used / limit and the upgrade path.

1 · Plan page

/settings/plan · desktop

Plan page
The header block shows the active Standard plan, its expiry and the three buttons Upgrade, Invoices and Downgrade next term; each metric has a progress bar with used / limit and a percentage, and Active students at 92% switches to the warning colour with the text "Near limit" (SUB-02, SUB-03). The footer shows the 500,000 ₫ credit, the last refresh time and a Recompute now button. The downgrade button is disabled with the explanation "92 students, Starter allows 50" (SUB-05, rule 7); the comparison of the four public plans marks the current one and leaves yearly and monthly prices as "To be decided".

2 · Upgrade quote

/settings/plan/upgrade?plan=advanced · desktop

Upgrade quote
Quote dialog per SUB-04 and rule 6: the 12-month price of Advanced, minus the remaining value of Standard (300,000 ₫ × 5 unused months, the month in progress counts as used), minus credit, equals 4,000,000 ₫ due; a 7-day payment deadline and the new term end. The fixed note "The current term ends at the moment of upgrade; a new 12-month term starts today" and the Confirm and create invoice button; the quote is valid for 24 hours (409 STALE_QUOTE). Amounts are illustrative; plan prices are a business decision.

3 · Subscription invoices

/settings/plan/invoices · desktop

Subscription invoices
Four stat tiles (amount due, paid this year, remaining credit, e-invoices) and a table with every column in the PRD: invoice number TS-INV-yyyy-nnnnnn, type, period, amount, credit applied, amount due, status, due date, and the actions View · PDF · E-invoice. Rows show the four statuses Unpaid, Paid, Overdue and Void, an add-on invoice prorated by remaining months rounded up (rule 8) and an upgrade invoice voided when replaced by a renewal invoice (rule 9).

4 · Invoice detail

/settings/plan/invoices/:id · desktop

Invoice detail
An unpaid upgrade invoice: two line items (the new 12-month plan and the deduction of the old plan's remaining value), subtotal, credit applied and amount due; the buyer block comes from the legal-entity profile (PRD 10) and the e-invoice block has the toggle for automatic issuance within 24 hours of full payment (rule 10). The right rail holds the bank-transfer QR to FLYER's account with the payment_code as transfer content, a note on SePay matching within 5 minutes and that partial payment does not activate the plan (SUB-06); the VNPay button is labelled P2.

5 · 90% warning banner

/students · desktop

90% warning banner
The student list when active students reach 92/100: a warning-coloured banner with text stating usage, remaining seats and what happens at 100, with a View plans button shown only to roles holding subscription.manage (SUB-03, rule 4). Enrolment still succeeds; the Active tab shows 92 / 100 so admissions staff see the threshold on the page they work in.

6 · 402 block dialog

/students · app-wide dialog · desktop

402 block dialog
When active students equal the limit, the API returns 402 PLAN_LIMIT_REACHED and the dialog states the metric, used / limit, the next plan that fits and the error code; the quote block summarises 6,000,000 − 1,500,000 − 500,000 = 4,000,000 ₫ so the owner can decide immediately (SUB-03, SUB-04). The record is not created; a note reminds that viewing, editing, attendance and payments keep working (rule 5); roles without plan management see Notify the owner instead of View plans.

7 · FLYER internal console

/admin/tenants · desktop

FLYER internal console
A page only for FLYER users with tenant.admin: four stat tiles, tabs All · Trials ending · Past due · Unmatched, filters for status, plan, expiring within N days, account manager, and search by name or center_id. The table has every column: center, owner, plan, status, term end, active students / limit with a bar, branches, normalised MRR; rows show trialing, active, expiring at T-21, past due within grace, suspended read-only, an unlimited Enterprise plan and the internal pilot and custom plans (rules 2, 11).

8 · Extend trial and apply credit

/admin/tenants/:id · drawer · desktop

Extend trial and apply credit
The detail drawer for Anh ngữ Sao Mai, a trial with 5 days left, with tabs Subscription · Usage · Invoices · Credits · Audit log. The Extend trial block offers 7 / 14 / 30 days and precomputes the new end date and the 44 / 90-day total so tenant.admin.override is not needed; the Apply credit block has amount, optional expiry and a required reason field currently showing a validation error because it is empty (SUB-08, rule 12). Below are the actions Set plan by contract, Suspend, Terminate and the Trust payment toggle.

1Purpose

Trường Số is sold as a prepaid service plan per center (tenant). Each plan sets the number of active students, branches, staff users, digital-resource storage, pooled ZNS messages per month and which modules are open. This module is the single source for "which plan is this center on, how much headroom is left, when does it expire and what does an upgrade cost". Every other module asks one shared enforcement point before creating more records. The center owner sees usage against limits on one page in Settings, is warned before hitting a ceiling, and upgrades without contacting FLYER's sales team.

In scope

Out of scope

2Today

The backend already has the tenant (CRM Center) and branch (FLYER School) per decision D1 in PRD 00, plus the onboarding flow that creates a center. Nothing records a tenant's plan, limits or term; every center is currently open without limits. The teacher app has a licence package for individual teachers; it is a reference for data shape, not a tenant plan.

Existing capabilityEndpointKeep?
List and detail of the user's centers (tenants)GET /v3/crm/centers existingKeep; extend the response with a subscription_summary block.
Onboarding: centers created, create a new centerGET /v3/crm/onboarding/centers, POST /v3/crm/onboarding/create-center existingKeep; create-center starts a 30-day trial subscription as the center is created.
Branches the user can accessGET /v3/crm/me/accessible-schools existingKeep; the source for the branch count and the place the read_only flag is carried when a tenant is suspended.
Personal licence package of a FLYER teacherGET /v3/teacher/package/my-subscriptions existingUnchanged for the teacher app; shape reference only (package, start, end, status).
SePay bank-transfer matching on the center's own accountSePay webhook (PRD 05) extendExtend to receive transfers into FLYER's account and match subscription invoices by payment_code.
Plans, limits, subscription, usage counters, subscription invoices, credit, internal consolenone newBuild under /v3/crm/subscription/* (tenant self-service) and /v3/admin/tenants/* (FLYER internal).

3User stories and acceptance criteria

SUB-0130-day trial on center creation
As a new center owner I use every feature without paying.
  • Given a center has just been created through onboarding
  • When creation completes
  • Then a trialing subscription is created on the trial plan (limits of the highest non-Enterprise plan, 1 branch, 5 staff users) with expiry = creation date + 30 days; the top bar shows "Dùng thử, còn N ngày"; no payment details are required; FLYER's sales team is notified.
SUB-02Usage against limits
As a center owner I know my headroom before I must upgrade.
  • Given I hold subscription.view
  • When I open Cài đặt → Gói dịch vụ
  • Then the page shows plan name, status, expiry date and one bar per metric as "used / limit" with a percentage; metrics at 90% or above switch to the warning colour with text; figures are at most 15 minutes old and a "Tính lại ngay" button exists.
SUB-03Soft warning at 90% and hard block beyond the limit
As an academic admin I am warned before seats run out; the system refuses records beyond the plan and points to the upgrade.
  • Given active students reach 90% of the plan limit
  • When any user creates or converts another student (PRD 01, PRD 02)
  • Then the action succeeds; a banner "Đã dùng 92/100 học viên của gói Tiêu chuẩn" appears on the student list and the dashboard; the owner gets one in-app notification and email per metric, repeated at most every 7 days.
  • Given active students equal the limit
  • When an academic admin enrols one more student or moves a paused student back to active
  • Then the API returns 402 PLAN_LIMIT_REACHED with metric, limit and usage; a dialog "Gói Tiêu chuẩn cho phép 100 học viên đang học. Nâng cấp lên gói Nâng cao để tiếp tục." offers Xem gói (owner) or Báo cho chủ trung tâm (other roles); the record is not created; viewing, editing, attendance and payments on existing records keep working.
SUB-04Upgrade with 12-month recalculation
As a center owner I upgrade mid-term and see the amount due before confirming.
  • Given a yearly subscription on Tiêu chuẩn, 7 of 12 months used
  • When I pick Nâng cao on the plan page
  • Then the quote shows the new plan's 12-month price, minus the old plan's remaining value (monthly rate × 5 unused months), minus credit, equals the amount due; new expiry = confirmation date + 12 months; on confirmation a subscription invoice with a QR code is created; the new limits apply once the invoice is fully paid, or immediately if the internal console has enabled "trust payment" for the tenant.
SUB-05Downgrade at term end
As a center owner I choose a lower plan for the next term.
  • Given usage fits the lower plan's limits
  • When I choose "Hạ xuống gói Khởi đầu từ kỳ tiếp theo"
  • Then the request is stored with effective date = current expiry; the page shows "Sẽ chuyển sang gói Khởi đầu vào dd/MM/yyyy" with Huỷ yêu cầu; at T-1 the system re-checks usage: if it exceeds the new limits the request is cancelled and the owner is told which metrics to reduce; otherwise the renewal invoice is issued on the new plan.
  • Given usage exceeds the lower plan's limits
  • When I choose to downgrade
  • Then the button is disabled with "Đang có 180 học viên, gói Khởi đầu cho phép 100".
SUB-06Paying a subscription invoice by bank transfer
As the center's accountant I pay and the subscription activates on its own.
  • Given an unpaid subscription invoice with a payment_code in its QR and transfer description
  • When a transfer reaches FLYER's account with the right code and full amount
  • Then within 5 minutes the invoice becomes paid, the subscription becomes active with the plan and expiry on the invoice, a PDF receipt is emailed to the payer and the owner; transfers with a wrong code or short amount land in the internal console's Chưa đối soát tab.
SUB-07Expiry, grace and read-only suspension
A center that forgets to renew loses no data and is not cut off abruptly.
  • Given an active subscription nearing expiry
  • When T-30, T-7, T-1 and T+0 arrive
  • Then the owner and accountant get an in-app notification and email at each milestone, with the renewal invoice pre-created at T-30; at T+0 the subscription becomes past_due, a red banner "Gói dịch vụ đã hết hạn, còn 14 ngày ân hạn" shows for every user and features keep working; at T+14 unpaid it becomes suspended: write endpoints return 423 TENANT_READ_ONLY, users can still sign in, view, search, export Excel and download receipts; guardians still see the parent portal; automated messages stop.
  • Given the subscription is suspended
  • When the renewal invoice is fully paid
  • Then it returns to active within 5 minutes; the new term starts from the old expiry date (grace and suspension days belong to the new term and are not added).
SUB-08FLYER staff set a plan, extend a trial, apply credit
FLYER's sales team handles contracts signed outside the system.
  • Given an internal user with tenant.admin
  • When I pick a center in the internal console, set Doanh nghiệp for 12 months from the signing date, enter the contract number and a reason
  • Then the subscription updates at once, a subscription invoice is created as paid outside the system (method offline, with the contract number), the owner receives a confirmation email, and the audit log records actor, before and after.
  • Given a trial center about to expire while negotiating
  • When I choose "Gia hạn dùng thử" for 7, 14 or 30 days with a reason
  • Then the expiry is extended; total trial time cannot exceed 90 days without tenant.admin.override.
  • Given a service incident to compensate
  • When I grant credit with a reason
  • Then the credit is added to the tenant's balance, deducted automatically from the next invoice, and shown on the plan page.

4Screens

Service plan /settings/plan
┌────────────────────────────────────────────────────┐ │ Gói Tiêu chuẩn · Đang hoạt động · Hết hạn 15/03/2027 │ │ [Nâng cấp] [Hoá đơn] [Hạ gói từ kỳ sau] │ ├────────────────────────────────────────────────────┤ │ Học viên đang học 92 / 100 █████████░ 92% (!) │ │ Chi nhánh 2 / 3 ██████░░░░ 67% │ │ Người dùng nhân sự 11 / 15 ███████░░░ 73% │ │ Dung lượng 1,4 / 2 GB ███████░░░ 70% │ │ Tin ZNS tháng 9 640 / 1.000 ██████░░░░ 64% │ │ Mô-đun: Giáo trình số Có · Học trực tuyến Không │ ├────────────────────────────────────────────────────┤ │ Tín dụng 500.000 đ · Cập nhật 10:42 · [Tính lại ngay]│ └────────────────────────────────────────────────────┘

Header: plan name, status chip (Đang hoạt động, Sắp hết hạn, Quá hạn, Dùng thử), expiry date, buttons Nâng cấp, Hoá đơn, Hạ gói từ kỳ sau. Body: one row per metric with a progress bar and percentage, a warning mark with text from 90%, a module row per plan. Footer: remaining credit, last computed time, Tính lại ngay. Below, a plan comparison table (yearly price, monthly price, each limit, modules) with the current plan highlighted. There is no empty state because every tenant always has a subscription; during trial the header reads "Dùng thử, còn N ngày" and the comparison table is expanded. Nâng cấp opens the quote dialog (/settings/plan/upgrade?plan=advanced): new plan 12-month price, minus the old plan's remaining value (monthly rate × unused months), minus credit, amount due, new expiry, the fixed note "Kỳ hiện tại kết thúc tại thời điểm nâng cấp; kỳ mới 12 tháng bắt đầu từ hôm nay", and a Xác nhận và tạo hoá đơn button.

Subscription invoices /settings/plan/invoices · /settings/plan/invoices/:id

List: invoice number (TS-INV-yyyy-nnnnnn), type (renewal, upgrade, add-on, adjustment), period, amount, credit applied, balance due, status (Chưa thanh toán, Đã thanh toán, Quá hạn, Đã huỷ), due date, actions (Xem, Tải PDF, Tải HĐĐT). Detail: line items, transfer QR with FLYER's account and the payment_code, Thanh toán qua VNPay button (P2), e-invoice block (number, lookup code, PDF), buyer taken from the legal profile (PRD 10). Empty state: "Chưa có hoá đơn. Hoá đơn đầu tiên được tạo khi chọn gói."

Limit banners and dialogs (app-wide) every page

Top-bar banner by status: trial (blue, "Dùng thử, còn N ngày. Chọn gói"), expiring (amber, from T-30), past due (red, grace-day countdown), suspended (red, "Chỉ xem. Thanh toán để tiếp tục"). The action button shows only for roles with subscription.manage or subscription.pay; other roles see "Liên hệ chủ trung tâm". Block dialog (402): limit, current usage, the next plan that fits, Xem gói or Báo cho chủ trung tâm. Read-only dialog (423): "Trung tâm đang ở chế độ chỉ xem do gói dịch vụ chưa được gia hạn."

Internal console (FLYER) /admin/tenants · /admin/tenants/:id

FLYER users with tenant.admin only, outside tenant navigation. List: filters status, plan, expiring within N days, account manager, search by name or center_id; columns center, owner, plan, status, expiry, active students / limit, branches, normalised MRR; tabs Dùng thử sắp hết · Quá hạn · Chưa đối soát. Detail: tabs Thuê bao (set plan and term, extend trial, suspend / reactivate, terminate, "trust payment"), Mức sử dụng (90 days), Hoá đơn (adjustment, mark paid offline, void), Tín dụng, Nhật ký. Every write requires a reason. Empty state per tab, for example "Không có trung tâm nào quá hạn."

5Business rules

  1. Each tenant (Center) has exactly one subscription, created automatically as trialing when the center is created; a subscription is never deleted, only transitioned.
  2. Valid transitions: trialing → active (payment or internal plan set) or → past_due (30 days elapsed); active → expiring (T-30, display flag only) → past_due (T+0) → suspended (T+14) → terminated (T+14+90 or on the owner's written request) → data purged 90 days after termination per PRD 12. Full payment in any state before terminated returns the subscription to active. Other transitions return 409 INVALID_TRANSITION. Milestones are end of day in Asia/Ho_Chi_Minh, stored in UTC.
  3. Counting: active students = students in status Đang học across all branches (Bảo lưu, Chờ xếp lớp, Học thử excluded); branches = non-archived Schools of the Center; staff users = non-deactivated login accounts (staff without accounts and guardian accounts excluded); storage = total size of files uploaded by the tenant (curriculum materials PRD 14, lessons PRD 18, attachments, receipt PDFs), FLYER learning content excluded; ZNS = messages delivered in the calendar month, reset on the 1st.
  4. The shared enforce_limit(metric) runs before every action that raises a metric: create student, convert lead, set status to Đang học, Excel import (PRD 11, whole file pre-checked), create branch, invite user, upload file, send ZNS. From 90%: allow and warn. From 100%: refuse with 402 PLAN_LIMIT_REACHED. ZNS at quota falls back through the PRD 07 channel order (Zalo OA, in-app, email) with reason "Hết hạn mức ZNS"; the tenant may enable "Cho phép vượt hạn mức ZNS" and pay the overage on the next invoice (P2).
  5. Viewing, editing existing records, attendance, payments and exports are never blocked by a count limit. While suspended, write endpoints return 423 TENANT_READ_ONLY, except paying a subscription invoice, sign-in, password change, guardian consent management and data-export requests.
  6. Upgrade fee = new plan price (12 months) − remaining value of the old plan, where remaining value = old plan monthly rate × unused months; the month in progress counts as used; the old term ends at the moment of upgrade and a new 12-month term starts on the confirmation date. Credit is deducted last; a negative result becomes credit, never a refund. The new plan takes effect when the upgrade invoice is fully paid. Upgrading during trial ends the trial at once; remaining trial days are not converted to money.
  7. Downgrade takes effect only on the current term's expiry date and only if every metric at T-1 fits the new plan; no refund of the difference; at most one pending downgrade per subscription.
  8. Add-ons (40 GB blocks, 1,000 ZNS/month blocks, extra branch, 5 extra users) share the subscription term; the first charge is prorated to the remaining months rounded up; the limit rises on payment; they roll into the renewal invoice at full-year price unless removed before T-1; purchasable only while active or expiring.
  9. Subscription invoices are numbered TS-INV-yyyy-nnnnnn from a system-wide sequence; due in 7 days for upgrade and add-on invoices, on the expiry date for renewal invoices. Partial payment does not activate a plan. The renewal invoice is created at T-30 on the current plan (or the requested downgrade), plus add-ons, minus credit; the new term always follows the old expiry date, even when paid during grace or suspension; an unpaid renewal invoice is voided when the subscription terminates.
  10. The subscription e-invoice is issued by FLYER to the tenant through FLYER's provider; the buyer comes from the center's legal profile (SchoolCompany); issued automatically within 24 hours of full payment when the tenant has enabled "Nhận hoá đơn điện tử", otherwise on request.
  11. Pilot centers and bespoke contracts use internal plans pilot or custom set from the internal console, with their own limits and an explicit expiry; they never appear in the tenant's comparison table.
  12. Every change of subscription state, plan, limits, credit or invoice and every internal-console action is audited (PRD 12) with actor, reason, before and after; the tenant sees its own history in Cài đặt → Nhật ký.

6Data model

EntityFieldsNotes
plan (system-wide)id uuid; code enum(trial, starter, standard, advanced, enterprise, pilot, custom); name_vi; name_en; price_month_vnd; price_year_vnd; billing_cycles [year | month]; is_public bool; sort_order; modules jsonb {curriculum bool, elearning bool}; retired_at nullnew; prices are "to be decided" placeholders; retired plans are kept so old subscriptions still resolve
plan_limitid; plan_id; metric enum(active_students, branches, staff_users, storage_gb, zns_month); limit_value int null (null = unlimited)new; unique (plan_id, metric)
subscription (tenant level)id uuid; center_id (unique); plan_id; status enum(trialing, active, expiring, past_due, suspended, terminated); billing_cycle; term_start; term_end; trial_end null; grace_end null; suspended_at null; terminated_at null; purge_after null; pending_downgrade_plan_id null; trust_payment bool; contract_no null; account_manager_id null; version int; updated_atnew; every notification milestone derives from term_end
subscription_addonid; subscription_id; type enum(storage_40gb, zns_1000, branch, staff_5); quantity int; unit_price_year_vnd; starts_at; ends_at (= term_end); auto_renew bool; invoice_idnew
subscription_eventid; subscription_id; type enum(trial_started, trial_extended, activated, plan_changed, downgrade_scheduled, downgrade_cancelled, addon_changed, state_changed, credit_applied); from jsonb; to jsonb; reason; actor_type enum(tenant_user, flyer_admin, system); actor_id; created_atnew; audit source
usage_counterid; center_id; metric; value numeric; limit_snapshot int null; computed_at; source enum(incremental, nightly, manual); history_daily jsonb (90 days)new; unique (center_id, metric); incremented on writes, fully recomputed nightly at 02:00
subscription_invoiceid uuid; number (TS-INV-yyyy-nnnnnn, unique); center_id; subscription_id; type enum(renewal, upgrade, addon, adjustment); status enum(unpaid, paid, overdue, void); lines jsonb [{description, plan_id | addon_type, qty, unit_price, amount}]; subtotal; credit_applied; total_due; paid_amount; payment_code (unique); due_at; paid_at null; payment_method enum(bank_transfer, vnpay, offline) null; payments jsonb [{amount, method, sepay_transaction_id, received_at}]; einvoice {provider, number, lookup_code, pdf_url} null; buyer jsonbnew; FLYER's invoice to the tenant, separate from Tuition in PRD 05
creditid; center_id; amount_vnd; remaining_vnd; reason; granted_by; expires_at null; created_atnew; consumed earliest-expiry first
Center (extended)+ subscription_id; + read_only bool (derived)extend; read_only is returned inside accessible-schools

Scoping: plan and plan_limit are system-wide data managed by FLYER. subscription, usage_counter, subscription_invoice and credit are tenant-level (center_id), never branch-level; the branch metric aggregates the Center's Schools. A tenant reads only records carrying its own center_id; another tenant's id returns 404 per PRD 12.

7API contract

MethodPathPurposeStatus
GET/v3/crm/centersCenter list; adds subscription_summary {plan_code, status, term_end, read_only}extend
GET/v3/crm/onboarding/centersCenters created in onboardingexisting
POST/v3/crm/onboarding/create-centerCreate center; afterwards start a 30-day trialing subscriptionextend
GET/v3/crm/me/accessible-schoolsUser's branches; adds read_only and subscription_statusextend
GET/v3/teacher/package/my-subscriptionsFLYER teacher's personal package; unchanged, shape reference onlyexisting
GET/v3/crm/subscription→ {subscription, plan, limits, usage: [{metric, value, limit, percent}], addons, credit_remaining, pending_downgrade}new
GET/v3/crm/subscription/plansPublic plan catalogue with limits and modules, per X-Localenew
GET/v3/crm/subscription/usagemetric, from, to → daily history; ?recompute=true recomputes now (once per 5 minutes)new
POST/v3/crm/subscription/upgrade-quote{plan_code, billing_cycle} → {new_price, remaining_value, months_unused, credit_applied, total_due, new_term_end, quote_hash}new
POST/v3/crm/subscription/upgrade{plan_code, billing_cycle, quote_hash} → subscription_invoice; 409 STALE_QUOTE when the quote is older than 24 h; Idempotency-Keynew
POST · DELETE/v3/crm/subscription/downgrade{plan_code} schedules a downgrade at term_end; 422 USAGE_EXCEEDS_PLAN with the offending metrics; DELETE cancelsnew
GET · POST · DELETE/v3/crm/subscription/addons{type, quantity} → prorated quote and invoice; DELETE turns off auto-renewnew
GET/v3/crm/subscription/invoices · /{id} · /{id}/pdf · /{id}/qrList, detail, PDF and transfer QR of subscription invoicesnew
POST/v3/crm/subscription/invoices/{id}/vnpay · /einvoice→ {redirect_url}, callback records payment (P2); {buyer} → issue e-invoice, returns number and lookup codenew
GET · POST/v3/crm/subscription/events · /notify-ownerTenant's subscription history, paginated; notify owners that a limit was hit (once per hour per user)new
POST/v3/internal/sepay/webhookExisting SePay webhook (PRD 05) also receives FLYER's account; matches TS-INV payment_codeextend
GET/v3/admin/tenants · /{center_id}status, plan_code, expiring_within_days, account_manager_id, q → list with usage; detail → subscription, usage, invoices, credits, events; tenant.admin onlynew
PATCH/v3/admin/tenants/{center_id}/subscription{plan_code, term_start, term_end, billing_cycle, contract_no, trust_payment, reason, version}; optionally creates a paid offline invoicenew
POST/v3/admin/tenants/{center_id}/trial-extend · /credits · /suspend · /reactivate · /terminate{days ∈ 7|14|30, reason} (over 90 days total needs tenant.admin.override); {amount_vnd, reason, expires_at?}; {reason}; terminate sets purge_after = now + 90 daysnew
GET · POST · PATCH/v3/admin/plans · /{id} · /v3/admin/subscription-transactions/unmatched · /{id}/matchPlan catalogue and limits (a PATCH to limits of a plan with live subscriptions applies to new terms only); unmatched SePay transfers on FLYER's account (table unmatched_subscription_transaction, same pattern as PRD 05) and assignment to an invoicenew
middlewareevery write endpoint under /v3/crm and /v3/school402 PLAN_LIMIT_REACHED {metric, limit, usage, suggested_plan}; 423 TENANT_READ_ONLY {term_end, grace_end}; header X-Plan-Warning: metric=active_students;percent=92 from 90%new

8Permissions

Permissionownerbranch_manageradmissionsacademic_adminaccountantteacherparent
subscription.view (plan page, usage)allall (read)nonoall (read)nono
subscription.manage (upgrade, downgrade, add-ons, enable e-invoice)yesnononononono
subscription.pay (subscription invoices, QR, VNPay, e-invoice download)yesnononoyesnono
subscription.notify_owner (alert the owner when blocked)not neededyesyesyesyesyesno
Subscription status banneryes, with action buttonyesyesyesyes, with pay buttonyesread-only notice on the parent portal only while suspended
tenant.admin, tenant.admin.override (internal console)Granted only to FLYER internal users through a system role; absent from the tenant's permissions-catalog; every action requires a reason and is audited.

9Analytics events

EventProperties
trial_startedcenter_id, source (onboarding|admin)
trial_converted · trial_expireddays_in_trial, plan_code, billing_cycle, actor_type, max_active_students
plan_page_viewedstatus, highest_percent_metric, highest_percent
limit_warning_shownmetric, percent, surface (banner|email|in_app)
limit_block_shownmetric, limit, usage, action (view_plans|notify_owner|dismiss), by_role
upgrade_quote_viewed · upgrade_completedfrom_plan, to_plan, months_unused, total_due, trigger (limit_block|plan_page|admin)
downgrade_scheduled · downgrade_cancelledfrom_plan, to_plan, cancelled_by (user|usage_check)
addon_purchasedtype, quantity, prorated_months, total_due
subscription_invoice_paidtype, method, hours_to_pay, matched (auto|manual)
expiry_notification_sentmilestone (T-30|T-7|T-1|T+0|T+14), channel
subscription_state_changedfrom, to, actor_type
admin_tenant_actionaction (set_plan|trial_extend|credit|suspend|reactivate|terminate), admin_id (no tenant personal data)

10Edge cases

11Open questions

Trường Số (truongso.net) · FLYER internal