Skip to content

QA Test Plan — Tab Chu kỳ khách ghé không mua

Version: 2.8
Date: 11/05/2026
Canonical Inputs: SOURCE_OF_TRUTH.md, EVIDENCE_PACK.md, prd.md.

v2.8 — 11/05/2026

Thay đổiSectionẢnh hưởng
Thêm 5 TC mới: TC-020 info strip, TC-021 sub-text, TC-022 column rename + regression purchase, TC-023 popup column tooltip, TC-024 19 tooltip coverageD3) Test casesQA
Update TC-014 note — coverage tooltip chuyển sang TC-024D3) TC-014QA
Update 4 row traceability TC-003/004/005/014 + thêm 5 row TC mớiD4) TraceabilityQA
Mở rộng D5 Exit — TC v2.8 pass + 19 tooltip coverage + mobile responsiveD5) Entry/ExitQA

QA dùng file này để kiểm thử đúng oracle nghiệp vụ. Khi expected result mâu thuẫn với PRD A10 hoặc SOURCE_OF_TRUTH.md, ưu tiên canonical truth.

D1) Test scope

In scopeOut of scope
Tab mới, persistence, filter, aggregate, matrix, popupExport Excel
Source consultant/do_service, is_zero_order=trueFilter nguồn visit visible
Loại khách đã chuyển đổi (DEC-014)Tự động hoá campaign chăm sóc
Empty/error/no-permission stateChurn score
Regression tab Chu kỳ mua hàngSửa thuật toán purchase cycle

D2) Seed data

SeedMô tả
CUST-A Nguyễn Thị Lan1 qualified visit consultant, is_zero_order=true, no order same day, no order sau ghé
CUST-B Trần Minh Anh2 qualified visits do_service, is_zero_order=true, different dates, no order sau visit cuối
CUST-C Lê Hoàng Namvisit consultant nhưng is_zero_order=false vì có order same day
CUST-D Phạm Bảo Ngọc2 qualified visits cùng ngày khác branch, no order sau visit
CUST-E Võ Gia Hânqualified visit, chưa từng mua
CUST-F Đỗ Quỳnh Thư1 qualified visit ngày 10/04, có order hợp lệ ngày 12/04 (sau ghé) → đã chuyển đổi, phải bị loại
CUST-G Bùi Khánh My2 qualified visits 10/04 + 20/04, có order hợp lệ ngày 15/04 (giữa 2 visit) → giữ vì sau mua vẫn ghé tiếp
CUST-H Hoàng Diệu Linh1 qualified visit 10/04, có order hợp lệ ngày 05/04 (trước ghé) → giữ vì đơn cũ trước ghé
CUST-I Ngô Phương Thảo1 qualified visit 10/04, có order hủy ngày 12/04 → giữ vì order không hợp lệ
CUST-K Lý Hoàng Yếnlatest qualified visit visit_source=['do_service','consultant'] (array order ngược cố ý) → test DEC-017 normalize
CUST-L Đặng Tuyết Mailatest qualified visit visit_source=['consultant'] only
CUST-M Cao Quỳnh Nhưlatest qualified visit visit_source=['do_service'] only
Branch Q1/Q3Dùng để test branch scope và multi-branch
Account Manager-Q1User chỉ có quyền branch Q1; dùng để test bypass DEC-016
Account Admin-HQUser Hasura role user nhưng ecommerce_user.branches=[] (HQ pattern không gắn branch); dùng để test no-branch-restriction DEC-016 đúng nghĩa
CUST-N Phan Hà My2 qualified visits: Q1 ngày 10/04 + Q3 ngày 20/04 (cả 2 consultant, is_zero_order=true); test detail branch_ids drift (DEC-026)

D3) Test cases

TC-001: Tab mới và persistence (gồm regression DEC-015)

FieldValue
FRFR-001
PreconditionsUser có quyền report group
Steps(1) Mở /r/reports/customer_cycle_report_group → chọn tab Chu kỳ khách ghé không mua → refresh; (2) Chọn tab Chu kỳ khách hàng → refresh; (3) Chọn tab Chu kỳ mua hàng → refresh; (4) Chọn tab Tổng quan → refresh
Expected(1) Tab mới active sau refresh; (2) Sau fix DEC-015: tab Chu kỳ khách hàng active sau refresh (trước fix sẽ rớt về Tổng quan); (3) tab Chu kỳ mua hàng active; (4) tab Tổng quan active. Tất cả tab render đúng panel tương ứng (không có panel sai do to mapping)
PriorityP0

