[
 {
  "id": "TS-OVW-001",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Sidebar shows the target navigation tree in the specified order",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Logged in as U-OWNER on tenant BIG at 1440 px.",
  "steps": [
   "Open /dashboard.",
   "Read the sidebar from top to bottom."
  ],
  "expected": "Order is exactly: Tổng quan; group TUYỂN SINH with Khách hàng, Lịch hẹn & cuộc gọi, Học thử, Kiểm tra xếp lớp, Nhập dữ liệu; group HỌC VỤ with Học viên, Lớp học, Thời khoá biểu, Điểm danh, Sổ liên lạc; group TÀI CHÍNH with Học phí, Công nợ, Thu chi, Kho; group NHÂN SỰ with Nhân sự, Bảng công; BÁO CÁO; CÀI ĐẶT. No other top-level items.",
  "trace": "00 §5 navigation map, D8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-002",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Each sidebar item routes to the specified path",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Click every sidebar item in turn.",
   "Read the browser URL after each click."
  ],
  "expected": "URLs are /dashboard, /crm/customers, /crm/schedules, /crm/trials, /crm/placement-tests, /crm/imports, /students, /classes, /timetable, /attendance, /communication, /finance/tuition, /finance/debt, /finance/cashbook, /inventory, /staff, /staff/timesheets, /reports, /settings. Each page renders an h1 matching the sidebar label.",
  "trace": "00 §5, D8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-003",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Legacy /crm/settings/* routes redirect to /settings",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Open /crm/settings/users directly.",
   "Open /crm/settings/roles directly.",
   "Open /crm/settings/tags directly."
  ],
  "expected": "Each request ends on /settings/users, /settings/roles, /settings/tags respectively (HTTP 301/302 or client redirect); the page content is the new settings page; no 404.",
  "trace": "00 §5 route prefix note, 10 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-004",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Existing CRM routes remain under /crm and old bookmarks keep working",
  "type": "regression",
  "priority": "P1",
  "preconditions": "As U-ADM.",
  "steps": [
   "Open /crm/customers.",
   "Open /crm/customers/<L1 id>."
  ],
  "expected": "Both pages render (Kanban/table list, then lead detail shell) with HTTP 200; no redirect to a root route.",
  "trace": "D8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-005",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Sidebar conforms to Cloud theme geometry",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "As U-OWNER at 1440 px.",
  "steps": [
   "Measure sidebar width with devtools.",
   "Inspect group heading font size and case.",
   "Inspect the active item style on /students."
  ],
  "expected": "Sidebar width 292 px, white background, 1 px right border; group headings uppercase 11 px; active item has light purple background (#F3EBFA) and purple text (#6A309B), no solid bar.",
  "trace": "Cloud theme §2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-006",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Top bar contains branch switcher, quick search, bell and user menu",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Open /students.",
   "Inspect the top bar left to right."
  ],
  "expected": "Controls present: branch dropdown showing \"Tất cả chi nhánh\", search field with ⌘K hint (opens on Cmd/Ctrl+K), notification bell with unread count, user avatar menu. Pipeline selector is NOT shown on /students.",
  "trace": "00 §5 top bar",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-007",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Pipeline selector appears only on CRM pages",
  "type": "functional",
  "priority": "P2",
  "preconditions": "As U-ADM.",
  "steps": [
   "Open /crm/customers and note the pipeline selector.",
   "Open /classes."
  ],
  "expected": "Pipeline selector visible on /crm/customers; absent on /classes and every non-/crm route.",
  "trace": "00 §5 top bar",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-008",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Branch switcher lists exactly the user's accessible branches",
  "type": "functional",
  "priority": "P0",
  "preconditions": "U-BM-A (branch A only) and U-OWNER.",
  "steps": [
   "As U-BM-A open the branch dropdown.",
   "As U-OWNER open the branch dropdown."
  ],
  "expected": "U-BM-A sees only \"Quận 1\" preselected and no \"Tất cả chi nhánh\" option. U-OWNER sees \"Tất cả chi nhánh\", \"Quận 1\", \"Thủ Đức\", \"Gò Vấp\".",
  "trace": "D1, 00 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-009",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Switching branch filters every branch-scoped list",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Select branch \"Thủ Đức\".",
   "Open /students, /classes, /attendance, /finance/tuition in turn.",
   "Select \"Tất cả chi nhánh\" and repeat."
  ],
  "expected": "With Thủ Đức: /students lists S2 but not S1; /classes lists C3 but not C1; /finance/tuition lists INV-2 but not INV-1. With all branches: both sets appear and a \"Chi nhánh\" column is shown on /students.",
  "trace": "D1, 02 §5 Chi nhánh column",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-010",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Branch selection persists across navigation and reload",
  "type": "functional",
  "priority": "P2",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Select \"Thủ Đức\".",
   "Navigate to /classes then reload the page."
  ],
  "expected": "Branch dropdown still shows \"Thủ Đức\" after navigation and after reload.",
  "trace": "D1",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-011",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Leads are tenant-level and not filtered by the branch switcher",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER with branch \"Thủ Đức\" selected.",
  "steps": [
   "Open /crm/customers table view.",
   "Search for L1 (school_id branch A)."
  ],
  "expected": "L1 is listed; the leads list ignores the branch switcher (leads are per center).",
  "trace": "00 §3 consequences",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-012",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Branch-A user cannot read branch-B rows by URL",
  "type": "security",
  "priority": "P0",
  "preconditions": "As U-BM-A.",
  "steps": [
   "Open /students/<S2 id> directly.",
   "Open /classes/<C3 id> directly.",
   "Call GET /api/v3/school/students/<S2 id> with the session cookie."
  ],
  "expected": "UI shows a not-found page (text \"Không tìm thấy\" / 404); API returns HTTP 404 (not 403) with no student fields in the body.",
  "trace": "12 §3 authorisation, 00 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-013",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Default roles exist and are locked",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Open /settings/roles."
  ],
  "expected": "Rows for Chủ trung tâm (owner), Quản lý chi nhánh (branch_manager), Tư vấn tuyển sinh (admissions), Giáo vụ (academic_admin), Kế toán (accountant), Giáo viên (teacher), Chỉ xem (viewer) exist with \"Mặc định\" tick; the delete action is absent or disabled for each; code and name fields are read-only.",
  "trace": "D2, 10 §4 default roles",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-014",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Role matrix per module matches PRD 00 §4 for owner",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Open /settings/roles, open Chủ trung tâm."
  ],
  "expected": "Every permission group shows scope \"Tất cả\" (all) for all nine module groups.",
  "trace": "00 §4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-015",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Role matrix for teacher matches PRD 00 §4",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Open /settings/roles, open Giáo viên."
  ],
  "expected": "Leads: Không; Students: Của mình (own classes); Classes: Của mình read; Attendance: mark own sessions; Finance: Không; Staff: self; Communication: own classes; Inventory: read; Reports: own classes; Settings: Không.",
  "trace": "00 §4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-016",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Viewer role is read-only everywhere",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-VIEWER.",
  "steps": [
   "Open /dashboard, /students, /classes, /finance/tuition.",
   "Look for any create/edit/delete control.",
   "Send POST /api/v3/school/students with a valid body."
  ],
  "expected": "Lists and dashboard render; no \"+ Thêm\", \"Sửa\", \"Thu tiền\" or kebab actions are shown; the API returns HTTP 403 with an error code.",
  "trace": "10 §4 viewer",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-017",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Permission change takes effect on the next request without re-login",
  "type": "functional",
  "priority": "P1",
  "preconditions": "U-OWNER and U-ACAD-A logged in in two browsers.",
  "steps": [
   "As U-OWNER, on role Giáo vụ untick finance.invoice.create and save.",
   "As U-ACAD-A (already logged in) open /students/<S1 id> and look for \"Thu học phí\".",
   "As U-ACAD-A call POST /api/v3/teacher/school-finance/tuitions."
  ],
  "expected": "Button \"Thu học phí\" is no longer shown after a page navigation; API returns 403. No logout was required.",
  "trace": "00 §4, 10 SET-03, test seed 00/10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-018",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Stub PermissionGate is removed: Kanban actions respect real permissions",
  "type": "security",
  "priority": "P0",
  "preconditions": "As U-VIEWER (or a role without customers.edit).",
  "steps": [
   "Open /crm/customers Kanban view.",
   "Attempt to drag a card to another stage.",
   "Open a card and look for Won/Lost/Delete buttons."
  ],
  "expected": "Drag is disabled or reverts with an error toast; Won/Lost/Delete are hidden; PATCH /customers/{id}/kanban-position returns 403 when sent manually.",
  "trace": "00 §6 known inconsistencies",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-019",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Scope vocabulary is own | branch | all everywhere",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Open the role matrix editor.",
   "Call GET /api/v3/crm/me/permissions."
  ],
  "expected": "Matrix scope radios are exactly Không / Của mình / Chi nhánh / Tất cả; the API returns scope values only from {own, branch, all}; no \"team\", \"read\", \"write\", \"manage\" values appear.",
  "trace": "00 §6, 10 §4 vocabulary",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-020",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "D3: every new module ships a table view and no board",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Open /students, /classes, /attendance, /finance/tuition, /finance/debt, /staff, /inventory."
  ],
  "expected": "Each page opens in a table list; no Kanban/board toggle exists on these pages. /crm/customers still offers Kanban and table.",
  "trace": "D3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-021",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "D4: an enrolled student always has a FLYER learner profile",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Open /students/<S1 id>.",
   "Check the right rail \"Tài khoản FLYER\".",
   "Attempt to create a student via /students/new leaving \"Tài khoản FLYER\" radio unselected."
  ],
  "expected": "S1 shows a username and last-active time. The new-student form blocks submit with an inline error under Tài khoản FLYER (\"Bắt buộc\" or equivalent).",
  "trace": "D4, 02 STU-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-022",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "D5: sessions are materialised rows",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Open /classes/<C1 id> tab Buổi học.",
   "Count rows."
  ],
  "expected": "48 numbered session rows exist, each with date, time, room, teacher, status; the 20 held sessions show \"Đã điểm danh\" or an attendance marker.",
  "trace": "D5, 03 §6",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-023",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "D6: invoices reuse the Tuition model with payment_code",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACC.",
  "steps": [
   "Open /finance/tuition/<INV-1 id>.",
   "Call GET /api/v3/teacher/school-finance/tuitions/<INV-1 id>."
  ],
  "expected": "Detail shows Mã HĐ TS1A0001 and a QR; the API response is a Tuition object with lines[], payment_code, qr_url.",
  "trace": "D6, 05 §2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-024",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "D7: no SMS channel anywhere in the product",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Open /settings/notifications.",
   "Open the announcement composer channel checkboxes.",
   "Search the vi and en message files for \"SMS\"."
  ],
  "expected": "Channel options are Zalo ZNS, Zalo OA, Ứng dụng (push), Email, Cổng phụ huynh only; no SMS checkbox; no \"SMS\" string in message files except in a \"not available\" explanation, if any.",
  "trace": "D7, 07 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-025",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "D9: switching locale to en translates every visible string",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Switch locale to English from the user menu.",
   "Open /dashboard, /students, /classes, /attendance, /finance/tuition, /settings.",
   "Screenshot each."
  ],
  "expected": "All labels, tabs, buttons, empty states and column headers are English; no Vietnamese string remains; no raw i18n keys (e.g. \"students.list.title\") are rendered.",
  "trace": "D9, 12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-026",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "D9: vi and en message files have identical key sets",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "Repo checkout of the build.",
  "steps": [
   "Run the i18n parity script (or diff the key sets of src/i18n/messages/vi.json and en.json)."
  ],
  "expected": "Zero keys missing on either side; the CI check passes.",
  "trace": "D9, 12 §8, 12 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-027",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "D10: phase 2 modules are feature-flagged per tenant",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Tenant BIG with flags inventory=false, staff_pay=false; tenant with flags on.",
  "steps": [
   "As U-OWNER on BIG open /inventory and /staff/timesheets.",
   "Repeat on a tenant with the flags on."
  ],
  "expected": "With flags off the sidebar hides Kho and Bảng công and direct URL shows a \"Tính năng chưa bật\" page (not a crash); with flags on both pages render.",
  "trace": "D10, 12 §10 feature flags",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-028",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Unauthenticated access redirects to login and back",
  "type": "security",
  "priority": "P0",
  "preconditions": "Logged out.",
  "steps": [
   "Open /students/<S1 id>.",
   "Log in as U-ACAD-A."
  ],
  "expected": "Redirect to the login page; after login the browser lands on /students/<S1 id> (return URL preserved).",
  "trace": "11 §1 auth 401 redirects",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-029",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Global search finds students, classes and leads",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Press Cmd/Ctrl+K.",
   "Type \"Nguyen An\" (no accents).",
   "Type \"MOV-2609\".",
   "Type \"0987000001\"."
  ],
  "expected": "Results group by type: Học viên S1 (Nguyễn An); Lớp học C1; Khách hàng L1 by phone. Enter on a result navigates to its detail. Accent-insensitive matching works.",
  "trace": "00 §5 ⌘K, 12 §8 collation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-030",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Onboarding checklist appears for a new tenant",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As owner of tenant EMPTY.",
  "steps": [
   "Open /dashboard."
  ],
  "expected": "A checklist card lists 7 items: thêm chi nhánh, mời nhân sự, tạo khoá học và gói phí, thêm phòng, kết nối tài khoản ngân hàng, kết nối Zalo, nhập học viên; each is a link to its settings page; none is ticked.",
  "trace": "10 SET-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-031",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Console has a minimum viewport of 1280 px; 1024 shows horizontal scroll inside tables only",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Set viewport 1024 x 768 and open /students.",
   "Set viewport 1440 x 900 and repeat."
  ],
  "expected": "At 1024 the sidebar collapses to icons or a drawer, the page body does not scroll horizontally, the table scrolls inside its own container. At 1440 the full layout shows with no overflow.",
  "trace": "12 §9 viewport",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-032",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "Error pages show a support code",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "Any user.",
  "steps": [
   "Open /students/does-not-exist.",
   "Trigger a server error via a stubbed 500 endpoint if available."
  ],
  "expected": "Not-found page shows a short message and a code in the format TS-4xxx; the 500 page shows TS-5xxx and a \"Thử lại\" button; no stack trace.",
  "trace": "12 §10 observability",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-OVW-033",
  "module": "00-overview-and-architecture",
  "module_title": "Shell, tenancy, roles and decisions",
  "prd": "00",
  "title": "User menu shows name, role and tenant; logout ends the session",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Open the user menu.",
   "Click Đăng xuất.",
   "Press browser Back."
  ],
  "expected": "Menu shows display name, role \"Giáo vụ\", tenant \"Big Center\", locale switch, Đăng xuất. After logout, Back does not show protected content (redirect to login).",
  "trace": "12 §3 authentication",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-001",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Create a lead with guardian and prospective learner block",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ADM on /crm/customers.",
  "steps": [
   "Click + Thêm khách hàng.",
   "Enter Họ tên \"Nguyễn Thị Mai\", SĐT 0988111222, source Facebook, channel Zalo OA.",
   "Under \"Học viên dự kiến\" enter Tên \"Nguyễn Gia Bảo\", Năm sinh 2017, Trường \"TH Lê Lợi\", Trình độ Starters.",
   "Save."
  ],
  "expected": "Lead created; detail sidebar shows group \"Học viên dự kiến\" with the four values; GET /customers/{id} returns prospect_learners[0] = {name, birth_year 2017, school, level \"starters\"}.",
  "trace": "ADM-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-002",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Duplicate check by phone still blocks on create",
  "type": "regression",
  "priority": "P0",
  "preconditions": "As U-ADM.",
  "steps": [
   "Open the create drawer.",
   "Enter phone 0987000001 (L1) and a different name.",
   "Blur the phone field / press Save."
  ],
  "expected": "Duplicate modal appears naming L1 (\"Phạm Thu Hà\") with link; Save is blocked until the user chooses an action (open existing / cancel).",
  "trace": "ADM-01, 01 §6 rule 1",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-003",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Soft duplicate warning on child name + birth year",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ADM; L1 has prospect \"Phạm Minh\" 2018.",
  "steps": [
   "Open the create drawer with a new phone 0988111333.",
   "Enter prospect Tên \"Phạm Minh\", Năm sinh 2018.",
   "Save."
  ],
  "expected": "A yellow warning \"Có học viên dự kiến trùng tên và năm sinh\" with link to L1 is shown; Save is still allowed and the lead is created.",
  "trace": "ADM-01 soft warning",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-004",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Prospect birth year validation",
  "type": "negative",
  "priority": "P2",
  "preconditions": "As U-ADM in the create drawer.",
  "steps": [
   "Enter Năm sinh 1899, then 2031, then \"abc\"."
  ],
  "expected": "Each is rejected inline with a message like \"Năm sinh không hợp lệ (1950–<current year>)\"; Save disabled while invalid.",
  "trace": "ADM-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-005",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Returning family: guardian phone already belongs to an enrolled student",
  "type": "edge",
  "priority": "P1",
  "preconditions": "As U-ADM; G1 phone 0901234567 is guardian of S1.",
  "steps": [
   "Open the create drawer and enter phone 0901234567."
  ],
  "expected": "Banner \"Phụ huynh đã có học viên\" with a link to S1 appears; consultant can continue and save the lead.",
  "trace": "01 §11 returning family",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-006",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Siblings are additional prospect entries on one lead",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ADM on L1 detail.",
  "steps": [
   "In group Học viên dự kiến click + Thêm.",
   "Add \"Phạm Lan\" 2020."
  ],
  "expected": "Group lists two prospects; GET returns prospect_learners length 2 with distinct keys.",
  "trace": "01 §6 rule 1",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-007",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Lead table shows new columns Học viên dự kiến, Học thử, Xếp lớp",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ADM in table view.",
  "steps": [
   "Open column settings.",
   "Enable the three columns."
  ],
  "expected": "Columns render: Học viên dự kiến shows \"Phạm Minh (2018)\" for L1; Học thử shows next/last trial with chip (Đã đặt / Đã học / Vắng); Xếp lớp shows placement level or \"-\". Column visibility persists after reload (view-configs).",
  "trace": "01 §5 list adjustments",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-008",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Quick filters above the table",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ADM.",
  "steps": [
   "Click each quick filter: 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."
  ],
  "expected": "Each click updates the list and the count badge; Của tôi shows L1 and L4 only; Gọi lại hôm nay includes L4; the active filter is visually distinct; URL query reflects the filter.",
  "trace": "01 §5",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-009",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Filters \"Có gọi lại\" and \"Không gọi\"",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Apply filter Có gọi lại.",
   "Clear; apply filter Không gọi."
  ],
  "expected": "Có gọi lại lists L4; Không gọi lists L3 only; counts match.",
  "trace": "01 §5",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-010",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Empty state of the lead list",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "As owner of tenant EMPTY.",
  "steps": [
   "Open /crm/customers table view."
  ],
  "expected": "Empty state with a short sentence and a primary \"+ Thêm khách hàng\" button (existing copy retained); demo mode fake data appears only in the dashboard admissions section, not in this list.",
  "trace": "09 §10 demo mode",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-011",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Callback queue orders overdue, today, upcoming",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ADM; L4 has overdue callback; create another lead with callback at today 17:00 and one at tomorrow.",
  "steps": [
   "Open /crm/schedules, tab Gọi lại."
  ],
  "expected": "Rows ordered: L4 (overdue, red chip \"Quá hạn\" with elapsed time), then today, then tomorrow. Columns: Khách hàng, SĐT, Giờ hẹn gọi, Quá hạn, Người phụ trách, Lần gọi gần nhất, action Gọi.",
  "trace": "ADM-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-012",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "One-click Gọi opens the quick-log dialog preselected; logging removes the row",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ADM on tab Gọi lại.",
  "steps": [
   "Click Gọi on L4.",
   "Choose outcome Quan tâm, save."
  ],
  "expected": "Dialog opens with L4 preselected and phone visible; after save the row disappears from the queue without reload and an interaction appears on L4's timeline.",
  "trace": "ADM-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-013",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Callback queue empty state",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "As a consultant with no callbacks.",
  "steps": [
   "Open tab Gọi lại."
  ],
  "expected": "Text exactly \"Không có yêu cầu gọi lại.\"",
  "trace": "01 §5 Lịch hẹn & cuộc gọi",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-014",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Callback queue filters by assignee and date range",
  "type": "functional",
  "priority": "P2",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Filter assignee = U-ADM.",
   "Set date range to yesterday only."
  ],
  "expected": "Only L4 remains; clearing filters restores all rows.",
  "trace": "ADM-02, 01 §8 interactions query",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-015",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Callback queue respects own scope",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-ADM (own scope); a callback exists on a lead assigned to another consultant.",
  "steps": [
   "Open tab Gọi lại."
  ],
  "expected": "Only callbacks on U-ADM's leads are listed; the other consultant's row is absent; API with assignee_id of the other user returns 403 or an empty list.",
  "trace": "01 §9 customers.view own",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-016",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Schedules tabs Hôm nay, Gọi lại, Cuộc hẹn, Học thử",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ADM.",
  "steps": [
   "Open /crm/schedules and click each tab."
  ],
  "expected": "Four tabs exist in this order; Hôm nay shows today-plan items; Cuộc hẹn lists meetings; Học thử lists trial bookings with the same columns as /crm/trials.",
  "trace": "01 §5",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-017",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Mark do-not-call with reason",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ADM on L1 detail.",
  "steps": [
   "Open the kebab and choose Đánh dấu không gọi.",
   "Enter reason \"Khách yêu cầu\" and confirm."
  ],
  "expected": "L1 shows a \"Không gọi\" chip; GET returns do_not_call_at, do_not_call_by (U-ADM) and reason; an audit entry exists.",
  "trace": "ADM-03",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-018",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "DNC reason is required",
  "type": "negative",
  "priority": "P1",
  "preconditions": "As U-ADM.",
  "steps": [
   "Open Đánh dấu không gọi and confirm with an empty reason."
  ],
  "expected": "Confirm disabled or inline error \"Vui lòng nhập lý do\"; no flag set.",
  "trace": "ADM-03",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-019",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Quick-log-call guard on a DNC lead",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ADM on L3.",
  "steps": [
   "Click Gọi (quick log call)."
  ],
  "expected": "Dialog shows a red guard \"Khách hàng đã yêu cầu không gọi\" and the call log form is blocked; no \"Vẫn gọi\" button for admissions.",
  "trace": "ADM-03, 01 §6 rule 4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-020",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Force call requires customers.force_call",
  "type": "security",
  "priority": "P1",
  "preconditions": "U-ADM and U-BM-A on L3.",
  "steps": [
   "As U-ADM try to remove the DNC mark; call DELETE /customers/{L3}/do-not-call.",
   "As U-BM-A remove the mark from the UI."
  ],
  "expected": "U-ADM: no remove action in UI; API returns 403. U-BM-A: mark removed, audit entry written, chip disappears.",
  "trace": "ADM-03, 01 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-021",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "DNC lead is skipped in bulk ZNS",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER; L3 has Zalo consent but DNC.",
  "steps": [
   "Select L3 and L1 in the table, click bulk Gửi ZNS, send an approved template."
  ],
  "expected": "Result summary shows L1 sent and L3 \"Bỏ qua: Không gọi\"; no message row for L3 with status sent.",
  "trace": "ADM-03, 07 skip reason",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-022",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "DNC does not block transactional messages to the guardian of an enrolled student",
  "type": "edge",
  "priority": "P2",
  "preconditions": "A lead marked DNC whose phone equals G2's phone; G2 child S6 is enrolled.",
  "steps": [
   "Save attendance on SES-TODAY with S6 absent."
  ],
  "expected": "Absence message to G2 is attempted per consent (skipped only for missing Zalo consent, reason \"Chưa có kênh liên lạc\" and email fallback), never with reason \"Không gọi\".",
  "trace": "01 §6 rule 4, 07 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-023",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Book a trial into a real session",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ADM on L1; C1 has allow_trial and 2 free trial seats; SES-TODAY is in the future (run before 18:00) or use the next C1 session.",
  "steps": [
   "Open tab Học thử & xếp lớp, click Đặt lịch học thử.",
   "Pick class C1 (dialog shows level Movers, teacher T1, \"Chỗ học thử: 2\").",
   "Pick the next session from the list of 5.",
   "Enter note \"Bé nhút nhát\", leave Gửi xác nhận Zalo on. Confirm."
  ],
  "expected": "Booking row appears with status Đã đặt; /crm/trials lists it; the session roster (/classes/C1 tab Buổi học > session drawer) shows \"Phạm Minh\" with chip Học thử; a message row type trial_confirm is created for the lead phone if consent exists; lead moves to the pipeline's trial stage if configured.",
  "trace": "ADM-04",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-024",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Trial dialog lists only next 5 sessions of the class",
  "type": "functional",
  "priority": "P2",
  "preconditions": "As U-ADM in the trial dialog with C1 selected.",
  "steps": [
   "Open the session picker."
  ],
  "expected": "Exactly 5 future sessions, ascending by date, each \"T3 dd/MM · 18:00–19:30 · P.201\"; past sessions absent.",
  "trace": "01 §5 trial dialog",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-025",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Trial validation: session must be in the future",
  "type": "negative",
  "priority": "P1",
  "preconditions": "As U-ADM.",
  "steps": [
   "Call POST /v3/crm/trial-bookings with session_id of SES-PAST."
  ],
  "expected": "HTTP 400/422 with a message naming the session date; no booking created.",
  "trace": "01 §5 validation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-026",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "No trial seats left returns 409 NO_TRIAL_SEATS",
  "type": "negative",
  "priority": "P0",
  "preconditions": "C1 next session already has 2 trial bookings.",
  "steps": [
   "As U-ADM book a third trial on that session."
  ],
  "expected": "Dialog shows \"Hết chỗ học thử\" (or the class shows 0 seats and is not selectable); API returns 409 code NO_TRIAL_SEATS.",
  "trace": "ADM-04, 01 §6 rule 3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-027",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Same prospect cannot hold two bookings on the same day",
  "type": "negative",
  "priority": "P1",
  "preconditions": "L1 prospect already booked on the next C1 session.",
  "steps": [
   "Book the same prospect into a C2 session on the same date."
  ],
  "expected": "API returns 409 DUPLICATE_TRIAL; UI shows \"Học viên đã có lịch học thử ngày này\".",
  "trace": "01 §5 validation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-028",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Class without allow_trial is not offered",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Class C4 has allow_trial false.",
  "steps": [
   "Open the trial dialog and search \"Flyers 3\"."
  ],
  "expected": "C4 does not appear in the picker (or appears disabled with \"Không nhận học thử\").",
  "trace": "ADM-04 Given",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-029",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Trial status updates when the teacher marks attendance",
  "type": "flow",
  "priority": "P0",
  "preconditions": "Trial booked for L1 on SES-TODAY; U-TEACH.",
  "steps": [
   "As U-TEACH open /teach/sessions/<SES-TODAY>/attendance.",
   "Mark \"Phạm Minh\" (Khách section) Có mặt, choose quick note Phù hợp, save.",
   "As U-ADM check /crm/trials and the notification bell."
  ],
  "expected": "Booking status Đã học; lead timeline has activity \"Học thử: đã học\"; U-ADM receives notification \"Học thử xong: Phạm Minh (Movers 2A)\" with the teacher note.",
  "trace": "ADM-04, ATT-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-030",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Trial no-show",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Trial booked; teacher marks the trial student Vắng.",
  "steps": [
   "Save the sheet with the trial row Vắng."
  ],
  "expected": "Booking status Vắng (no_show); consultant notified; lead activity written.",
  "trace": "04 §10 trial absent",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-031",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Trials page columns and actions",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ADM with bookings in every status.",
  "steps": [
   "Open /crm/trials."
  ],
  "expected": "Columns: Học viên (link to lead), Phụ huynh / SĐT, Lớp (name + level chip), Buổi, Trạng thái chips Đã đặt (blue) · Đã học (green) · Vắng (red) · Đã huỷ (grey), Nhận xét GV (truncated with tooltip), Người phụ trách, Thao tác with Đổi buổi · Huỷ · Chuyển đổi.",
  "trace": "01 §5 Học thử table",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-032",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Reschedule a trial (Đổi buổi)",
  "type": "functional",
  "priority": "P1",
  "preconditions": "A booked trial.",
  "steps": [
   "Click Đổi buổi, pick another future session with seats, confirm."
  ],
  "expected": "session_id updated (PATCH), the old session roster no longer lists the prospect, the new one does; status stays Đã đặt.",
  "trace": "01 §8 PATCH trial-bookings",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-033",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Cancel a trial",
  "type": "functional",
  "priority": "P1",
  "preconditions": "A booked trial.",
  "steps": [
   "Click Huỷ, confirm."
  ],
  "expected": "Status Đã huỷ; trial seat released (seat count back to 2); Chuyển đổi still available.",
  "trace": "01 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-034",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "School cancels a session with trial bookings",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Trial booked on the next C1 session; U-ACAD-A.",
  "steps": [
   "As U-ACAD-A cancel that session with reason.",
   "As U-ADM check notifications."
  ],
  "expected": "Booking becomes Đã huỷ; U-ADM gets a notification with a \"Đặt lại\" action that opens the trial dialog for the same prospect.",
  "trace": "01 §11 trial session cancelled",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-035",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Placement test status labels match the API enum",
  "type": "i18n",
  "priority": "P0",
  "preconditions": "Tests exist in each API status.",
  "steps": [
   "Open /crm/placement-tests."
  ],
  "expected": "Labels are exactly Chưa bắt đầu (not_started), Đang làm (in_progress), Hoàn thành (completed), Đã huỷ (cancelled); no \"Đã lên lịch\"/\"scheduled\" label anywhere. In en: Not started, In progress, Completed, Cancelled.",
  "trace": "ADM-05, 00 §6 inconsistency",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-036",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Create a placement test without a linked learner and submit manual scores",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ADM on L1 (no linked learner).",
  "steps": [
   "Click Tạo bài kiểm tra; fill Trình độ Movers, Thời gian kiểm tra tomorrow 09:00, Giám khảo T1, Ghi chú.",
   "Save; then open Nhập kết quả.",
   "Enter Điểm tổng 72, Nghe 80, Nói 60, Đọc 75, Viết 70, Link kết quả, Ghi chú; save."
  ],
  "expected": "Status Chưa bắt đầu after create; after submit status Hoàn thành; result block shows the five scores and read-only \"Đề xuất lớp: Movers\" computed from the level mapping (72 in Movers band).",
  "trace": "ADM-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-037",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Placement test with linked learner sends the FLYER test",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Lead with linked learner (L2).",
  "steps": [
   "Create a placement test on L2 with Mã đề FLYER."
  ],
  "expected": "POST /v3/teacher/placement-test/send is called (stub log shows it); status Chưa bắt đầu; result fields are read-only pending auto-fill.",
  "trace": "ADM-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-038",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "All placement form labels are localised",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "As U-ADM in en locale.",
  "steps": [
   "Open the placement test form and the result modal."
  ],
  "expected": "No Vietnamese text appears; labels Level, Test time, Deadline, Examiner, FLYER test code, Note; result modal Overall, Listening, Speaking, Reading, Writing, Result link, Note.",
  "trace": "ADM-05, 01 §5",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-039",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Placement result submitted twice: last write wins with audit",
  "type": "edge",
  "priority": "P2",
  "preconditions": "A completed test.",
  "steps": [
   "Submit the result again with Điểm tổng 55."
  ],
  "expected": "Result shows 55 and suggested level recomputed (Starters band); lead audit log has two placement_result_submitted entries.",
  "trace": "01 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-040",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Placement scores validation",
  "type": "negative",
  "priority": "P2",
  "preconditions": "Result modal open.",
  "steps": [
   "Enter Điểm tổng 101, then -1, then blank."
  ],
  "expected": "Each rejected inline (\"0–100\"); save disabled.",
  "trace": "ADM-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-041",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Chuyển đổi button visibility and primary state",
  "type": "functional",
  "priority": "P1",
  "preconditions": "L1 open (not won); L2 won.",
  "steps": [
   "As U-ADM open L1 detail.",
   "Open L2 detail."
  ],
  "expected": "L1: Chuyển đổi present as a secondary button (U-ADM has customers.convert). L2: Chuyển đổi rendered as the primary button. No feature-flag gating.",
  "trace": "ADM-06, 01 §5 lead detail",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-042",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard step 1: learner and branch",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ADM on L1.",
  "steps": [
   "Click Chuyển đổi.",
   "Step 1: choose \"Tạo học viên mới\"; verify the name prefills \"Phạm Minh\"; enter Ngày sinh 15/03/2018, Giới tính Nam, Trường TH Lê Lợi; Chi nhánh defaults to Quận 1.",
   "Click Tiếp."
  ],
  "expected": "Step title \"1 Học viên\"; validation passes; step indicator advances to 2 of 4.",
  "trace": "ADM-06 step 1",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-043",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard step 1 validation: birthday required under 18, branch required",
  "type": "negative",
  "priority": "P1",
  "preconditions": "In step 1 with \"Tạo học viên mới\".",
  "steps": [
   "Leave Ngày sinh empty, click Tiếp.",
   "Clear Chi nhánh, click Tiếp."
  ],
  "expected": "Inline errors \"Bắt buộc với học viên dưới 18 tuổi\" and \"Chọn chi nhánh\"; cannot proceed.",
  "trace": "01 §5 wizard validation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-044",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard step 2: guardian prefill and consent",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Step 2 reached.",
  "steps": [
   "Verify SĐT prefilled 0987000001 and Họ tên prefilled from lead.",
   "Set Quan hệ Mẹ; tick \"Đồng ý nhận thông báo\" (Zalo); enter Email.",
   "Click Tiếp."
  ],
  "expected": "Consent text version is displayed next to the checkbox (e.g. v1.0); step 3 opens.",
  "trace": "ADM-06 step 2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-045",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard step 2 phone validation",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Step 2.",
  "steps": [
   "Change SĐT to \"12345\" and click Tiếp."
  ],
  "expected": "Inline error \"Số điện thoại không hợp lệ\"; cannot proceed.",
  "trace": "01 §5",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-046",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard step 3: class picker filtered by branch shows seats and schedule",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Step 3, branch Quận 1.",
  "steps": [
   "Open the Lớp picker."
  ],
  "expected": "Only branch A classes appear (C1, C2, C4; not C3); each shows \"11/12\", \"T3, T5 · 18:00–19:30\", level chip; C2 shows \"12/12 · Đầy\"; option \"Chờ xếp lớp\" is present at the top. Level suggestion (from placement) sorts matching level first.",
  "trace": "ADM-06 step 3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-047",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard step 3: fee plan options come from the course",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Step 3 with C1 selected.",
  "steps": [
   "Open Gói học phí."
  ],
  "expected": "FP1 \"Movers 2 trọn khoá · 9,600,000 đ\", FP3 \"Gói 24 buổi · 6,000,000 đ\" listed; FP2 monthly if attached; each shows price formatted with \".\" thousands and trailing \"đ\".",
  "trace": "ADM-06, FIN-01, 12 §8 money",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-048",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard step 3: discount above consultant limit shows \"Cần duyệt\"",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Step 3; admissions limit 10%.",
  "steps": [
   "Enter Giảm giá 15%, reason \"Anh chị em\"."
  ],
  "expected": "Chip \"Cần duyệt\" appears next to the discount; summary in step 4 states an approval request will be created.",
  "trace": "ADM-06 step 3, 05 §6 discount limits",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-049",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard step 3: full class blocks enrollment with waiting option",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Step 3.",
  "steps": [
   "Select C2 (12/12)."
  ],
  "expected": "Inline message \"Lớp đã đầy\" and an offer to add to Danh sách chờ or choose Chờ xếp lớp; Tiếp blocked until resolved.",
  "trace": "01 §5 capacity check, CLS-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-050",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard step 4 summary and toggles",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Step 4.",
  "steps": [
   "Read the summary."
  ],
  "expected": "Summary lists learner, branch, guardian with consent state, class, start date, fee plan, discount, total; toggles \"Tạo hoá đơn ngay\" default ON and \"Gửi thông báo nhập học\" present.",
  "trace": "ADM-06 step 4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-051",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Confirm conversion creates all records in one transaction",
  "type": "flow",
  "priority": "P0",
  "preconditions": "Steps 1–4 completed with C1, FP1, no discount, Tạo hoá đơn ngay on.",
  "steps": [
   "Click Xác nhận.",
   "Follow the three result links."
  ],
  "expected": "Result screen shows three link cards Học viên · Lớp · Hoá đơn and \"Chuyển đổi tiếp\". Student exists with status Đang học, code HV-0001xx, guardian G with phone 0987000001; C1 roster shows the student from start date (12/12 now); invoice exists unpaid 9,600,000 with QR; lead status won with won_at; activity \"Đã chuyển thành học viên\" on timeline; a CustomerLearner link exists.",
  "trace": "ADM-06 Then",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-052",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Conversion atomicity: failure in step 3 leaves nothing",
  "type": "negative",
  "priority": "P0",
  "preconditions": "Stub configured so enrollment creation fails (e.g. class deleted between steps or stub error flag).",
  "steps": [
   "Run the wizard to Xác nhận."
  ],
  "expected": "Error toast names the failed step (\"Không tạo được đăng ký lớp\"); no student, guardian, invoice or lead status change persisted; lead still open; wizard stays on step 4 for retry.",
  "trace": "ADM-06 failure, test seed 01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-053",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Convert with \"Chờ xếp lớp\"",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Wizard step 3.",
  "steps": [
   "Choose Chờ xếp lớp, FP3, confirm."
  ],
  "expected": "Student status Chờ xếp lớp; no enrollment; invoice created; result screen shows Lớp card as \"Chưa xếp lớp\" link to /students/:id.",
  "trace": "01 §11 Chờ xếp lớp",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-054",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Convert a second prospect on an already-won lead",
  "type": "functional",
  "priority": "P1",
  "preconditions": "L2 (p1 converted, p2 not).",
  "steps": [
   "Click Chuyển đổi on L2."
  ],
  "expected": "Step 1 lists only p2; p1 is shown as \"Đã chuyển đổi\" with a link to S9; lead stays won after converting p2.",
  "trace": "01 §6 rule 2, 01 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-055",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Converting the same prospect twice is blocked",
  "type": "negative",
  "priority": "P0",
  "preconditions": "L2 p1 already converted.",
  "steps": [
   "Call POST /v3/crm/customers/{L2}/convert with prospect_key p1."
  ],
  "expected": "HTTP 409 with a code such as ALREADY_CONVERTED; no new student created.",
  "trace": "01 §6 rule 2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-056",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Discount above limit: approval request created, sessions not credited",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Wizard with FP3 package and 15% discount by U-ADM.",
  "steps": [
   "Confirm the wizard.",
   "Open the invoice and the student's Học phí tab."
  ],
  "expected": "Invoice status Chờ duyệt; approval_request_id returned; U-BM-A sees a notification; student balance shows 0 sessions (package pending) until approval and payment.",
  "trace": "ADM-06, FIN-02, test seed 01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-057",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Reuse existing guardian record by phone",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Lead whose phone equals G1 phone.",
  "steps": [
   "Run the wizard; in step 2 observe the guardian block."
  ],
  "expected": "Step 2 shows \"Phụ huynh đã có: Trần Thị Hoa\" and reuses guardian G1 (no duplicate guardian row; G1 now linked to the new student and S1, S2).",
  "trace": "01 §11, STU-03 shared guardian",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-058",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard \"Chuyển đổi tiếp\" for siblings",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Result screen after converting p1 of a two-prospect lead.",
  "steps": [
   "Click Chuyển đổi tiếp."
  ],
  "expected": "Wizard reopens at step 1 with the remaining prospect preselected and guardian data prefilled.",
  "trace": "01 §5 result screen",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-059",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard keyboard and focus order",
  "type": "a11y",
  "priority": "P2",
  "preconditions": "Wizard open.",
  "steps": [
   "Tab through step 1 from the first field.",
   "Press Escape."
  ],
  "expected": "Focus moves top-to-bottom through fields then Tiếp; focus trapped inside the modal; Escape asks for confirmation before discarding (\"Huỷ chuyển đổi?\").",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-060",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Wizard at 390 px is usable",
  "type": "ui-ux",
  "priority": "P3",
  "preconditions": "Mobile viewport.",
  "steps": [
   "Open the wizard on a 390 px viewport."
  ],
  "expected": "Steps render full-screen, controls ≥ 44 px, no horizontal scroll; the console minimum is 1280 so a graceful \"Dùng máy tính để chuyển đổi\" message is also acceptable.",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-061",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Transfer a lead to another branch",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-BM-A on L1 (school A). Note: U-BM-A needs customers.transfer.",
  "steps": [
   "Kebab → Chuyển chi nhánh; choose Thủ Đức and assignee (a branch-B consultant); note \"Gần nhà\".",
   "Confirm."
  ],
  "expected": "L1 school_id = B; assignments at A ended with reason \"transfer\"; new assignee receives a notification; audit entry lead_transferred; U-BM-A (branch A) can no longer open L1 (404).",
  "trace": "ADM-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-062",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Admissions cannot transfer",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-ADM.",
  "steps": [
   "Look for Chuyển chi nhánh in the kebab.",
   "Call POST /v3/crm/customers/{L1}/transfer-school."
  ],
  "expected": "Action absent; API 403.",
  "trace": "01 §9 customers.transfer",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-063",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Sources settings page reads and writes the API",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Open /settings/sources; add \"TikTok\" with colour; rename to \"TikTok Ads\"; drag to reorder; deactivate \"Tờ rơi\".",
   "Reload the page in a fresh browser session."
  ],
  "expected": "All changes persist after reload (no localStorage); network shows POST/PATCH /v3/crm/sources and reorder call; deactivated \"Tờ rơi\" no longer in the lead form picker but still displayed on historical leads.",
  "trace": "ADM-08",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-064",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Channels settings page reads and writes the API",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Repeat the previous case on /settings/channels with \"Hotline\"."
  ],
  "expected": "Same persistence and picker behaviour for /v3/crm/channels.",
  "trace": "ADM-08",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-065",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Non-owner cannot manage sources",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-BM-A.",
  "steps": [
   "Open /settings/sources.",
   "Call POST /v3/crm/sources."
  ],
  "expected": "Page hidden or read-only (no add/edit controls); API 403.",
  "trace": "01 §9 config.manage owner only",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-066",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Won requires conversion or an explicit reason",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ADM on L1.",
  "steps": [
   "Click Thắng (Won) without converting."
  ],
  "expected": "Dialog offers \"Chuyển đổi ngay\" or \"Thắng không chuyển đổi\" with a required reason; choosing the latter with reason \"Học ở chi nhánh ngoài hệ thống\" sets status won and logs event lead_won_without_conversion.",
  "trace": "01 §6 rule 7",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-067",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Lead retention warning chip 30 days before anonymisation",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Seeded lead with last activity 23 months and 5 days ago, not won.",
  "steps": [
   "Open the lead in the table and detail."
  ],
  "expected": "Chip \"Sắp ẩn danh hoá\" (yellow) with the date; leads with recent activity have no chip.",
  "trace": "01 §6 rule 5, 12 §4 retention",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-068",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Stage automation only for trial and conversion",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Pipeline with trial_stage_id set.",
  "steps": [
   "Book a trial for a lead in stage Liên hệ mới.",
   "Log a call outcome Quan tâm on another lead."
  ],
  "expected": "The first lead moves to the trial stage; the second lead does not move.",
  "trace": "01 §6 rule 8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-069",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Pipeline settings expose trial stage and terminal statuses",
  "type": "functional",
  "priority": "P2",
  "preconditions": "As U-OWNER in the pipeline editor.",
  "steps": [
   "Open pipeline settings."
  ],
  "expected": "Fields Giai đoạn học thử, Trạng thái thành công, Trạng thái thất bại are editable selects; saved via PATCH /v3/crm/pipelines/{id}.",
  "trace": "01 §8 pipelines extend",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-070",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Kanban drag with stale cursor",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Two browsers as U-ADM and U-OWNER.",
  "steps": [
   "Browser 1 loads the Kanban.",
   "Browser 2 moves L1 to another stage.",
   "Browser 1 drags L1 (stale position) to a third stage."
  ],
  "expected": "Either the move succeeds with a refreshed position or a toast \"Dữ liệu đã thay đổi, tải lại\" appears; no duplicate card, no lost card.",
  "trace": "test seed 01 kanban stale cursor",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-071",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Bulk export xlsx of the lead table includes new columns",
  "type": "regression",
  "priority": "P2",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Click Xuất Excel on the table with the new columns visible."
  ],
  "expected": "Downloaded xlsx contains columns Học viên dự kiến, Học thử, Xếp lớp matching the screen; row count equals the filtered count.",
  "trace": "01 §2 export",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-072",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Import dedupe strategies still work",
  "type": "regression",
  "priority": "P1",
  "preconditions": "As U-OWNER with a 3-row xlsx including L1 phone.",
  "steps": [
   "Run the import wizard with strategy Bỏ qua, then Gộp, then Ghi đè."
  ],
  "expected": "Bỏ qua: 2 created, 1 skipped; Gộp: L1 blank fields filled; Ghi đè: L1 fields replaced; the error report lists the reason for each skipped row.",
  "trace": "test seed 01 import dedupe",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-073",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Lead detail tab \"Học thử & xếp lớp\"",
  "type": "functional",
  "priority": "P1",
  "preconditions": "L1 with one trial and one placement test.",
  "steps": [
   "Open the tab."
  ],
  "expected": "Two sections listing the trial booking (status, session, note) and the placement test (status, score, suggested level) with buttons Đặt lịch học thử and Tạo bài kiểm tra.",
  "trace": "01 §5 lead detail",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ADM-074",
  "module": "01-admissions-crm",
  "module_title": "Admissions CRM",
  "prd": "01",
  "title": "Analytics events fire for lead_created and lead_converted",
  "type": "functional",
  "priority": "P3",
  "preconditions": "Analytics stub enabled.",
  "steps": [
   "Create a lead via form; convert it."
  ],
  "expected": "Stub log contains lead_created {source, channel, has_prospect true, via form} and lead_converted {with_class, with_tuition, discount_pct_bucket, days_from_lead_created, trial_before}. No student personal data in payloads.",
  "trace": "01 §10, 12 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-001",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Student list header, tabs and counts",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A (branch A) on /students.",
  "steps": [
   "Read the header and tab bar."
  ],
  "expected": "h1 \"Học viên\"; buttons Nhập từ Excel and + Thêm học viên; tabs in order 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ỉ, each with a count; Đang học includes S1, S6, S7; Chờ xếp lớp includes S3; Bảo lưu S4; Sắp hết buổi S6 (2 left) and S1 is NOT included (5 left); Nợ học phí S7; Đã nghỉ S5.",
  "trace": "STU list §5",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-002",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Student list columns match the spec",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER with \"Tất cả chi nhánh\".",
  "steps": [
   "Read the column headers."
  ],
  "expected": "Mã HV, Họ tên, Lớp, Trạng thái, Phụ huynh, Buổi còn lại, Công nợ, Điểm danh 30 ngày, Chi nhánh, Ngày nhập học, Thao tác. With one branch selected the Chi nhánh column is hidden.",
  "trace": "02 §5 columns",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-003",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Row formatting rules",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "S1, S3, S6, S7 visible.",
  "steps": [
   "Inspect the rows."
  ],
  "expected": "S1: code HV-000123, name link with small text \"8 tuổi\", class chip \"Movers 2A\", chip Đang học green; S3: dashed chip \"Chưa xếp lớp\"; S6: Buổi còn lại \"2\" in red; S7: Công nợ \"3.000.000 đ\" in red; monthly-plan S2 shows \"-\" for Buổi còn lại; guardian phone click copies to clipboard with toast \"Đã sao chép\".",
  "trace": "02 §5 notes",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-004",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Row height 44 px and 50 rows per page",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "Tenant BIG with > 50 students in branch A.",
  "steps": [
   "Measure a row in devtools.",
   "Scroll to the pagination."
  ],
  "expected": "Row height 44 px; pagination shows 50 per page with total count; Cloud table header uppercase 12 px light grey.",
  "trace": "02 §5, Cloud theme §2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-005",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Search by name without accents, guardian phone, code, username",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Type \"nguyen an\" in search.",
   "Type \"0901234567\".",
   "Type \"HV-000128\".",
   "Type S1's FLYER username."
  ],
  "expected": "Each query returns the matching student within ~300 ms; the guardian-phone match highlights the phone cell; no client-side full-list filtering (network request per query with q=).",
  "trace": "STU-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-006",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Filters: branch, class, level, guardian consent, start date range",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Apply class = C1.",
   "Add consent = Chưa đồng ý Zalo.",
   "Add start date range covering only this month."
  ],
  "expected": "Result set narrows at each step and counts update; URL reflects filters; Xoá bộ lọc resets.",
  "trace": "02 §5 filters",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-007",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Empty state text",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "Owner of tenant EMPTY.",
  "steps": [
   "Open /students."
  ],
  "expected": "Exactly \"Chưa có học viên. Chuyển đổi từ khách hàng hoặc thêm trực tiếp.\" with a primary + Thêm học viên button.",
  "trace": "02 §5 empty state",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-008",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Column visibility persists via view-configs",
  "type": "functional",
  "priority": "P2",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Hide column Ngày nhập học; reload; log in from another browser."
  ],
  "expected": "Column stays hidden in both browsers; GET /v3/crm/view-configs/student_list returns the config.",
  "trace": "02 §5 view key student_list",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-009",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Bulk actions: Gửi thông báo, Xuất Excel, Chuyển lớp",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Select S1 and S6.",
   "Read the bulk bar.",
   "Click Xuất Excel."
  ],
  "expected": "Bulk bar shows \"2 đã chọn\" and the three actions; xlsx downloads with 2 rows and the visible columns.",
  "trace": "02 §5 bulk, students.export",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-010",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Teacher sees only students of own classes",
  "type": "security",
  "priority": "P0",
  "preconditions": "As U-TEACH (C1 at A, C3 at B).",
  "steps": [
   "Open /students.",
   "Open /students/<S7 id> (in C2)."
  ],
  "expected": "List contains only C1 and C3 students (S1, S6, S2, ...) and not S7; direct URL for S7 returns 404 page; API 404.",
  "trace": "02 §9 students.view own classes",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-011",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Admissions read scope shows limited fields",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-ADM.",
  "steps": [
   "Open /students/<S1 id>."
  ],
  "expected": "Only name, class, guardian visible; tabs Học phí, Tài liệu, Ghi chú, Nhật ký hidden; no Sửa button.",
  "trace": "02 §9 admissions read",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-012",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Create a walk-in student with all required fields",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Click + Thêm học viên.",
   "Fill Họ và tên \"Bùi Minh Khang\", Ngày sinh 02/02/2017, Giới tính Nam, Chi nhánh Quận 1, Trình độ Starters, Phụ huynh 1: \"Bùi Văn Long\", Bố, 0977000010, consent ticked; Tài khoản FLYER: tạo mới.",
   "Save."
  ],
  "expected": "Drawer closes; toast shows the generated username and a one-time password with \"Sao chép\" (shown once); student appears in tab Chờ xếp lớp with status Chờ xếp lớp; code is the next sequential HV-number; no lead is created in /crm/customers; guardian record created with E.164 phone +84977000010 displayed as 0977 000 010.",
  "trace": "STU-02, 12 §8 phone",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-013",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Student form validation matrix",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Create drawer open.",
  "steps": [
   "Enter name \"A\" (1 char) then 81 chars.",
   "Enter birthday giving age 2, then age 81.",
   "Leave Chi nhánh empty.",
   "Leave Phụ huynh phone empty for a child.",
   "Upload a 6 MB photo."
  ],
  "expected": "Inline errors: \"2–80 ký tự\"; \"Tuổi 3–80\"; \"Chọn chi nhánh\"; \"Bắt buộc\"; \"Tối đa 5 MB\". Save stays disabled until all are fixed.",
  "trace": "02 §5 student form validation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-014",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Adult learner (18+) does not need a guardian phone",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Create drawer.",
  "steps": [
   "Enter birthday giving age 20; leave guardian empty; choose Tài khoản FLYER liên kết có sẵn and pick S8's account or create new.",
   "Save."
  ],
  "expected": "Save allowed; guardian block shows \"Bản thân\" relationship auto-created with the student's own phone if entered; no \"Thiếu phụ huynh\" chip.",
  "trace": "02 §6 rule 3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-015",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Existing guardian suggested by phone in the form",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Create drawer.",
  "steps": [
   "Type 0901234567 in Phụ huynh 1 phone."
  ],
  "expected": "Suggestion \"Trần Thị Hoa (mẹ của Nguyễn An, Nguyễn Bình)\" appears; selecting it links G1 instead of creating a duplicate.",
  "trace": "02 §5 guardian suggested by phone",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-016",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Link an existing FLYER account by username or phone",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Create drawer.",
  "steps": [
   "Choose \"liên kết có sẵn\"; search by a known username."
  ],
  "expected": "Search returns the learner with display name; selecting fills the link; after save the profile rail shows that username.",
  "trace": "STU-02 Tài khoản FLYER radio",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-017",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Same learner at a second branch creates a second record",
  "type": "edge",
  "priority": "P2",
  "preconditions": "S1 exists at A.",
  "steps": [
   "Create a student at branch B linking S1's learner profile."
  ],
  "expected": "Save succeeds; a second record with its own code exists at B; both profiles show a note \"Cũng học tại Quận 1 / Thủ Đức\"; creating a third record at A with the same learner fails with a unique-constraint message.",
  "trace": "02 §6 rule 1",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-018",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Twins with same guardian and birthday allowed after warning",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Student \"Nguyễn An\" born 2018 with G1 exists.",
  "steps": [
   "Create \"Nguyễn Anh\" same birthday, same guardian."
  ],
  "expected": "Duplicate warning listing S1; admin confirms; second student created.",
  "trace": "02 §11 twins",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-019",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Profile header and right rail",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A on /students/<S1 id>.",
  "steps": [
   "Read header and right rail."
  ],
  "expected": "Header: avatar, \"Nguyễn An\", HV-000123, \"8 tuổi\", chip Đang học, branch Quận 1, class chip Movers 2A, chip \"Cần lưu ý\"; buttons Sửa · Xếp lớp · Thu học phí · Gửi tin · kebab with Bảo lưu, Nghỉ học, Chuyển chi nhánh, Ẩn danh hoá. Right rail: Phụ huynh chính (Trần Thị Hoa · Mẹ · 0901 234 567 · Zalo: Đã đồng ý), Số dư buổi 5, Công nợ 9.600.000 đ, Tài khoản FLYER username + last active.",
  "trace": "02 §5 profile",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-020",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Profile tabs in the specified order",
  "type": "functional",
  "priority": "P1",
  "preconditions": "On S1 profile.",
  "steps": [
   "Read the tab bar."
  ],
  "expected": "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ý; each tab is deep-linkable (#attendance etc.).",
  "trace": "02 §5 tabs",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-021",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Tổng quan tab shows personal info, consent summary and timeline",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 profile.",
  "steps": [
   "Open Tổng quan."
  ],
  "expected": "Birthday, gender, school, address, level, allergies (health note) shown; consent summary lists Zalo granted with date; timeline entries created, converted, enrolled in chronological order with actor.",
  "trace": "02 §5 Tổng quan",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-022",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Kebab actions hidden for roles without students.status",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-ADM and U-ACC.",
  "steps": [
   "Open S1 profile kebab."
  ],
  "expected": "Bảo lưu / Nghỉ học / Chuyển chi nhánh / Ẩn danh hoá absent; U-ACC sees Học phí tab and read-only profile.",
  "trace": "02 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-023",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Add a second guardian with flags",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 profile, tab Phụ huynh.",
  "steps": [
   "Click + Thêm phụ huynh; name \"Nguyễn Văn Tùng\", Bố, 0903000001, email, Zalo consent ticked, flags Liên hệ chính off, Người thanh toán ON.",
   "Save."
  ],
  "expected": "Two cards; G1 keeps Liên hệ chính; new guardian has Người thanh toán and G1 loses it (exactly one payer); guardian_added event fired.",
  "trace": "STU-03",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-024",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Exactly one primary contact and one payer",
  "type": "negative",
  "priority": "P1",
  "preconditions": "S1 with two guardians.",
  "steps": [
   "Untick Liên hệ chính on G1 without setting it on the other."
  ],
  "expected": "Save refused with \"Cần một liên hệ chính\"; setting it on the other guardian moves the flag.",
  "trace": "STU-03 exactly one",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-025",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Guardian shared by siblings is one record",
  "type": "functional",
  "priority": "P1",
  "preconditions": "G1 on S1 and S2.",
  "steps": [
   "Edit G1 email from S1 profile.",
   "Open S2 tab Phụ huynh."
  ],
  "expected": "S2 shows the updated email; GET /v3/school/guardians?phone=0901234567 returns one record.",
  "trace": "STU-03, 02 §7 unique (tenant, phone)",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-026",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Changing a guardian phone writes audit and re-verifies Zalo",
  "type": "functional",
  "priority": "P1",
  "preconditions": "G1 on S1.",
  "steps": [
   "Change phone to 0901234568; save.",
   "Open tab Nhật ký."
  ],
  "expected": "Audit row \"guardian.update\" with before/after phone; Zalo status on the card changes to \"Đang kiểm tra\" then a result (stub); an audit entry on both students sharing G1.",
  "trace": "STU-03, 12 §6 mandatory audit",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-027",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Guardian phone uniqueness: entering another guardian's phone merges",
  "type": "edge",
  "priority": "P2",
  "preconditions": "S6 has G2 (0912000002).",
  "steps": [
   "On S1 add a guardian with phone 0912000002."
  ],
  "expected": "Dialog \"Số điện thoại thuộc Lê Văn Nam\" offers \"Liên kết\" (links G2 to S1) or cancel; no duplicate record.",
  "trace": "02 §7, test seed 02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-028",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Gửi lại thông tin đăng nhập sends credentials to primary contact",
  "type": "functional",
  "priority": "P2",
  "preconditions": "S1 profile tab Phụ huynh; ZALO stub.",
  "steps": [
   "Click Gửi lại thông tin đăng nhập; confirm."
  ],
  "expected": "POST /students/{id}/credentials/resend called; message row type credentials to G1 via ZNS (template credentials) with status sent; toast \"Đã gửi\".",
  "trace": "02 §8 credentials/resend, 07 §6 credentials to primary contact",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-029",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Guardian phone displayed as 0xxx xxx xxx and stored E.164",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "G1.",
  "steps": [
   "Read the phone on the card.",
   "Read the API value."
  ],
  "expected": "Card shows \"0901 234 567\"; API returns \"+84901234567\".",
  "trace": "12 §8 phone",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-030",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Enroll a Waiting student from the profile",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S3 (Waiting) as U-ACAD-A; C1 has 11/12.",
  "steps": [
   "Click Xếp lớp; choose C1, start date next Tuesday, fee plan FP1.",
   "Confirm."
  ],
  "expected": "Enrollment created; S3 status Đang học; C1 shows 12/12 and status Đầy; S3 appears on rosters only for sessions on or after the start date; sessions before the start date do not count as absences in S3 attendance tab.",
  "trace": "STU-04",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-031",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Enroll into a full class offers waiting list",
  "type": "negative",
  "priority": "P1",
  "preconditions": "S3 Waiting; C2 12/12.",
  "steps": [
   "Xếp lớp → C2."
  ],
  "expected": "API 409 CLASS_FULL; UI offers \"Thêm vào danh sách chờ\"; accepting creates enrollment status waiting; S3 stays Chờ xếp lớp.",
  "trace": "CLS-07, 03 §8 enrollments 409",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-032",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Enroll a Withdrawn student re-activates the record",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S5 Withdrawn.",
  "steps": [
   "Xếp lớp S5 into C1 (after freeing a seat) or C4."
  ],
  "expected": "Status Đang học; same code HV-000127 (record reused); status history shows withdrawn → active.",
  "trace": "02 §6 rule 6",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-033",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Pause a student (Bảo lưu)",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 Active, package expiry E.",
  "steps": [
   "Kebab → Bảo lưu; from today, expected return in 30 days, reason \"Về quê\".",
   "Confirm."
  ],
  "expected": "Status Bảo lưu; roster of C1 sessions in the window no longer lists S1; package expiry becomes E + pause length (visible on Học phí tab after resume, \"frozen\" chip meanwhile); a reminder task dated 7 days before expected return appears in the today plan of the care owner; status history row with dates.",
  "trace": "STU-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-034",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Pause requires reason and dates",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Pause dialog.",
  "steps": [
   "Submit with empty reason; then with return date before start."
  ],
  "expected": "Inline errors; API 400 when called directly.",
  "trace": "STU-05, 02 §6 manual transitions need reason",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-035",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Pause longer than 90 days needs branch manager approval",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Pause S1 for 120 days."
  ],
  "expected": "Dialog shows \"Cần quản lý chi nhánh duyệt\"; status Bảo lưu with an approval pending marker; U-BM-A gets a notification; as U-BM-A the same request completes without approval.",
  "trace": "02 §6 rule 5",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-036",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Resume asks for the class and re-activates",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S4 Paused.",
  "steps": [
   "Kebab → Tiếp tục học; choose class C1 (or new).",
   "Confirm."
  ],
  "expected": "Status Đang học; enrollment in C1 resumes from the resume date; package expiry shows the extension (original + paused days).",
  "trace": "STU-05 resume, 02 §11 pause overlapping expiry",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-037",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Pausing a Waiting student is an invalid transition",
  "type": "negative",
  "priority": "P1",
  "preconditions": "S3 Waiting.",
  "steps": [
   "Call POST /students/{S3}/status {to: paused, ...}."
  ],
  "expected": "HTTP 409 INVALID_TRANSITION; UI does not offer Bảo lưu for a Waiting student.",
  "trace": "02 §3 lifecycle, 02 §8 409",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-038",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Withdraw with reason shows refund estimate",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 Active with 5 remaining sessions on FP3 (unit price 250,000).",
  "steps": [
   "Kebab → Nghỉ học; date today; reason select \"chi phí\"; confirm."
  ],
  "expected": "All enrollments end today; status Đã nghỉ; dialog result shows \"Buổi chưa dùng: 5 · Ước tính hoàn: 1.250.000 đ\" (minus admin fee if policy) with link \"Tạo phiếu hoàn\"; churn report lists S1 with reason \"chi phí\".",
  "trace": "STU-06",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-039",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Withdraw reason list and free text",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Withdraw dialog.",
  "steps": [
   "Open the reason select."
  ],
  "expected": "Options exactly: chi phí, chuyển nhà, không hài lòng, hoàn thành mục tiêu, khác; choosing khác requires free text.",
  "trace": "STU-06",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-040",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Withdrawn student is hidden from rosters and pickers",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S5 Withdrawn.",
  "steps": [
   "Open C1 roster; open the attendance sheet of the next session; open the enrollment picker."
  ],
  "expected": "S5 absent from roster and sheet; picker lists S5 with chip Đã nghỉ (re-enrollment allowed) but not as default.",
  "trace": "02 §6 rule 6",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-041",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Transfer a student to another branch with package balance",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-BM-A? (branch manager needs branch B scope; use U-OWNER). S1 at A with 5 sessions.",
  "steps": [
   "Kebab → Chuyển chi nhánh; to Thủ Đức, effective next Monday, target class C3, note.",
   "Confirm."
  ],
  "expected": "A enrollment ends; a new B record exists with the same learner profile and code convention; guardians shared; balance 5 at B and 0 at A; Học phí tab at A shows entry \"chuyển đi\", at B \"chuyển đến\"; timeline entry on both profiles; API returns new_student_id.",
  "trace": "STU-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-042",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Academic admin cannot transfer",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Look for Chuyển chi nhánh; call POST /students/{id}/transfer."
  ],
  "expected": "Action absent; API 403.",
  "trace": "02 §9 students.transfer",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-043",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Học tập tab shows FLYER data",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 with FLYER activity in the stub.",
  "steps": [
   "Open tab Học tập."
  ],
  "expected": "Sections: study time this month, assignments assigned/completed, latest scores per skill (4), placement result, latest exam entries; values match the stub /v3/parent/learner/{id}/report.",
  "trace": "STU-08",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-044",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Học tập empty state",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "A student whose learner has no FLYER usage.",
  "steps": [
   "Open tab Học tập."
  ],
  "expected": "Exactly \"Chưa có dữ liệu học tập\"; no zeros rendered as fake data.",
  "trace": "STU-08 empty",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-045",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Learner deleted on FLYER side",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Stub returns 404 for the learner.",
  "steps": [
   "Open the profile."
  ],
  "expected": "Right rail shows chip \"Tài khoản đã xoá\"; Học tập tab empty; the rest of the profile works.",
  "trace": "02 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-046",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Teacher can view learning data only for own-class students",
  "type": "security",
  "priority": "P2",
  "preconditions": "As U-TEACH.",
  "steps": [
   "Open S1 Học tập tab.",
   "Call GET learner report for S7."
  ],
  "expected": "S1 works; S7 returns 404.",
  "trace": "02 §9 students.learning.view",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-047",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Upload a document with type",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 profile tab Tài liệu as U-ACAD-A.",
  "steps": [
   "Upload birth-cert.pdf (2 MB), type Giấy khai sinh.",
   "Click the file link."
  ],
  "expected": "Row with type, name, uploader, time; link is a signed URL that expires (opening after 15 min gives 403); the file is not reachable without the signature.",
  "trace": "STU-09, 12 §3 uploads",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-048",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Reject disallowed file types and size",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Tab Tài liệu.",
  "steps": [
   "Upload a .exe; upload a 25 MB pdf."
  ],
  "expected": "Errors \"Định dạng không hỗ trợ (pdf, jpg, png, xlsx, docx)\" and \"Tối đa 20 MB\".",
  "trace": "12 §3 file uploads",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-049",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Flagged note appears on roster and attendance sheet for teachers",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 note \"Dị ứng đậu phộng\" flagged Cần lưu ý.",
  "steps": [
   "As U-TEACH open the C1 attendance sheet and the class roster."
  ],
  "expected": "S1 row shows \"Cần lưu ý: dị ứng đậu phộng\"; a non-flagged staff-only note is not visible to the teacher anywhere.",
  "trace": "STU-09, 02 §6 rule 8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-050",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Note visibility staff vs teachers",
  "type": "security",
  "priority": "P1",
  "preconditions": "A note with visibility staff on S1.",
  "steps": [
   "As U-TEACH call GET /students/{S1}/notes."
  ],
  "expected": "Response excludes staff-only notes; includes flagged notes only.",
  "trace": "02 §6 rule 8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-051",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Teacher can add notes but not edit profile",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-TEACH on S1.",
  "steps": [
   "Add a note.",
   "Try PATCH /students/{S1} display_name."
  ],
  "expected": "Note saved with author and time; PATCH 403; no Sửa button.",
  "trace": "02 §9 students.edit teacher notes only",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-052",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Anonymise a withdrawn student",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER on S5 (Withdrawn) with a document and a paid invoice.",
  "steps": [
   "Kebab → Ẩn danh hoá; enter request reference \"REQ-2026-001\"; confirm."
  ],
  "expected": "Name becomes \"Học viên đã xoá\", phone/email of the guardian null (if not shared), documents deleted; the invoice list still shows the invoice with the anonymised name and amounts intact; audit entry student.anonymise with the reference; event student_anonymised.",
  "trace": "STU-10, 12 §4 deletion",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-053",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Anonymise refused for Active student and for non-owner",
  "type": "negative",
  "priority": "P1",
  "preconditions": "S1 Active; U-BM-A.",
  "steps": [
   "As U-OWNER try on S1.",
   "As U-BM-A try on S5."
  ],
  "expected": "S1: action disabled with tooltip \"Chỉ áp dụng cho học viên đã nghỉ\"; API 409. U-BM-A: action absent; API 403.",
  "trace": "STU-10 Given, 02 §9 students.anonymise owner only",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-054",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Student code is tenant-wide sequential and never reused",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Note the highest code.",
  "steps": [
   "Create a student at branch A, then one at branch B.",
   "Delete nothing; anonymise one; create another."
  ],
  "expected": "Codes increase by one across branches; the anonymised code is not reissued.",
  "trace": "02 §6 rule 2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-055",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Import creates students without guardians with warning chip",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Import of a student row lacking g1_phone for a child (warning tolerated by import for age ≥ 18 only; for a child this is an error). Use an adult row without guardian and a child row with \"Bỏ qua dòng lỗi\".",
  "steps": [
   "Run the import and open the list."
  ],
  "expected": "Adult imported without chip; a child imported only when the import explicitly allows; such a record shows chip \"Thiếu phụ huynh\" and the list tab filter \"Thiếu phụ huynh\" lists it.",
  "trace": "02 §11 import without guardians",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-056",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Student attendance tab month view",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 with SES-PAST marked.",
  "steps": [
   "Open tab Điểm danh."
  ],
  "expected": "Month calendar with a chip per session (Có mặt green etc.), rates for 30 and 90 days, absences table (date, class, status, reason, make-up booked), button + Thêm phép.",
  "trace": "02 §5 Điểm danh tab, 04 §4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-057",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Student Học phí tab",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1.",
  "steps": [
   "Open tab Học phí."
  ],
  "expected": "Package balance card (5 / 24, expiry date), tuition list with status chips (INV-1 Chưa thanh toán, INV-4 Đã thanh toán), receipts list with PT numbers, button Tạo hoá đơn.",
  "trace": "02 §5 Học phí tab",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-058",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Edit drawer prefilled and saves partial changes",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1.",
  "steps": [
   "Click Sửa; change Địa chỉ; save."
  ],
  "expected": "PATCH sends only changed fields; toast \"Đã lưu\"; Tổng quan shows the new address; audit entry student.update with before/after.",
  "trace": "02 §8 PATCH partial",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-059",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Profile page in en locale",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "As U-ACAD-A in en.",
  "steps": [
   "Open S1 profile."
  ],
  "expected": "Tabs Overview · Guardians · Classes & history · Attendance · Tuition · Learning · Documents · Notes · Audit log; status chip \"Active\"; money \"9,600,000 đ\" or \"VND 9,600,000\" consistently.",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-060",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Profile at 1024 px and 390 px",
  "type": "ui-ux",
  "priority": "P3",
  "preconditions": "S1.",
  "steps": [
   "View at 1024 and 390."
  ],
  "expected": "1024: right rail moves below the tabs or stays with no horizontal overflow. 390: header stacks; tabs scroll horizontally; console minimum notice acceptable.",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STU-061",
  "module": "02-students-sis",
  "module_title": "Students (SIS)",
  "prd": "02",
  "title": "Keyboard: student list rows and actions reachable",
  "type": "a11y",
  "priority": "P2",
  "preconditions": "/students.",
  "steps": [
   "Tab from the search field through the table."
  ],
  "expected": "Focus reaches the tab bar, filters, each row link and action buttons in order; Enter opens the profile; focus ring visible.",
  "trace": "12 §9 WCAG",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-001",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Create a course with extension fields",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A on /settings/courses.",
  "steps": [
   "Click + Thêm khoá học: name \"Flyers 1\", level Flyers, age 9–11, 48 lessons, duration 90 min, default capacity 12, price 9,600,000, pricing ONE_TIME.",
   "Save.",
   "Open /classes/new and the Khoá học select."
  ],
  "expected": "Course saved (PUT/POST to the existing course endpoint with extension fields); appears in the class form; selecting it fills level, duration 90, capacity 12, lessons 48.",
  "trace": "CLS-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-002",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Course validation",
  "type": "negative",
  "priority": "P2",
  "preconditions": "Course form.",
  "steps": [
   "Duration 0; capacity 61; age_from > age_to; currency other than VND."
  ],
  "expected": "Each rejected inline; currency locked to VND.",
  "trace": "CLS-01, 05 §11 currency VND",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-003",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class form step 1 fields and auto values",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A on /classes/new.",
  "steps": [
   "Choose Chi nhánh Quận 1, Khoá học Movers 2.",
   "Observe Tên lớp, Mã lớp.",
   "Pick weekdays T3 and T5 with 18:00.",
   "Pick teacher T2, room P.202, capacity 12, trial seats 2, Khai giảng next Tuesday, Kết thúc theo số buổi (48)."
  ],
  "expected": "Tên lớp auto-suggests \"Movers 2 · T3/T5 18:00\"; Mã lớp auto \"MOV-<YYMM>-02\" editable; per-weekday time-to fills 19:30 from duration 90; holidays list preloaded from tenant holidays.",
  "trace": "CLS-02, 03 §5 form",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-004",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class form validation",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Class form.",
  "steps": [
   "Leave weekday empty; set from 19:00 to 18:00; capacity 0 and 61; trial seats 6; duplicate name of C1 in branch A; leave room empty."
  ],
  "expected": "Errors: \"Chọn ít nhất một ngày\"; \"Giờ kết thúc phải sau giờ bắt đầu\"; \"1–60\"; \"Tối đa 5\"; \"Tên lớp đã tồn tại\"; \"Chọn phòng\" (unless the virtual room is chosen).",
  "trace": "03 §5 validation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-005",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Step 2 preview lists generated sessions and conflicts",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Step 1 completed with 48 sessions.",
  "steps": [
   "Click Tiếp."
  ],
  "expected": "Preview shows 48 rows with seq, date, time, room, teacher; holidays skipped and the end date extended accordingly; button label exactly \"Tạo lớp và 48 buổi\"; dry_run=true request seen in the network log.",
  "trace": "CLS-02, 03 §8 dry_run",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-006",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Create class generates sessions and appears in the list",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Preview shown.",
  "steps": [
   "Click Tạo lớp và 48 buổi."
  ],
  "expected": "Redirect to /classes/:id; tab Buổi học has 48 rows numbered 1..48; /classes tab Sắp khai giảng lists it with status Sắp mở; a ClassSchedule is created only when a FLYER program was chosen.",
  "trace": "CLS-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-007",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Pattern with two sessions on the same weekday",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Class form.",
  "steps": [
   "Add T7 08:00–09:30 and T7 14:00–15:30."
  ],
  "expected": "Both rows accepted; preview shows two sessions per Saturday.",
  "trace": "03 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-008",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class spanning Tết skips holidays and extends end date",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Tenant holidays include a 7-day Tết block within the class range.",
  "steps": [
   "Create a class across that block."
  ],
  "expected": "No sessions on holiday dates; end date extends; preview footer shows \"Kết thúc dự kiến: <date>\".",
  "trace": "03 §11 Tết",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-009",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Session generation performance",
  "type": "perf",
  "priority": "P2",
  "preconditions": "Class with 200 sessions.",
  "steps": [
   "Create the class and time the dry_run and commit."
  ],
  "expected": "Preview and creation each complete in ≤ 2 s.",
  "trace": "12 §2 timetable generation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-010",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Room conflict shown inline with override for permitted roles",
  "type": "functional",
  "priority": "P0",
  "preconditions": "C1 uses P.201 T3/T5 18:00–19:30.",
  "steps": [
   "As U-ACAD-A create a class in P.201 on T3 18:30–20:00.",
   "Observe preview.",
   "Repeat as U-BM-A."
  ],
  "expected": "Preview rows carry a red chip \"Trùng phòng: Movers 2A 18:00–19:30\"; U-ACAD-A has no \"Vẫn lưu\" (no classes.override_conflict) and cannot submit; U-BM-A sees \"Vẫn lưu\" and can create; API returns 409 CONFLICT with conflicts[] until override_conflict=true.",
  "trace": "CLS-03",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-011",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Teacher conflict is checked across branches",
  "type": "functional",
  "priority": "P0",
  "preconditions": "T1 teaches C3 at branch B T2/T4 17:00–18:30.",
  "steps": [
   "Create a class at branch A with teacher T1 on T2 17:30–19:00 in P.202."
  ],
  "expected": "Conflict chip \"Trùng giáo viên: Movers 1C (Thủ Đức)\"; 409 CONFLICT.",
  "trace": "CLS-03, 03 §6 teacher conflict any branch",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-012",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Buffer between sessions tenant setting",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Set buffer 15 min in settings.",
  "steps": [
   "Create sessions in P.201 ending 19:30 and starting 19:40."
  ],
  "expected": "Conflict reported; with buffer 0 no conflict.",
  "trace": "03 §6 buffer",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-013",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Cancelled sessions do not conflict",
  "type": "edge",
  "priority": "P2",
  "preconditions": "A cancelled C1 session on date D.",
  "steps": [
   "Create a one-off session in P.201 at the same time on D."
  ],
  "expected": "No conflict.",
  "trace": "03 §6 both not cancelled",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-014",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Edit one session marks it adjusted",
  "type": "functional",
  "priority": "P0",
  "preconditions": "C1 planned session next week.",
  "steps": [
   "Open tab Buổi học, edit the session: move to next Wednesday 18:00, room P.202.",
   "Save."
  ],
  "expected": "Only that row changes; chip \"Đã điều chỉnh\"; other sessions untouched; guardians of enrolled students receive a schedule_change message if the notification policy is enabled (status Chờ duyệt template → fallback push/email).",
  "trace": "CLS-04",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-015",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Apply to following regenerates future planned sessions only",
  "type": "functional",
  "priority": "P0",
  "preconditions": "C1 with 20 held sessions.",
  "steps": [
   "Edit a future session, change time to 18:30–20:00 with \"Áp dụng cho các buổi sau\"."
  ],
  "expected": "All future planned sessions move to 18:30; the 20 held sessions unchanged; numbering preserved by date order; audit entry.",
  "trace": "CLS-04, 03 §6 regeneration",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-016",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Session edit conflict on save",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Another class occupies P.202 at the target time.",
  "steps": [
   "Move a C1 session into that slot."
  ],
  "expected": "409 CONFLICT inline list; Vẫn lưu only for override roles.",
  "trace": "CLS-03",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-017",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Concurrent session edit returns STALE",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Two admins open the same session drawer.",
  "steps": [
   "Admin 1 saves a room change.",
   "Admin 2 saves a time change with the old updated_at."
  ],
  "expected": "Admin 2 receives 409 STALE and a toast \"Buổi học đã được thay đổi, tải lại\"; no silent overwrite.",
  "trace": "03 §11 optimistic concurrency",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-018",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Cancel a session with make-up appended",
  "type": "functional",
  "priority": "P0",
  "preconditions": "C1 planned session 3 days ahead.",
  "steps": [
   "Cancel with reason \"GV ốm\", choose \"Thêm buổi bù cuối khoá\", notify on."
  ],
  "expected": "Status Đã huỷ; a new session appended after the last one with chip Học bù (is_makeup) at the same weekday/time; no package sessions consumed; guardians receive session_cancelled; total non-cancelled count stays 48.",
  "trace": "CLS-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-019",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Late cancellation within 24 h marks cancelled_late",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Session starting in 10 hours.",
  "steps": [
   "Cancel it, \"Không bù\"."
  ],
  "expected": "Status cancelled_late (chip \"Huỷ muộn\"); visible for PRD 06 pay rules; no make-up added.",
  "trace": "CLS-05, 03 §6 threshold",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-020",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Cancelling a session with a saved sheet is blocked",
  "type": "negative",
  "priority": "P1",
  "preconditions": "SES-PAST (sheet saved).",
  "steps": [
   "Try to cancel it."
  ],
  "expected": "Blocked with \"Xoá phiếu điểm danh trước\" (admin) and the delete action requires attendance.delete_sheet.",
  "trace": "04 §5 rule 8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-021",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Assign a substitute",
  "type": "functional",
  "priority": "P0",
  "preconditions": "SES-TODAY.",
  "steps": [
   "Edit the session, set Dạy thay = T2. Save.",
   "As U-TEACH2 open /teach/today."
  ],
  "expected": "Session row shows T1 (main) and T2 (dạy thay); T2 sees the session in /teach/today and can open the sheet; T1 still sees it with a substitute marker.",
  "trace": "CLS-06",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-022",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Teacher can only request a substitute",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-TEACH on SES-TODAY.",
  "steps": [
   "Open the session; look for substitute assignment.",
   "Call PATCH /sessions/{id} substitute_staff_id."
  ],
  "expected": "UI offers \"Yêu cầu dạy thay\" (creates a request/notification) but no direct assignment; PATCH returns 403.",
  "trace": "03 §9 sessions.manage teacher request only",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-023",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Add student to class up to capacity then waiting list",
  "type": "functional",
  "priority": "P0",
  "preconditions": "C1 11/12; S3 Waiting; another Waiting student S10.",
  "steps": [
   "Tab Học viên → + Thêm học viên → S3, start date today.",
   "Repeat with S10."
  ],
  "expected": "After S3: 12/12, class chip Đầy, list tab Đầy includes C1. S10: dialog 409 CLASS_FULL offers waiting list; S10 appears in the Danh sách chờ section with position 1.",
  "trace": "CLS-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-024",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Roster table columns and level-match hint",
  "type": "functional",
  "priority": "P1",
  "preconditions": "C1 tab Học viên.",
  "steps": [
   "Read the roster and open the add dialog."
  ],
  "expected": "Columns: name, guardian phone, start date, status, sessions attended/total, balance, flags (Cần lưu ý); actions Chuyển lớp, Kết thúc, Gửi tin; the add dialog searches branch students and shows a level match chip (Movers → \"Phù hợp\").",
  "trace": "03 §5 Học viên tab",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-025",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Transfer class ends and starts on the same day",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 in C1; C4 has seats.",
  "steps": [
   "Chuyển lớp S1 → C4 effective today."
  ],
  "expected": "C1 enrollment ends today; C4 enrollment starts today; S1 on today's C1 roster (end ≥ date) and on C4 rosters from today; Lớp & lịch sử shows both rows.",
  "trace": "CLS-07 transfer, 04 §10 transferred out on the date",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-026",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Ending an enrollment early asks for reason",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S6 in C1.",
  "steps": [
   "Click Kết thúc on S6 with end date today."
  ],
  "expected": "Reason dialog required; S6 with no other enrollment becomes Đã nghỉ per lifecycle; enrollment status ended with end_reason.",
  "trace": "CLS-07, 02 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-027",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Capacity counts trials and make-ups per session",
  "type": "edge",
  "priority": "P1",
  "preconditions": "C1 12/12 with a make-up booking on session X.",
  "steps": [
   "Open session X roster."
  ],
  "expected": "Roster shows 12 enrolled + 1 Học bù; trial seats counted separately (\"Học thử 1/2\"); attendance sheet lists all 13.",
  "trace": "03 §6 capacity",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-028",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Waiting list promotion when a seat frees",
  "type": "functional",
  "priority": "P2",
  "preconditions": "C1 full with S10 waiting.",
  "steps": [
   "End S6's enrollment."
  ],
  "expected": "Admin gets a prompt/notification \"Có chỗ trống: Movers 2A · Danh sách chờ: Trần S10\"; S10 is not auto-enrolled.",
  "trace": "03 §5 waiting list section",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-029",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Book a make-up into another class of the same level",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 has an excused absence on SES-PAST; tenant allow make-up; class C2 same level with a free seat next week.",
  "steps": [
   "On S1 attendance tab click Học bù on the absence; pick C2 session; consumes_package per policy."
  ],
  "expected": "makeup_booking created; C2 session roster shows S1 with chip Học bù; attending does not consume a package session when policy says so; S1 absences table shows \"Đã đặt học bù\".",
  "trace": "CLS-08",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-030",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Make-up as a one-off session",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Same as above.",
  "steps": [
   "Choose \"Tạo buổi bù riêng\": date, time, room, teacher."
  ],
  "expected": "A one-off session (is_makeup) is created with conflict check; booking created.",
  "trace": "CLS-08, 03 §8 POST sessions one-off",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-031",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Make-up blocked when policy disallows",
  "type": "negative",
  "priority": "P2",
  "preconditions": "Tenant allow_makeup false or class allow_makeup false.",
  "steps": [
   "Try to book."
  ],
  "expected": "Button hidden; API 409 or 403 with message.",
  "trace": "CLS-08 Given policy",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-032",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Timetable week grid by room",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A, /timetable?view=room&week=<this week>.",
  "steps": [
   "Open the page."
  ],
  "expected": "Columns P.201, P.202, Trực tuyến; rows 07:00–22:00 in 30-min steps; C1 block on Tue/Thu 18:00–19:30 with class name, T1, \"11/12\"; block colour by level; clicking opens the session drawer.",
  "trace": "CLS-09",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-033",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Timetable views by teacher and by class, date navigation",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Timetable.",
  "steps": [
   "Switch to By teacher, then By class; move to next week and back."
  ],
  "expected": "Columns change to teachers (T1, T2...) then classes; URL week param updates (2026-W38 etc.); filters branch and level apply.",
  "trace": "CLS-09",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-034",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Drag a block to move with conflict check on drop",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Desktop timetable.",
  "steps": [
   "Drag the C1 Tuesday block to Wednesday 18:00 in P.202.",
   "Drag another block onto an occupied slot."
  ],
  "expected": "First drop asks for confirmation and updates the session (marked adjusted); second drop shows the conflict and reverts unless overridden.",
  "trace": "03 §5 timetable drag",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-035",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Print produces A4 landscape per room",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "Timetable by room.",
  "steps": [
   "Click In and inspect the print preview."
  ],
  "expected": "One A4 landscape page per room with header (branch, week), grid legible; no sidebar or top bar printed.",
  "trace": "CLS-09 print",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-036",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "iCal export for teacher view is a signed link",
  "type": "security",
  "priority": "P2",
  "preconditions": "Timetable by teacher.",
  "steps": [
   "Click Xuất iCal for T1; open the URL; modify one character of the signature."
  ],
  "expected": "Valid URL returns text/calendar with the sessions; tampered URL returns 401/403; the link can be revoked from the staff profile.",
  "trace": "03 §8 timetable.ics signed, test seed 03",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-037",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Timetable day view on mobile",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "390 px.",
  "steps": [
   "Open /timetable."
  ],
  "expected": "A day view (not the week grid) with a date stepper; blocks tappable; no horizontal scroll.",
  "trace": "03 §5 day view mobile",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-038",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Teacher timetable scope",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-TEACH.",
  "steps": [
   "Open /timetable."
  ],
  "expected": "Only T1's sessions (C1 and C3) appear; view switcher limited to own view; other teachers not listed.",
  "trace": "03 §9 timetable.view own",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-039",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Close a class",
  "type": "functional",
  "priority": "P0",
  "preconditions": "C1 as U-ACAD-A; S1 has only the C1 enrollment.",
  "steps": [
   "Kebab → Kết thúc lớp; confirm."
  ],
  "expected": "Class status Đã kết thúc; enrollments ended; S1 status Hoàn thành; /classes tab Đã kết thúc lists C1; planned future sessions cancelled or removed; no attendance sheets deleted.",
  "trace": "CLS-10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-040",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class auto-completes after the last session is held",
  "type": "functional",
  "priority": "P2",
  "preconditions": "A class whose last session is marked today.",
  "steps": [
   "Save attendance on the last session."
  ],
  "expected": "Class status becomes completed automatically (or on the nightly job); students with no other enrollment become Hoàn thành.",
  "trace": "CLS-10 Given last session held",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-041",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class list tabs, columns and status chips",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER, branch A.",
  "steps": [
   "Open /classes; read tabs and rows for C1, C2, C4, C5."
  ],
  "expected": "Tabs Đang học · Sắp khai giảng · Đầy · Đã kết thúc · Tất cả; columns Mã lớp, Tên lớp (link + level chip), Lịch (\"T3, T5 · 18:00–19:30\"), Phòng, Giáo viên (avatar + name), Sĩ số (\"11/12\" with bar; C2 \"12/12\" red with \"Chờ: 1\"), Tiến độ (\"20/48\"), Khai giảng · Kết thúc, Trạng thái chips Sắp mở (blue) / Đang học (green) / Đầy (yellow) / Đã kết thúc (grey), Thao tác Xem lịch · Thêm học viên · Điểm danh hôm nay.",
  "trace": "03 §5 class list",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-042",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class list filters",
  "type": "functional",
  "priority": "P1",
  "preconditions": "/classes.",
  "steps": [
   "Filter teacher T1; weekday T3; time band tối; has seats."
  ],
  "expected": "Only C1 remains (C2 has no seats); clearing restores.",
  "trace": "03 §5 filters",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-043",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class list empty state",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "Tenant EMPTY.",
  "steps": [
   "Open /classes."
  ],
  "expected": "Exactly \"Chưa có lớp. Tạo khoá học trước, rồi mở lớp từ khoá học.\" with buttons + Tạo lớp and a link to courses.",
  "trace": "03 §5 empty",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-044",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class detail header and right rail per Cloud lop-hoc frame",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "C1.",
  "steps": [
   "Open /classes/<C1 id>."
  ],
  "expected": "Breadcrumb; name; subtitle \"T3 & T5, 18:00 · 11 học viên · Movers\"; buttons Sửa · Giao bài · kebab (Nhân bản lớp, Kết thúc lớp); tabs Tổng quan · Học viên · Buổi học · Điểm danh · Bài tập · Học phí · Cài đặt; right rail Thông tin lớp with code + copy icon, teacher, room, capacity, course, program, fill rate bar, next session.",
  "trace": "03 §5 class detail",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-045",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Buổi học tab toolbar and per-row actions",
  "type": "functional",
  "priority": "P1",
  "preconditions": "C1.",
  "steps": [
   "Open tab Buổi học."
  ],
  "expected": "Toolbar Tạo buổi, Tạo lại lịch, Nghỉ lễ; columns No., date, time, room, teacher, substitute, status, attendance marked, note; row actions edit, cancel, substitute; held rows are not editable for date/time.",
  "trace": "03 §5 Buổi học",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-046",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Regenerate never touches held or cancelled sessions",
  "type": "functional",
  "priority": "P0",
  "preconditions": "C1 with 20 held, 1 cancelled.",
  "steps": [
   "Tạo lại lịch with a new pattern from today."
  ],
  "expected": "Held and cancelled rows identical before/after (ids and timestamps); planned rows regenerated; numbering recalculated among non-cancelled by date.",
  "trace": "03 §6 rules 1–2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-047",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Nhân bản lớp clones settings without students",
  "type": "functional",
  "priority": "P2",
  "preconditions": "C1.",
  "steps": [
   "Kebab → Nhân bản lớp."
  ],
  "expected": "Class form opens prefilled with course, pattern, room, teacher, capacity; name suffix changed; no enrollments copied.",
  "trace": "03 §5 kebab",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-048",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Rooms settings: unique per branch, deactivate blocked with future sessions",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A on /settings/rooms.",
  "steps": [
   "Add room \"P.201\" in Quận 1 (duplicate).",
   "Deactivate P.201 (has future sessions).",
   "Add \"P.203\" cap 10 and deactivate it."
  ],
  "expected": "Duplicate rejected \"Tên phòng đã tồn tại\"; deactivating P.201 blocked with \"Còn N buổi học sắp tới, chuyển phòng trước\"; P.203 created then deactivated successfully.",
  "trace": "03 §5 rooms, 03 §11 room deactivated",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-049",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Virtual room \"Trực tuyến\" allowed for online classes",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Class form.",
  "steps": [
   "Choose room Trực tuyến."
  ],
  "expected": "Class saves; no room conflicts computed for the virtual room (or conflicts only for teacher).",
  "trace": "03 §12 open question proposed",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-050",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Levels catalog with score bands drives placement suggestion",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER /settings/levels.",
  "steps": [
   "Read the seeded list.",
   "Change Movers band to 60–79."
  ],
  "expected": "Seeded: Pre-A1, A1, A2, B1, B2, C1 and Starters, Movers, Flyers, KET, PET with bands; after edit, a placement result of 72 suggests Movers.",
  "trace": "03 §6 level mapping",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-051",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Teacher removed from branch with future sessions",
  "type": "edge",
  "priority": "P2",
  "preconditions": "T2 assigned to future sessions; remove T2 from branch A.",
  "steps": [
   "Perform the removal in staff settings."
  ],
  "expected": "A list of affected sessions is shown for reassignment; sessions keep T2 until changed.",
  "trace": "03 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-052",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Holiday list management",
  "type": "functional",
  "priority": "P2",
  "preconditions": "/settings/tenant holidays.",
  "steps": [
   "Add a holiday next month; open the class form."
  ],
  "expected": "Holiday preloaded in Ngày nghỉ; new classes skip it; existing planned sessions are not auto-moved (admin uses Nghỉ lễ tool).",
  "trace": "03 §8 holidays",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-053",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Timezone at month boundary",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Session on the 30th at 23:00 local.",
  "steps": [
   "Create it and view in timetable and API."
  ],
  "expected": "Displayed on the 30th; API returns timestamptz with +07:00; not shifted to the next day.",
  "trace": "03 §11 time zone, 06 §11 month boundary",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-054",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class pages in en locale",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "en locale.",
  "steps": [
   "Open /classes and /classes/<C1>."
  ],
  "expected": "Tabs and chips translated (Open, Full, Ended, Upcoming); schedule text \"Tue, Thu · 18:00–19:30\".",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-CLS-055",
  "module": "03-classes-and-scheduling",
  "module_title": "Classes and scheduling",
  "prd": "03",
  "title": "Class form keyboard navigation and focus",
  "type": "a11y",
  "priority": "P2",
  "preconditions": "Class form.",
  "steps": [
   "Tab through weekday chips and time fields; use arrow keys in the room select."
  ],
  "expected": "Chips toggle with Space; all fields reachable; error text linked via aria-describedby.",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-001",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Teacher today list on mobile",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-TEACH at 390 px; SES-TODAY planned; SES-YEST unmarked.",
  "steps": [
   "Open /teach/today."
  ],
  "expected": "SES-YEST pinned on top in amber with chip \"Chưa điểm danh\"; SES-TODAY listed with time 18:00–19:30, \"Movers 2A\", P.201, roster count 11, chip Chưa điểm danh; bottom tab bar Hôm nay · Lịch tuần · Lớp của tôi · Tài khoản; no sidebar.",
  "trace": "ATT §4 /teach/today",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-002",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Open the sheet: all rostered students default to Có mặt",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-TEACH within 30 min before SES-TODAY start or after.",
  "steps": [
   "Tap SES-TODAY."
  ],
  "expected": "Header \"Movers 2A · T3 dd/MM · 18:00–19:30 · P.201\" and counters \"Có mặt 11 · Muộn 0 · Vắng 0 · Có phép 0\"; every student row shows the four-segment control with Có mặt selected; S1 row shows \"Cần lưu ý: dị ứng đậu phộng\"; S6 row shows \"Số dư: 2 buổi\" flag; save button sticky at bottom.",
  "trace": "ATT-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-003",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Sheet cannot be opened more than 30 min before start",
  "type": "negative",
  "priority": "P2",
  "preconditions": "As U-TEACH, 2 hours before a session.",
  "steps": [
   "Tap the session."
  ],
  "expected": "Message \"Mở phiếu từ 30 phút trước giờ học\" and no editable controls (read-only roster allowed).",
  "trace": "04 §10 pre-mark from 30 min before",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-004",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Mark exceptions and save in one request",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Sheet open.",
  "steps": [
   "Tap Muộn on S1 → choose preset 10.",
   "Tap Vắng on S6.",
   "Type class note \"Ôn unit 3\".",
   "Tap Lưu điểm danh with Gửi thông báo PH ticked."
  ],
  "expected": "Exactly one PUT /v3/school/sessions/{id}/attendance with records[] (12 entries incl. trial if any), note, notify_guardians true; counters read \"Có mặt 9 · Muộn 1 · Vắng 1 · Có phép 0\" before save (11 enrolled); after save a summary sheet lists notification delivery per guardian and a \"Sửa\" button.",
  "trace": "ATT-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-005",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Late minutes presets and range",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Sheet open.",
  "steps": [
   "Tap Muộn on a student; observe presets; enter 0; enter 121; enter 45."
  ],
  "expected": "Presets 5/10/15/30; 0 and 121 rejected (\"1–120\"); 45 accepted and still Muộn (not converted to Vắng).",
  "trace": "04 §5 late 1–120, threshold",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-006",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Tap targets are 44 px",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "Sheet on 390 px.",
  "steps": [
   "Measure the segmented control height and the save button."
  ],
  "expected": "Each segment ≥ 44 px tall; save button ≥ 44 px; rows do not wrap awkwardly for a 20-character name.",
  "trace": "04 §4 rows 44 px, 12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-007",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Save with all present is valid",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Sheet open, no changes.",
  "steps": [
   "Tap Lưu điểm danh."
  ],
  "expected": "Saved; sheet counts present 11; today list chip \"Đã điểm danh 11/11\".",
  "trace": "04 §5 rule 2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-008",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Offline save is queued and synced",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Sheet open; devtools network offline.",
  "steps": [
   "Mark S6 Vắng and save while offline.",
   "Go online."
  ],
  "expected": "Banner \"Chưa đồng bộ\" appears after the offline save; the today list marks the session as pending; on reconnect the PUT is sent automatically, banner disappears, summary shows delivery statuses.",
  "trace": "ATT-01 offline",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-009",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Offline queue with two saves: last wins by client_saved_at",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Offline.",
  "steps": [
   "Save with S6 Vắng; then save again with S6 Có phép; go online."
  ],
  "expected": "Final server record for S6 is Có phép; no duplicate sheets; idempotent by (session_id, client_saved_at).",
  "trace": "04 §10 offline queue, 04 §7 idempotent",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-010",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Version conflict between main and substitute",
  "type": "edge",
  "priority": "P0",
  "preconditions": "T2 assigned as substitute of SES-TODAY; both open the sheet.",
  "steps": [
   "T2 saves.",
   "T1 saves with the stale version."
  ],
  "expected": "T1 receives 409 STALE_VERSION and the sheet shows \"Buổi này đã được Nguyễn T2 điểm danh lúc HH:mm\" with \"Xem\"; no overwrite.",
  "trace": "04 §10 two teachers, 04 §4 conflict state",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-011",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Loading skeleton and empty roster state",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "Class C4 session with 0 enrolled.",
  "steps": [
   "Open its sheet on a throttled network."
  ],
  "expected": "Skeleton rows appear during load; then \"Lớp chưa có học viên\"; save button disabled.",
  "trace": "04 §4 states",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-012",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Edit within 24 h without reason (teacher)",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Sheet saved 1 hour ago by T1.",
  "steps": [
   "As T1 tap Sửa, change S6 to Có mặt, save."
  ],
  "expected": "Saved without a reason prompt; record history shows from absent to present by T1.",
  "trace": "04 §5 rule 4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-013",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Teacher edit after 24 h is blocked",
  "type": "negative",
  "priority": "P0",
  "preconditions": "SES-PAST saved 7 days ago.",
  "steps": [
   "As T1 open the sheet and try to change S6."
  ],
  "expected": "Controls read-only with note \"Liên hệ giáo vụ để sửa\"; PATCH /attendance/records/{id} returns 403.",
  "trace": "04 §5 rule 4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-014",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Teacher can mark only own sessions",
  "type": "security",
  "priority": "P0",
  "preconditions": "As U-TEACH2 (no classes).",
  "steps": [
   "Open /teach/sessions/<SES-TODAY>/attendance directly.",
   "Call PUT on it."
  ],
  "expected": "404 page; API 404/403; not visible in /teach/today.",
  "trace": "04 §8 attendance.mark own sessions",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-015",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Substitute can mark",
  "type": "functional",
  "priority": "P1",
  "preconditions": "T2 assigned as substitute of SES-TODAY.",
  "steps": [
   "As U-TEACH2 open and save the sheet."
  ],
  "expected": "Allowed; sheet marked_by = T2.",
  "trace": "04 §5 rule 4 main or substitute",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-016",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Sheet in en locale",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "en locale.",
  "steps": [
   "Open the sheet."
  ],
  "expected": "Segments Present / Late / Absent / Excused; counters and buttons translated; date format dd/MM/yyyy retained.",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-017",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Sheet on 1440 desktop route /attendance/sessions/:id",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Open /attendance/sessions/<SES-TODAY>."
  ],
  "expected": "Same sheet in the desktop shell with the sidebar; identical behaviour; header counters live.",
  "trace": "04 §4 desktop",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-018",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Add an excuse in advance pre-marks Có phép",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A on S1; next C1 session.",
  "steps": [
   "Tab Điểm danh → + Thêm phép: session next C1, reason Ốm, reported by \"Trần Thị Hoa\", channel Zalo, offer make-up on.",
   "As U-TEACH open that session sheet."
  ],
  "expected": "Excuse row saved; on the sheet S1 is preselected Có phép with an excuse icon; G1 receives a confirmation message (type per template) or push fallback; a make-up suggestion appears on S1 attendance tab.",
  "trace": "ATT-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-019",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Excuse by date range covers several sessions",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1.",
  "steps": [
   "Add excuse date range covering two C1 sessions."
  ],
  "expected": "Both sessions pre-marked Có phép; the excuse table shows one row with the range.",
  "trace": "ATT-02 date range",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-020",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Excuse form fields and validation",
  "type": "negative",
  "priority": "P2",
  "preconditions": "Excuse form.",
  "steps": [
   "Submit without reason; choose Khác without text; pick a past session."
  ],
  "expected": "Errors shown; past sessions not selectable (or a warning for retroactive excuse requiring attendance.edit_any).",
  "trace": "04 §4 excuse form",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-021",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Parent excuse request flows to desk and is applied on accept",
  "type": "flow",
  "priority": "P1",
  "preconditions": "G1 in the parent portal; U-ACAD-A.",
  "steps": [
   "G1 submits Xin phép nghỉ for the next C1 session, reason Ốm.",
   "As U-ACAD-A open Điểm danh → Yêu cầu and accept."
  ],
  "expected": "Request appears in today plan and the Yêu cầu tab; on accept an excuse is created and the sheet pre-marks Có phép; G1 receives confirmation.",
  "trace": "COM-08, ATT-02",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-022",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Absent triggers ZNS within 2 minutes with the exact template",
  "type": "functional",
  "priority": "P0",
  "preconditions": "ZALO stub; G1 consent; save sheet with S1 Vắng at 18:05.",
  "steps": [
   "Save.",
   "Open the summary and the message log."
  ],
  "expected": "Message to G1 channel Zalo ZNS type attendance_absent, body \"Học viên Nguyễn An vắng buổi Movers 2A ngày dd/MM lúc 18:00. Vui lòng liên hệ Quận 1 <SĐT chi nhánh>.\"; status sent within 2 min; summary sheet shows the status per guardian.",
  "trace": "ATT-03",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-023",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Late notifies with minutes",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 Muộn 10.",
  "steps": [
   "Save."
  ],
  "expected": "Message type attendance_late containing \"muộn 10 phút\".",
  "trace": "04 §2 late notifies yes with minutes",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-024",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Present does not notify by default; tenant setting enables it",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Default policy.",
  "steps": [
   "Save all present; check the log.",
   "Enable notify present in /settings/attendance; save another session."
  ],
  "expected": "First: no attendance_present messages. Second: attendance_present messages sent.",
  "trace": "04 §2 present optional default off",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-025",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Excused does not notify",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 Có phép.",
  "steps": [
   "Save."
  ],
  "expected": "No attendance message for S1 (only the earlier excuse confirmation).",
  "trace": "04 §2 excused no",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-026",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Guardian without consent: skipped with reason and fallback",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S6 absent; G2 has no Zalo consent but has email.",
  "steps": [
   "Save."
  ],
  "expected": "Log shows ZNS skipped \"Chưa có kênh liên lạc\" (or \"Chưa đồng ý Zalo\"), then in-app attempt (skipped, no app) then email sent to nam@test.vn; the desk follow-up list shows S6 with the reason.",
  "trace": "ATT-03 fallback, 04 §10 guardian without consent",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-027",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Zalo failure falls back to in-app then email",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Stub set to fail ZNS.",
  "steps": [
   "Save with S1 absent."
  ],
  "expected": "Log: ZNS failed (3 attempts over 15 min in the stub timeline or immediately in test mode), then push, then email; final status visible on the session sheet.",
  "trace": "ATT-03, COM-02 retries",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-028",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Quiet hours hold non-urgent, evening absence sends immediately",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Clock stub at 21:45.",
  "steps": [
   "Save a sheet with S1 Muộn (non-urgent) and S6 Vắng."
  ],
  "expected": "Late message status queued until 07:00; absent message sent immediately.",
  "trace": "04 §5 rule 6",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-029",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Notification toggle off skips all messages",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Sheet.",
  "steps": [
   "Untick Gửi thông báo PH and save with an absence."
  ],
  "expected": "No messages created; log shows skipped \"Giáo viên tắt thông báo\" or none; the summary states \"Không gửi thông báo\".",
  "trace": "04 §4 notify toggle",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-030",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Per-student notes are never sent to guardians",
  "type": "security",
  "priority": "P1",
  "preconditions": "Sheet with a per-student note \"Quậy\" on S1.",
  "steps": [
   "Save with S1 absent."
  ],
  "expected": "Message body contains no note text; the note is visible only to staff on the record.",
  "trace": "04 §10 notes staff-visible only",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-031",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Admin correction after the session day requires reason and writes audit",
  "type": "functional",
  "priority": "P0",
  "preconditions": "SES-PAST; as U-ACAD-A.",
  "steps": [
   "Open the sheet; change S6 from Vắng to Có mặt; enter reason \"GV nhầm\"; save."
  ],
  "expected": "PATCH with reason; attendance_record_history row from absent to present with actor and reason; audit log entry attendance.edit; S6 package balance recalculated (absent consumed under default policy → now present, still consumed; no change) and the optional \"correction\" message prompt appears.",
  "trace": "ATT-04",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-032",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Correction reason is mandatory after the edit window",
  "type": "negative",
  "priority": "P1",
  "preconditions": "SES-PAST.",
  "steps": [
   "PATCH /attendance/records/{id} without reason."
  ],
  "expected": "HTTP 400 \"reason required\"; UI blocks save with an inline error.",
  "trace": "04 §7 reason required after window",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-033",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Correction excused → present recalculates package",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 marked Có phép on SES-PAST (not consumed).",
  "steps": [
   "Correct to Có mặt."
  ],
  "expected": "One session consumed; S1 balance 5 → 4; session_consumption row created.",
  "trace": "04 §5 package consumption",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-034",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Correction present → excused reverses consumption",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 present on SES-PAST (consumed).",
  "steps": [
   "Correct to Có phép."
  ],
  "expected": "Consumption reversed (reversed_at set); balance 4 → 5.",
  "trace": "05 §7 session_consumption reversed_at",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-035",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Unmarked tab lists sessions ended > 2 h",
  "type": "functional",
  "priority": "P0",
  "preconditions": "SES-YEST unmarked; as U-ACAD-A.",
  "steps": [
   "Open /attendance tab Chưa điểm danh."
  ],
  "expected": "SES-YEST listed under T1 with actions Nhắc GV and Điểm danh thay; a session that ended 1 hour ago is not listed.",
  "trace": "ATT-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-036",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Nhắc GV sends in-app push",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Tab Chưa điểm danh.",
  "steps": [
   "Click Nhắc GV on SES-YEST."
  ],
  "expected": "POST remind-teacher; T1 sees a bell notification \"Chưa điểm danh: Movers 2A hôm qua\"; button shows \"Đã nhắc HH:mm\".",
  "trace": "ATT-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-037",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Điểm danh thay opens the sheet for the admin",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Tab Chưa điểm danh.",
  "steps": [
   "Click Điểm danh thay; save."
  ],
  "expected": "Sheet marked_by U-ACAD-A; session leaves the unmarked list.",
  "trace": "ATT-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-038",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "21:00 teacher reminder and 24 h manager digest",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Clock stub.",
  "steps": [
   "Advance to 21:00 with SES-TODAY unmarked.",
   "Advance 24 h."
  ],
  "expected": "T1 receives a reminder at 21:00; U-BM-A receives a digest listing sessions unmarked > 24 h.",
  "trace": "ATT-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-039",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Attendance overview Hôm nay table",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Open /attendance tab Hôm nay."
  ],
  "expected": "Columns time, class, room, teacher, roster, marked, present/late/absent/excused counts, notifications sent; action Mở phiếu; filters branch, teacher, class; tabs Hôm nay · Chưa điểm danh · Theo lớp · Theo học viên · Báo cáo.",
  "trace": "04 §4 overview",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-040",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Two consecutive absences create a follow-up task",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S6 absent on the previous C1 session; thresholds default.",
  "steps": [
   "Save SES-TODAY with S6 Vắng."
  ],
  "expected": "Task \"Liên hệ phụ huynh: Phan Giang vắng 2 buổi\" created for the branch care owner; appears in the today plan; /students tab \"Cần quan tâm\" includes S6.",
  "trace": "ATT-06",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-041",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Three absences in 30 days trigger the monthly rule",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S7 absent twice non-consecutively in the last 30 days.",
  "steps": [
   "Save a third absence."
  ],
  "expected": "Task created with trigger monthly; not duplicated if a task is already open for S7.",
  "trace": "ATT-06",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-042",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Trial and make-up rows at the bottom under Khách",
  "type": "functional",
  "priority": "P0",
  "preconditions": "SES-TODAY with a trial (L1 prospect) and a make-up (S2).",
  "steps": [
   "Open the sheet."
  ],
  "expected": "Section \"Khách\" after the enrolled list; trial row with chip Học thử and only [Có mặt][Vắng]; make-up row with chip Học bù; each with a note field.",
  "trace": "ATT-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-043",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Marking a trial present prompts the 3-option note",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Trial row.",
  "steps": [
   "Tap Có mặt on the trial; save."
  ],
  "expected": "Prompt with exactly Phù hợp / Cần lớp thấp hơn / Cần lớp cao hơn; choice stored as teacher_note and sent to the consultant.",
  "trace": "ATT-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-044",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Make-up attendance respects consumes_package flag",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Make-up booking with consumes_package false.",
  "steps": [
   "Mark the make-up student present; save."
  ],
  "expected": "Booking status attended; no session consumed; with flag true one session consumed.",
  "trace": "ATT-07, CLS-08",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-045",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Attendance report by class",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-BM-A, date range last 30 days.",
  "steps": [
   "Open /attendance tab Báo cáo, group by class."
  ],
  "expected": "Rows per class with marked, present, late, absent, excused, rate = (present+late)/(present+late+absent); late-minutes distribution chart; absences by reason; unmarked sessions by teacher; export xlsx button.",
  "trace": "ATT-08",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-046",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Attendance rate formula check",
  "type": "functional",
  "priority": "P0",
  "preconditions": "C1 SES-PAST: 10 present, 1 late, 1 absent.",
  "steps": [
   "Read the rate for C1 on that day in the report."
  ],
  "expected": "Rate 11/12 = 91.7% (displayed \"91,7%\" in vi, \"91.7%\" in en).",
  "trace": "04 §5 rule 7",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-047",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Student list sortable by absences",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Report tab Theo học viên.",
  "steps": [
   "Sort by absences desc."
  ],
  "expected": "S6/S7 on top; counts match their records.",
  "trace": "ATT-08",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-048",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Report export xlsx",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Report.",
  "steps": [
   "Click Xuất Excel."
  ],
  "expected": "File downloads (format=xlsx); rows equal the on-screen rows; totals row present.",
  "trace": "ATT-08, attendance.export",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-049",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Teacher report scope own classes",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-TEACH.",
  "steps": [
   "Open the report."
  ],
  "expected": "Only C1 and C3 appear; branch filter limited.",
  "trace": "04 §8 attendance.report own classes",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-050",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Absent consumes per tenant setting",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 balance 5; policy absent_consumes on.",
  "steps": [
   "Save S1 Vắng; check balance.",
   "Turn the setting off; save another absence."
  ],
  "expected": "First: balance 4. Second: balance unchanged.",
  "trace": "04 §5 rule 5, FIN-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-051",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Delete sheet requires attendance.delete_sheet and is audited",
  "type": "security",
  "priority": "P1",
  "preconditions": "SES-PAST.",
  "steps": [
   "As U-ACAD-A call DELETE /sessions/{id}/attendance.",
   "As U-BM-A delete from the UI."
  ],
  "expected": "U-ACAD-A: 403 and no UI action. U-BM-A: confirmation dialog, sheet deleted, consumptions reversed, audit entry attendance.delete_sheet.",
  "trace": "04 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-052",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Attendance policy editable by owner only",
  "type": "security",
  "priority": "P1",
  "preconditions": "/settings/attendance.",
  "steps": [
   "As U-BM-A open the page.",
   "As U-OWNER change late_threshold to 30 and save."
  ],
  "expected": "U-BM-A read-only or hidden; owner change persisted in school-config attendance_policy block.",
  "trace": "04 §8 attendance.policy",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-053",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Students enrolled after the date are not retro-added",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Enroll S3 into C1 today.",
  "steps": [
   "Open SES-PAST sheet."
  ],
  "expected": "S3 absent from that sheet; present on SES-TODAY.",
  "trace": "04 §5 rule 1",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-054",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Save performance for a 30-student roster",
  "type": "perf",
  "priority": "P2",
  "preconditions": "Class with 30 students.",
  "steps": [
   "Save the sheet; measure the PUT."
  ],
  "expected": "p95 ≤ 300 ms in the stub; UI optimistic (summary appears immediately, reverts on failure with a retry).",
  "trace": "12 §2 attendance",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-055",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Chips carry text and icon, never colour alone",
  "type": "a11y",
  "priority": "P1",
  "preconditions": "Sheet and reports.",
  "steps": [
   "Inspect chips; view in grayscale (devtools emulate)."
  ],
  "expected": "Every status chip has visible text (Có mặt / Muộn / Vắng / Có phép) and an icon; distinguishable in grayscale.",
  "trace": "04 §2 colours, 12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-056",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Colour tokens for status chips",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "Sheet.",
  "steps": [
   "Inspect computed colours."
  ],
  "expected": "ok #16A34A on #DCFCE7; late #D97706 on #FEF3C7; abs #DC2626 on #FEE2E2; exc #2563EB on #DBEAFE.",
  "trace": "04 §2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-ATT-057",
  "module": "04-attendance",
  "module_title": "Attendance",
  "prd": "04",
  "title": "Parent sees own child attendance only",
  "type": "security",
  "priority": "P1",
  "preconditions": "G1 portal.",
  "steps": [
   "GET /v3/parent/learner/{S1}/attendance and for S7."
  ],
  "expected": "S1 ok; S7 404.",
  "trace": "04 §8 parent own children",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-001",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Add fee plans of each type to a course",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACC on /settings/fee-plans.",
  "steps": [
   "On course Movers 2 add: one_time \"Trọn khoá\" 9,600,000 with 2 instalments (50% now, 50% after 30 days); monthly 1,200,000; session_package 24 sessions 6,000,000 validity 6 months; material \"Sách\" 180,000 VAT 5.",
   "Open the conversion wizard step 3 and the enrollment form for a Movers 2 class."
  ],
  "expected": "Four plans listed under the course with type chips Trọn khoá / Theo tháng / Gói buổi / Tài liệu; all appear in both pickers; deactivating one hides it from pickers while old invoices still show it.",
  "trace": "FIN-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-002",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Fee plan validation",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Fee plan form.",
  "steps": [
   "Price -1; package sessions 0; validity 0; VAT 11; instalment pcts summing to 90."
  ],
  "expected": "Each rejected inline; VAT range 0–10; instalments must sum to 100.",
  "trace": "FIN-01, 05 §5 form",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-003",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Default VAT per type: fee 0, material 5",
  "type": "functional",
  "priority": "P1",
  "preconditions": "New fee plan form.",
  "steps": [
   "Choose type one_time then material."
  ],
  "expected": "VAT field defaults 0 then 5; editable; tenant defaults changeable in /settings/finance.",
  "trace": "05 §11 VAT",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-004",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Create an invoice from a fee plan with QR",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACC on /finance/tuition/new?student_id=<S3>.",
  "steps": [
   "Student prefilled S3; line = FP1 qty 1; due date default today+7; Tài khoản nhận BA1; Gửi thông báo Zalo ticked.",
   "Save."
  ],
  "expected": "Invoice created via POST tuitions with lines[]; footer showed subtotal 9.600.000, discount 0, VAT 0, total 9.600.000; after save the drawer shows the QR image (POST /v3/payment/qr) with payment_code and a \"Thu tiền ngay\" button; list shows status Chưa thanh toán; invoice_created message to payer G.",
  "trace": "FIN-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-005",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Invoice total formula with line discount, invoice discount and VAT",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Invoice form.",
  "steps": [
   "Lines: FP1 9,600,000 line discount 100,000; MAT1 qty 2 unit 180,000 VAT 5%.",
   "Invoice discount 5%."
  ],
  "expected": "Subtotal 9,860,000; invoice discount 493,000; VAT = 5% of material lines after discount (2×180,000 = 360,000 → 18,000; if the 5% invoice discount is applied proportionally, 17,100); total is a whole VND number and equals Σ(qty×unit − line_discount) − invoice_discount + VAT as displayed; no decimals anywhere.",
  "trace": "05 §6 rule 1",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-006",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Discount above role limit sets pending approval",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A (limit 5%).",
  "steps": [
   "Create an invoice with 10% discount, reason \"Anh chị em\"."
  ],
  "expected": "Response 202 PENDING_APPROVAL; status chip Chờ duyệt; U-BM-A notified; invoice appears in tab Chờ duyệt; payment button disabled until approved.",
  "trace": "FIN-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-007",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Approve and reject a pending invoice",
  "type": "functional",
  "priority": "P0",
  "preconditions": "INV-5 pending; as U-BM-A.",
  "steps": [
   "Open INV-5; click Duyệt.",
   "On another pending invoice click Từ chối with note."
  ],
  "expected": "Approved → status Chưa thanh toán, approved_by set, creator notified; rejected → status Đã huỷ (void) with note; audit entries for both.",
  "trace": "05 §8 approve/reject",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-008",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Accountant cannot approve discounts",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-ACC on INV-5.",
  "steps": [
   "Look for Duyệt; call POST /tuitions/{id}/approve."
  ],
  "expected": "Button absent; 403.",
  "trace": "05 §9 finance.discount.approve",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-009",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Invoice form validation",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Invoice form.",
  "steps": [
   "No lines; unit price -1; VAT 12; due date empty; instalments summing less than total; no receiving account."
  ],
  "expected": "Errors: \"Cần ít nhất một dòng\"; \"≥ 0\"; \"0–10\"; required; \"Tổng trả góp phải bằng tổng hoá đơn\"; \"Chọn tài khoản nhận\" (required for QR).",
  "trace": "05 §5 invoice form",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-010",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Session package credits on full payment by default",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S3 invoice FP3 unpaid; credit_on_create off.",
  "steps": [
   "Check S3 balance after creating the invoice.",
   "Record full payment."
  ],
  "expected": "Before payment balance 0 (package pending); after payment balance 24 with expiry = today + 6 months, status active.",
  "trace": "FIN-02 Then, 05 §6 rule 4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-011",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Tenant setting credits on creation",
  "type": "functional",
  "priority": "P1",
  "preconditions": "credit_on_create on.",
  "steps": [
   "Create an FP3 invoice for S3."
  ],
  "expected": "Balance 24 immediately; package status active with expiry from credit date.",
  "trace": "05 §6 rule 4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-012",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Voucher applied at invoice creation",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Voucher group \"BACK2SCHOOL\" 10%, valid, applicable to Movers 2.",
  "steps": [
   "Enter voucher code on the FP1 invoice."
  ],
  "expected": "POST /vouchers/validate returns the discount; total 8,640,000; voucher_use recorded; a second invoice for the same single-use code fails \"Mã đã dùng\".",
  "trace": "FIN-09",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-013",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Voucher not stackable with percent discount unless flagged",
  "type": "negative",
  "priority": "P2",
  "preconditions": "Voucher and 5% discount on the same invoice.",
  "steps": [
   "Save."
  ],
  "expected": "Error \"Không kết hợp mã giảm giá với giảm giá %\" unless the group is stackable.",
  "trace": "05 §6 rule 7",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-014",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Invoice list KPI tiles and tabs",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACC, branch A.",
  "steps": [
   "Open /finance/tuition."
  ],
  "expected": "Tiles Đã lập tháng này, Đã thu, Còn phải thu, Quá hạn with VND values; tabs Tất cả · Chưa thanh toán · Thanh toán một phần · Quá hạn · Chờ duyệt · Đã thanh toán · Đã huỷ with counts; INV-1 in Chưa thanh toán, INV-2 in Thanh toán một phần, INV-3 in Quá hạn (chip \"45 ngày\"), INV-4 in Đã thanh toán, INV-5 in Chờ duyệt.",
  "trace": "05 §5 Học phí",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-015",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Invoice list columns and formatting",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "/finance/tuition.",
  "steps": [
   "Read columns for INV-3."
  ],
  "expected": "Mã HĐ, Học viên (name + class chip), Nội dung, Tổng tiền, Đã thu, Còn lại (red for overdue), Hạn (date + \"Quá hạn 45 ngày\" chip), Trạng thái, Nhắc gần nhất (date + channel), Thao tác Thu tiền · Gửi nhắc · In; amounts \"3.000.000 đ\".",
  "trace": "05 §5 columns",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-016",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Invoice list filters and search",
  "type": "functional",
  "priority": "P1",
  "preconditions": "/finance/tuition.",
  "steps": [
   "Filter fee plan type Gói buổi; search \"TS1A0001\"; search \"Nguyễn An\"."
  ],
  "expected": "Each narrows correctly; payment code search finds INV-1.",
  "trace": "05 §5 filters",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-017",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Invoice detail sections and buttons",
  "type": "functional",
  "priority": "P1",
  "preconditions": "INV-4 (paid).",
  "steps": [
   "Open /finance/tuition/<INV-4>."
  ],
  "expected": "Header code, student, chip Đã thanh toán, total, paid, remaining 0, due; sections lines, payments (date, method, amount, PT-Q1-202609-0001, cashier, SePay id, PDF), credit notes, reminders log, e-invoice block, audit; buttons Thu tiền disabled, Gửi nhắc, In hoá đơn, Tạo phiếu giảm, Huỷ disabled (has payments), Xuất HĐĐT.",
  "trace": "05 §5 invoice detail",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-018",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Void an invoice only without payments",
  "type": "functional",
  "priority": "P1",
  "preconditions": "INV-1 (no payments) and INV-2 (partial).",
  "steps": [
   "Huỷ INV-1 with reason.",
   "Try Huỷ on INV-2."
  ],
  "expected": "INV-1 status Đã huỷ, excluded from KPI tiles; INV-2 Huỷ disabled and API 409.",
  "trace": "05 §8 void only when no valid payments",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-019",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Bulk monthly billing preview and commit with proration",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACC; C3 monthly plan FP2; S2 active whole month; a student who started mid-month (10 of 20 sessions remaining); S4 paused whole month.",
  "steps": [
   "Click Lập hoá đơn tháng; select C3; month next month; prorate on; channels Zalo.",
   "Read the preview; commit."
  ],
  "expected": "Preview table rows: S2 1,200,000; mid-month student 600,000 (10/20); S4 absent from the list (\"Bảo lưu cả tháng\"); commit creates a GroupInvoice with one Tuition per row; notifications per channel; billing_run_created event.",
  "trace": "FIN-03",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-020",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Billing run dry_run does not write",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Preview open.",
  "steps": [
   "Cancel at preview."
  ],
  "expected": "No invoices created (list unchanged); network shows dry_run=true only.",
  "trace": "05 §8 group-invoices dry_run",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-021",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Record a cash payment and issue a numbered receipt",
  "type": "functional",
  "priority": "P0",
  "preconditions": "INV-1 unpaid; as U-ACC.",
  "steps": [
   "Click Thu tiền: amount default 9,600,000; method Tiền mặt; account CD-A; payer \"Trần Thị Hoa\"; date now; Gửi biên lai Zalo on.",
   "Confirm."
  ],
  "expected": "Payment created; receipt number PT-Q1-<YYYYMM>-0002 (next after 0001); PDF preview opens with company legal info, branch, payer, amount in words; invoice status Đã thanh toán; receipt message to payer via ZNS; cash book shows the entry.",
  "trace": "FIN-04",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-022",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Partial cash payment",
  "type": "functional",
  "priority": "P0",
  "preconditions": "INV-1 unpaid.",
  "steps": [
   "Thu tiền 4,000,000."
  ],
  "expected": "Status Thanh toán một phần; Còn lại 5,600,000; receipt issued for 4,000,000.",
  "trace": "FIN-04 partial",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-023",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Overpayment refused",
  "type": "negative",
  "priority": "P0",
  "preconditions": "INV-2 remaining 700,000.",
  "steps": [
   "Thu tiền 800,000."
  ],
  "expected": "Inline error \"Vượt số còn lại (700.000 đ)\"; API 400/409; no payment.",
  "trace": "05 §6 rule 2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-024",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Payment date cannot be in the future",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Payment dialog.",
  "steps": [
   "Set date tomorrow."
  ],
  "expected": "Rejected \"Không được chọn ngày tương lai\".",
  "trace": "05 §5 payment dialog",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-025",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Receipt numbering is sequential under concurrency",
  "type": "edge",
  "priority": "P0",
  "preconditions": "Two cashiers (U-ACC in two sessions).",
  "steps": [
   "Record two cash payments at the same second on different invoices at branch A."
  ],
  "expected": "Numbers are consecutive and unique (e.g. 0003 and 0004); no duplicate; no gap.",
  "trace": "05 §6 rule 3, test seed 05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-026",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Receipt numbering per branch per month",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Branch B payment.",
  "steps": [
   "Record a cash payment at branch B; next month record another at A."
  ],
  "expected": "B receipt PT-TD-<YYYYMM>-0001; A next month restarts at 0001.",
  "trace": "05 §6 rule 3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-027",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Void a payment keeps the number and requires reason",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Payment PT-Q1-...-0002.",
  "steps": [
   "Click Huỷ phiếu on the payment; enter reason."
  ],
  "expected": "Payment status void with reason; number retained in the list with chip Đã huỷ; invoice returns to Chưa thanh toán / partial; cash book shows the reversal; audit entry payment.void.",
  "trace": "05 §6 rule 3, 05 §8 payments void",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-028",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Card payment method",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Payment dialog.",
  "steps": [
   "Choose Thẻ; record."
  ],
  "expected": "Payment method CARD stored; receipt issued; appears in cash book under the chosen account.",
  "trace": "05 §8 method CARD",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-029",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Receipt PDF signed URL",
  "type": "security",
  "priority": "P1",
  "preconditions": "A payment.",
  "steps": [
   "Open GET /payments/{id}/receipt.pdf link; tamper the signature."
  ],
  "expected": "Valid link serves the PDF; tampered link 403; link expires after 15 min.",
  "trace": "05 §8 receipt.pdf signed, 12 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-030",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Accountant vs academic admin payment permission",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-ACAD-A without cash grant.",
  "steps": [
   "Look for Thu tiền on INV-1; call pay."
  ],
  "expected": "Button absent; 403; with the grant enabled by owner, cash only (bank/card methods hidden).",
  "trace": "05 §9 finance.payment.create",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-031",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "SePay transfer with payment code auto-matches",
  "type": "functional",
  "priority": "P0",
  "preconditions": "INV-1 unpaid, code TS1A0001; SEPAY stub.",
  "steps": [
   "POST the stub with amount 9,600,000, description \"TS1A0001 hoc phi\", id sp-001."
  ],
  "expected": "Payment created with method BANK_TRANSFER and sepay_transaction_id sp-001; receipt PT issued; invoice Đã thanh toán; receipt message sent to payer; no manual step.",
  "trace": "FIN-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-032",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Transfer without code lands in Chưa đối soát",
  "type": "functional",
  "priority": "P0",
  "preconditions": "SEPAY stub.",
  "steps": [
   "POST amount 1,200,000 description \"hoc phi be Binh\" id sp-002.",
   "Open /finance/cashbook tab Chưa đối soát."
  ],
  "expected": "Row sp-002 with amount, description, received time and action \"Gán vào hoá đơn\"; no payment created yet.",
  "trace": "FIN-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-033",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Manual match of an unmatched transfer to an invoice",
  "type": "functional",
  "priority": "P0",
  "preconditions": "sp-002 unmatched; INV-2 remaining 700,000.",
  "steps": [
   "Gán vào hoá đơn → INV-2 for 700,000 and the rest 500,000 to student credit."
  ],
  "expected": "INV-2 Đã thanh toán; S2 credit 500,000 shown on profile; sp-002 matched_by U-ACC; next invoice form for S2 offers \"Dùng số dư\".",
  "trace": "FIN-05, 05 §11 early payment credit",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-034",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Partial transfer amount accepted as partial payment",
  "type": "functional",
  "priority": "P1",
  "preconditions": "INV-1 code; transfer 5,000,000 with code.",
  "steps": [
   "POST the stub."
  ],
  "expected": "Payment 5,000,000; invoice partial.",
  "trace": "FIN-05 partial amounts",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-035",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Duplicate SePay webhook deduped",
  "type": "edge",
  "priority": "P0",
  "preconditions": "sp-001 already processed.",
  "steps": [
   "POST the identical webhook again."
  ],
  "expected": "No second payment; invoice not overpaid; log shows deduped on sepay_transaction_id.",
  "trace": "05 §11 dedupe",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-036",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "One transfer for two children split",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Unmatched 2,400,000; INV for S1 and S2 each 1,200,000.",
  "steps": [
   "Gán vào hoá đơn with split across both invoices."
  ],
  "expected": "Two payments created, both invoices paid, one SePay id referenced on both.",
  "trace": "05 §11 split",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-037",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Debt list aging buckets and columns",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACC on /finance/debt.",
  "steps": [
   "Open the page."
  ],
  "expected": "Aging tiles 0–7, 8–30, 31–60, >60 with sums; S7 row in bucket 31–60 with guardian phone, invoices count 1, total due 3,000,000, oldest due date, last reminder, promise date, care owner; row actions Gửi nhắc, Ghi cuộc gọi, Kế hoạch trả, Thu tiền; bulk Gửi nhắc with template picker.",
  "trace": "FIN-06",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-038",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Send reminder in bulk with template",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Debt list.",
  "steps": [
   "Select S7 and another debtor; bulk Gửi nhắc; pick template fee_reminder; send."
  ],
  "expected": "Batch notify called with template_id; per-channel outcomes shown; Nhắc gần nhất updated on invoices.",
  "trace": "FIN-06, 05 §8 notify template_id",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-039",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Set a payment plan",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S7 INV-3.",
  "steps": [
   "Kế hoạch trả: 2 parts 1,500,000 on two dates."
  ],
  "expected": "Plan saved; invoice detail shows parts; sum must equal remaining (validation).",
  "trace": "05 §8 payment-plan",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-040",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Debt reconciliation with invoice list",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Debt list totals.",
  "steps": [
   "Sum \"Còn lại\" of overdue invoices in /finance/tuition tab Quá hạn."
  ],
  "expected": "Equals the sum of debt tiles for the same branch.",
  "trace": "REP-02 style reconciliation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-041",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Package consumption FIFO by expiry and low-balance alert at 3",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 with two packages: 2 sessions expiring next month, 3 sessions expiring in 6 months (balance 5).",
  "steps": [
   "Mark S1 present on two sessions.",
   "Mark a third."
  ],
  "expected": "First two consumptions come from the earlier-expiry package; after the third, balance 2 and a \"Sắp hết buổi\" message to G1 plus a desk notification was sent when balance reached 3.",
  "trace": "FIN-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-042",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Balance 0 shows Hết buổi and red chip on roster",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S6 balance 2.",
  "steps": [
   "Consume two sessions."
  ],
  "expected": "S6 in list tab \"Hết buổi\" / Sắp hết buổi with 0; roster and sheet show a red \"Hết buổi\" chip; further consumption creates no negative balance.",
  "trace": "FIN-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-043",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Consumption from an expired package refused",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Package expired yesterday with 3 sessions.",
  "steps": [
   "Mark present."
  ],
  "expected": "Consumption refused; student shows \"Hết hạn gói\"; attendance still saved.",
  "trace": "05 §6 rule 5",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-044",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Pause freezes expiry",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 package expiry E; pause 30 days; resume.",
  "steps": [
   "Perform pause and resume."
  ],
  "expected": "Expiry E + 30; frozen_days 30 on the package.",
  "trace": "FIN-07 pause freezes",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-045",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Manual package adjustment requires approval",
  "type": "functional",
  "priority": "P2",
  "preconditions": "As U-ACC.",
  "steps": [
   "POST /packages/{id}/adjust +2 reason \"Bù lỗi\"."
  ],
  "expected": "Adjustment pending until U-BM-A approves; then balance +2; audit.",
  "trace": "05 §8 adjust approval",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-046",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Refund estimate on withdrawal",
  "type": "functional",
  "priority": "P1",
  "preconditions": "S1 5 sessions unused, unit 250,000, admin fee 10%.",
  "steps": [
   "GET /refund-estimate?student_id=S1."
  ],
  "expected": "Proposal 1,125,000 (5×250,000 − 10%) with breakdown; editable with reason.",
  "trace": "FIN-08",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-047",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Credit note and refund with approval threshold",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 withdrawn; as U-ACC; threshold 5,000,000.",
  "steps": [
   "Create credit note 1,125,000 on INV-1 (paid); create refund cash from CD-A to \"Trần Thị Hoa\".",
   "Repeat with 6,000,000 on another invoice."
  ],
  "expected": "First: credit shown on invoice, refund PC-Q1-... in cash book as payment out, balance zeroed. Second: status pending approval; U-BM-A approves; then paid.",
  "trace": "FIN-08",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-048",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Credit note bounds",
  "type": "negative",
  "priority": "P1",
  "preconditions": "INV-1 total 9,600,000 with an existing credit 1,000,000.",
  "steps": [
   "Create credit 9,000,000.",
   "Refund 9,600,000 when paid 4,000,000."
  ],
  "expected": "Both refused: \"Vượt tổng hoá đơn trừ phiếu giảm\" / \"Vượt số đã thu\".",
  "trace": "05 §6 rule 8",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-049",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Create an expense and see it in the cash book",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACC on /finance/cashbook.",
  "steps": [
   "+ Phiếu chi: category Điện nước, 1,500,000, payee EVN, account CD-A, today, attachment."
  ],
  "expected": "Row PC-Q1-<YYYYMM>-0001 type Chi; totals update; attachment link signed.",
  "trace": "FIN-10",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-050",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Expense above threshold needs approval",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Threshold 5,000,000.",
  "steps": [
   "Create an expense 6,000,000."
  ],
  "expected": "Status pending; U-BM-A approve; only then counted in Chi phí tiles.",
  "trace": "FIN-10",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-051",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Daily cash close with difference",
  "type": "functional",
  "priority": "P1",
  "preconditions": "CD-A with today's cash payments 9,600,000 and expense 1,500,000.",
  "steps": [
   "Chốt quỹ ngày: counted 8,000,000; note."
  ],
  "expected": "Expected 8,100,000; difference −100,000 highlighted; close recorded; today's cash payments become non-editable (void only); cash_closed event difference_nonzero.",
  "trace": "FIN-10, 05 §6 rule 10",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-052",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Void after cash close still allowed, edit blocked",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Closed day.",
  "steps": [
   "Try to edit payer name on a closed-day payment; void it."
  ],
  "expected": "Edit blocked; void allowed with reason and audit.",
  "trace": "05 §6 rule 10",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-053",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Cash book filters and export",
  "type": "functional",
  "priority": "P2",
  "preconditions": "/finance/cashbook.",
  "steps": [
   "Filter account CD-A, type Thu, date range; Xuất Excel."
  ],
  "expected": "Rows filtered; xlsx matches; columns date, code, type, category, counterparty, in, out, account, created by, attachment.",
  "trace": "05 §5 Thu chi",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-054",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "E-invoice issue success and failure",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Provider stub; INV-4 paid.",
  "steps": [
   "Xuất HĐĐT with buyer details (success mode).",
   "Repeat in failure mode."
  ],
  "expected": "Success: number and lookup code stored, PDF link shown. Failure: provider message displayed with Thử lại; nothing stored.",
  "trace": "FIN-11",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-055",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Finance report tiles and reconciliation",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER, this month, branch A.",
  "steps": [
   "Open Báo cáo tài chính.",
   "Compare Thực thu with the cash book total type Thu for the same range; compare Đã lập with the invoice list created-in-period total excluding void."
  ],
  "expected": "Tiles billed, collected, outstanding, overdue, refunds, expenses, net cash, collection rate, revenue by course and by fee plan type, deferred revenue (unused sessions × unit price); both comparisons equal to the VND.",
  "trace": "FIN-12, REP-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-056",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Report drill-down to invoices and export",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Finance report.",
  "steps": [
   "Click billed tile; click Xuất Excel."
  ],
  "expected": "Lands on /finance/tuition with the period filter applied; xlsx downloads.",
  "trace": "FIN-12",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-057",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Finance report scope by role",
  "type": "security",
  "priority": "P1",
  "preconditions": "U-BM-A and U-ACAD-A.",
  "steps": [
   "Open /reports finance as each."
  ],
  "expected": "U-BM-A sees branch A only; U-ACAD-A has no finance report (403 / hidden).",
  "trace": "05 §9 finance.report",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-058",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Every money write is audited with before/after",
  "type": "security",
  "priority": "P0",
  "preconditions": "Perform: create invoice, pay, void payment, discount approve.",
  "steps": [
   "Open /settings/audit-log filtered by entity tuition/payment."
  ],
  "expected": "Four entries with actions tuition.create, tuition.pay, payment.void, tuition.approve; each has before/after including amount and status fields, actor, IP, request_id.",
  "trace": "05 §6 rule 11, 12 §6",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-059",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Idempotency-Key on pay prevents double payment",
  "type": "edge",
  "priority": "P1",
  "preconditions": "INV-1.",
  "steps": [
   "Send POST pay twice with the same Idempotency-Key."
  ],
  "expected": "One payment; second response returns the same payment; no second receipt number.",
  "trace": "11 §1 idempotency",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-060",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Parent sees only own invoices",
  "type": "security",
  "priority": "P1",
  "preconditions": "G1.",
  "steps": [
   "GET /v3/parent/tuitions; GET /v3/parent/tuitions/<INV-3>."
  ],
  "expected": "Lists INV-1/INV-2 (children S1, S2); INV-3 (S7) → 404.",
  "trace": "05 §9 parent own children",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-061",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Money formatting vi and en",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "Invoice list in vi then en.",
  "steps": [
   "Read 9,600,000 in both locales."
  ],
  "expected": "vi \"9.600.000 đ\"; en \"9,600,000 đ\" (or \"VND 9,600,000\" consistently across the app); no decimals.",
  "trace": "12 §8 money",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-062",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Currency locked to VND",
  "type": "negative",
  "priority": "P2",
  "preconditions": "Course with currency USD in the stub.",
  "steps": [
   "Open the fee plan form for it."
  ],
  "expected": "Warning \"Chỉ hỗ trợ VND\"; plan creation blocked until currency VND.",
  "trace": "05 §11 currency",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-063",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Instalment invoice: partial pay then withdraw",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Two-part instalment, first part paid.",
  "steps": [
   "Withdraw the student; open refund estimate."
  ],
  "expected": "Credit note proposed on the unpaid second instalment; refund limited to the paid portion minus consumed value.",
  "trace": "05 §11 instalment",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-064",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Sibling discount reason and automatic percent",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Tenant sibling percent 5%; G1 has S1 and S2.",
  "steps": [
   "Create an invoice for S2 in the wizard/form."
  ],
  "expected": "Discount 5% prefilled with reason \"Anh chị em\"; editable.",
  "trace": "05 §11 sibling",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-065",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Package moves with the student on branch transfer; revenue stays",
  "type": "edge",
  "priority": "P2",
  "preconditions": "S1 transferred to B with 5 sessions.",
  "steps": [
   "Open finance report for A and B."
  ],
  "expected": "A revenue unchanged; deferred revenue for the 5 sessions moves to B with an inter-branch note.",
  "trace": "05 §11 package transfer",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-066",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Invoice form keyboard: add line, remove line, submit",
  "type": "a11y",
  "priority": "P2",
  "preconditions": "Invoice form.",
  "steps": [
   "Use Tab/Enter to add a line and remove it; submit with Enter in the last field."
  ],
  "expected": "All actions reachable; focus returns to a sensible element after removal; totals update live.",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FIN-067",
  "module": "05-tuition-and-finance",
  "module_title": "Tuition and finance",
  "prd": "05",
  "title": "Finance pages in en locale",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "en.",
  "steps": [
   "Open tuition list, debt, cashbook."
  ],
  "expected": "Tabs Unpaid / Partial / Overdue / Pending approval / Paid / Void; no Vietnamese leftovers.",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-001",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Create a staff record with account and teaching role",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACAD-A on /staff.",
  "steps": [
   "+ Thêm nhân sự: name \"Lê Thu Trang\", code auto NV-####, title Giáo viên, full_time, branches Quận 1, start today, phone 0966000001, email; step 2 create account on, role Giáo viên, Là giáo viên on.",
   "Save."
  ],
  "expected": "Record created; invitation sent; Users list shows Chờ mời; TeacherProfile created; tab Đang làm việc count +1; Tài khoản chip Chờ mời.",
  "trace": "STF-01",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-002",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Duplicate phone shows existing record",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Staff form.",
  "steps": [
   "Enter T1's phone."
  ],
  "expected": "Form shows the existing record with \"Mở hồ sơ\"; no duplicate created.",
  "trace": "STF-01",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-003",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Staff form validation",
  "type": "negative",
  "priority": "P2",
  "preconditions": "Staff form.",
  "steps": [
   "Name 1 char; start date today + 100 days; CCCD 11 digits; email empty with account on."
  ],
  "expected": "Each rejected inline.",
  "trace": "06 §5",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-004",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Unlinked teachers tab and Tạo hồ sơ prefill",
  "type": "functional",
  "priority": "P1",
  "preconditions": "A FLYER teacher in the school without a staff record.",
  "steps": [
   "Open tab Chưa có hồ sơ; click Tạo hồ sơ."
  ],
  "expected": "Form prefilled from TeacherProfile (name, phone, email); after save the teacher disappears from the tab.",
  "trace": "STF-02",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-005",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Staff list columns and empty state",
  "type": "functional",
  "priority": "P1",
  "preconditions": "/staff.",
  "steps": [
   "Read columns; open on EMPTY."
  ],
  "expected": "Mã NV, Họ tên, Chức danh, Loại chip, Chi nhánh chips with +N overflow, Lớp đang dạy, Hợp đồng, Tài khoản chip, Thao tác kebab; empty text exactly \"Chưa có nhân sự. Thêm giáo viên và nhân viên để phân lớp và tính công.\"; default sort by name vi collation.",
  "trace": "06 §5",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-006",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Contract with pay rule; overlap refused; expiry warning",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACC on T1 profile tab Hợp đồng.",
  "steps": [
   "Add contract labour, start 1 year ago, end in 25 days, pay rule \"Theo buổi 250k\".",
   "Add another overlapping contract."
  ],
  "expected": "First saved; row chip Sắp hết hạn; notification to U-BM-A and U-ACC; second refused \"Hợp đồng trùng thời gian\".",
  "trace": "STF-03",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-007",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Pay rule with override and cancelled-late percent",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACC /staff/pay-rules.",
  "steps": [
   "Create per_session 250,000 with override class type \"IELTS\" 400,000, pay cancelled-late 50%, effective from last month."
  ],
  "expected": "Rule saved; staff count using it shown; overrides count 1.",
  "trace": "STF-04",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-008",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Generate monthly timesheet from held sessions",
  "type": "functional",
  "priority": "P0",
  "preconditions": "T1 with 20 held C1 sessions last month, one cancelled_late, T2 substituted one session.",
  "steps": [
   "As U-ACC generate the timesheet for last month."
  ],
  "expected": "T1 timesheet: 19 held lines at 250,000 (the substituted session absent) + cancelled_late line at 125,000; T2 timesheet has the substituted session; status draft; totals correct; planned/cancelled sessions absent.",
  "trace": "STF-05, 06 §6 rules",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-009",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Recalculate banner after a session edit",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Timesheet generated; then correct one session.",
  "steps": [
   "Open the timesheet."
  ],
  "expected": "Banner \"Có 1 buổi thay đổi sau khi tạo bảng công\" with Tính lại; recalculating updates the lines.",
  "trace": "STF-05",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-010",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Teacher flags a line and confirms",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-TEACH on own timesheet.",
  "steps": [
   "Flag a line \"tôi dạy thay buổi này\"; press Xác nhận."
  ],
  "expected": "Flag open → goes to U-BM-A; timesheet cannot reach confirmed until the flag is resolved; other teachers' timesheets not visible.",
  "trace": "STF-06",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-011",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Approve locks lines; export payroll xlsx",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Confirmed timesheet.",
  "steps": [
   "As U-BM-A approve; as U-ACC export."
  ],
  "expected": "Status approved; lines read-only; regenerate refused (409); xlsx with one row per teacher and a detail sheet; export logged.",
  "trace": "STF-07",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-012",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Rate snapshot survives pay rule edit",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Approved timesheet at 250,000.",
  "steps": [
   "Change the rule to 300,000."
  ],
  "expected": "Approved timesheet totals unchanged.",
  "trace": "06 §6 rate snapshot",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-013",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Adjustment requires reason and permission",
  "type": "security",
  "priority": "P1",
  "preconditions": "U-ACC and U-BM-A.",
  "steps": [
   "Add adjustment −100,000 as U-ACC with reason; try as U-BM-A."
  ],
  "expected": "U-ACC ok and audited; U-BM-A 403.",
  "trace": "06 §9 staff.timesheets.adjust",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-014",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Evaluation shared only after marking",
  "type": "functional",
  "priority": "P2",
  "preconditions": "As U-BM-A evaluate T1.",
  "steps": [
   "Score criteria; save without sharing; log in as U-TEACH; then share."
  ],
  "expected": "Teacher sees nothing until shared; then sees the score; profile shows a trend.",
  "trace": "STF-08",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-015",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Off-board clears future assignments and deactivates the account",
  "type": "functional",
  "priority": "P1",
  "preconditions": "T2 with future sessions.",
  "steps": [
   "Set status Đã nghỉ with end date today."
  ],
  "expected": "Affected sessions listed for reassignment; account deactivated at end of day (stub); history visible; T2 hidden from teacher pickers.",
  "trace": "STF-09",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-016",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Cost fields hidden from non-finance roles",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-ACAD-A on a staff profile.",
  "steps": [
   "Open tabs Hợp đồng and Bảng công."
  ],
  "expected": "Pay rule and amounts hidden; API strips rate fields or returns 403 for timesheets.",
  "trace": "06 §9",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-STF-017",
  "module": "06-teachers-and-staff",
  "module_title": "Teachers and staff (P2 smoke)",
  "prd": "06",
  "title": "Staff pages in en locale",
  "type": "i18n",
  "priority": "P3",
  "preconditions": "en.",
  "steps": [
   "Open /staff and /staff/timesheets."
  ],
  "expected": "Translated tabs and chips (Full-time / Part-time / Contractor; Draft / Pending confirmation / Confirmed / Approved).",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-001",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Zalo OA connection card and template registry",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER on /settings/integrations (ZALO stub).",
  "steps": [
   "Open the Zalo card; enter app id, secret, OA id, tokens; run Kiểm tra kết nối.",
   "Open the template registry."
  ],
  "expected": "Card shows Đã kết nối with OA name and avatar; registry lists attendance_absent, attendance_late, invoice_created, receipt, fee_reminder, schedule_change, credentials, welcome with Zalo template id, status chips (Đã duyệt / Chờ duyệt / Từ chối) and parameter list; schedule_change shows Chờ duyệt.",
  "trace": "COM-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-002",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Secrets masked after save",
  "type": "security",
  "priority": "P0",
  "preconditions": "Zalo connected.",
  "steps": [
   "Reload the integrations page; call GET /integrations/zalo."
  ],
  "expected": "Secret and tokens displayed as \"••••\" + last 4 chars; API response contains no full secret.",
  "trace": "12 §3 secrets",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-003",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Non-owner cannot see or edit Zalo integration",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-BM-A and U-ACC.",
  "steps": [
   "Open /settings/integrations."
  ],
  "expected": "Zalo card hidden for both; U-ACC sees SePay and e-invoice only; API 403 for POST /integrations/zalo.",
  "trace": "07 §9 integrations.zalo",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-004",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Template gap: unapproved template falls back and settings shows the gap",
  "type": "functional",
  "priority": "P1",
  "preconditions": "schedule_change template Chờ duyệt.",
  "steps": [
   "Move a C1 session (schedule change).",
   "Open /settings/notifications."
  ],
  "expected": "Message delivered via push/email fallback with log reason \"Mẫu ZNS chưa duyệt\"; settings page shows a warning row for schedule_change.",
  "trace": "07 §6 rule 3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-005",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Transactional message per event, per guardian flag",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 with G1 primary contact and G3 payer (different people).",
  "steps": [
   "Save attendance with S1 absent.",
   "Record a payment on S1's invoice."
  ],
  "expected": "Absence message goes to G1 only; receipt goes to G3 only; each visible on the source record (sheet / payment) and in /communication tab Tin đã gửi.",
  "trace": "COM-02, 07 §6 rule 1",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-006",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Message status lifecycle and retries",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Stub set to fail twice then succeed.",
  "steps": [
   "Trigger an absence message."
  ],
  "expected": "Status queued → failed (attempt 1) → failed (2) → sent (3) within 15 min stub time; attempts = 3 in the log.",
  "trace": "COM-02 retries",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-007",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Every message is written to the portal inbox regardless of channel",
  "type": "functional",
  "priority": "P1",
  "preconditions": "G2 (no Zalo consent).",
  "steps": [
   "Trigger a fee reminder for S6."
  ],
  "expected": "Portal inbox for G2 has the message; external channel shows skipped/email as per consent.",
  "trace": "07 §3 portal inbox always",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-008",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Message log columns, filters and empty state",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A on /communication tab Tin đã gửi.",
  "steps": [
   "Read columns; apply filters date/type/channel/status/class; open on tenant EMPTY."
  ],
  "expected": "Columns Thời gian, Loại chip, Học viên · Phụ huynh links, Kênh, Trạng thái chips Đã gửi/Đã nhận (green) · Thất bại (red) · Bỏ qua: lý do (grey) · Chờ (yellow), Nội dung preview, Thao tác Gửi lại · Xem; filters work; empty text exactly \"Chưa có tin nào được gửi.\"",
  "trace": "07 §5 message log",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-009",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Resend a failed message",
  "type": "functional",
  "priority": "P1",
  "preconditions": "A failed message.",
  "steps": [
   "Click Gửi lại."
  ],
  "expected": "POST /messages/{id}/resend; new attempt logged; status updates.",
  "trace": "07 §8 resend",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-010",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "KPI tiles on Sổ liên lạc",
  "type": "functional",
  "priority": "P2",
  "preconditions": "/communication.",
  "steps": [
   "Read tiles."
  ],
  "expected": "Sent today, failed today, unreachable guardians, ZNS spend this month (VND) with cost from the log.",
  "trace": "07 §5 KPI tiles",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-011",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Rendered content stored and personal data masked in logs",
  "type": "security",
  "priority": "P1",
  "preconditions": "A sent message.",
  "steps": [
   "Open the message; inspect server logs (stub log)."
  ],
  "expected": "Rendered body visible in the message log; application logs contain masked phone (e.g. 0901***567) and no full name.",
  "trace": "07 §6 rule 9, 12 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-012",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Placeholder with missing data renders cleanly",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Session without a room.",
  "steps": [
   "Trigger schedule_change for it."
  ],
  "expected": "Body has no dangling \"tại phòng \" text; uses the fallback wording with branch.",
  "trace": "07 §11 placeholder missing",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-013",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Quiet hours hold non-urgent messages",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Clock 22:00.",
  "steps": [
   "Create an invoice (invoice_created)."
  ],
  "expected": "Message status Chờ with scheduled 07:00; sent after the clock passes 07:00.",
  "trace": "07 §3 quiet hours",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-014",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "ZNS monthly cap pauses ZNS and falls back",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Cap set to 1,000 VND; spend already 900.",
  "steps": [
   "Trigger two absence messages."
  ],
  "expected": "First ZNS sent; second falls back to push/email with reason \"Vượt hạn mức ZNS\"; settings shows the cap reached.",
  "trace": "07 §6 rule 4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-015",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Consent revoked mid-month stops ZNS",
  "type": "functional",
  "priority": "P1",
  "preconditions": "G1 revokes Zalo consent (staff action on the guardian card).",
  "steps": [
   "Trigger an absence message."
  ],
  "expected": "ZNS skipped with reason; portal inbox continues; guardian card shows \"Chỉ liên hệ qua điện thoại\".",
  "trace": "07 §11 revoke",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-016",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Teacher class note to parents via OA/push only",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-TEACH after saving SES-TODAY.",
  "steps": [
   "Write \"Hôm nay học Unit 3\" and choose Gửi phụ huynh."
  ],
  "expected": "POST /messages/send type custom to guardians of present and absent students with each student's status line; channel OA or push, never ZNS; log rows visible; teacher cannot choose a ZNS template.",
  "trace": "COM-04",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-017",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Teacher cannot send to other classes",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-TEACH.",
  "steps": [
   "POST /messages/send with student_ids from C2."
  ],
  "expected": "403 or the C2 students are rejected in the response.",
  "trace": "07 §9 communication.view_log own classes",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-018",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Announcement composer with audience preview and unreachable list",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Compose title, body with {ten_hoc_vien}, attach 1 PDF, audience class C1, guardian flag primary contact, channels OA + push + email, send now."
  ],
  "expected": "Preview shows counts per channel and lists unreachable guardians (G2); the message renders with the student name substituted; after send, delivered/read counts update; announcement appears in the class page and the portal.",
  "trace": "COM-03",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-019",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Announcement validation",
  "type": "negative",
  "priority": "P2",
  "preconditions": "Composer.",
  "steps": [
   "Title 121 chars; body 2,001 chars; 6 attachments; 11 MB file; audience with zero reachable."
  ],
  "expected": "Each rejected inline; the last shows a warning with the unreachable list and blocks send.",
  "trace": "07 §5 composer",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-020",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Bulk announcement over 500 recipients requires announce_bulk",
  "type": "security",
  "priority": "P2",
  "preconditions": "As U-ACAD-A; audience whole branch (> 500).",
  "steps": [
   "Send."
  ],
  "expected": "403 / \"Cần quyền gửi hàng loạt\"; U-OWNER succeeds.",
  "trace": "07 §6 rule 5",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-021",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Scheduled announcement resolves audience at send time",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Schedule for +10 min; then enroll S3 into C1.",
  "steps": [
   "Wait for send."
  ],
  "expected": "S3's guardian receives it.",
  "trace": "07 §11",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-022",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Progress report batch: draft, comment, publish, PDF",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A; C1; period last month.",
  "steps": [
   "Create batch; as U-TEACH fill comments in the list view; as admin publish."
  ],
  "expected": "One draft per student with attendance rate, assignments, accuracy, study time, balance; teacher comments saved inline; publish sends report_published message with portal link and PDF; published reports immutable (edit creates version 2).",
  "trace": "COM-05",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-023",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Publish without teacher comment blocked unless override",
  "type": "negative",
  "priority": "P2",
  "preconditions": "A draft with empty comment.",
  "steps": [
   "Publish."
  ],
  "expected": "Blocked \"Thiếu nhận xét giáo viên\" with an admin override option that is audited.",
  "trace": "07 §6 rule 7",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-024",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Parent OTP login and children switcher",
  "type": "functional",
  "priority": "P0",
  "preconditions": "G1 phone; portal at /parent/login (390 px).",
  "steps": [
   "Enter 0901234567; enter the stub OTP."
  ],
  "expected": "Lands on /parent with child chips Nguyễn An and Nguyễn Bình (both branches); tabs Lịch học, Điểm danh, Học phí, Kết quả, Thông báo, Hồ sơ; session persists 30 days (cookie expiry).",
  "trace": "COM-06",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-025",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Portal login error states and rate limits",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Unknown phone; known phone.",
  "steps": [
   "Enter 0999999999.",
   "Request OTP 11 times in an hour for G1.",
   "Enter a wrong code 5 times."
  ],
  "expected": "Text \"Số điện thoại chưa được đăng ký với trung tâm. Vui lòng liên hệ ...\" ; 11th OTP request refused (10/hour); after 5 wrong attempts the code is invalid and a new one is required; resend cooldown 60 s visible.",
  "trace": "07 §5 login, 12 §3 rate limits",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-026",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Portal invoice with QR and post-payment update",
  "type": "flow",
  "priority": "P1",
  "preconditions": "G1; INV-1 unpaid.",
  "steps": [
   "Open /parent/tuition → INV-1; note the QR and code.",
   "POST the SePay stub with the code."
  ],
  "expected": "Lines, total, due date, QR with payment code shown; after the webhook the status becomes Đã thanh toán and a receipt PDF link appears.",
  "trace": "COM-07",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-027",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Portal excuse and make-up requests",
  "type": "functional",
  "priority": "P1",
  "preconditions": "G1.",
  "steps": [
   "Submit Xin phép nghỉ for the next C1 session; submit Đăng ký học bù for an excused absence."
  ],
  "expected": "Two parent_request rows (excuse, makeup) open; staff sees them under Điểm danh → Yêu cầu and the today plan; G1 gets confirmation.",
  "trace": "COM-08",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-028",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Portal consent management and correction request",
  "type": "functional",
  "priority": "P1",
  "preconditions": "G1 /parent/profile.",
  "steps": [
   "Toggle Zalo consent off; submit a birthday correction request."
  ],
  "expected": "Consent record revoked_at set with channel portal; a task for academic admin created; the birthday itself unchanged until staff edits it (audited).",
  "trace": "COM-09, 12 §4",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-029",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Portal shows only linked children",
  "type": "security",
  "priority": "P0",
  "preconditions": "G1.",
  "steps": [
   "GET /v3/parent/learner/<S7>/schedule."
  ],
  "expected": "404.",
  "trace": "12 §3 parents see only linked learners",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-030",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Portal works inside Zalo in-app browser UA",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "Playwright with Zalo UA string, 390 px.",
  "steps": [
   "Open /parent from a message link."
  ],
  "expected": "Renders with bottom tab bar, 16 px base font, no sidebar; no horizontal scroll.",
  "trace": "07 §5 design",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-031",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Portal session revocation by staff",
  "type": "security",
  "priority": "P2",
  "preconditions": "G1 logged in.",
  "steps": [
   "Staff revokes the portal session from the guardian card."
  ],
  "expected": "Next portal request redirects to login.",
  "trace": "07 §6 rule 8",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-032",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Templates page and test send",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER /settings/templates.",
  "steps": [
   "Open attendance_absent; verify placeholder chips; click Gửi thử to my phone."
  ],
  "expected": "Chips {ten_hoc_vien}, {lop}, {ngay}, {gio}, {so_tien}, {ma_thanh_toan}, {link}; test send logs a message to the owner's phone via the stub.",
  "trace": "07 §5 templates",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-033",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Notification policy table per type",
  "type": "functional",
  "priority": "P1",
  "preconditions": "/settings/notifications.",
  "steps": [
   "Read the matrix; untick email for receipt; save; record a payment."
  ],
  "expected": "Rows per message type × channels checkboxes with quiet-hours toggle; receipt no longer attempts email.",
  "trace": "07 §7 notification_policy",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-034",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Communication pages in en locale",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "en.",
  "steps": [
   "Open /communication and the composer."
  ],
  "expected": "Tabs Announcements · Sent messages · Progress reports · Parent requests · Templates; status chips translated.",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-COM-035",
  "module": "07-parent-communication",
  "module_title": "Parent communication",
  "prd": "07",
  "title": "Message copy uses the formal register",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "Default templates.",
  "steps": [
   "Read the vi bodies of all default templates."
  ],
  "expected": "Address \"Quý phụ huynh\", \"học viên\", \"giáo viên\"; no \"bạn\", \"bé\" as the address form, no emoji.",
  "trace": "12 §8 formal register",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-001",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Create a catalog item; duplicate SKU rejected",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACC on /inventory tab Danh mục.",
  "steps": [
   "Create SKU FF2, name \"Family and Friends 2\", unit cuốn, category Sách, sale 180,000, cost 120,000, reorder 5, chargeable on, course Movers 2.",
   "Create another with SKU FF2."
  ],
  "expected": "First saved active; second shows inline \"Mã hàng đã tồn tại\"; SKU forced uppercase A–Z0–9-; delete not offered (deactivate only).",
  "trace": "INV-01",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-002",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Stock-in document increases on-hand and becomes read-only",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Item FF2.",
  "steps": [
   "Post a stock-in at branch A: FF2 qty 40, unit cost 120,000, reference \"NCC-001\"."
  ],
  "expected": "Document code NK-YYYYMM-####; on-hand 40 at A (0 at B); document immutable after posting; movement type Nhập.",
  "trace": "INV-02",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-003",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Issue to a student creates a finance line",
  "type": "functional",
  "priority": "P0",
  "preconditions": "S1 active; on-hand 40.",
  "steps": [
   "From S1 profile issue FF2 qty 1 chargeable."
  ],
  "expected": "Toast \"Đã phát 1 cuốn Family and Friends 2 cho Nguyễn An\" with link to the finance line; on-hand 39; Materials tab lists the item with payment status; tuition line type material 180,000 exists.",
  "trace": "INV-03",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-004",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Insufficient stock blocks the issue",
  "type": "negative",
  "priority": "P0",
  "preconditions": "On-hand 1.",
  "steps": [
   "Issue qty 2."
  ],
  "expected": "Form blocks with \"Không đủ tồn kho (còn 1)\"; API 409 INSUFFICIENT_STOCK; no movement.",
  "trace": "INV-03",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-005",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Withdrawn student cannot receive items",
  "type": "negative",
  "priority": "P1",
  "preconditions": "S5.",
  "steps": [
   "Issue to S5."
  ],
  "expected": "Blocked with a status message; event inventory_issue_blocked reason student_status.",
  "trace": "08 §5",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-006",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Class hand-out with shortfall offers partial issue",
  "type": "functional",
  "priority": "P1",
  "preconditions": "C1 12 students; on-hand 10.",
  "steps": [
   "Issue FF2 to class C1."
  ],
  "expected": "Dialog shows shortfall 2; option to issue to the first 10 by display name or cancel; one movement per student in one transaction; toast lists the shortfall.",
  "trace": "INV-04",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-007",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Transfer between branches is atomic; same-branch refused",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "Transfer 5 FF2 from A to B.",
   "Attempt A to A."
  ],
  "expected": "Paired transfer_out/transfer_in with the same document_id; A 34, B 5; same-branch rejected client- and server-side.",
  "trace": "INV-05",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-008",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Count adjustment above threshold needs approval",
  "type": "functional",
  "priority": "P1",
  "preconditions": "On-hand 34; count 20.",
  "steps": [
   "Post a count sheet with reason kiểm kê."
  ],
  "expected": "Adjust −14 pending (> 10 units); U-BM-A approves; on-hand 20.",
  "trace": "INV-06",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-009",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Low-stock tab and daily notification",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Reorder 5; on-hand 4 after issues.",
  "steps": [
   "Open tab Sắp hết; trigger the 08:00 job."
  ],
  "expected": "FF2 listed with chip Sắp hết; in-app notification to users with inventory.manage.",
  "trace": "INV-07",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-010",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Return in new and damaged condition",
  "type": "functional",
  "priority": "P1",
  "preconditions": "An issue movement.",
  "steps": [
   "Return as new; return another as used."
  ],
  "expected": "New: on-hand +1; used: damaged +1 (Hỏng column shows); chargeable line flagged for credit, not deleted.",
  "trace": "INV-08",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-011",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Concurrent issue of the last copy",
  "type": "edge",
  "priority": "P1",
  "preconditions": "On-hand 1; two admins.",
  "steps": [
   "Both issue simultaneously."
  ],
  "expected": "One succeeds; the other gets 409.",
  "trace": "08 §10",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-012",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Cost price visible only to owner and accountant",
  "type": "security",
  "priority": "P1",
  "preconditions": "U-ACAD-A.",
  "steps": [
   "Open the stock table and item form."
  ],
  "expected": "Giá vốn and Giá trị tồn hidden; API omits cost fields.",
  "trace": "08 §8 inventory.view_cost",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-013",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Stock table formatting and empty state",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "/inventory.",
  "steps": [
   "Read the table; open on EMPTY."
  ],
  "expected": "SKU monospace; Tồn right-aligned red when ≤ reorder; Hỏng hidden when 0; VND with thousands separator; empty text \"Chưa có hàng hoá. Tạo danh mục trước, sau đó nhập kho.\" with button Tạo mặt hàng; pagination 50.",
  "trace": "08 §4",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-INV-014",
  "module": "08-inventory",
  "module_title": "Inventory (P2 smoke)",
  "prd": "08",
  "title": "Movements export CSV",
  "type": "functional",
  "priority": "P2",
  "preconditions": "As U-ACC.",
  "steps": [
   "Export movements for A this month."
  ],
  "expected": "CSV downloads with the movement columns; teacher gets 403.",
  "trace": "08 §7 export",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-001",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Executive dashboard layout and filters",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER on /dashboard, period This month, All branches.",
  "steps": [
   "Read the page."
  ],
  "expected": "Filters branch, period (this week, this month, last month, this quarter, custom), compare toggle; rows: Money (Doanh thu lập hoá đơn, Thực thu, Công nợ quá hạn with aging mini-bars, Chi phí), Students (Học viên đang học, Nhập học mới, Nghỉ học, Sắp hết buổi / hết hạn), Classes (Tỷ lệ lấp đầy, Chuyên cần, Buổi chưa điểm danh), Admissions (Khách mới stacked by source, Chuyển đổi, pipeline value and funnel), charts Thu theo ngày and Học viên theo tháng, table Theo chi nhánh; each tile shows value, delta vs previous period, link.",
  "trace": "09 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-002",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Widgets load independently with skeletons within 1.5 s",
  "type": "perf",
  "priority": "P1",
  "preconditions": "Throttle one widget endpoint in the stub.",
  "steps": [
   "Load /dashboard."
  ],
  "expected": "Other tiles render while the slow one shows a skeleton; no whole-page spinner; p95 ≤ 1.5 s for the rest.",
  "trace": "REP-01, 12 §2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-003",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Every tile drills down to the matching filtered list",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Dashboard.",
  "steps": [
   "Click each tile in turn and read the URL."
  ],
  "expected": "Doanh thu → /finance/tuition?created=...; Thực thu → /finance/cashbook?type=in; Công nợ → /finance/debt; Chi phí → /finance/cashbook?type=out; Đang học → /students?status=active; Nhập học mới → /students?enrolled_from=; Nghỉ học → /students?status=withdrawn; Sắp hết → /students?low_balance=1; Lấp đầy → /classes?tab=active; Chuyên cần → /attendance?tab=report; Chưa điểm danh → /attendance?tab=unmarked; Khách mới → /crm/customers?created=; Chuyển đổi → /crm/customers?status=won; the same period is applied on landing.",
  "trace": "REP-01, 09 §3 drill-down",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-004",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Money tiles reconcile with cash book and invoice list",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Dashboard this month, branch A.",
  "steps": [
   "Compare Thực thu with /finance/cashbook type Thu total; Doanh thu with invoice list created this month excluding void; Công nợ quá hạn with /finance/debt total."
  ],
  "expected": "All three equal to the VND.",
  "trace": "REP-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-005",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Student counts reconcile with list counts",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Dashboard.",
  "steps": [
   "Compare Học viên đang học with /students tab Đang học count; Nghỉ học with withdrawn-in-period count."
  ],
  "expected": "Equal.",
  "trace": "REP-02, test seed 09",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-006",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Churn rate formula",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Seeded: 100 active at period start, 5 withdrawn in period.",
  "steps": [
   "Read Nghỉ học tile."
  ],
  "expected": "5 with rate \"5%\" (withdrawn / active at start).",
  "trace": "09 §3 Nghỉ học",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-007",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Fill rate formula",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Active classes: C1 11/12, C2 12/12.",
  "steps": [
   "Read Tỷ lệ lấp đầy for branch A with only these active."
  ],
  "expected": "23/24 = 95,8%.",
  "trace": "09 §3 Tỷ lệ lấp đầy",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-008",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Compare to previous period shows deltas",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Compare toggle on.",
  "steps": [
   "Read any tile."
  ],
  "expected": "Delta in percent with sign and colour (green up for revenue, red up for churn); tooltip lists value and delta.",
  "trace": "09 §3 chart rules",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-009",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Branch table row switches the branch filter",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Dashboard All branches.",
  "steps": [
   "Click row Thủ Đức."
  ],
  "expected": "Branch filter becomes Thủ Đức and all tiles refresh.",
  "trace": "09 §3 table",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-010",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Chart rules: purple primary, grey comparison, no pie > 5 slices",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "Dashboard.",
  "steps": [
   "Inspect charts."
  ],
  "expected": "Primary series brand purple, comparison grey; axis labels 11.5 px; no 3D; Khách mới by source uses stacked bars not a pie when > 5 sources.",
  "trace": "09 §3 chart rules",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-011",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Period boundary in tenant timezone",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Payment at 23:50 on the last day of last month (ICT).",
  "steps": [
   "Select period Last month."
  ],
  "expected": "Payment included in last month, not this month.",
  "trace": "09 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-012",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Void recomputes the daily aggregate",
  "type": "edge",
  "priority": "P1",
  "preconditions": "Void a payment from 3 days ago.",
  "steps": [
   "Reload the dashboard."
  ],
  "expected": "Thực thu decreases by the voided amount; cash book agrees.",
  "trace": "09 §10 voided payments",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-013",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Branch with zero data shows zeros, not blanks",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "Branch C (no data).",
  "steps": [
   "Select Gò Vấp."
  ],
  "expected": "Tiles show \"0\" / \"0 đ\"; charts render empty axes; no NaN or \"-\".",
  "trace": "09 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-014",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Demo mode only in the admissions section for empty tenants",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Tenant EMPTY.",
  "steps": [
   "Open /dashboard."
  ],
  "expected": "Admissions section shows the existing demo data with a \"Dữ liệu mẫu\" label; other sections show the onboarding checklist and zeros; no fake money numbers.",
  "trace": "09 §10 demo mode",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-015",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Role home: academic admin",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACAD-A.",
  "steps": [
   "Open /dashboard."
  ],
  "expected": "Widgets: today's sessions with unmarked count, waiting students by level (S3 under Movers), classes near capacity (C1), classes ending in 14 days, parent requests open, substitute needed; no money tiles.",
  "trace": "09 §4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-016",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Role home: accountant",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-ACC.",
  "steps": [
   "Open /dashboard."
  ],
  "expected": "Today's payments, unmatched transfers (sp-002), invoices pending approval (INV-5), overdue by bucket, cash close status per branch, e-invoice failures.",
  "trace": "09 §4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-017",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Role home: teacher",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-TEACH.",
  "steps": [
   "Open /dashboard."
  ],
  "expected": "Today's sessions, unmarked sessions (SES-YEST), my week, students needing attention (S6 low balance, absences), FLYER assignments due; no other teachers' data.",
  "trace": "09 §4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-018",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Role home: admissions keeps sale cards plus trials and callbacks",
  "type": "regression",
  "priority": "P1",
  "preconditions": "As U-ADM.",
  "steps": [
   "Open /dashboard."
  ],
  "expected": "Existing \"Việc tôi cần làm hôm nay\" cards and today plan; new cards Học thử tuần này and Gọi lại quá hạn (L4).",
  "trace": "09 §4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-019",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Executive dashboard denied to roles without reports.executive",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-ACAD-A and U-TEACH.",
  "steps": [
   "GET /v3/school/dashboard/executive."
  ],
  "expected": "403; UI shows the role home instead.",
  "trace": "09 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-020",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Report library page pattern",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER /reports.",
  "steps": [
   "Open Lấp đầy lớp."
  ],
  "expected": "Title, filter bar with Cloud dropdown buttons, totals row, sticky header table, pagination 100, buttons Xuất Excel and Lưu bộ lọc.",
  "trace": "09 §5",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-021",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Saved filters per user via view-configs",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Report Lấp đầy lớp.",
  "steps": [
   "Set branch A + level Movers; Lưu bộ lọc; reload."
  ],
  "expected": "Filters restored; another user does not see them.",
  "trace": "09 §5 Lưu bộ lọc",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-022",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Export above 5,000 rows runs as a job with a signed link",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Report with 20,000 rows (Big Center students).",
  "steps": [
   "Click Xuất Excel."
  ],
  "expected": "Toast \"Đang tạo tệp\"; job id polled; notification with signed link within 60 s; audit entry for personal-data export.",
  "trace": "REP-04",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-023",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Weekly digest content",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Digest enabled; clock Monday 07:30.",
  "steps": [
   "Trigger the digest; open /reports/digest."
  ],
  "expected": "Email to owner and branch managers with last week numbers per branch and links; same content on the page.",
  "trace": "REP-03",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-024",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Attendance report in library uses the same rate formula as the module",
  "type": "functional",
  "priority": "P1",
  "preconditions": "C1 last 30 days.",
  "steps": [
   "Compare /reports Chuyên cần theo lớp with /attendance report."
  ],
  "expected": "Identical numbers.",
  "trace": "09 §5 attendance",
  "data": "Fixtures below",
  "phase": "P2",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-025",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Dashboard keyboard and focus",
  "type": "a11y",
  "priority": "P2",
  "preconditions": "Dashboard.",
  "steps": [
   "Tab through tiles."
  ],
  "expected": "Each tile is a focusable link with a visible ring; Enter drills down.",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-026",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Dashboard in en locale",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "en.",
  "steps": [
   "Open /dashboard."
  ],
  "expected": "Tile labels Billed revenue, Collected, Overdue debt, Expenses, Active students, New enrollments, Withdrawals, Low balance / expiring, Fill rate, Attendance, Unmarked sessions, New leads, Conversion; money with \",\" separators.",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-REP-027",
  "module": "09-reports-and-dashboards",
  "module_title": "Reports and dashboards",
  "prd": "09",
  "title": "Dashboard at 1024 px",
  "type": "ui-ux",
  "priority": "P3",
  "preconditions": "1024 viewport.",
  "steps": [
   "Open /dashboard."
  ],
  "expected": "Tiles reflow to 2 columns; charts resize; no horizontal page scroll.",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-001",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Settings left sub-navigation lists every page in order",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER on /settings.",
  "steps": [
   "Read the left navigation."
  ],
  "expected": "Items in order: Trung tâm, Chi nhánh, Người dùng, Vai trò, Quy trình tuyển sinh, Nguồn & Kênh, Nhãn, Trường tuỳ chỉnh, Gói tư vấn, Khoá học & Gói phí, Trình độ, Phòng học, Chức danh, Danh mục chi, Tài chính, Điểm danh, Thông báo, Tích hợp, Nhật ký hệ thống, Dữ liệu; sub-nav width 220 px; each routes to /settings/<slug>.",
  "trace": "10 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-002",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Settings navigation is role-filtered",
  "type": "security",
  "priority": "P1",
  "preconditions": "U-BM-A, U-ACC, U-ACAD-A.",
  "steps": [
   "Open /settings as each."
  ],
  "expected": "U-BM-A: Chi nhánh (own profile), Người dùng (invite non-owner), Phòng học, Nhật ký (branch), Dữ liệu (imports). U-ACC: Trung tâm legal info, Danh mục chi, Khoá học & Gói phí, Tài chính, Tích hợp (SePay, e-invoice), Nhật ký (finance), Dữ liệu. U-ACAD-A: Trình độ, Phòng học, Khoá học, Dữ liệu (students, classes). Direct URLs to hidden pages return 403 page.",
  "trace": "10 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-003",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Onboarding checklist ticks items when done",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Owner of EMPTY.",
  "steps": [
   "Complete \"add branch details\" via /settings/branches; return to /dashboard."
  ],
  "expected": "That item shows a tick and a done date; GET /onboarding-checklist reflects it; event onboarding_checklist_item_done.",
  "trace": "SET-01",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-004",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Invite a user with role and branches",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER on /settings/users.",
  "steps": [
   "Click Mời người dùng: name, email new@test.vn, role Giáo vụ, branches Quận 1.",
   "Send."
  ],
  "expected": "Row with status Chờ mời, actions Gửi lại and Thu hồi; invitation has a 7-day expiry; email stub records the link.",
  "trace": "SET-02",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-005",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Accept an invitation creates memberships",
  "type": "flow",
  "priority": "P0",
  "preconditions": "Invitation link from the previous case.",
  "steps": [
   "Open the link in a fresh browser; register or sign in."
  ],
  "expected": "User lands in the console as Giáo vụ with branch Quận 1 only; Users list shows Đã kích hoạt; sale profile and school membership (ADMIN derived) created.",
  "trace": "SET-02, D2 derived school role",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-006",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Expired or revoked invitation link",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Revoke the invitation; also simulate 8 days elapsed.",
  "steps": [
   "Open the link."
  ],
  "expected": "Page \"Lời mời không còn hiệu lực\" with a contact hint; no account created.",
  "trace": "SET-02 7-day link",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-007",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Invite an existing member converts to a role change",
  "type": "edge",
  "priority": "P2",
  "preconditions": "U-ACAD-A already a member.",
  "steps": [
   "Invite the same email with role Kế toán."
  ],
  "expected": "No new invitation; the user's role changes (or a confirm dialog says so); audit entry.",
  "trace": "10 §10 edge",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-008",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Branch manager can invite only non-owner roles for own branch",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-BM-A.",
  "steps": [
   "Open the invite form."
  ],
  "expected": "Role list excludes Chủ trung tâm; branches limited to Quận 1; API refuses role owner (403).",
  "trace": "10 §9 settings.users branch",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-009",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Deactivate and reactivate a user",
  "type": "functional",
  "priority": "P0",
  "preconditions": "U-ACAD-A logged in elsewhere.",
  "steps": [
   "As U-OWNER click Vô hiệu on U-ACAD-A.",
   "As U-ACAD-A perform any action.",
   "Reactivate."
  ],
  "expected": "Status Vô hiệu; U-ACAD-A's next request is rejected (session revoked, redirect to login); history (audit rows) still visible; after reactivation login works again.",
  "trace": "10 §4 deactivation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-010",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Last owner cannot be removed or deactivated",
  "type": "negative",
  "priority": "P0",
  "preconditions": "Single owner tenant.",
  "steps": [
   "Try to deactivate U-OWNER; try to change their role to Giáo vụ."
  ],
  "expected": "Both refused with \"Cần ít nhất một Chủ trung tâm\"; API 409.",
  "trace": "10 §6 last owner, 12 §5",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-011",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Users list columns and status chips",
  "type": "functional",
  "priority": "P1",
  "preconditions": "/settings/users.",
  "steps": [
   "Read the table."
  ],
  "expected": "Họ tên, Email/SĐT, Vai trò chips, Chi nhánh chips, Đăng nhập gần nhất, Trạng thái (Đã kích hoạt / Chờ mời / Vô hiệu); actions Sửa, Vô hiệu, Gửi lại lời mời.",
  "trace": "10 §6 Người dùng",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-012",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Branch scope update replaces the non-spec school-access endpoint",
  "type": "functional",
  "priority": "P1",
  "preconditions": "U-ACAD-A.",
  "steps": [
   "Edit branches to Quận 1 + Thủ Đức; save; log in as U-ACAD-A."
  ],
  "expected": "PUT /sale-profiles/{id}/branches {school_ids} called; the branch switcher now offers both; no call to users/{id}/school-access.",
  "trace": "10 §8, 00 §6 inconsistency",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-013",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Removing the last branch from a user is refused",
  "type": "negative",
  "priority": "P1",
  "preconditions": "U-ACAD-A with one branch.",
  "steps": [
   "Uncheck the only branch and save."
  ],
  "expected": "Refused \"Người dùng cần ít nhất một chi nhánh; hãy vô hiệu hoá thay vì bỏ chi nhánh\".",
  "trace": "10 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-014",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Clone Giáo vụ into Lễ tân and remove a permission",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER on /settings/roles.",
  "steps": [
   "Clone Giáo vụ; rename \"Lễ tân\"; untick finance.payment.create; save.",
   "Assign to a user and test."
  ],
  "expected": "POST /roles/{id}/clone then PUT /roles/{id}/permissions with [{code, scope}]; the new role appears with cloned_from; the assigned user has no Thu tiền button and gets 403 on pay.",
  "trace": "SET-03",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-015",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Role create is real, not a mock toast",
  "type": "regression",
  "priority": "P0",
  "preconditions": "/settings/roles.",
  "steps": [
   "Create a new role from scratch; reload."
  ],
  "expected": "Role persists (POST /v3/crm/roles); no \"mock\" toast.",
  "trace": "10 §2 roles create mock",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-016",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Default role permissions can be adjusted and reset",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Giáo viên role.",
  "steps": [
   "Change students.view scope to Chi nhánh; save; click Khôi phục mặc định."
  ],
  "expected": "Change persists; reset returns to own; code/name stay locked; audit entries role.permissions.put.",
  "trace": "10 §4 Khôi phục mặc định",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-017",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Role delete refused while assigned",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Role Lễ tân assigned to a user.",
  "steps": [
   "Delete it."
  ],
  "expected": "Refused \"Đang có 1 người dùng; phân lại vai trò trước\".",
  "trace": "10 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-018",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Matrix shows permissions grouped by module with scope radios",
  "type": "ui-ux",
  "priority": "P1",
  "preconditions": "Role drawer.",
  "steps": [
   "Read the matrix."
  ],
  "expected": "Rows grouped by module in PRD order; columns Không / Của mình / Chi nhánh / Tất cả; labels localised from the permissions catalog (X-Locale).",
  "trace": "10 §6 Vai trò",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-019",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Elevation events are audited and the user notified",
  "type": "security",
  "priority": "P1",
  "preconditions": "Change U-ACAD-A role to branch_manager.",
  "steps": [
   "Perform the change; log in as U-ACAD-A."
  ],
  "expected": "Audit entry role change; in-app notification \"Vai trò của bạn đã được cập nhật\".",
  "trace": "12 §5 elevation events",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-020",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Branch profile fields persist and are used on receipts and messages",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER /settings/branches → Quận 1.",
  "steps": [
   "Edit address, phone 02838220001, map link, opening hours, default receiving account BA1, cash drawer name, logo override; save.",
   "Record a cash payment; trigger an absence message."
  ],
  "expected": "Values persist after reload; receipt PDF shows the address and phone; absence message renders {chi_nhanh} \"Quận 1\" and {sdt} 02838220001.",
  "trace": "SET-04",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-021",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Add a branch creates a school and updates the switcher",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER.",
  "steps": [
   "+ Thêm chi nhánh: name \"Bình Thạnh\", code BT."
  ],
  "expected": "POST /v3/school/branches; owner is a member; branch switcher lists Bình Thạnh; branch list row shows 0 students, 0 classes, 1 user.",
  "trace": "SET-05",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-022",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Branch code unique",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Code Q1 exists.",
  "steps": [
   "Add a branch with code Q1."
  ],
  "expected": "Inline \"Mã chi nhánh đã tồn tại\".",
  "trace": "10 §6 Chi nhánh list",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-023",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Branch manager edits only own branch profile",
  "type": "security",
  "priority": "P1",
  "preconditions": "As U-BM-A.",
  "steps": [
   "Open /settings/branches."
  ],
  "expected": "Only Quận 1 editable; no + Thêm chi nhánh; PATCH /branches/<B> → 403/404.",
  "trace": "10 §9 settings.branches",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-024",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Tenant profile persists",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER /settings/tenant.",
  "steps": [
   "Edit name, logo, legal info (tax id 0312345678), default locale, timezone, business hours; save; reload."
  ],
  "expected": "All persisted (PATCH /centers/{id}, PUT /company, school-config); no \"not persisted\" behaviour; logo shows in the sidebar.",
  "trace": "10 §2 tenant not persisted",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-025",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Tax id and citizen id validation",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Tenant legal info.",
  "steps": [
   "Tax id 9 digits; 11 digits; citizen id 11 digits."
  ],
  "expected": "Errors \"10 hoặc 13 chữ số\" and \"12 chữ số\".",
  "trace": "10 §6 Trung tâm",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-026",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Generic master-data page behaviour (levels, rooms, job titles, expense categories)",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER.",
  "steps": [
   "On /settings/job-titles add \"Lễ tân\", reorder by drag, deactivate \"Trợ giảng\" (in use)."
  ],
  "expected": "Inline create; drag reorder persists position; deactivate keeps history and hides from pickers; delete not offered for items in use; name unique per tenant.",
  "trace": "10 §6 generic MasterDataPage",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-027",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Custom fields and CRM packages surfaced under settings",
  "type": "functional",
  "priority": "P2",
  "preconditions": "/settings/custom-fields and /settings/packages.",
  "steps": [
   "Open both."
  ],
  "expected": "Custom fields list with create; packages list (Gói tư vấn) distinct from fee plans.",
  "trace": "10 §2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-028",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Finance settings: receipt prefix, discount limits, thresholds, VAT defaults, credit rule, refund policy",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-ACC /settings/finance.",
  "steps": [
   "Set discount limit academic_admin 8%; approval threshold refund 3,000,000; credit sessions on creation; refund admin fee 10%; save.",
   "Create an invoice as U-ACAD-A with 7% discount."
  ],
  "expected": "Values persist; the 7% invoice no longer needs approval; percent fields reject > 100.",
  "trace": "10 §6 Tài chính",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-029",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Bank accounts list with SePay status",
  "type": "regression",
  "priority": "P1",
  "preconditions": "/settings/finance.",
  "steps": [
   "Read the bank account list."
  ],
  "expected": "BA1 with chip \"SePay: Đã kết nối\"; manual accounts with \"Thủ công\"; link-token flow button present.",
  "trace": "05 §2 bank accounts",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-030",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Integration cards show status, last sync, errors, disconnect",
  "type": "functional",
  "priority": "P1",
  "preconditions": "/settings/integrations.",
  "steps": [
   "Read SePay, Zalo OA, E-invoice, Webhooks cards."
  ],
  "expected": "Each card: status chip, last sync time, last error (if any), Ngắt kết nối; Zalo shows \"Mẫu đã duyệt 5/8\".",
  "trace": "SET-07",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-031",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Outgoing webhook config and delivery",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Webhook card.",
  "steps": [
   "Set URL to the stub receiver and a secret; save; record a payment."
  ],
  "expected": "Stub receives an event payment.recorded with an HMAC header; secret masked after save.",
  "trace": "10 §6 Webhooks, 11 §1 events",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-032",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Zalo OA already used by another center",
  "type": "edge",
  "priority": "P2",
  "preconditions": "Stub returns \"OA in use\".",
  "steps": [
   "Connect."
  ],
  "expected": "Guidance text shown; card stays disconnected.",
  "trace": "10 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-033",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Audit log viewer filters and diff",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER /settings/audit-log after several writes.",
  "steps": [
   "Filter actor U-ACC, action tuition.pay, date today; expand a row."
  ],
  "expected": "Columns Thời gian, Người thực hiện, Hành động, Đối tượng, Chi nhánh, IP; expanded row shows before/after diff of changed fields; rows read-only; Xuất CSV downloads.",
  "trace": "SET-06",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-034",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Audit log is append-only",
  "type": "security",
  "priority": "P1",
  "preconditions": "API.",
  "steps": [
   "Attempt PATCH/DELETE /v3/school/audit-logs/{id}."
  ],
  "expected": "405 or 404; no route exists.",
  "trace": "12 §6 append-only",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-035",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Audit scope by role",
  "type": "security",
  "priority": "P1",
  "preconditions": "U-BM-A and U-ACC.",
  "steps": [
   "Open /settings/audit-log."
  ],
  "expected": "U-BM-A sees branch A entries only; U-ACC sees finance actions only.",
  "trace": "10 §9 settings.audit",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-036",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Data page: imports, exports, anonymisation requests, PDPD template",
  "type": "functional",
  "priority": "P1",
  "preconditions": "As U-OWNER /settings/data.",
  "steps": [
   "Read the page."
  ],
  "expected": "Sections Nhập dữ liệu (links to the wizard per entity), Xuất toàn bộ (POST /exports/full → job), Yêu cầu ẩn danh hoá (list), download link for \"Hồ sơ đánh giá tác động\" template.",
  "trace": "10 §3 Dữ liệu, 12 §4 Art. 24, 12 §7",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-037",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Full tenant export job",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Owner.",
  "steps": [
   "Request full export."
  ],
  "expected": "Job created; signed download within the SLA (24 h; stub immediate) containing CSV per entity and a documents zip; audited.",
  "trace": "12 §7 tenant export",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-038",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Settings pages in en locale",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "en.",
  "steps": [
   "Open /settings and three sub-pages."
  ],
  "expected": "Left nav Center, Branches, Users, Roles, Admissions pipelines, Sources & Channels, Tags, Custom fields, Sales packages, Courses & Fee plans, Levels, Rooms, Job titles, Expense categories, Finance, Attendance, Notifications, Integrations, Audit log, Data; forms translated.",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-SET-039",
  "module": "10-settings-and-administration",
  "module_title": "Settings and administration",
  "prd": "10",
  "title": "Settings forms keyboard and error linking",
  "type": "a11y",
  "priority": "P2",
  "preconditions": "Any settings form.",
  "steps": [
   "Submit with an invalid value; inspect the error element."
  ],
  "expected": "Error text linked via aria-describedby; focus moves to the first invalid field.",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-001",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "List endpoints paginate with the standard envelope",
  "type": "functional",
  "priority": "P1",
  "preconditions": "API session.",
  "steps": [
   "GET /api/v3/school/students?limit=50&page=2.",
   "GET with limit=500."
  ],
  "expected": "Response ListResponse with pagination {limit, page, prev_page, next_page, total_pages, total_count}; limit above 200 clamped or rejected (400).",
  "trace": "11 §1 envelopes, 12 §2 limit ≤ 200",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-002",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Error envelope and domain codes",
  "type": "functional",
  "priority": "P1",
  "preconditions": "API.",
  "steps": [
   "Trigger CLASS_FULL, CONFLICT, STALE_VERSION, INVALID_TRANSITION, PENDING_APPROVAL, INSUFFICIENT_STOCK (P2)."
  ],
  "expected": "Each returns ErrorRespone {code, message, details} with the documented code and HTTP 409 (202 for PENDING_APPROVAL).",
  "trace": "11 §1 errors",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-003",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "X-Locale header changes API labels",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "API.",
  "steps": [
   "GET /permissions-catalog with X-Locale vi then en."
  ],
  "expected": "Labels Vietnamese then English; error messages likewise.",
  "trace": "11 §1 auth header, 12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-004",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Cross-tenant ids return 404 on every new /v3/school endpoint",
  "type": "security",
  "priority": "P0",
  "preconditions": "Two tenants; ids from the other tenant.",
  "steps": [
   "For each new endpoint family (students, guardians, classes, sessions, enrollments, attendance, fee-plans, tuitions, payments, debt, packages, messages, rooms, levels, staff, audit-logs) request a foreign id."
  ],
  "expected": "404 for every one; never 200 or 403; no data leak in the body.",
  "trace": "12 §3 authorisation, test seed 00/10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-005",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Tenant is resolved from the token, not from client ids",
  "type": "security",
  "priority": "P0",
  "preconditions": "API as U-ACAD-A.",
  "steps": [
   "POST /school/students with school_id of branch B (not in scope) and with a school_id from another tenant."
  ],
  "expected": "Branch B: 403 (out of scope); other tenant: 404; nothing created.",
  "trace": "12 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-006",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Jobs pattern for long operations",
  "type": "functional",
  "priority": "P2",
  "preconditions": "API.",
  "steps": [
   "Start an import; GET /v3/school/jobs/{id} repeatedly."
  ],
  "expected": "Status pending → running (progress %) → done with result or error report; UI never blocks.",
  "trace": "11 §1 jobs",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-007",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Domain events published for writes",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Event stub log.",
  "steps": [
   "Change a student status; save attendance; record a payment."
  ],
  "expected": "Events student.status_changed, attendance.saved, payment.recorded appear with tenant_id and ids; messaging and aggregates consume them (message row / dashboard tile updates).",
  "trace": "11 §1 events",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-008",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Import templates per entity download with allowed values",
  "type": "functional",
  "priority": "P0",
  "preconditions": "As U-OWNER /crm/imports (or /settings/data).",
  "steps": [
   "Choose entity Học viên; download template."
  ],
  "expected": "xlsx with the VI header row exactly per PRD 11 (Mã HV, Họ tên, Ngày sinh, Giới tính, Chi nhánh, Trạng thái, Trình độ, Trường, Địa chỉ, PH1 họ tên, PH1 SĐT, PH1 quan hệ, PH1 email, PH1 Zalo đồng ý, PH2 ..., Tài khoản FLYER, Ghi chú), one example row, hidden sheet with enums and branch codes.",
  "trace": "11 §4 templates",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-009",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Column mapping auto-maps by header and allows remap",
  "type": "functional",
  "priority": "P1",
  "preconditions": "A students file with one renamed header.",
  "steps": [
   "Upload; read the mapping step."
  ],
  "expected": "Known headers auto-mapped; the renamed column listed as unmapped with a manual select; custom fields offered for students.",
  "trace": "11 §4 wizard",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-010",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Validation preview with errors, warnings, error file, skip toggle",
  "type": "functional",
  "priority": "P0",
  "preconditions": "Students file: 10 rows, 2 invalid phones, 1 missing birthday, 1 unknown level.",
  "steps": [
   "Run validation."
  ],
  "expected": "Counts: 3 errors (phones and birthday), 1 warning (level created as custom); download error file lists row numbers and reasons; \"Bỏ qua dòng lỗi\" toggle enables commit of 7 rows.",
  "trace": "11 §4 validation preview",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-011",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Phone normalisation rules",
  "type": "functional",
  "priority": "P1",
  "preconditions": "File with phones \"0901234567\", \"+84 901 234 567\", \"84901234567\", \"12345\".",
  "steps": [
   "Validate."
  ],
  "expected": "First three normalise to +84901234567 (and warn about duplicates); \"12345\" is an error for a guardian.",
  "trace": "11 §4 data quality",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-012",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Date parsing: dd/MM/yyyy, yyyy-MM-dd and Excel serial",
  "type": "functional",
  "priority": "P1",
  "preconditions": "File with birthdays \"15/03/2018\", \"2018-03-15\", serial 43174.",
  "steps": [
   "Validate and commit."
  ],
  "expected": "All three become 2018-03-15.",
  "trace": "11 §4 dates",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-013",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Names trimmed and double spaces collapsed, casing preserved",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Name \"  nguyễn   Văn  A \".",
  "steps": [
   "Commit."
  ],
  "expected": "Stored \"nguyễn Văn A\".",
  "trace": "11 §4 names",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-014",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Missing branch code offers create for owner only",
  "type": "functional",
  "priority": "P1",
  "preconditions": "File with branch code \"BD\" (missing).",
  "steps": [
   "Validate as U-OWNER, then as U-ACAD-A."
  ],
  "expected": "Owner: prompt \"Tạo chi nhánh BD?\"; academic admin: error row only.",
  "trace": "11 §4 branch codes",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-015",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Dedupe strategies for students: skip, merge, overwrite",
  "type": "functional",
  "priority": "P0",
  "preconditions": "File containing S1 (same code) with a new address and a second guardian.",
  "steps": [
   "Commit with Bỏ qua; then Gộp; then Ghi đè."
  ],
  "expected": "Skip: S1 untouched. Merge: address filled only if blank, second guardian added, existing fields kept. Overwrite: fields replaced. Guardians merged by phone in all modes.",
  "trace": "11 §4 dedupe",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-016",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Commit runs as a job with progress and result tabs",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Valid file 200 rows.",
  "steps": [
   "Commit."
  ],
  "expected": "Progress bar; success/failed counts; detail page with Thành công and Lỗi tabs; cancel available while pending.",
  "trace": "11 §4 commit",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-017",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Rollback within 24 h when unmodified",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Completed import.",
  "steps": [
   "Click Hoàn tác immediately.",
   "On another import, edit one created student, then Hoàn tác."
  ],
  "expected": "First: created rows deleted, merged rows kept with audit note. Second: refused \"Có bản ghi đã được sửa sau khi nhập\".",
  "trace": "11 §4 rollback",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-018",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Full import order on Empty Center",
  "type": "flow",
  "priority": "P0",
  "preconditions": "Tenant EMPTY; fixture files for steps 1–8.",
  "steps": [
   "Import staff, courses and fee plans, rooms, classes, students and guardians, enrollments, opening balances, attendance history in that order."
  ],
  "expected": "Each step succeeds referencing the previous (teacher by phone, room by name, class by code); classes generate sessions from max(start_date, import date) with earlier sessions held; enrollments with capacity warnings not blocked; opening balances create packages with sessions_total = remaining flagged is_opening and a tuition \"Số dư đầu kỳ\" for debt; attendance history creates sheets marked by \"Import\".",
  "trace": "11 §4 import order",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-019",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Classes import pattern parsing",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Pattern \"T3 18:00-19:30; T5 18:00-19:30\" and an invalid \"Thứ ba 6h\".",
  "steps": [
   "Validate."
  ],
  "expected": "First parses into two weekday rows; second is an error with the row number.",
  "trace": "11 §4 step 4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-020",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Opening balance requires at least one of sessions or debt",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Row with only student code.",
  "steps": [
   "Validate."
  ],
  "expected": "Error \"Cần Buổi còn lại hoặc Còn nợ\".",
  "trace": "11 §4 step 7",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-021",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Attendance history requires an existing session",
  "type": "negative",
  "priority": "P1",
  "preconditions": "Row for a class date with no session.",
  "steps": [
   "Validate."
  ],
  "expected": "Error \"Không có buổi học ngày dd/MM cho lớp X\".",
  "trace": "11 §4 step 8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-022",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Import of 5,000 rows validates within 60 s asynchronously",
  "type": "perf",
  "priority": "P1",
  "preconditions": "5,000-row students file.",
  "steps": [
   "Validate."
  ],
  "expected": "Completes ≤ 60 s; UI responsive during processing; progress shown.",
  "trace": "12 §2 import",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-023",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Import permissions by role",
  "type": "security",
  "priority": "P1",
  "preconditions": "U-BM-A, U-ACC, U-ACAD-A, U-TEACH.",
  "steps": [
   "Open the wizard as each and choose entity."
  ],
  "expected": "BM: all entities; ACC: finance (balances) only; ACAD: students, classes, enrollments; teacher: no access.",
  "trace": "10 §9 settings.data",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-024",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Leads import unchanged",
  "type": "regression",
  "priority": "P1",
  "preconditions": "Leads file.",
  "steps": [
   "Run the existing leads import."
  ],
  "expected": "Works as before with dedupe, round-robin assignment and error report.",
  "trace": "11 §4 bulk-customers existing",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-025",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Identifier formats",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Seed data.",
  "steps": [
   "Read codes on a student, class, receipt, expense."
  ],
  "expected": "HV-000123; MOV-2609-01; PT-Q1-202609-0001; PC-Q1-202609-0001; invoice code = payment_code.",
  "trace": "11 §5 identifiers",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-026",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Retention job anonymises stale leads",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Lead with last activity 25 months ago; nightly job trigger.",
  "steps": [
   "Run the job."
  ],
  "expected": "Lead name/phone anonymised; audit entry; leads with recent activity untouched.",
  "trace": "11 §5 leads retention, 12 §4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-DAT-027",
  "module": "11-data-model-and-api",
  "module_title": "Data model, API and imports",
  "prd": "11",
  "title": "Import wizard in en locale",
  "type": "i18n",
  "priority": "P2",
  "preconditions": "en.",
  "steps": [
   "Open the wizard."
  ],
  "expected": "Steps and messages English; template headers stay Vietnamese (documented) with English key row if provided.",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-001",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "List pages time-to-interactive and API p95",
  "type": "perf",
  "priority": "P1",
  "preconditions": "Big Center, branch A, 4G throttling in Playwright.",
  "steps": [
   "Load /students, /finance/tuition, /attendance with a 50-row page; record trace and API timings."
  ],
  "expected": "TTI ≤ 2.0 s; API p95 ≤ 400 ms per 50-row page.",
  "trace": "12 §2",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-002",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "No client-side filtering of full datasets",
  "type": "perf",
  "priority": "P1",
  "preconditions": "Network log.",
  "steps": [
   "Type a search on /students; change a filter."
  ],
  "expected": "Each change issues a server request with q/filters; no request fetches more than limit rows.",
  "trace": "12 §2 design rules",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-003",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Dashboard initial JS bundle ≤ 350 KB gzipped",
  "type": "perf",
  "priority": "P2",
  "preconditions": "Build output.",
  "steps": [
   "Read next build output for the dashboard route; or measure transferred JS on first load."
  ],
  "expected": "≤ 350 KB gzipped; module routes are code-split.",
  "trace": "12 §2 bundle",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-004",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Health endpoint",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Server running.",
  "steps": [
   "GET /api/health."
  ],
  "expected": "200 with checks for database, queue, object storage (stubbed statuses).",
  "trace": "12 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-005",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Login lockout after 5 failures",
  "type": "security",
  "priority": "P1",
  "preconditions": "Login page.",
  "steps": [
   "Enter a wrong password 5 times for U-ACAD-A; then the correct one."
  ],
  "expected": "6th attempt refused with \"Tài khoản tạm khoá 15 phút\"; correct password rejected until the lock ends.",
  "trace": "12 §3 rate limits",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-006",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Write rate limit 60/min per user",
  "type": "security",
  "priority": "P2",
  "preconditions": "API.",
  "steps": [
   "Send 61 POST requests within a minute."
  ],
  "expected": "61st returns 429 with Retry-After.",
  "trace": "12 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-007",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Session expiry after 30 days inactivity and password change revokes sessions",
  "type": "security",
  "priority": "P2",
  "preconditions": "Two sessions for U-ACAD-A.",
  "steps": [
   "Change the password in session 1.",
   "Use session 2."
  ],
  "expected": "Session 2 rejected (401 → login).",
  "trace": "12 §3 authentication",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-008",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Auth token never reaches third-party origins",
  "type": "security",
  "priority": "P1",
  "preconditions": "Network log on any page.",
  "steps": [
   "Inspect all requests."
  ],
  "expected": "Auth token header only on same-origin /api/v3 proxy calls; no request to external origins carries it.",
  "trace": "12 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-009",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Security headers present",
  "type": "security",
  "priority": "P1",
  "preconditions": "curl.",
  "steps": [
   "curl -I the login page and an authenticated page."
  ],
  "expected": "Content-Security-Policy present without unsafe inline third-party scripts; Strict-Transport-Security; cookies SameSite=Lax; X-Content-Type-Options nosniff.",
  "trace": "12 §3 application security",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-010",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Server-side permission checks (UI hiding is not relied on)",
  "type": "security",
  "priority": "P0",
  "preconditions": "API as U-TEACH.",
  "steps": [
   "Call 10 write endpoints outside the teacher's permissions (create student, create class, pay, void, approve, invite user, change role, delete sheet, create expense, transfer student)."
  ],
  "expected": "All return 403; nothing changes.",
  "trace": "12 §3 authorisation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-011",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Upload security: MIME allow-list, size, signed URLs 15 min",
  "type": "security",
  "priority": "P1",
  "preconditions": "Any upload point.",
  "steps": [
   "Upload .svg renamed .png; 21 MB pdf; open a signed URL after 16 min."
  ],
  "expected": "Content-sniffed rejection; size rejection; expired URL 403.",
  "trace": "12 §3 uploads",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-012",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "OWASP basics: XSS in names and notes",
  "type": "security",
  "priority": "P1",
  "preconditions": "Student name \"<img src=x onerror=alert(1)>\".",
  "steps": [
   "Create the student; view list, profile, attendance sheet, message preview."
  ],
  "expected": "Rendered as text everywhere; no script executes.",
  "trace": "12 §3 ASVS",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-013",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Dependency scan clean of critical CVEs",
  "type": "security",
  "priority": "P2",
  "preconditions": "Repo.",
  "steps": [
   "Run npm audit (or the CI scan)."
  ],
  "expected": "Zero critical.",
  "trace": "12 §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-014",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Consent captured at every guardian entry point",
  "type": "security",
  "priority": "P0",
  "preconditions": "Conversion wizard step 2, student form, guardian add, import, portal.",
  "steps": [
   "Create a guardian through each path with consent ticked."
  ],
  "expected": "A consent row with type, version, granted_at, channel (web/portal/paper or import) and actor exists for each; missing consent means no external channel used.",
  "trace": "12 §4 Art. 11, 12 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-015",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Consent text is tenant-editable with FLYER default",
  "type": "functional",
  "priority": "P2",
  "preconditions": "/settings (tenant or notifications).",
  "steps": [
   "Edit the consent text; open the wizard."
  ],
  "expected": "New text and a new version id shown; older consents keep their version.",
  "trace": "12 §4",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-016",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "No direct messaging to students under 16",
  "type": "security",
  "priority": "P1",
  "preconditions": "S1 (age 8) with own phone field filled.",
  "steps": [
   "Trigger any message."
  ],
  "expected": "Recipient is always a guardian; no message row targets the student.",
  "trace": "12 §4 Art. 20",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-017",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Marketing consent separate from service consent",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Lead form.",
  "steps": [
   "Create a lead with the marketing tick box unticked; then ticked."
  ],
  "expected": "marketing_consent_at null then set; bulk ZNS marketing skips the first.",
  "trace": "12 §4 marketing consent",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-018",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Anonymise keeps finance for 10 years",
  "type": "security",
  "priority": "P1",
  "preconditions": "S5 anonymised with a paid invoice.",
  "steps": [
   "Open the invoice and cash book."
  ],
  "expected": "Rows intact with \"Học viên đã xoá\"; PDF receipt still retrievable.",
  "trace": "12 §4 deletion",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-019",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Cross-border disclosure in consent text",
  "type": "functional",
  "priority": "P3",
  "preconditions": "Default consent text.",
  "steps": [
   "Read it."
  ],
  "expected": "Mentions storage in Singapore (ap-southeast-1).",
  "trace": "12 §4 cross-border",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-020",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Mandatory audited actions all write entries",
  "type": "security",
  "priority": "P0",
  "preconditions": "Perform each: finance write, attendance edit after day, student status change, guardian contact change, permission change, personal-data export, login from a new device.",
  "steps": [
   "Open the audit log after each."
  ],
  "expected": "One entry per action with actor snapshot, action code, entity, before/after, ip, user_agent, request_id; money and status fields always included.",
  "trace": "12 §6, 12 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-021",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Audit export per tenant as CSV",
  "type": "functional",
  "priority": "P2",
  "preconditions": "Owner.",
  "steps": [
   "Export the audit log."
  ],
  "expected": "CSV with all columns; itself logged as an export.",
  "trace": "12 §6",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-022",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Restore drill report available",
  "type": "functional",
  "priority": "P3",
  "preconditions": "Release note.",
  "steps": [
   "Ask for the restore drill evidence."
  ],
  "expected": "Written report for the release week snapshot exists (out of scope for local run; record N/A with reason).",
  "trace": "12 §7, 12 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-023",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Date and time formats",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "Any list.",
  "steps": [
   "Read dates and times in vi and en."
  ],
  "expected": "dd/MM/yyyy and HH:mm 24-hour in both; timezone Asia/Ho_Chi_Minh; API stores UTC.",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-024",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Vietnamese collation in sorting",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "Students \"Đặng\", \"Dương\", \"Đinh\", \"Doãn\".",
  "steps": [
   "Sort by name."
  ],
  "expected": "Order Doãn, Dương, Đặng, Đinh (D before Đ, accents ignored for equal base) per ICU vi.",
  "trace": "12 §8 collation",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-025",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Accent-insensitive search",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "/students.",
  "steps": [
   "Search \"tran chi\"."
  ],
  "expected": "Finds \"Trần Chi\".",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-026",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "No raw i18n keys anywhere",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "Crawl all routes in vi and en.",
  "steps": [
   "Grep page text for patterns like \"[a-z]+\\.[a-z]+\\.[a-z]+\" that match message keys."
  ],
  "expected": "Zero matches.",
  "trace": "12 §8",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-027",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Hard-coded English removed from settings and form bodies",
  "type": "i18n",
  "priority": "P1",
  "preconditions": "vi locale.",
  "steps": [
   "Open every settings page and every form drawer."
  ],
  "expected": "No English UI strings in vi (except brand names and codes).",
  "trace": "00 §6 inconsistency",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-028",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Large font toggle scales the console 110%",
  "type": "a11y",
  "priority": "P3",
  "preconditions": "Tenant setting \"Cỡ chữ lớn\".",
  "steps": [
   "Enable it."
  ],
  "expected": "Base font 13.5 → ~14.9 px; layout intact.",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-029",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "axe run with zero serious violations on main screens",
  "type": "a11y",
  "priority": "P1",
  "preconditions": "Playwright + axe-core.",
  "steps": [
   "Run on /dashboard, /students, /students/:id, /classes, /classes/:id, /attendance, sheet, /finance/tuition, /finance/debt, /settings/users."
  ],
  "expected": "Zero serious or critical violations.",
  "trace": "12 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-030",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Contrast ≥ 4.5:1 for text",
  "type": "a11y",
  "priority": "P1",
  "preconditions": "Any page.",
  "steps": [
   "Check ink and secondary text against backgrounds."
  ],
  "expected": "Ink and muted colours pass 4.5:1 on white and on #fafafa; chip text passes on chip backgrounds. Note: PRD 12 cites #212B36/#637381 while the Cloud reference uses #1b1b1f/#71717a; either passes; record which is used.",
  "trace": "12 §9, Cloud theme §3",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-031",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Keyboard walkthrough of the daily loop",
  "type": "a11y",
  "priority": "P1",
  "preconditions": "Keyboard only.",
  "steps": [
   "Create a student, enroll, open attendance sheet and save, create an invoice, record a payment."
  ],
  "expected": "Every step completable with keyboard; focus ring always visible; no trap; modals return focus to the trigger on close.",
  "trace": "12 §9, 12 §11",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": true,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-032",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Tables have proper headers and forms have labels",
  "type": "a11y",
  "priority": "P1",
  "preconditions": "Any table and form.",
  "steps": [
   "Inspect th scope and label/for or aria-labelledby."
  ],
  "expected": "All columns have th; all inputs labelled; error text linked by aria-describedby.",
  "trace": "12 §9",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-033",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Browser matrix",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "Chrome, Edge, Safari, Firefox latest two.",
  "steps": [
   "Smoke the daily loop in each (Playwright channels where available)."
  ],
  "expected": "No layout or functional differences.",
  "trace": "12 §9 browsers",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-034",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Feature flag off hides P2 modules cleanly",
  "type": "functional",
  "priority": "P1",
  "preconditions": "Flags.",
  "steps": [
   "Turn inventory off."
  ],
  "expected": "Nav hides Kho; /inventory shows \"Tính năng chưa bật\"; no console errors.",
  "trace": "12 §10 feature flags",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-035",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Logs contain no personal data",
  "type": "security",
  "priority": "P1",
  "preconditions": "Server log stub.",
  "steps": [
   "Create a guardian; send a message; grep logs for the phone and name."
  ],
  "expected": "No matches (masked).",
  "trace": "12 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-036",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Error tracking code shown to user",
  "type": "ui-ux",
  "priority": "P2",
  "preconditions": "Force an API 500.",
  "steps": [
   "Observe the UI."
  ],
  "expected": "Toast/page with code TS-5xxx and Thử lại; no raw error JSON.",
  "trace": "12 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-037",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Analytics events exclude student personal data",
  "type": "security",
  "priority": "P2",
  "preconditions": "Analytics stub.",
  "steps": [
   "Create a student; save attendance."
  ],
  "expected": "Payloads contain ids and buckets, no names or phones.",
  "trace": "12 §10",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-NFR-038",
  "module": "12-non-functional",
  "module_title": "Non-functional requirements",
  "prd": "12",
  "title": "Maintenance banner announces windows",
  "type": "functional",
  "priority": "P3",
  "preconditions": "Set a maintenance window in the stub.",
  "steps": [
   "Load any page."
  ],
  "expected": "In-app banner with the window time appears 48 h ahead.",
  "trace": "12 §2 availability",
  "data": "Fixtures below",
  "phase": "P1",
  "regression": false,
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-01",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Lead to first payment (the daily loop)",
  "type": "flow",
  "priority": "P0",
  "preconditions": "Tenant BIG; class C1 with 1 free seat and 2 trial seats; FP3 package plan on Movers 2; ZNS templates approved.",
  "steps": [
   "U-ADM imports a 1-row xlsx creating lead \"Ngô Thị Lan\" 0977123456 with prospect \"Ngô Gia Hân\" 2018.",
   "U-ADM logs a call outcome Yêu cầu gọi lại at tomorrow 10:00.",
   "U-ADM books a trial into the next C1 session with Gửi xác nhận Zalo on.",
   "U-TEACH marks the trial present on the phone and picks \"Phù hợp\".",
   "U-ADM creates a placement test and submits 72 overall.",
   "U-ADM runs Chuyển đổi: new learner, guardian consent Zalo, class C1 start next session, FP3, no discount, Tạo hoá đơn ngay.",
   "SePay stub posts a transfer with the invoice payment code, 6,000,000.",
   "U-TEACH marks the next C1 session with the new student present.",
   "U-OWNER opens /dashboard this month."
  ],
  "expected": "1. Import job done 1/1; lead visible in /crm/customers with Học viên dự kiến \"Ngô Gia Hân (2018)\". | 2. Row appears in /crm/schedules → Gọi lại under \"Sắp tới\". | 3. Booking Đã đặt; trial_confirm message row sent (stub); session roster shows the prospect with Học thử chip; trial seats 1/2. | 4. Booking Đã học; U-ADM notification \"Học thử xong: Ngô Gia Hân (Movers 2A)\". | 5. Status Hoàn thành; Đề xuất lớp Movers. | 6. Result cards Học viên · Lớp · Hoá đơn; student Đang học; C1 12/12; invoice 6,000,000 unpaid with QR; lead won. | 7. Payment auto-created with SePay id; receipt PT-Q1-YYYYMM-#### issued; invoice Đã thanh toán; receipt ZNS to the guardian; package credited 24 sessions with expiry +6 months. | 8. Balance 23; session_consumption row; attendance rate 100% on the profile. | 9. Thực thu includes 6,000,000; Nhập học mới +1; Học viên đang học +1; Chuyển đổi +1; all four drill-downs list the new records.",
  "trace": "Linked: TS-ADM-001, TS-ADM-012, TS-ADM-023, TS-ADM-029, TS-ADM-036, TS-ADM-051, TS-FIN-031, TS-FIN-010, TS-FIN-041, TS-REP-004",
  "data": "U-ADM, U-ACAD-A, U-TEACH, U-ACC, G (guardian of the new student), SePay stub, Zalo stub",
  "phase": "P1",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-ADM imports a 1-row xlsx creating lead \"Ngô Thị Lan\" 0977123456 with prospect \"Ngô Gia Hân\" 2018.",
    "checkpoint": "Import job done 1/1; lead visible in /crm/customers with Học viên dự kiến \"Ngô Gia Hân (2018)\"."
   },
   {
    "action": "U-ADM logs a call outcome Yêu cầu gọi lại at tomorrow 10:00.",
    "checkpoint": "Row appears in /crm/schedules → Gọi lại under \"Sắp tới\"."
   },
   {
    "action": "U-ADM books a trial into the next C1 session with Gửi xác nhận Zalo on.",
    "checkpoint": "Booking Đã đặt; trial_confirm message row sent (stub); session roster shows the prospect with Học thử chip; trial seats 1/2."
   },
   {
    "action": "U-TEACH marks the trial present on the phone and picks \"Phù hợp\".",
    "checkpoint": "Booking Đã học; U-ADM notification \"Học thử xong: Ngô Gia Hân (Movers 2A)\"."
   },
   {
    "action": "U-ADM creates a placement test and submits 72 overall.",
    "checkpoint": "Status Hoàn thành; Đề xuất lớp Movers."
   },
   {
    "action": "U-ADM runs Chuyển đổi: new learner, guardian consent Zalo, class C1 start next session, FP3, no discount, Tạo hoá đơn ngay.",
    "checkpoint": "Result cards Học viên · Lớp · Hoá đơn; student Đang học; C1 12/12; invoice 6,000,000 unpaid with QR; lead won."
   },
   {
    "action": "SePay stub posts a transfer with the invoice payment code, 6,000,000.",
    "checkpoint": "Payment auto-created with SePay id; receipt PT-Q1-YYYYMM-#### issued; invoice Đã thanh toán; receipt ZNS to the guardian; package credited 24 sessions with expiry +6 months."
   },
   {
    "action": "U-TEACH marks the next C1 session with the new student present.",
    "checkpoint": "Balance 23; session_consumption row; attendance rate 100% on the profile."
   },
   {
    "action": "U-OWNER opens /dashboard this month.",
    "checkpoint": "Thực thu includes 6,000,000; Nhập học mới +1; Học viên đang học +1; Chuyển đổi +1; all four drill-downs list the new records."
   }
  ],
  "links": [
   "TS-ADM-001",
   "TS-ADM-012",
   "TS-ADM-023",
   "TS-ADM-029",
   "TS-ADM-036",
   "TS-ADM-051",
   "TS-FIN-031",
   "TS-FIN-010",
   "TS-FIN-041",
   "TS-REP-004"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-02",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Walk-in student without a lead",
  "type": "flow",
  "priority": "P0",
  "preconditions": "Tenant BIG; C4 scheduled with seats; FP1 on its course.",
  "steps": [
   "U-ACAD-A creates the student from /students/new with guardian and new FLYER account.",
   "U-ACAD-A clicks Xếp lớp → C4, start = class start date, FP1.",
   "U-ACC creates the invoice from the profile (Tạo hoá đơn) and records a partial cash payment 4,000,000.",
   "U-ACC opens Công nợ after moving the due date to yesterday (or wait)."
  ],
  "expected": "1. Credentials shown once; status Chờ xếp lớp; no lead created; guardian consent recorded. | 2. Status Đang học; C4 1/12; enrollment row in Lớp & lịch sử. | 3. Invoice Thanh toán một phần, remaining 5,600,000; receipt numbered; cash book entry. | 4. Student listed in bucket 0–7 with remaining 5,600,000; Gửi nhắc works and updates Nhắc gần nhất.",
  "trace": "Linked: TS-STU-012, TS-STU-030, TS-FIN-004, TS-FIN-022, TS-FIN-037",
  "data": "U-ACAD-A, U-ACC",
  "phase": "P1",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-ACAD-A creates the student from /students/new with guardian and new FLYER account.",
    "checkpoint": "Credentials shown once; status Chờ xếp lớp; no lead created; guardian consent recorded."
   },
   {
    "action": "U-ACAD-A clicks Xếp lớp → C4, start = class start date, FP1.",
    "checkpoint": "Status Đang học; C4 1/12; enrollment row in Lớp & lịch sử."
   },
   {
    "action": "U-ACC creates the invoice from the profile (Tạo hoá đơn) and records a partial cash payment 4,000,000.",
    "checkpoint": "Invoice Thanh toán một phần, remaining 5,600,000; receipt numbered; cash book entry."
   },
   {
    "action": "U-ACC opens Công nợ after moving the due date to yesterday (or wait).",
    "checkpoint": "Student listed in bucket 0–7 with remaining 5,600,000; Gửi nhắc works and updates Nhắc gần nhất."
   }
  ],
  "links": [
   "TS-STU-012",
   "TS-STU-030",
   "TS-FIN-004",
   "TS-FIN-022",
   "TS-FIN-037"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-03",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Pause and return",
  "type": "flow",
  "priority": "P0",
  "preconditions": "S1 active in C1 with package expiry E and 5 sessions.",
  "steps": [
   "U-ACAD-A pauses S1 for 30 days with reason.",
   "U-TEACH opens a sheet inside the pause window.",
   "U-ACAD-A resumes S1 into C2 (a different class with a seat) on day 31.",
   "U-ACC opens the finance report deferred revenue.",
   "U-BM-A checks the timesheet preview (P2) or session list for the pause window."
  ],
  "expected": "1. Status Bảo lưu; C1 sessions in the window no longer list S1 on the sheet; reminder task 7 days before return in the today plan. | 2. S1 absent from the roster; counters exclude S1. | 3. Status Đang học; new enrollment in C2; package expiry E + 30 (or E + actual paused days); balance still 5. | 4. S1 unused sessions still counted; no revenue change. | 5. Teacher sessions unaffected by the pause.",
  "trace": "Linked: TS-STU-033, TS-STU-036, TS-FIN-044, TS-FIN-055, TS-ATT-002",
  "data": "U-ACAD-A, U-TEACH, U-BM-A",
  "phase": "P1",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-ACAD-A pauses S1 for 30 days with reason.",
    "checkpoint": "Status Bảo lưu; C1 sessions in the window no longer list S1 on the sheet; reminder task 7 days before return in the today plan."
   },
   {
    "action": "U-TEACH opens a sheet inside the pause window.",
    "checkpoint": "S1 absent from the roster; counters exclude S1."
   },
   {
    "action": "U-ACAD-A resumes S1 into C2 (a different class with a seat) on day 31.",
    "checkpoint": "Status Đang học; new enrollment in C2; package expiry E + 30 (or E + actual paused days); balance still 5."
   },
   {
    "action": "U-ACC opens the finance report deferred revenue.",
    "checkpoint": "S1 unused sessions still counted; no revenue change."
   },
   {
    "action": "U-BM-A checks the timesheet preview (P2) or session list for the pause window.",
    "checkpoint": "Teacher sessions unaffected by the pause."
   }
  ],
  "links": [
   "TS-STU-033",
   "TS-STU-036",
   "TS-FIN-044",
   "TS-FIN-055",
   "TS-ATT-002"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-04",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Withdrawal with refund",
  "type": "flow",
  "priority": "P0",
  "preconditions": "S1 active; 5 sessions unused on a paid package; admin fee 10%; refund threshold 5,000,000.",
  "steps": [
   "U-ACAD-A withdraws S1, reason \"chi phí\".",
   "U-ACC follows the link and creates the credit note with the proposed amount (P2).",
   "U-ACC pays the refund in cash from CD-A to Trần Thị Hoa (P2).",
   "U-ACC runs Chốt quỹ ngày (P2).",
   "U-OWNER opens the churn report / dashboard Nghỉ học.",
   "G1 opens the portal (P2)."
  ],
  "expected": "1. Status Đã nghỉ; enrollments ended today; dialog shows unused 5 and estimate 1,125,000 with link Tạo phiếu hoàn. | 2. Credit shown on the invoice; approval not required (< threshold). | 3. PC-Q1-... entry in cash book as payment out; S1 balance 0; package status refunded. | 4. Expected cash reflects the refund; difference 0 when counted correctly. | 5. S1 listed with reason \"chi phí\" and refund amount; Nghỉ học tile +1. | 6. Invoice shows the credit and the refund receipt.",
  "trace": "Linked: TS-STU-038, TS-FIN-046, TS-FIN-047, TS-FIN-051, TS-REP-006, TS-COM-026",
  "data": "U-ACAD-A, U-ACC, U-BM-A, G1",
  "phase": "P1/P2",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-ACAD-A withdraws S1, reason \"chi phí\".",
    "checkpoint": "Status Đã nghỉ; enrollments ended today; dialog shows unused 5 and estimate 1,125,000 with link Tạo phiếu hoàn."
   },
   {
    "action": "U-ACC follows the link and creates the credit note with the proposed amount (P2).",
    "checkpoint": "Credit shown on the invoice; approval not required (< threshold)."
   },
   {
    "action": "U-ACC pays the refund in cash from CD-A to Trần Thị Hoa (P2).",
    "checkpoint": "PC-Q1-... entry in cash book as payment out; S1 balance 0; package status refunded."
   },
   {
    "action": "U-ACC runs Chốt quỹ ngày (P2).",
    "checkpoint": "Expected cash reflects the refund; difference 0 when counted correctly."
   },
   {
    "action": "U-OWNER opens the churn report / dashboard Nghỉ học.",
    "checkpoint": "S1 listed with reason \"chi phí\" and refund amount; Nghỉ học tile +1."
   },
   {
    "action": "G1 opens the portal (P2).",
    "checkpoint": "Invoice shows the credit and the refund receipt."
   }
  ],
  "links": [
   "TS-STU-038",
   "TS-FIN-046",
   "TS-FIN-047",
   "TS-FIN-051",
   "TS-REP-006",
   "TS-COM-026"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-05",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Teacher substitution",
  "type": "flow",
  "priority": "P0",
  "preconditions": "SES-TODAY planned with T1 main.",
  "steps": [
   "U-ACAD-A assigns T2 as substitute.",
   "T2 opens the sheet and saves with one absence.",
   "T1 opens the same sheet (stale) and tries to save.",
   "U-ACC generates the month timesheet (P2)."
  ],
  "expected": "1. Session shows T1 + dạy thay T2; T2 sees it in /teach/today; T1 sees a substitute marker. | 2. Sheet marked_by T2; absence message sent to the guardian. | 3. 409 STALE_VERSION; banner \"Buổi này đã được T2 điểm danh lúc HH:mm\". | 4. T2 timesheet has the session; T1 timesheet excludes it (pay_main_when_substituted off).",
  "trace": "Linked: TS-CLS-021, TS-ATT-015, TS-ATT-010, TS-STF-008",
  "data": "U-ACAD-A, U-TEACH (T1), U-TEACH2 (T2), U-ACC",
  "phase": "P1/P2",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-ACAD-A assigns T2 as substitute.",
    "checkpoint": "Session shows T1 + dạy thay T2; T2 sees it in /teach/today; T1 sees a substitute marker."
   },
   {
    "action": "T2 opens the sheet and saves with one absence.",
    "checkpoint": "Sheet marked_by T2; absence message sent to the guardian."
   },
   {
    "action": "T1 opens the same sheet (stale) and tries to save.",
    "checkpoint": "409 STALE_VERSION; banner \"Buổi này đã được T2 điểm danh lúc HH:mm\"."
   },
   {
    "action": "U-ACC generates the month timesheet (P2).",
    "checkpoint": "T2 timesheet has the session; T1 timesheet excludes it (pay_main_when_substituted off)."
   }
  ],
  "links": [
   "TS-CLS-021",
   "TS-ATT-015",
   "TS-ATT-010",
   "TS-STF-008"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-06",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Branch transfer of a student with package balance",
  "type": "flow",
  "priority": "P1",
  "preconditions": "S1 at A in C1 with 5 sessions; C3 at B with a seat; G1 has S2 at B.",
  "steps": [
   "U-OWNER transfers S1 to Thủ Đức effective Monday into C3.",
   "U-TEACH opens the next C3 sheet at B.",
   "U-OWNER opens the finance report for A and B.",
   "U-BM-A (branch A) opens /students.",
   "G1 opens the portal (P2)."
  ],
  "expected": "1. A record: enrollment ended, balance 0, Học phí entry \"chuyển đi\"; B record created with balance 5 and \"chuyển đến\"; guardians shared (G1 now has 3 links). | 2. S1 on the roster; marking present consumes from the transferred package (4 left). | 3. A revenue unchanged; B shows deferred revenue for the package with an inter-branch note. | 4. S1 A record visible with status Đã chuyển/Đã nghỉ marker; the B record is not visible (404 by URL). | 5. Both S1 histories (A and B) visible under the child.",
  "trace": "Linked: TS-STU-041, TS-FIN-065, TS-OVW-012, TS-STU-025",
  "data": "U-OWNER, U-TEACH, G1",
  "phase": "P1/P2",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-OWNER transfers S1 to Thủ Đức effective Monday into C3.",
    "checkpoint": "A record: enrollment ended, balance 0, Học phí entry \"chuyển đi\"; B record created with balance 5 and \"chuyển đến\"; guardians shared (G1 now has 3 links)."
   },
   {
    "action": "U-TEACH opens the next C3 sheet at B.",
    "checkpoint": "S1 on the roster; marking present consumes from the transferred package (4 left)."
   },
   {
    "action": "U-OWNER opens the finance report for A and B.",
    "checkpoint": "A revenue unchanged; B shows deferred revenue for the package with an inter-branch note."
   },
   {
    "action": "U-BM-A (branch A) opens /students.",
    "checkpoint": "S1 A record visible with status Đã chuyển/Đã nghỉ marker; the B record is not visible (404 by URL)."
   },
   {
    "action": "G1 opens the portal (P2).",
    "checkpoint": "Both S1 histories (A and B) visible under the child."
   }
  ],
  "links": [
   "TS-STU-041",
   "TS-FIN-065",
   "TS-OVW-012",
   "TS-STU-025"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-07",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Multi-branch user day",
  "type": "flow",
  "priority": "P1",
  "preconditions": "BIG tenant; T1 teaches C1 (A) and C3 (B).",
  "steps": [
   "U-OWNER at 1440 px opens /dashboard with Tất cả chi nhánh, then switches to Thủ Đức.",
   "U-OWNER opens /timetable by teacher.",
   "U-TEACH on a 390 px phone opens /teach/today.",
   "U-TEACH marks C3 at B then C1 at A within the same day.",
   "U-TEACH opens /students."
  ],
  "expected": "1. Tiles refresh; Theo chi nhánh table has 3 rows; after switch only B numbers; the choice persists to /students. | 2. T1 column shows both C1 and C3 blocks across branches (teacher view is cross-branch). | 3. Both today sessions listed with branch labels; the bottom tab bar present. | 4. Both sheets saved; each notifies guardians with the correct {chi_nhanh} and branch phone. | 5. Only C1 and C3 students; branch filter limited to A and B.",
  "trace": "Linked: TS-OVW-008, TS-OVW-009, TS-OVW-010, TS-CLS-033, TS-ATT-001, TS-STU-010",
  "data": "U-OWNER, U-TEACH",
  "phase": "P1",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-OWNER at 1440 px opens /dashboard with Tất cả chi nhánh, then switches to Thủ Đức.",
    "checkpoint": "Tiles refresh; Theo chi nhánh table has 3 rows; after switch only B numbers; the choice persists to /students."
   },
   {
    "action": "U-OWNER opens /timetable by teacher.",
    "checkpoint": "T1 column shows both C1 and C3 blocks across branches (teacher view is cross-branch)."
   },
   {
    "action": "U-TEACH on a 390 px phone opens /teach/today.",
    "checkpoint": "Both today sessions listed with branch labels; the bottom tab bar present."
   },
   {
    "action": "U-TEACH marks C3 at B then C1 at A within the same day.",
    "checkpoint": "Both sheets saved; each notifies guardians with the correct {chi_nhanh} and branch phone."
   },
   {
    "action": "U-TEACH opens /students.",
    "checkpoint": "Only C1 and C3 students; branch filter limited to A and B."
   }
  ],
  "links": [
   "TS-OVW-008",
   "TS-OVW-009",
   "TS-OVW-010",
   "TS-CLS-033",
   "TS-ATT-001",
   "TS-STU-010"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-08",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Role escalation attempts",
  "type": "flow",
  "priority": "P0",
  "preconditions": "BIG tenant; second tenant with known ids.",
  "steps": [
   "U-TEACH opens /settings/users and /settings/roles by URL.",
   "U-TEACH calls PUT /v3/crm/roles/{teacher role}/permissions granting all.",
   "U-TEACH calls PUT /v3/crm/sale-profiles/{self}/branches adding branch C.",
   "U-ADM calls POST /v3/school/students directly.",
   "U-VIEWER attempts drag on Kanban and POST /tuitions/{id}/pay.",
   "G1 (portal token) calls GET /v3/school/students.",
   "U-OWNER of BIG requests 10 foreign-tenant ids across modules.",
   "U-OWNER changes U-TEACH role to branch_manager, then back."
  ],
  "expected": "1. 403 page with code; nothing rendered from the pages. | 2. 403; role unchanged (verify as owner). | 3. 403; branch switcher unchanged after reload. | 4. 403 (create only via convert). | 5. Drag refused; 403. | 6. 401/403; portal tokens cannot reach staff endpoints. | 7. 404 for all. | 8. Two audit entries; U-TEACH receives in-app notification each time; effective on next request.",
  "trace": "Linked: TS-OVW-016, TS-OVW-018, TS-NFR-010, TS-DAT-004, TS-SET-019, TS-SET-002, TS-COM-029",
  "data": "U-TEACH, U-ADM, U-VIEWER, G1 (parent), a foreign-tenant id list",
  "phase": "P1",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-TEACH opens /settings/users and /settings/roles by URL.",
    "checkpoint": "403 page with code; nothing rendered from the pages."
   },
   {
    "action": "U-TEACH calls PUT /v3/crm/roles/{teacher role}/permissions granting all.",
    "checkpoint": "403; role unchanged (verify as owner)."
   },
   {
    "action": "U-TEACH calls PUT /v3/crm/sale-profiles/{self}/branches adding branch C.",
    "checkpoint": "403; branch switcher unchanged after reload."
   },
   {
    "action": "U-ADM calls POST /v3/school/students directly.",
    "checkpoint": "403 (create only via convert)."
   },
   {
    "action": "U-VIEWER attempts drag on Kanban and POST /tuitions/{id}/pay.",
    "checkpoint": "Drag refused; 403."
   },
   {
    "action": "G1 (portal token) calls GET /v3/school/students.",
    "checkpoint": "401/403; portal tokens cannot reach staff endpoints."
   },
   {
    "action": "U-OWNER of BIG requests 10 foreign-tenant ids across modules.",
    "checkpoint": "404 for all."
   },
   {
    "action": "U-OWNER changes U-TEACH role to branch_manager, then back.",
    "checkpoint": "Two audit entries; U-TEACH receives in-app notification each time; effective on next request."
   }
  ],
  "links": [
   "TS-OVW-016",
   "TS-OVW-018",
   "TS-NFR-010",
   "TS-DAT-004",
   "TS-SET-019",
   "TS-SET-002",
   "TS-COM-029"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-09",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Month close",
  "type": "flow",
  "priority": "P1",
  "preconditions": "C3 monthly students; overdue invoices; two branches with cash drawers.",
  "steps": [
   "U-ACC runs Lập hoá đơn tháng for all monthly classes with proration preview.",
   "U-ACC bulk-sends reminders from Công nợ for buckets > 7 days.",
   "SePay stub delivers 3 transfers: 2 with codes, 1 without.",
   "U-ACC records 2 cash payments at A and 1 at B, then Chốt quỹ at each branch (P2).",
   "U-BM-A approves timesheets; U-ACC exports payroll (P2).",
   "U-OWNER opens the executive dashboard for the month."
  ],
  "expected": "1. Preview per student with prorated amounts; commit creates one GroupInvoice per run and Tuition per enrollment; notifications sent per channel. | 2. Per-channel outcomes; Nhắc gần nhất updated; skipped reasons for guardians without consent. | 3. 2 auto-matched with receipts; 1 in Chưa đối soát; U-ACC matches it. | 4. Receipt numbers sequential per branch; cash close per branch with difference 0. | 5. xlsx exported; export logged. | 6. Thực thu = Σ cash book Thu across branches; Theo chi nhánh rows sum to the top tiles; Công nợ quá hạn equals /finance/debt total.",
  "trace": "Linked: TS-FIN-019, TS-FIN-038, TS-FIN-031, TS-FIN-032, TS-FIN-025, TS-FIN-051, TS-STF-011, TS-REP-004",
  "data": "U-ACC, U-BM-A, U-OWNER, SePay stub",
  "phase": "P1/P2",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-ACC runs Lập hoá đơn tháng for all monthly classes with proration preview.",
    "checkpoint": "Preview per student with prorated amounts; commit creates one GroupInvoice per run and Tuition per enrollment; notifications sent per channel."
   },
   {
    "action": "U-ACC bulk-sends reminders from Công nợ for buckets > 7 days.",
    "checkpoint": "Per-channel outcomes; Nhắc gần nhất updated; skipped reasons for guardians without consent."
   },
   {
    "action": "SePay stub delivers 3 transfers: 2 with codes, 1 without.",
    "checkpoint": "2 auto-matched with receipts; 1 in Chưa đối soát; U-ACC matches it."
   },
   {
    "action": "U-ACC records 2 cash payments at A and 1 at B, then Chốt quỹ at each branch (P2).",
    "checkpoint": "Receipt numbers sequential per branch; cash close per branch with difference 0."
   },
   {
    "action": "U-BM-A approves timesheets; U-ACC exports payroll (P2).",
    "checkpoint": "xlsx exported; export logged."
   },
   {
    "action": "U-OWNER opens the executive dashboard for the month.",
    "checkpoint": "Thực thu = Σ cash book Thu across branches; Theo chi nhánh rows sum to the top tiles; Công nợ quá hạn equals /finance/debt total."
   }
  ],
  "links": [
   "TS-FIN-019",
   "TS-FIN-038",
   "TS-FIN-031",
   "TS-FIN-032",
   "TS-FIN-025",
   "TS-FIN-051",
   "TS-STF-011",
   "TS-REP-004"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-10",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Class lifecycle",
  "type": "flow",
  "priority": "P0",
  "preconditions": "Course Movers 2 with FP1; rooms P.201/P.202; tenant holidays include one date in range.",
  "steps": [
   "U-ACAD-A creates a course \"Flyers 1\" and then a class from it with T3/T5 18:00, room P.202, T2, capacity 3, 10 sessions, one holiday in range.",
   "U-ACAD-A edits the class to room P.201 (conflict with C1).",
   "U-ACAD-A enrolls 3 students then a 4th.",
   "U-ACAD-A cancels a session 10 hours ahead with make-up appended.",
   "U-TEACH marks sessions until the last one is held.",
   "U-ACAD-A closes the class."
  ],
  "expected": "1. Preview 10 sessions skipping the holiday; created with \"Tạo lớp và 10 buổi\". | 2. Conflict chip; no Vẫn lưu; U-BM-A can override and the audit shows session_conflict_overridden. | 3. 3/3 Đầy; 4th goes to the waiting list. | 4. cancelled_late; make-up session appended; guardians notified; still 10 non-cancelled sessions. | 5. Tiến độ 10/10. | 6. Đã kết thúc; students with no other enrollment Hoàn thành; class in tab Đã kết thúc.",
  "trace": "Linked: TS-CLS-001, TS-CLS-005, TS-CLS-010, TS-CLS-023, TS-CLS-019, TS-CLS-039",
  "data": "U-ACAD-A, U-BM-A, U-TEACH",
  "phase": "P1",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-ACAD-A creates a course \"Flyers 1\" and then a class from it with T3/T5 18:00, room P.202, T2, capacity 3, 10 sessions, one holiday in range.",
    "checkpoint": "Preview 10 sessions skipping the holiday; created with \"Tạo lớp và 10 buổi\"."
   },
   {
    "action": "U-ACAD-A edits the class to room P.201 (conflict with C1).",
    "checkpoint": "Conflict chip; no Vẫn lưu; U-BM-A can override and the audit shows session_conflict_overridden."
   },
   {
    "action": "U-ACAD-A enrolls 3 students then a 4th.",
    "checkpoint": "3/3 Đầy; 4th goes to the waiting list."
   },
   {
    "action": "U-ACAD-A cancels a session 10 hours ahead with make-up appended.",
    "checkpoint": "cancelled_late; make-up session appended; guardians notified; still 10 non-cancelled sessions."
   },
   {
    "action": "U-TEACH marks sessions until the last one is held.",
    "checkpoint": "Tiến độ 10/10."
   },
   {
    "action": "U-ACAD-A closes the class.",
    "checkpoint": "Đã kết thúc; students with no other enrollment Hoàn thành; class in tab Đã kết thúc."
   }
  ],
  "links": [
   "TS-CLS-001",
   "TS-CLS-005",
   "TS-CLS-010",
   "TS-CLS-023",
   "TS-CLS-019",
   "TS-CLS-039"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-11",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Absence follow-up loop",
  "type": "flow",
  "priority": "P1",
  "preconditions": "S1 in C1; thresholds 2 consecutive.",
  "steps": [
   "G1 informs the desk; U-ACAD-A adds an excuse for the next session with make-up offered.",
   "U-TEACH marks S1 absent on the following two sessions.",
   "Care owner logs a call from the task.",
   "U-ACAD-A books a make-up for the excused absence into C2.",
   "U-TEACH of C2 marks S1 present on the make-up."
  ],
  "expected": "1. Sheet pre-marks Có phép; G1 confirmation message; make-up suggestion on the profile. | 2. Absent ZNS each time within 2 min; after the second, task \"Liên hệ phụ huynh: Nguyễn An vắng 2 buổi\" for the care owner; S1 in tab Cần quan tâm. | 3. Task done; activity on the student timeline. | 4. Học bù chip on the C2 session; S1 absences table shows Đã đặt học bù. | 5. Booking attended; no package consumed (policy); attendance rate excludes the excused session.",
  "trace": "Linked: TS-ATT-018, TS-ATT-022, TS-ATT-040, TS-CLS-029, TS-ATT-044",
  "data": "U-ACAD-A, U-TEACH, G1, U-ADM (care owner)",
  "phase": "P1",
  "regression": true,
  "checkpoints": [
   {
    "action": "G1 informs the desk; U-ACAD-A adds an excuse for the next session with make-up offered.",
    "checkpoint": "Sheet pre-marks Có phép; G1 confirmation message; make-up suggestion on the profile."
   },
   {
    "action": "U-TEACH marks S1 absent on the following two sessions.",
    "checkpoint": "Absent ZNS each time within 2 min; after the second, task \"Liên hệ phụ huynh: Nguyễn An vắng 2 buổi\" for the care owner; S1 in tab Cần quan tâm."
   },
   {
    "action": "Care owner logs a call from the task.",
    "checkpoint": "Task done; activity on the student timeline."
   },
   {
    "action": "U-ACAD-A books a make-up for the excused absence into C2.",
    "checkpoint": "Học bù chip on the C2 session; S1 absences table shows Đã đặt học bù."
   },
   {
    "action": "U-TEACH of C2 marks S1 present on the make-up.",
    "checkpoint": "Booking attended; no package consumed (policy); attendance rate excludes the excused session."
   }
  ],
  "links": [
   "TS-ATT-018",
   "TS-ATT-022",
   "TS-ATT-040",
   "TS-CLS-029",
   "TS-ATT-044"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-12",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Onboarding an empty tenant to first attendance",
  "type": "flow",
  "priority": "P0",
  "preconditions": "Tenant EMPTY; fixture files for import steps 1–6.",
  "steps": [
   "Owner completes branch details and connects the bank account (stub).",
   "Owner invites an academic admin and a teacher; both accept.",
   "Owner imports staff, courses/fee plans, rooms, classes, students, enrollments in order.",
   "Teacher opens /teach/today on a phone and saves the first sheet.",
   "Owner opens /dashboard."
  ],
  "expected": "1. Checklist ticks 2 of 7. | 2. Users active with branch scope; checklist ticks \"mời nhân sự\". | 3. Each job done with 0 errors; classes have generated sessions; students Đang học with balances from opening balances. | 4. Sheet saved; absence messages skipped with reason where consent missing; checklist complete or shows remaining items. | 5. No demo data outside admissions; tiles show real counts equal to list counts.",
  "trace": "Linked: TS-OVW-030, TS-SET-003, TS-SET-004, TS-SET-005, TS-DAT-018, TS-ATT-004, TS-REP-014",
  "data": "Owner of EMPTY, invited academic admin and teacher",
  "phase": "P1",
  "regression": true,
  "checkpoints": [
   {
    "action": "Owner completes branch details and connects the bank account (stub).",
    "checkpoint": "Checklist ticks 2 of 7."
   },
   {
    "action": "Owner invites an academic admin and a teacher; both accept.",
    "checkpoint": "Users active with branch scope; checklist ticks \"mời nhân sự\"."
   },
   {
    "action": "Owner imports staff, courses/fee plans, rooms, classes, students, enrollments in order.",
    "checkpoint": "Each job done with 0 errors; classes have generated sessions; students Đang học with balances from opening balances."
   },
   {
    "action": "Teacher opens /teach/today on a phone and saves the first sheet.",
    "checkpoint": "Sheet saved; absence messages skipped with reason where consent missing; checklist complete or shows remaining items."
   },
   {
    "action": "Owner opens /dashboard.",
    "checkpoint": "No demo data outside admissions; tiles show real counts equal to list counts."
   }
  ],
  "links": [
   "TS-OVW-030",
   "TS-SET-003",
   "TS-SET-004",
   "TS-SET-005",
   "TS-DAT-018",
   "TS-ATT-004",
   "TS-REP-014"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-13",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Invoice and payment reversals",
  "type": "flow",
  "priority": "P0",
  "preconditions": "INV-1 unpaid; INV-4 paid in cash; sp-001 processed.",
  "steps": [
   "U-ACC voids INV-1 with reason.",
   "U-ACC voids the cash payment on INV-4 with reason.",
   "SePay stub re-sends sp-001.",
   "U-ACC tries to void INV-2 (partial).",
   "U-OWNER reads the dashboard."
  ],
  "expected": "1. Đã huỷ; excluded from Đã lập tile and debt. | 2. Payment void, number kept; INV-4 back to Chưa thanh toán; cash book reversal; audit entries. | 3. Deduped; no new payment. | 4. Refused 409. | 5. Thực thu reduced by the voided cash payment; aggregates recomputed for that day.",
  "trace": "Linked: TS-FIN-018, TS-FIN-027, TS-FIN-035, TS-REP-012, TS-FIN-058",
  "data": "U-ACC, U-BM-A, SePay stub",
  "phase": "P1",
  "regression": true,
  "checkpoints": [
   {
    "action": "U-ACC voids INV-1 with reason.",
    "checkpoint": "Đã huỷ; excluded from Đã lập tile and debt."
   },
   {
    "action": "U-ACC voids the cash payment on INV-4 with reason.",
    "checkpoint": "Payment void, number kept; INV-4 back to Chưa thanh toán; cash book reversal; audit entries."
   },
   {
    "action": "SePay stub re-sends sp-001.",
    "checkpoint": "Deduped; no new payment."
   },
   {
    "action": "U-ACC tries to void INV-2 (partial).",
    "checkpoint": "Refused 409."
   },
   {
    "action": "U-OWNER reads the dashboard.",
    "checkpoint": "Thực thu reduced by the voided cash payment; aggregates recomputed for that day."
   }
  ],
  "links": [
   "TS-FIN-018",
   "TS-FIN-027",
   "TS-FIN-035",
   "TS-REP-012",
   "TS-FIN-058"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 },
 {
  "id": "TS-FLW-14",
  "module": "flows",
  "module_title": "End-to-end flows",
  "prd": "seed §3",
  "title": "Parent portal journey",
  "type": "flow",
  "priority": "P1",
  "preconditions": "Portal enabled (P2); G1 with S1 (A) and S2 (B); INV-1 unpaid.",
  "steps": [
   "G1 opens the portal link from a Zalo message and logs in with OTP.",
   "G1 opens Học phí → INV-1 and pays by QR (stub webhook).",
   "G1 submits Xin phép nghỉ for the next C1 session.",
   "G1 revokes Zalo consent and requests a birthday correction.",
   "G1 tries GET /v3/parent/learner/<S7>/attendance."
  ],
  "expected": "1. Children switcher shows both; Lịch học shows next sessions per child. | 2. Status updates to Đã thanh toán; receipt PDF link appears. | 3. Request open for the desk; U-ACAD-A accepts; sheet pre-marks Có phép; G1 confirmation in inbox. | 4. Consent revoked with timestamp; task for academic admin; next absence message skips ZNS with reason. | 5. 404.",
  "trace": "Linked: TS-COM-024, TS-COM-026, TS-COM-027, TS-COM-028, TS-COM-029",
  "data": "G1, U-ACAD-A, SePay stub",
  "phase": "P2",
  "regression": true,
  "checkpoints": [
   {
    "action": "G1 opens the portal link from a Zalo message and logs in with OTP.",
    "checkpoint": "Children switcher shows both; Lịch học shows next sessions per child."
   },
   {
    "action": "G1 opens Học phí → INV-1 and pays by QR (stub webhook).",
    "checkpoint": "Status updates to Đã thanh toán; receipt PDF link appears."
   },
   {
    "action": "G1 submits Xin phép nghỉ for the next C1 session.",
    "checkpoint": "Request open for the desk; U-ACAD-A accepts; sheet pre-marks Có phép; G1 confirmation in inbox."
   },
   {
    "action": "G1 revokes Zalo consent and requests a birthday correction.",
    "checkpoint": "Consent revoked with timestamp; task for academic admin; next absence message skips ZNS with reason."
   },
   {
    "action": "G1 tries GET /v3/parent/learner/<S7>/attendance.",
    "checkpoint": "404."
   }
  ],
  "links": [
   "TS-COM-024",
   "TS-COM-026",
   "TS-COM-027",
   "TS-COM-028",
   "TS-COM-029"
  ],
  "result": {
   "run": null,
   "status": "not_run",
   "bug_ids": [],
   "note": ""
  }
 }
]