TC-002: Qualified visit filter

FieldValue
FRFR-002
StepsChọn kỳ chứa CUST-A/B/C/D/E, branch all
ExpectedCUST-A/B/D/E được tính; CUST-C bị loại vì is_zero_order=false
PriorityP0

TC-003: Overview copy và no-data

FieldValue
FRFR-003
StepsMở tab với seed có data; sau đó chọn kỳ không có qualified visit
ExpectedCard/donut dùng copy visit-based; kỳ rỗng hiện empty state tiếng Việt, không NaN, không toast lỗi
PriorityP0

TC-004: Matrix row semantics

FieldValue
FRFR-004
StepsMở matrix với CUST-A/B/D
ExpectedRow order đúng: Tổng, Tỷ lệ KH theo thời gian chưa ghé lại, KH chưa quay lại ghé, Cùng ngày, bucket động; CUST-A vào KH chưa quay lại ghé; CUST-D có thể vào Cùng ngày
PriorityP0

TC-005: Popup detail

FieldValue
FRFR-005
StepsClick bucket có CUST-B/D/E -> search theo tên/SĐT -> click row; verify popup title format
ExpectedPopup có columns visit-based, không export, không service filters; title format {Row} • {Column} (DEC-021) ví dụ KH chưa quay lại ghé • 31 - 60 ngày; CUST-D branch Nhiều chi nhánh; CUST-E Đơn mua gần nhất = Chưa từng mua; click row mở hồ sơ; core tooltip theo UI B9 hiển thị đúng nếu vị trí tương ứng được render
PriorityP0

TC-006: Regression tab mua hàng

FieldValue
FRFR-006
StepsChạy cùng filter trên tab Chu kỳ mua hàng trước/sau feature
ExpectedSố liệu/tab labels/popup purchase không đổi; action cũ vẫn gọi đúng
PriorityP0

TC-008: Branch scope — happy path

FieldValue
FRFR-002, FR-005
StepsLogin Manager chỉ có Branch Q1, mở tab không chọn chi nhánh
ExpectedChỉ thấy khách thuộc branch Q1; popup không leak branch khác
PriorityP0

TC-009: Trạng thái lỗi hệ thống

FieldValue
FRFR-003, FR-005
StepsMock action aggregate/detail lỗi hệ thống
ExpectedUI hiện Không thể tải báo cáo. Vui lòng thử lại. hoặc Không thể tải danh sách khách. Vui lòng thử lại.
PriorityP1

TC-010: Loại khách đã chuyển đổi (DEC-014)

FieldValue
FRFR-002
PreconditionsSeed CUST-F/G/H/I tồn tại trong kỳ filter
StepsChọn kỳ chứa CUST-F/G/H/I, branch all → kiểm tra matrix totals → mở popup từng bucket
ExpectedCUST-F bị loại (order 12/04 sau visit 10/04); CUST-G được giữ (order 15/04 nằm giữa 2 visit, last_visit=20/04 > 15/04); CUST-H được giữ (order 05/04 trước visit 10/04); CUST-I được giữ (order hủy không tính); matrix total_customer đếm CUST-G/H/I, không đếm CUST-F; popup không xuất hiện CUST-F
PriorityP0

TC-012: Branch scope BE enforcement (DEC-016 — RoleUser footgun + bypass attempt)

FieldValue
FRFR-002, FR-006
PreconditionsUser Manager Q1 với role Hasura user, ecommerce_user.branches=[Q1]; user Admin/HQ với role Hasura user, ecommerce_user.branches=[] (rỗng); seed data ở cả Q1 và Q3. Quan trọng: Hasura action role là user cho cả 2 → Actor.IsAdmin() luôn trả true. Test này verify resolveBranchScope KHÔNG dùng IsAdmin().
Steps(a) Manager Q1 mở DevTools → trigger query với payload branch_ids=[Q3-uuid] (branch ngoài scope); (b) Manager Q1 trigger payload branch_ids=[Q1-uuid, Q3-uuid] (1 trong scope, 1 ngoài); (c) Manager Q1 không gửi branch_ids → expect kết quả Q1 only; (d) Admin/HQ trigger payload branch_ids=[Q3-uuid]; (e) Admin/HQ không gửi branch_ids; (f) Lặp tương tự với detail action customer_ids chứa khách Q3
Expected(a) Empty payload ([Q3] ∩ [Q1] = [] → DEC-012), không error, không leak data Q3; (b) Trả data Q1 only ([Q1, Q3] ∩ [Q1] = [Q1]); (c) Trả data Q1 only (allowed=[Q1], requested=[]→fallback allowed); (d) Trả data Q3 (user.branches=[] → no branch restriction, trust input); (e) Trả data toàn bộ (user.branches=[] + input rỗng); (f) Detail action filter ra customer_ids không thuộc scope effective_branches, response không chứa khách Q3 cho Manager Q1. Critical fail mode: nếu BE dùng Actor.IsAdmin() → cả Manager Q1 lẫn Admin-HQ đều bypass intersect → Manager Q1 leak data Q3 — đây là chính xác bug từ review C-01.
PriorityP0

TC-013: Loại ghé multi-source format (DEC-017)

FieldValue
FRFR-005
PreconditionsSeed thêm CUST-K: latest qualified visit có visit_source = ['do_service', 'consultant'] (cố ý array order ngược); CUST-L: visit_source = ['consultant'] only; CUST-M: visit_source = ['do_service'] only
StepsMở popup chứa CUST-K/L/M
ExpectedCUST-K cột Loại ghé = Tư vấn + Làm dịch vụ (thứ tự cố định, không phải Làm dịch vụ + Tư vấn); CUST-L = Tư vấn; CUST-M = Làm dịch vụ; separator đúng + (1 dấu cách 2 bên), không xuống dòng, không chip
PriorityP0

TC-015: Cell click guard (DEC-019)

FieldValue
FRFR-004
PreconditionsTab mới có matrix với ít nhất 1 cell value 0 và 1 cell value > 0 (CUST-D bucket khác bucket có data)
Steps(1) Hover cell value 0 → kiểm cursor; (2) Click cell value 0 → kiểm popup; (3) Hover cell value > 0 → kiểm cursor; (4) Click cell value > 0 → kiểm popup; (5) Lặp với cột Tổng của row data có value 0 và value > 0.
Expected(1) Cursor default, không có hover highlight; (2) Không mở popup, không crash; (3) Cursor pointer + hover highlight; (4) Mở popup SCR-02 đúng title row + column; (5) Cùng behavior với cột Tổng.
PriorityP1

TC-016: Performance benchmark (DEC-020)

FieldValue
FRFR-006
PreconditionsTest env có seed 100k qualified visits + 500k qualified visits (2 dataset riêng); benchmark tool đo P95
Steps(1) Run aggregate action với 100k seed × 5 lần → ghi P95; (2) Run aggregate action với 500k seed × 5 lần → ghi P95; (3) Run detail action với customer_ids = 1k → ghi P95 ở cả 2 dataset. (4) Compare với target. (5) Nếu fail → BE switch sang SQL aggregation pattern (xem dev-spec C9 fallback) → re-run benchmark.
ExpectedP95 aggregate < 3s @ 100k AND < 5s @ 500k. P95 detail < 1s @ 100k AND < 2s @ 500k. Pass → ghi báo cáo benchmark vào handoff. Fail → block GA, switch sang SQL fallback và re-test.
PriorityP1

TC-007: Month semantics (mở rộng cho DEC-018)

FieldValue
FRFR-002, FR-004
StepsChọn Chu kỳ theo tháng, period 3, kỳ 9 tháng
ExpectedBucket labels: ≤ 3 tháng, 4 - 6 tháng, 7 - 9 tháng (DEC-018, KHÔNG dùng pattern purchase quirk 3 tháng / 3 - 6 / 6 - 9); bucket boundaries: 90, 180, 270 ngày; khách có days_since_last = 90 → vào bucket ≤ 3 tháng (lower-exclusive bound: 0 < days ≤ 90); khách = 91 → vào 4 - 6 tháng
PriorityP1

TC-017: Tab label responsive (DEC-022)

FieldValue
FRFR-001
PreconditionsTab Chu kỳ khách ghé không mua đã visible
Steps(1) Mở tab ở viewport ≥768px (desktop) → kiểm label tab; (2) Resize browser xuống <768px (mobile) → kiểm label; (3) Click tab ở mobile → kiểm URL + tab key + panel render đúng; (4) Resize ngược lại → kiểm label đổi lại full
Expected(1) Hiển thị Chu kỳ khách ghé không mua; (2) Hiển thị Khách ghé không mua (short form); (3) URL/tab key không đổi (CUSTOMER_CYCLE_VISIT_NO_PURCHASE), panel render đúng; (4) Label trở lại full khi vượt 768px
PriorityP1

TC-018: Filter validation — date range + duration (DEC-023, DEC-024)

FieldValue
FRFR-002
PreconditionsTab visible, có data
Steps(1) Date range > 365 ngày: chọn from = 01/01/2025, to = 30/04/2026 (~485 ngày). (2) Duration by_day = 0: nhập 0 vào bước chu kỳ ở chế độ ngày. (3) Duration by_day = 200. (4) Duration by_day = 30 (valid). (5) Switch sang by_month, duration = 13. (6) Duration by_month = 6 (valid). (7) Mở DevTools, gửi payload duration=9999 lên BE trực tiếp.
Expected(1) FE clamp from = 01/05/2025 + toast Phạm vi báo cáo tối đa 1 năm. Đã điều chỉnh ngày bắt đầu thành 01/05/2025.. (2) Inline error Bước chu kỳ phải từ 1 đến 180 ngày, query không chạy. (3) Cùng inline error. (4) Query chạy bình thường. (5) Inline error Bước chu kỳ phải từ 1 đến 12 tháng. (6) Query chạy bình thường. (7) BE trả error invalid_duration với message tiếng Việt, không crash.
PriorityP1

TC-014: Copy + core tooltip verification (v2.7)

FieldValue
FRFR-003, FR-004, FR-005
PreconditionsTab Chu kỳ khách ghé không mua có data đủ render matrix + popup (CUST-A → CUST-M)
Steps(1) Verify card label = Số lượng khách có lượt ghé không mua; donut 1 = Cơ cấu khách: đã/chưa quay lại ghé; donut 2 = Tỉ lệ KH theo thời gian chưa ghé lại. (2) Verify matrix có row Cùng ngày (không phải 0 ngày); row order: Tổng, Tỷ lệ KH theo thời gian chưa ghé lại, KH chưa quay lại ghé, Cùng ngày, bucket động. (3) Verify popup column headers: Khách hàng, SĐT, Chi nhánh, Lượt ghé gần nhất, Loại ghé, Số lượt ghé không mua trong kỳ, Đơn mua gần nhất, Số ngày chưa ghé lại. (4) Verify CUST-E (chưa từng mua) → cột Đơn mua gần nhất = Chưa từng mua. (5) Hover core tooltip: filter Chi nhánh; matrix rows KH chưa quay lại ghé, Cùng ngày; popup columns Loại ghé, Số lượt ghé không mua trong kỳ, Đơn mua gần nhất, Số ngày chưa ghé lại.
ExpectedCopy đúng theo UI B0.2/B6; core tooltip render đúng nội dung UI B9, không cắt chữ, không hiển thị key i18n. Tooltip ngoài core là optional/backlog, không block GA nếu chưa implement.
PriorityP1

V2.8 update: Coverage tooltip required đầy đủ chuyển sang TC-024. TC-014 chỉ verify copy chính của label/row/column (không bao gồm tooltip coverage). Backlog tooltip đã được lift hoàn toàn vào required (DEC-030).

TC-011: Edge case order cùng ngày last visit

FieldValue
FRFR-002
PreconditionsSeed thêm 1 khách có qualified visit ngày 10/04 và order hợp lệ cùng ngày 10/04 nhưng is_zero_order đã đánh đúng (visit và order trên cùng ngày được gom thì is_zero_order=false, đã loại ở filter qualified)
StepsTạo khách CUST-J: visit_date=10/04 source=consultant, order created_at=10/04 12:30 (cùng ngày)
ExpectedTheo migration is_zero_order của visit ngày 10/04 sẽ = false → CUST-J bị loại ngay ở filter qualified visit, không cần trigger DEC-014. Nếu seed cố ép is_zero_order=true để test bypass: order cùng ngày last_visit_date KHÔNG loại khách (theo công thức > chứ không phải >=) → giữ.
PriorityP1

TC-019: Detail action branch_ids consistency (DEC-026, C-03)

FieldValue
FRFR-005
PreconditionsUser có quyền Q1 + Q3; seed CUST-N: 1 qualified visit Q1 ngày 10/04, 1 qualified visit Q3 ngày 20/04 (cả 2 đều consultant, is_zero_order=true, không có order sau ghé)
Steps(1) User filter branch_ids=[Q1], mở matrix → click bucket chứa CUST-N → kiểm popup; (2) User filter branch_ids=[Q3] → kiểm popup; (3) User filter branch_ids=[] (all) → kiểm popup; (4) Mở DevTools, lặp lại các steps nhưng FE detail action gọi không có branch_ids → kiểm response.
Expected(1) Popup CUST-N: Lượt ghé gần nhất=10/04, branch_name=Q1, Số lượt ghé không mua trong kỳ=1. (2) Popup: last_visit=20/04, branch_name=Q3, count=1. (3) Popup: last_visit=20/04 (max), branch_name=Q3, count=2. (4) BE detail KHÔNG có branch_ids → fallback về resolveBranchScope với input rỗng = toàn bộ allowed = giống case (3). Documentation note: FE BẮT BUỘC truyền branch_ids để tránh drift.
PriorityP0

TC-020: Info strip rendering + persistence + a11y (DEC-028, FR-007)

FieldValue
FRFR-007
PreconditionsTab Chu kỳ khách ghé không mua deployed v2.8; user có quyền report group
Steps(1) Mở tab → verify info strip visible above filter bar với copy đúng theo customer_cycle.info_strip. (2) Đổi filter (loại chu kỳ, thời gian, chi nhánh) → verify strip vẫn visible. (3) Refresh trang ở tab → verify strip visible sau reload. (4) Switch sang tab Chu kỳ mua hàng rồi quay lại tab này → verify strip vẫn visible. (5) Resize browser xuống <768px → verify strip auto-wrap, không cắt chữ. (6) DevTools: kiểm tra localStorage không có key dismissed (no banner state). (7) Filter cho ra empty state (thời gian không có data) → verify strip vẫn visible. (8) Screen reader simulator: verify strip có role="status" + aria-live="polite".
Expected(1) Strip visible đúng copy. (2,3,4) Strip persistent qua mọi state change. (5) Mobile wrap không cắt. (6) localStorage clean — không có dismiss key. (7) Strip render độc lập với data state. (8) A11y attributes hiện diện.
PriorityP0

TC-021: Sub-text rendering tất cả 8 widget (DEC-029)

FieldValue
FRFR-003, FR-004
PreconditionsTab có data render đủ overview + matrix (CUST-A → CUST-M); total_customer thực tế > 0
Steps(1) Verify Card 1 dưới label Số lượng khách có lượt ghé không mua có sub-text Đã loại khách chuyển đổi sau ghé. (2) Verify Donut 1 dưới label có sub-text Trên tổng {N} KH với {N} interpolate đúng = total_customer (vd 1.250). (3) Donut 2 sub-text giống Donut 1. (4) Matrix header dọc sub-text = Khoảng cách trung bình giữa các lượt ghé. (5) Matrix header ngang sub-text = Tính tới ngày kết thúc kỳ. (6) Row Cùng ngày sub-text = Nhiều lượt ghé qualified trong 1 ngày. (7) Row KH chưa quay lại ghé sub-text = Chỉ có 1 lượt ghé qualified trong kỳ. (8) DevTools inspect: font-size sub-text < font-size label, color muted (gray-500 hoặc text-secondary). (9) Mobile viewport <768px: verify sub-text auto-wrap, không cắt.
ExpectedToàn bộ 8 sub-text render đúng copy theo B6 i18n key. Runtime interpolate {total} đúng giá trị. Style smaller + muted. Mobile wrap đúng.
PriorityP1

TC-022: Column rename T6 + regression tab purchase (DEC-029)

FieldValue
FRFR-004
PreconditionsTab mới có data; tab Chu kỳ mua hàng cũng có data để regression
Steps(1) Mở tab Chu kỳ khách ghé không mua → kiểm tra header cột cuối matrix. (2) Hover ℹ️ icon trên header cột (nếu render — DEC-030). (3) Verify logic data binding: với row Khách hàng chưa quay lại value=800, total=1250 → cột cuối phải hiển thị 64.00%. (4) Switch sang tab Chu kỳ mua hàng → kiểm header cột cuối matrix (regression).
Expected(1) Cột cuối tên % trên tổng (KHÔNG phải Tỷ lệ). (2) Tooltip ℹ️ giải thích đúng. (3) Logic không đổi — vẫn render đúng %. (4) Tab Chu kỳ mua hàng cột cuối VẪN tên Tỷ lệ (không bị regression đổi sang % trên tổng).
PriorityP0

TC-023: Popup column tooltip coverage (DEC-029, DEC-030)

FieldValue
FRFR-005
PreconditionsPopup detail mở được với data (click bucket có khách trong matrix)
Steps(1) Mở popup. (2) Cho mỗi 6 column header (Chi nhánh, Lượt ghé gần nhất, Loại ghé, Số lượt ghé không mua trong kỳ, Đơn mua gần nhất, Số ngày chưa ghé lại): verify có icon ℹ️ visible inline cạnh tên column. (3) Hover từng icon ℹ️ → verify tooltip hiển thị đúng copy theo UI B9 required. (4) Mobile/tap: long-press hoặc tap icon → verify tooltip toggle. (5) Verify tooltip không cắt chữ trên mobile viewport.
Expected6 column header có icon ℹ️ visible. Hover/tap hiển thị tooltip đúng copy. Mobile tooltip không cắt.
PriorityP1

TC-024: 19 required tooltip full coverage (DEC-030)

FieldValue
FRFR-002, FR-003, FR-004, FR-005
PreconditionsTab Chu kỳ khách ghé không mua có data render đủ filter + overview + matrix + popup
StepsVerify hover ℹ️ trên 19 vị trí theo UI B9 required v2.8: 4 filter (Chi nhánh, Loại chu kỳ, Bước chu kỳ, Thời gian báo cáo); 3 overview (Card 1, Donut 1, Donut 2); 6 matrix (Header dọc, Header ngang, Row Tổng, Row Tỷ lệ KH theo thời gian, Row KH chưa quay lại ghé, Row Cùng ngày); 6 popup (Chi nhánh, Lượt ghé gần nhất, Loại ghé, Số lượt ghé không mua trong kỳ, Đơn mua gần nhất, Số ngày chưa ghé lại). Tổng 19. Mỗi tooltip verify nội dung khớp B9 (không hiển thị i18n key thô, không cắt chữ trên mobile).
Expected19/19 tooltip render đúng nội dung. Không có item nào còn classification "Optional"/"Backlog".
PriorityP1

D4) Traceability

TCFRDEC
TC-001FR-001DEC-001, DEC-011, DEC-015, DEC-022
TC-002FR-002DEC-002, DEC-003
TC-003FR-003DEC-004, DEC-012, DEC-029 (sub-text overview)
TC-004FR-004DEC-005, DEC-006, DEC-029 (sub-text matrix)
TC-005FR-005DEC-007, DEC-008, DEC-009, DEC-021, DEC-029 (popup column tooltip)
TC-006FR-006DEC-010, DEC-013
TC-007FR-002, FR-004DEC-006, DEC-018
TC-008FR-002, FR-005DEC-016 (happy path)
TC-009FR-003, FR-005DEC-012
TC-010FR-002DEC-014
TC-011FR-002DEC-014
TC-012FR-002, FR-006DEC-016 (bypass attempt + RoleUser footgun)
TC-013FR-005DEC-017
TC-014FR-003, FR-004, FR-005Copy verification (v2.8: tooltip coverage chuyển sang TC-024)
TC-015FR-004DEC-019
TC-016FR-006DEC-020
TC-017FR-001DEC-022
TC-018FR-002DEC-023, DEC-024
TC-019FR-005DEC-026 (detail branch_ids — review C-03)
TC-020FR-007DEC-028 (info strip permanent)
TC-021FR-003, FR-004DEC-029 (sub-text rendering)
TC-022FR-004DEC-029 (column rename T6 + regression tab purchase)
TC-023FR-005DEC-029, DEC-030 (popup column tooltip)
TC-024FR-002, FR-003, FR-004, FR-005DEC-030 (19 tooltip required full coverage)

D5) Entry/Exit

Entry

  • BE actions deployed to test env.
  • FE codegen includes new action types.
  • Seed data for CUST-A/B/C/D/E exists.
  • QA can access at least one Admin and one branch-scoped Manager account.

Exit

  • All P0 pass.
  • No purchase-tab regression.
  • No English lazy state copy in new tab.
  • No unresolved mismatch between matrix totals and popup detail counts.
  • TC-020 P0 pass (info strip persistent + a11y).
  • TC-022 P0 pass (column rename không break tab purchase).
  • TC-021, TC-023, TC-024 P1 pass.
  • 19 tooltip required 100% coverage (DEC-030).
  • Mobile responsive verify (info strip + sub-text wrap + tooltip not cut).