Appearance
Kế hoạch kiểm thử (QA Test Plan) — Tích hợp Pancake CRM (Pancake CRM Integration)
Tham chiếu: PRD v1.0 | Ngày: 15/05/2026
Mục đích: Chuyển 18 FR + 25 DEC thành phạm vi test, dữ liệu seed, test case để QA bám đúng. Profile L → chaos test outage + load test bắt buộc. Đọc trước:
decision-brief.md→ §D1 Phạm vi → §D2 Test case → §D3 Seed → §D5 Entry/Exit. Canonical traceability:dev-spec.mdC12. File này là view QA (FR → TC mapping + execution detail).
Đầu vào chuẩn (Canonical Inputs)
| File | Vai trò |
|---|---|
SOURCE_OF_TRUTH.md | 25 DEC + Solution Lock — ưu tiên cao nhất |
decision-brief.md | Tóm tắt rủi ro + Pilot W1-W4 |
prd.md | FR-001..018 + LIFECYCLE-001/002 + 6 FORMULA + A7 RSK |
ui-spec.md | Permission Matrix B5 + State Matrix B6 + Tooltip B9 |
dev-spec.md | C5 flow 15 STEP + C7 migration + C8 security + C12 traceability |
D1) Phạm vi kiểm thử
18 FR coverage matrix
| FR | Mô tả | Độ ưu tiên test | Test type | Số TC dự kiến |
|---|---|---|---|---|
| FR-001 | Webhook receiver endpoint fail-open 200 | P0 | Integration + Load | 8 |
| FR-002 | Persist raw event audit trail | P0 | Integration | 3 |
| FR-003 | Idempotency 3-tuple UNIQUE | P0 | Integration | 2 |
| FR-004 | Async processor Hasura event trigger | P0 | Integration | 10 (15 step coverage) |
| FR-005 | Phone E.164 normalize + match/upsert | P0 | Unit + Integration | 5 |
| FR-006 | Advisory lock chống race | P0 | Concurrency stress | 1 |
| FR-007 | Auto-create contact_book | P0 | Integration | 3 |
| FR-008 | Round-robin REUSE GetTicketUpdates | P0 | Integration | 5 |
| FR-009 | Smart update Q2.b | P0 | Integration | 6 |
| FR-010 | Tạo ticket standard | P0 | Integration | 4 |
| FR-011 | Notification (telesale + admin) | P0 | Integration + Notification | 3 |
| FR-012 | Settings UI 4 tabs | P0 (UI) | E2E + Permission | 20 |
| FR-013 | REST API consumption /sources + circuit breaker | P1 | Integration + Chaos | 5 |
| FR-014 | 4-layer outage recovery + DLQ replay | P0 | Chaos test | 15 |
| FR-015 | Feature flag 3 mức + pilot rollout | P0 | E2E | 3 |
| FR-016 | Test mode event | P1 | Integration | 3 |
| FR-017 | Opt-out compliance customer_consent | P0 | Integration | 2 |
| FR-018 | FE delta dropdown ticket source slot 9 | P0 (UI) | E2E | 1 |
Tổng dự kiến: ~99 test case (45 P0 functional + 20 P0 UI + 15 chaos + 5 load + 14 P1/P2)
Out of scope test (không test trong MVP-1)
- Outbound Diva → Pancake (defer MVP-2)
- Pancake POS / Botcake integration
- Multi-workspace
- Bulk import source routing CSV
- Geo address normalize
- ML lead scoring
- Pancake
ticketevent handling - Mobile UI Settings (admin desktop only MVP-1)
D2) Test case detail
TC-001 — FR-001 Webhook receiver fail-open 200
Luồng thành công (P0):
| TC | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | Độ ưu tiên |
|---|---|---|---|---|---|
| TC-001-001 | Pancake admin đã setup webhook URL với token hợp lệ; IP whitelist có IP Pancake; app_setting pancake_integration.enabled=true; pancake_connection.status='active'; 1 source is_active=true | Pancake POST webhook payload happy path | JSON {record_id: "rec_test_001", modified_on: "2026-05-28T10:00:00Z", phone_number: "+84912345678", source: [{id: "src_fb_diva_hcm", name: "FB Diva HCM"}], name: "Nguyễn Thị Lan", email: "lan@email.com"} | (1) Response 200 trong < 1s. (2) pancake_webhook_event có 1 row với status='received' (sau promote). (3) Hasura trigger fire. (4) crm-api handler được gọi trong 1-2s. (5) Status cuối = 'processed'. (6) Account mới + Ticket mới + Notification đã gửi telesale | P0 |
Ngoài luồng thành công (P0/P1 BẮT BUỘC):
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-001-002 | Âm tính | Token URL sai | Pancake POST với /api/pancake/record/INVALID_TOKEN | Bất kỳ payload | Response 200 (KHÔNG 401), pancake_webhook_event.status='auth_failed', alert log nếu accumulate > 10 lần/phút | P0 |
| TC-001-003 | Âm tính | IP không trong whitelist (sau PD-002 enforced W4) | Pancake POST từ IP 1.2.3.4 không whitelist | Happy payload | Response 200, status='ip_blocked', source_ip='1.2.3.4' logged | P0 (W4) |
| TC-001-004 | Âm tính | JSON schema sai | POST với body {"invalid": "schema"} (thiếu record_id) | — | Response 200, status='parse_error', error_message logged | P0 |
| TC-001-005 | Biên | Idempotency duplicate | POST 2 lần cùng 3-tuple (record_id, modified_on, payload_hash) | Cùng payload TC-001-001 | Lần 1: status='received' rồi 'processed'. Lần 2: status='skipped_duplicate', UPDATE last_received_at. KHÔNG tạo ticket trùng | P0 |
| TC-001-006 | Biên | Source disabled | pancake_source_routing.is_active=false cho source trong payload | Payload với source.id='src_fb_diva_hcm' đã disabled | Status='skipped_source_disabled', KHÔNG tạo ticket | P0 |
| TC-001-007 | Biên | Kill switch ON | app_setting.pancake_integration.enabled=false | Bất kỳ payload | Status='skipped_kill_switch', KHÔNG fire trigger | P0 |
| TC-001-008 | Load | Sustained 100 req/s × 10 phút | K6 load test | 60000 payload distinct record_id | Latency p95 < 1s, error rate < 0.1%, no Pancake suspension (error < 80%) | P0 (W3) |
TC-002 — FR-002 Persist raw event
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-002-001 | Happy | Webhook nhận request hợp lệ | INSERT row | TC-001-001 payload | pancake_webhook_event có row với raw_payload jsonb match payload, raw_headers jsonb có HTTP headers, source_ip resolve từ X-Forwarded-For | P0 |
| TC-002-002 | Âm tính | DB connection error | INSERT khi DB down | — | Webhook trả 500 (Pancake retry), log fatal | P1 |
| TC-002-003 | Biên | Payload rất lớn (1MB) | POST với payload 1MB | Random 1MB jsonb | INSERT thành công, raw_payload lưu nguyên | P1 |
TC-003 — FR-003 Idempotency UNIQUE 3-tuple
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-003-001 | Happy | Đã có row với 3-tuple (rec_A, ts_1, hash_X) | INSERT row mới cùng 3-tuple | Cùng payload | UNIQUE violation → catch → UPDATE last_received_at, status='skipped_duplicate' | P0 |
| TC-003-002 | Biên | Cùng record_id + modified_on nhưng body khác (Pancake bug) | INSERT 2 row | Body khác nhau → hash khác | 2 row riêng biệt, cả 2 xử lý | P1 |
TC-004 — FR-004 Async processor 15-step
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-004-001 | Happy | status='received' | Trigger fire | Account chưa exist + source mapped branch + telesale active | STEP 0 pass → 1-14 success → status='processed', ticket có assignee_id | P0 |
| TC-004-002 | Filter | status='auth_failed' | Trigger fire (Hasura fire mọi UPDATE column status) | — | STEP 0 filter ra (NEW.status != 'received') → no-op return 200 | P0 |
| TC-004-003 | TX rollback | STEP 5 exception (DB constraint fail) | Trigger fire | Payload với phone null | TX rollback, status='dead_letter', retry_count=1, error_message logged | P0 |
| TC-004-004 | Idempotent STEP 1 | Re-fetch FOR UPDATE | Trigger fire 2 lần đồng thời (Hasura retry) | Cùng event | Lần 1 process, lần 2 STEP 1 thấy status đã 'processing' → skip return | P0 |
| TC-004-005 | DLQ | STEP 1-14 fail 3 lần | Manual replay từ Tab 3 | event_id của row dead_letter | Reset status='received', retry_count=0 → fire lại | P0 |
| TC-004-006 | Permanently failed | retry_count >= 3 | Auto detect | — | status='permanently_failed', alert manual review | P1 |
| TC-004-007 | STEP 7 opt-out | customer_consent.marketing='false' | Trigger fire | Account có consent opt-out | STEP 7 detect → status='skipped_opt_out', account update only, KHÔNG tạo ticket | P0 |
| TC-004-008 | STEP 8 smart update no-change | Account có sẵn, cùng phone/source/tag | Trigger fire update payload | Same phone, same source, same tag | STEP 8 detect no important change → status='processed', KHÔNG tạo ticket, chỉ update account.display_name | P0 |
| TC-004-009 | STEP 8 smart update phone change | Account có sẵn, phone đổi | Trigger fire | new phone E.164 khác old | Tạo ticket mới | P0 |
| TC-004-010 | STEP 8 smart update source new | Account có sẵn, source mới chưa map | Trigger fire | new source_id | Tạo ticket mới | P0 |
TC-005 — FR-005 Phone E.164 normalize
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-005-001 | Happy local VN | — | Webhook payload phone format 0912345678 | Phone string 0912345678 | Normalize thành +84912345678, match/create account với normalized_phone='+84912345678', phone_code=84, phone_number='912345678', phone_enabled=true | P0 |
| TC-005-002 | Happy E.164 | — | Phone đã E.164 +84912345678 | +84912345678 | Normalize cùng, KHÔNG đổi | P0 |
| TC-005-003 | Edge format | — | Phone +84 912 345 678 (có space) | Trim space → normalize | +84912345678 | P0 |
| TC-005-004 | Âm tính | — | Phone sai format 912345678abc | — | Log warning, normalized_phone=NULL, fallback match by (phone_code, phone_number) raw | P1 |
| TC-005-005 | Edge duplicate | 2 account legacy cùng normalize thành +84912345678 | Match | — | Match account created_at cũ nhất, log warning | P1 |
TC-006 — FR-006 Advisory lock concurrent
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-006-001 | Concurrency stress | Phone +84912345678 chưa có account | 10 webhook POST đồng thời cùng phone | 10 payload distinct record_id cùng phone | CHỈ 1 account được tạo, 10 ticket riêng (mỗi event 1 ticket), không có duplicate account | P0 |
TC-007 — FR-007 Auto-create contact_book
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-007-001 | Happy new account | Account vừa được tạo ở STEP 5 (chưa có contact) | STEP 5b query-before-insert | account_id mới | 1 row contact_book mới với primary_phone=normalized_phone, FK valid | P0 |
| TC-007-002 | Reuse existing | Account đã có contact_book row | STEP 5b query | account_id cũ | KHÔNG tạo mới, reuse | P0 |
| TC-007-003 | Race | 2 webhook đồng thời tạo contact cho account mới | Advisory lock đã serialize ở STEP 3 | — | Chỉ 1 contact tạo, không vi phạm UNIQUE constraint | P1 |
TC-008 — FR-008 Round-robin REUSE
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-008-001 | Round-robin 5 telesale | Branch CN-Q1 có 5 telesale active T1..T5; last assignee T2 | 3 webhook đồng thời gửi cho branch CN-Q1 | 3 record different | 3 ticket assign cho T3, T4, T5 (next in rotation từ T2), ticket_distribute INSERT 3 row | P0 |
| TC-008-002 | Loose mode branch=NULL | Source chưa map branch | Webhook gửi với source unmapped | record_id | Ticket assignee_id=NULL, branch_id=NULL. Notification noti_pancake_unmapped_branch gửi admin | P0 |
| TC-008-003 | No active telesale | Branch CN-Q5 có 0 telesale active (ngoài giờ) | Webhook gửi | record_id | Ticket branch_id set, assignee_id=NULL. Notification gửi manager branch | P0 |
| TC-008-004 | Refactor verify backward compat | Existing flow tạo ticket (Stringee/iCall) sau khi refactor GetTicketUpdates thành public | Tạo ticket manual qua admin UI | — | Round-robin tiếp tục hoạt động cho ticket existing, no regression | P0 |
| TC-008-005 | Bulk 100 webhook 1 branch | Branch có 10 telesale active | 100 webhook trong 1 phút | 100 record | 100 ticket assign theo round-robin xoay vòng → mỗi telesale ~10 ticket (cân tải) | P1 |
TC-009 — FR-009 Smart update Q2.b
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-009-001 | New account | account chưa exist | Webhook lần đầu | record_id mới | STEP 8: account mới → luôn tạo ticket | P0 |
| TC-009-002 | No important change | Account exist, update name/address | Webhook update | Phone/source/tag không đổi | KHÔNG tạo ticket, chỉ update account.display_name | P0 |
| TC-009-003 | Phone change | Account exist | Webhook với phone mới | new phone E.164 | Tạo ticket mới | P0 |
| TC-009-004 | Source new | Account exist, đã có source A | Webhook với source B chưa từng có | source.id='src_new' | Tạo ticket mới, append vào pancake_metadata.source_ids[] | P0 |
| TC-009-005 | VIP tag new | Account exist, chưa có tag VIP | Webhook với tags chứa "VIP" (case-insensitive match pancake_connection.vip_tag_names) | tags=["vip"] | Tạo ticket mới, set pancake_metadata.had_vip_tag=true | P0 |
| TC-009-006 | VIP tag exist already | Account đã có had_vip_tag=true | Webhook update với cùng tag VIP | tags=["vip"] | KHÔNG tạo ticket (đã từng có) | P1 |
TC-010 — FR-010 Tạo ticket standard
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-010-001 | Happy field mapping | — | STEP 10 INSERT ticket | Webhook happy payload | Ticket có: source_id='ticket_source_pancake', target_id='telesales', branch_id=resolved, due_date=today 23:59:59 Asia/Ho_Chi_Minh, status_id='ticket_status_new', customer_id=account.id, customer_name=record.name, customer_phone_number=normalized_phone | P0 |
| TC-010-002 | Input note template | — | Webhook với tags + address + email | tags=["vip","hot lead"], full_address="Q1, HCM", email="x@y.com" | input_note chứa: "Lead từ Pancake CRM\nPancake Record ID: rec_xxx\nNguồn (Pancake source): FB Diva HCM\nFB tags: vip, hot lead\nĐịa chỉ: Q1, HCM\nEmail: x@y.com" | P0 |
| TC-010-003 | Due date timezone | Server timezone UTC, customer timezone Asia/Ho_Chi_Minh | Webhook trước 17:00 UTC ngày 28/05 | — | due_date = '2026-05-28 16:59:59+00' (= 23:59:59 +07) | P0 |
| TC-010-004 | Created_by system | — | Webhook | — | ticket.created_by='system_pancake_webhook' | P1 |
TC-011 — FR-011 Notification
| TC | Loại | Điều kiện ban đầu | Khi thao tác | Dữ liệu test | Kỳ vọng | P |
|---|---|---|---|---|---|---|
| TC-011-001 | Telesale assigned | Ticket có assignee_id | STEP 12 fire | telesale_id=T3 | Push notification gửi T3 với template noti_ticket_assigned_pancake: "Bạn vừa nhận lead Pancake CRM: [Khách: Nguyễn Thị Lan, Phone: +84912345678, Nguồn: FB Diva HCM]. Phản hồi trong 30 phút." Trong 5 giây | P0 |
| TC-011-002 | Admin Loose mode | branch_id=NULL | STEP 12 fire | — | Notification noti_pancake_unmapped_branch gửi Admin role + branch-manager NULL | P0 |
| TC-011-003 | Notification down | notification-v2-api 5xx | STEP 12 fire | — | Log warning, KHÔNG fail STEP 13. Ticket vẫn tạo. Alert ops nếu fail rate > 5%/5min | P1 |
TC-012 — FR-012 Settings UI 4 tabs (E2E)
20 TC chi tiết → xem ui-spec.md B-POST.1-4 Verification. Section này tóm tắt critical TC:
| TC | Loại | Điều kiện | Thao tác | Kỳ vọng | P |
|---|---|---|---|---|---|
| TC-012-001 | Permission hide | Manager login | Truy cập /admin/settings/pancake-crm | Redirect home, menu Settings/Pancake không hiển thị | P0 |
| TC-012-002 | Permission hide POS | POS-only role login | URL gõ trực tiếp | Redirect, no access | P0 |
| TC-012-003 | Admin tab 1 form save | Admin login | Nhập api_key + click "Lưu" | UPDATE pancake_connection, toast success | P0 |
| TC-012-004 | Test connection button | Admin tab 1 | Click "Kiểm tra kết nối" | Call Pancake REST /sources. Success → hiển thị list sources detected. Fail → banner đỏ với error code | P0 |
| TC-012-005 | Tab 2 inline edit branch | Admin tab 2 | Change branch dropdown | Autosave debounce 500ms, UPDATE pancake_source_routing.diva_branch_id | P0 |
| TC-012-006 | Tab 2 toggle is_active | Admin tab 2 | Click toggle | UPDATE is_active, source ngay lập tức skip/process webhook | P0 |
| TC-012-007 | Tab 2 sync sources manual | Admin tab 2 | Click "Đồng bộ từ Pancake" | Call Pancake /sources → upsert pancake_source_routing → new sources hiển thị với is_active=false | P0 |
| TC-012-008 | Tab 3 filter combo | Admin tab 3 | Date range + status + source | Table render đúng filter | P0 |
| TC-012-009 | Tab 3 drawer detail | Admin tab 3 | Click row event | Drawer mở với raw_payload formatted JSON, error_message, retry_count | P0 |
| TC-012-010 | Tab 3 replay single | Admin tab 3 | Click "Phát lại" trên 1 event status='dead_letter' | Call pancake_replay_event(event_id) action, status reset 'received', refetch list | P0 |
| TC-012-011 | Tab 3 bulk replay rate limit | Admin tab 3 | Select 100 events → "Phát lại hàng loạt" | Replay 5 events/giây, progress bar, success toast | P0 |
| TC-012-012 | Tab 3 replay permission | User thiếu replay_dlq action | Click "Phát lại" | Button hidden | P0 |
| TC-012-013 | Tab 3 replay non-dead-letter | Event status='processed' | Click "Phát lại" | Button disabled, hover tooltip "Sự kiện này không thể phát lại" | P1 |
| TC-012-014 | Tab 4 health metrics realtime | Admin tab 4 | Page load | Cards hiển thị: event success 24h, latency p95, current outage state, last_event_received_at | P0 |
| TC-012-015 | Tab 4 outage history | Admin tab 4 | Scroll xuống | List outage history với MTTR per incident | P1 |
| TC-012-016 | Cross-browser Chrome | Admin Chrome 120+ | Smoke 4 tabs | Render OK | P0 |
| TC-012-017 | Cross-browser Safari | Admin Safari 16+ | Smoke 4 tabs | Render OK | P0 |
| TC-012-018 | Cross-browser Firefox | Admin Firefox 120+ | Smoke 4 tabs | Render OK | P0 |
| TC-012-019 | Kill switch toggle | Admin tab 1 | Toggle kill switch ON | UPDATE app_setting, toast warning "Đã tắt tất cả webhook Pancake" | P0 |
| TC-012-020 | VIP tag names save | Admin tab 1 | Nhập multi-line "VIP\nHot Lead\nKhách lớn" | Trim whitespace + lowercase, UPDATE pancake_connection.vip_tag_names text[] | P0 |
TC-013 — FR-013 REST API consumption + circuit breaker
| TC | Loại | Điều kiện | Thao tác | Kỳ vọng | P |
|---|---|---|---|---|---|
| TC-013-001 | Happy /sources | Cron 3 fire hourly | Call Pancake /sources | Upsert pancake_source_routing, new sources is_active=false | P0 |
| TC-013-002 | Timeout | Pancake API timeout 30s | Cron 3 fire | Circuit breaker counter+1, log warning | P1 |
| TC-013-003 | Circuit breaker open | 5 fail/60s | Cron 3 fire 6 lần | Breaker open, Cron 3 skip dùng cache, log alert | P0 |
| TC-013-004 | Circuit breaker half-open | Sau 30s breaker open | Cron 3 next | Allow 1 request → success → close; fail → open lại | P1 |
| TC-013-005 | Rate limit 429 | Pancake trả 429 | Cron 3 fire | Exponential backoff, log warning | P1 |
TC-014 — FR-014 Outage recovery + DLQ replay (CHAOS TEST P0)
| TC | Loại | Điều kiện | Thao tác | Kỳ vọng | P |
|---|---|---|---|---|---|
| TC-014-001 | Cron 4 detect outage | Block Pancake webhook 5 phút trong giờ business | Cron 4 chạy mỗi phút | pancake_outage_state.status='outage_started', outage_started_at=now, notification ops | P0 |
| TC-014-002 | Cron 6 fallback start | outage_started | Cron 6 activate | Call Pancake REST /records?modified_since=last_event_received_at interval 60s, inject events vào pancake_webhook_event với event_type='polled' | P0 |
| TC-014-003 | Cron 6 interval increase | Outage > 5 phút | Cron 6 next | Interval tăng 60s → 120s → ... → 900s max | P0 |
| TC-014-004 | Outage recovered | Unblock Pancake webhook | Webhook nhận event mới | status='outage_recovered', Cron 6 stop sau 5 phút sustain, status='healthy' | P0 |
| TC-014-005 | Cron 7 reconciliation no miss | Daily 02:00 AM, no outage | Cron 7 fire | Query Pancake /records?modified_since=NOW()-25h compare DB → miss_count=0, log info | P0 |
| TC-014-006 | Cron 7 reconciliation miss detected | Simulate webhook drop 3 events trong 24h | Cron 7 fire | miss_count=3, inject vào pancake_webhook_event với event_type='reconciled', alert nếu > 5/ngày | P0 |
| TC-014-007 | Cron 7 critical threshold | 100 missing events | Cron 7 fire | Alert critical, notify ops | P1 |
| TC-014-008 | DLQ replay single | Event status='dead_letter', retry_count=2 | Admin Tab 3 click "Phát lại" | Reset status='received', retry_count=0, fire trigger lại, success → status='processed' | P0 |
| TC-014-009 | DLQ replay fail 3 times | Event status='dead_letter', retry_count=3 | Replay | Reject với error "Đã đạt giới hạn retry. Cần review thủ công." Status='permanently_failed' | P0 |
| TC-014-010 | DLQ bulk replay rate limit | Select 100 events | "Phát lại hàng loạt" | Replay 5 events/giây, total 20s, no DB lock contention | P0 |
| TC-014-011 | Chaos: Pancake down 1 giờ | Block Pancake API + webhook 1 giờ | Wait | Outage detected → Cron 6 fallback → on recovery, Cron 7 daily catches any miss → DLQ replay nếu lỗi | P0 |
| TC-014-012 | Chaos: Diva crm-api crash | Kill crm-api pod 5 phút | Webhook vẫn nhận, status='received' accumulate | Khi crm-api restart, Hasura retry trigger fire, events được process tuần tự | P0 |
| TC-014-013 | Chaos: DB down 1 phút | Stop PostgreSQL primary 1 phút | Webhook trả 500 (Pancake retry), Hasura buffer events | Khi DB up, replay buffered events, status='processed' | P0 |
| TC-014-014 | Chaos: Hasura down 5 phút | Stop Hasura 5 phút | Webhook receiver vẫn INSERT status='ingested' nhưng KHÔNG fire trigger | Khi Hasura up, manual UPDATE status='received' rồi fire lại | P1 |
| TC-014-015 | Chaos: Pancake gửi 1000 duplicate | Pancake retry x10 cho 100 event | — | 100 event processed, 900 status='skipped_duplicate', no duplicate ticket | P0 |
TC-015 — FR-015 Feature flag 3 mức
| TC | Loại | Điều kiện | Thao tác | Kỳ vọng | P |
|---|---|---|---|---|---|
| TC-015-001 | Kill switch tổng | app_setting.pancake_integration.enabled=false | Webhook nhận | status='skipped_kill_switch' | P0 |
| TC-015-002 | Connection status paused | pancake_connection.status='paused' | Webhook nhận | Tất cả events skip, status='skipped_kill_switch' | P0 |
| TC-015-003 | Per-source toggle | 1 source is_active=false trong 10 sources active | Webhook source này | status='skipped_source_disabled'. Source khác vẫn xử lý | P0 |
TC-016 — FR-016 Test mode
| TC | Loại | Điều kiện | Thao tác | Kỳ vọng | P |
|---|---|---|---|---|---|
| TC-016-001 | Test flag from payload | Payload is_test=true | Webhook process | event.is_test=true, account.pancake_metadata.is_test=true, ticket.input_note prefix "[TEST]", KHÔNG fire notification | P1 |
| TC-016-002 | KPI filter | Test events exist | Query KPI dashboard | is_test=false filter, test events không count | P1 |
| TC-016-003 | Cleanup cron | Test event > 24h | Cron pancake_test_event_cleanup daily 03:00 AM | Soft-delete (disabled=true), KHÔNG hard-delete | P1 |
TC-017 — FR-017 Opt-out
| TC | Loại | Điều kiện | Thao tác | Kỳ vọng | P |
|---|---|---|---|---|---|
| TC-017-001 | Opt-out detected | account_id có customer_consent.consent_data->>'marketing'='false' | Webhook process | STEP 7 detect, status='skipped_opt_out', account update only, KHÔNG tạo ticket | P0 |
| TC-017-002 | No consent row | account_id chưa có customer_consent row | Webhook process | Default marketing=true, tạo ticket bình thường | P0 |
TC-018 — FR-018 FE delta dropdown
| TC | Loại | Điều kiện | Thao tác | Kỳ vọng | P |
|---|---|---|---|---|---|
| TC-018-001 | Dropdown render slot 9 | Migration INSERT crm_master_data done + FE deploy | Telesale login → Tickets filter dropdown | Hiển thị 9 sources gồm "Pancake CRM" cuối cùng | P0 |
Coverage Lifecycle (PRD LIFECYCLE-001 — pancake_webhook_event.status)
| TC | Từ | Event | Guard | Sang | Side effects | P |
|---|---|---|---|---|---|---|
| TC-ST-001 | [*] | INSERT webhook step 1 | — | ingested | INSERT row | P0 |
| TC-ST-002 | ingested | UPDATE verify pass | token OK, IP OK, schema OK, dedup pass | received | Fire Hasura trigger | P0 |
| TC-ST-003 | ingested | UPDATE token fail | token sai | auth_failed | Audit log, no trigger fire (filter STEP 0) | P0 |
| TC-ST-004 | received | trigger fire | NEW.status='received' AND record_id NOT NULL | processing | crm-api STEP 1 begin TX | P0 |
| TC-ST-005 | processing | STEP 7 opt-out detect | consent.marketing='false' | skipped_opt_out | Account update only, COMMIT | P0 |
| TC-ST-006 | processing | STEP 14 COMMIT | All steps OK | processed | processed_at=NOW(), notification sent | P0 |
| TC-ST-007 | processing | Exception | STEP 1-14 fail | dead_letter | retry_count++, error_message logged, ROLLBACK | P0 |
| TC-ST-008 | dead_letter | Admin replay | retry_count < 3 | received | Reset retry_count=0, fire trigger lại | P0 |
| TC-ST-009 | dead_letter | Cron check retry exceeded | retry_count >= 3 | permanently_failed | Alert manual review | P1 |
Coverage Lifecycle (PRD LIFECYCLE-002 — pancake_outage_state)
| TC | Từ | Event | Guard | Sang | Side effects | P |
|---|---|---|---|---|---|---|
| TC-ST-010 | healthy | Cron 4 detect no event 5 phút business hours | last_event_received_at > NOW()-5min | outage_started | outage_started_at=NOW, Cron 6 activate, notify ops | P0 |
| TC-ST-011 | outage_started | Webhook nhận event mới | — | outage_recovered | last_event_received_at=NOW | P0 |
| TC-ST-012 | outage_recovered | Sustain 5 phút healthy | No new outage | healthy | Cron 6 stop, outage_ended_at=NOW, MTTR computed | P0 |
Coverage Dynamic Permission (Diva RBAC pattern)
| TC | Điều kiện | Thao tác | Kỳ vọng UI | Kỳ vọng API | P |
|---|---|---|---|---|---|
| TC-PERM-001 | Manager không có pancake_crm_integration.access | Mở Settings menu | Menu "Tích hợp Pancake" ẩn | Hasura query bảng pancake_* permission denied (no SELECT) | P0 |
| TC-PERM-002 | Admin có full | Mở Settings/Pancake | 4 tabs render | Queries return data | P0 |
| TC-PERM-003 | Admin có access nhưng KHÔNG có replay_dlq | Tab 3 view event dead_letter | Button "Phát lại" hidden | Action pancake_replay_event rejected: UNAUTHORIZED | P0 |
| TC-PERM-004 | Telesale có quyền xem ticket | Xem ticket có source='ticket_source_pancake' | Hiển thị bình thường (filter dropdown ticket source nhận slot 9) | Ticket queries trả đầy đủ | P0 |
| TC-PERM-005 | POS-only role | Mở admin URL Settings/Pancake | Redirect/no access | Hasura admin metadata không expose cho POS portal | P0 |
D3) Dữ liệu seed kiểm thử
Dataset DS-001 — 1 source low-volume cho W1 pilot
Cách tạo: SQL script + Pancake admin setup
sql
-- 1. Seed pancake_connection
INSERT INTO pancake_connection (workspace_id, api_key_encrypted, webhook_token, vip_tag_names, status)
VALUES ('ws_diva_main', encrypt('test_api_key_xxx'), 'test_token_abc123', ARRAY['vip', 'hot lead'], 'active');
-- 2. Seed pancake_source_routing với 1 source W1
INSERT INTO pancake_source_routing (pancake_source_id, pancake_source_name, diva_branch_id, is_active)
VALUES ('src_fb_diva_hcm_q1', 'FB Diva HCM Q1', '<uuid_branch_q1>', true);
-- 3. app_setting kill switch OFF ban đầu
UPDATE app_setting SET app_settings = jsonb_set(app_settings, '{pancake_integration}', '{"enabled": true, "kill_switch": false}') WHERE id=1;
-- 4. customer_consent test cho opt-out scenario
INSERT INTO customer_consent (account_id, consent_data)
VALUES ('<test_account_id_opt_out>', '{"marketing": "false"}');
-- Verify
SELECT * FROM pancake_connection;
SELECT * FROM pancake_source_routing;
SELECT app_settings->'pancake_integration' FROM app_setting WHERE id=1;Dataset DS-002 — 10 Pancake sample payloads
File: qa/test-data/pancake_sample_payloads.json
json
[
{
"name": "happy_path_new_account",
"payload": {
"record_id": "rec_001",
"modified_on": "2026-05-28T10:00:00Z",
"phone_number": "+84912345678",
"source": [{"id": "src_fb_diva_hcm_q1", "name": "FB Diva HCM Q1"}],
"name": "Nguyễn Thị Lan",
"email": "lan.nguyen@email.com",
"full_address": "Q1, HCM",
"tags": ["mới quan tâm"]
}
},
{
"name": "happy_path_existing_account_update_address",
"payload": { /* same phone, name change, KHÔNG tạo ticket */ }
},
{
"name": "happy_path_phone_change",
"payload": { /* phone đổi → tạo ticket mới */ }
},
{
"name": "vip_tag_new",
"payload": { /* tags=["VIP"] → tạo ticket */ }
},
{
"name": "opt_out_marketing",
"payload": { /* account đã opt-out → status='skipped_opt_out' */ }
},
{
"name": "phone_local_format",
"payload": { /* phone="0912345678" → normalize +84912345678 */ }
},
{
"name": "phone_invalid_format",
"payload": { /* phone="abc123" → fallback raw match */ }
},
{
"name": "source_unmapped",
"payload": { /* source.id chưa map → Loose mode, branch=NULL */ }
},
{
"name": "test_mode",
"payload": { /* is_test=true → KPI exclude */ }
},
{
"name": "schema_error",
"payload": { /* thiếu record_id → status='parse_error' */ }
}
]Dataset DS-003 — Load test K6 script
File: qa/load-test/pancake_webhook_k6.js
javascript
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 }, // ramp up
{ duration: '10m', target: 100 }, // sustained 100 req/s
{ duration: '2m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<1000'], // p95 < 1s
http_req_failed: ['rate<0.001'], // error rate < 0.1%
},
};
export default function() {
const payload = generateDistinctPayload(); // mock function
const res = http.post('https://staging.diva.com.vn/api/pancake/record/test_token_abc123', JSON.stringify(payload));
check(res, { 'status 200': (r) => r.status === 200 });
sleep(0.01);
}Dataset DS-004 — Chaos test scripts
File: qa/chaos-test/
simulate_pancake_outage.sh— block Pancake API + webhook bằng iptables 5-60 phútsimulate_crm_api_crash.sh— kill crm-api pod, đo recoverysimulate_db_failover.sh— stop primary DB, promote replicasimulate_duplicate_retry.sh— Pancake gửi 1000 webhook duplicate
D4) Truy vết (view QA)
Canonical traceability ở
dev-spec.mdC12. Bảng dưới là QA-side mapping FR → TC count + execution status.
| FR | TC range | Count | Status |
|---|---|---|---|
| FR-001 | TC-001-001..008 | 8 | Chờ |
| FR-002 | TC-002-001..003 | 3 | Chờ |
| FR-003 | TC-003-001..002 | 2 | Chờ |
| FR-004 | TC-004-001..010 | 10 | Chờ |
| FR-005 | TC-005-001..005 | 5 | Chờ |
| FR-006 | TC-006-001 | 1 | Chờ |
| FR-007 | TC-007-001..003 | 3 | Chờ |
| FR-008 | TC-008-001..005 | 5 | Chờ |
| FR-009 | TC-009-001..006 | 6 | Chờ |
| FR-010 | TC-010-001..004 | 4 | Chờ |
| FR-011 | TC-011-001..003 | 3 | Chờ |
| FR-012 | TC-012-001..020 | 20 | Chờ |
| FR-013 | TC-013-001..005 | 5 | Chờ |
| FR-014 | TC-014-001..015 | 15 | Chờ |
| FR-015 | TC-015-001..003 | 3 | Chờ |
| FR-016 | TC-016-001..003 | 3 | Chờ |
| FR-017 | TC-017-001..002 | 2 | Chờ |
| FR-018 | TC-018-001 | 1 | Chờ |
| Lifecycle 001 | TC-ST-001..009 | 9 | Chờ |
| Lifecycle 002 | TC-ST-010..012 | 3 | Chờ |
| Permission | TC-PERM-001..005 | 5 | Chờ |
| TỔNG | 116 TC |
25 DEC mapping → cách kiểm chứng đầy đủ trong
dev-spec.mdC12.
D5) Tiêu chí vào / ra
Tiêu chí vào (Entry — bắt đầu test execution)
- [ ] BE deploy xong: 8 migration + Hasura metadata + 7 cron + Hasura action
pancake_replay_event - [ ] FE deploy xong staging: 5 Settings pages + 3 file delta dropdown + routes + menu
- [ ] Seed data DS-001 + DS-002 sẵn sàng
- [ ] Test accounts: Admin x2, Manager x2, Telesale x5 (active trong branch test), POS-only x1
- [ ] Pancake webhook URL setup ở Pancake admin (test workspace) — pointer tới staging
- [ ] Permission seed: module
pancake_crm_integrationvới 6 actions + default Admin role - [ ] Notification template 3 mới đã seed
- [ ] Reverse proxy (staging) + TLS cert valid
- [ ] Prometheus + alertmanager rules deployed staging
- [ ] PD-003 confirmed (
customer_consent.consent_data.marketingkey đã verify với PO) - [ ]
ui-spec.mdB0 As-Is UI Inventory verified — không có gap/drift unresolved
Tiêu chí ra (Exit — kết thúc test, ready for production)
- [ ] P0 test case PASS: 100% (≥ 90/116 TC marked P0)
- [ ] P1 test case PASS: ≥ 95% hoặc có waiver PO cho specific failures
- [ ] P2 test case: documented as known issue
- [ ] Chaos test outage 4-layer recovery: PASS (TC-014-001..015)
- [ ] Load test: Latency p95 < 1s sustained 100 req/s × 10 phút (TC-001-008)
- [ ] Permission audit: Không bug leak dữ liệu qua UI/API/export khi non-admin (TC-PERM-001..005)
- [ ] Lifecycle coverage: 100% transitions tested (TC-ST-001..012)
- [ ] DLQ replay end-to-end: PASS bao gồm bulk + rate limit + permission (TC-012-010..013)
- [ ] Cross-browser: Chrome + Safari + Firefox smoke pass (TC-012-016..018)
- [ ] No critical/major bug open (P0 blocker, security, data integrity)
- [ ] Smoke test post-deploy staging: PASS — sẵn sàng promote production
- [ ] Performance baseline: Capacity 100k events/tháng verified, DB index hiệu quả
Tiêu chí ra cho Pilot W1-W4 (per wave)
- W1 exit: Event success rate ≥ 99.5%, latency p95 < 30s, telesale survey ≥ 4/5, no Pancake suspension
- W2 exit: Cumulative success rate ≥ 99.5%, manager intervention < 20%, auto-assign correct ≥ 85%
- W3 exit: Load test 100 req/s pass, chaos test pass, circuit breaker test pass, DB no seq scan
- W4 exit: Toàn bộ 5 KPI đạt target (per PRD A8), manager survey 100% ngừng copy-paste, no Pancake suspension toàn pilot
→ Chi tiết per-wave gates xem go-live-checklist.md E1.
D6) Test execution strategy
Phase 1 — Unit + Integration test (Sprint 1)
- Owner: BE Dev + QA
- Run trên CI: mỗi PR merge
- Coverage: TC-001 (no Pancake real, mock), TC-002..010 (DB integration với testdata), TC-005 (unit phone normalize)
- Tool:
go test ./...+ testify + mockery cho external
Phase 2 — E2E test staging với Pancake simulation (Sprint 2)
- Owner: QA
- Run: daily smoke + nightly full regression
- Coverage: TC-001 (real webhook.site simulate Pancake), TC-012 (FE E2E Playwright/Cypress), TC-013 (Pancake REST sandbox), TC-014 (chaos test scripts)
- Tool: Playwright + webhook.site + iptables chaos
Phase 3 — Load + Chaos test (Sprint 2-3, W3 dedicated)
- Owner: QA + DevOps
- Run: W3 dedicated 3 ngày
- Coverage: TC-001-008 (load 100 req/s × 10 phút), TC-014-001..015 (chaos outage), TC-006-001 (concurrency stress)
- Tool: K6 + custom chaos scripts
Phase 4 — Pilot W1-W4 real production (Sprint 3)
- Owner: QA + PO observation
- Run: Continuous monitoring + daily review
- Coverage: Live traffic from 1 → 40+ sources
- Tool: Grafana dashboard + Pancake admin monitor + Slack alerts
Hết QA Test Plan v1.0. Tổng 116 test case across 18 FR + 12 lifecycle + 5 permission. Sẵn sàng entry criteria Sprint 1.
D7) Pass 1 Resolutions (Phase 5.2 — additions)
Date: 15/05/2026 | Trigger: QA Lead + DevOps Lead + FE/UX review. Section này thêm 11 TC mới + đổi 1 TC priority.
D7.1) New test cases (P0/P1)
| TC ID | FR/DEC | Loại | Mô tả | Priority |
|---|---|---|---|---|
| TC-006-002 | FR-006 | Concurrency stress | 100 concurrent webhook same phone + 100 concurrent distinct phone — measure advisory lock contention, deadlock detection, p95 latency. Verify pgxpool DEC-028 architecture mới | P0 (QA P0-Q1) |
| TC-001-009 | FR-001 | Load burst | Stress 500 req/s × 2 phút burst (simulate Pancake retry storm). Verify rate limit reverse proxy 1000 req/s/IP không trigger sai + circuit breaker không open mistake | P0 (QA P0-Q4) |
| TC-014-016 | FR-014 DEC-015 | Chaos network partition | Latency injection 5s vào Hasura trigger call (toxiproxy/iptables) — verify timeout handling + retry behavior + DLQ population | P0 (QA P0-Q5) |
| TC-014-017 | FR-014 | Chaos Cron concurrency | Cron 7 reconciliation chạy lúc Cron 6 adaptive polling active — verify không duplicate inject event_type='polled' + event_type='reconciled' cho cùng record_id | P1 (QA P1-Q3) |
| TC-PERM-006 | DEC-027 | Dynamic Permission override | Admin grant Marketing role pancake_crm_integration.access qua DP UI → Marketing thấy Tab 3 audit read-only, KHÔNG có button "Phát lại" (do thiếu update action) | P0 (QA P0-Q2) |
| TC-PERM-007 | DEC-027 | Permission revoke mid-session | Admin đang xem SCR-03, admin khác revoke access → next API call 401 → FE refetch + redirect home | P0 (QA P0-Q2) |
| TC-009-007 | FR-009 | Smart update race rapid | Pancake gửi 3 webhook update cùng record trong <1s — verify ordering preservation theo modified_on DESC | P1 (QA P1-Q1) |
| TC-008-006 | FR-008 | Round-robin telesale offline | Telesale offline giữa rotation — verify ticket_distribute skip + next person assigned | P1 (QA P1-Q2) |
| TC-012-021 | FR-012 | Tab 3 pagination >5k row | Filter date 30 ngày + status all → 5000+ rows. Verify server-side pagination 20/page response time < 500ms | P1 (QA P1-Q4) |
| TC-DEC-006 | DEC-006 v1.1 | Migration #3 idempotency | Run migration #3 lần 2 → DROP ADD COLUMN primary_phone đã exist → KHÔNG fail. Verify UNIQUE (account_id) constraint applied | P0 (verify Pass 1 fix) |
| TC-DEC-028 | DEC-028 | Direct pgxpool Pancake handler | Verify pgxpool connection isolated từ Hasura. Test: kill Hasura → Pancake handler vẫn process events (qua direct pgx). Notification dispatch fail acceptable (non-atomic) | P0 (verify architecture) |
D7.2) Priority upgrade
| TC | Cũ | Mới | Lý do |
|---|---|---|---|
| TC-011-003 (notification down) | P1 | P0 | QA P0 — silent failure mode: telesale không nhận lead nhưng ticket vẫn tạo → SLA p95 fail âm thầm |
D7.3) Updated DS-002 (QA P0-Q3) — realistic Diva spa data
json
[
{ "name": "happy_new_account_hcm",
"payload": { "record_id": "rec_001", "phone_number": "+84912345678", "source": [{"id": "src_fb_diva_hcm_q1", "name": "FB Diva HCM Q1"}], "name": "Nguyễn Thị Lan", "email": "lan.nguyen@gmail.com", "full_address": "215 Hai Bà Trưng, Q1, TP HCM" }},
{ "name": "vip_tag_dn",
"payload": { "record_id": "rec_002", "phone_number": "0905123456", "source": [{"id": "src_zalo_diva_dn", "name": "Zalo OA Diva Đà Nẵng"}], "name": "Đặng Thị Ánh Sương", "tags": ["VIP", "Hot Lead"], "full_address": "152 Trần Phú, Hải Châu, Đà Nẵng" }},
{ "name": "opt_out_hn",
"payload": { "record_id": "rec_003", "phone_number": "+84 983 142 578", "source": [{"id": "src_tiktok_diva", "name": "TikTok Diva 💎 Premium"}], "name": "Phạm Văn Hùng", "_consent_pre_seed": { "marketing": "false" } }},
{ "name": "phone_invalid",
"payload": { "record_id": "rec_004", "phone_number": "912345abc", "source": [{"id": "src_fb_diva_q3", "name": "FB Diva HCM Q3"}], "name": "Lê Hoàng Việt" }},
{ "name": "duplicate_retry",
"payload": { "record_id": "rec_001", "modified_on": "2026-05-28T10:00:00Z", "phone_number": "+84912345678" }},
{ "name": "source_unmapped",
"payload": { "record_id": "rec_005", "phone_number": "+84937123456", "source": [{"id": "src_lazada_diva_new", "name": "Lazada Diva (mới chưa map)"}], "name": "Trần Hồng Nhung" }},
{ "name": "test_mode",
"payload": { "record_id": "rec_test_001", "is_test": true, "phone_number": "+84900000001", "source": [{"id": "src_fb_diva_test", "name": "FB Diva Test"}], "name": "Test User" }},
{ "name": "schema_error_no_record_id",
"payload": { "phone_number": "+84912000111", "source": [{"id": "src_fb", "name": "Generic FB"}], "name": "Missing record_id" }},
{ "name": "unicode_branch",
"payload": { "record_id": "rec_006", "phone_number": "+84913456789", "source": [{"id": "src_fb_cau_giay", "name": "FB Diva HN Cầu Giấy"}], "name": "Hoàng Mai Phương", "full_address": "Trần Duy Hưng, Cầu Giấy, Hà Nội" }},
{ "name": "long_record_id",
"payload": { "record_id": "rec_extremely_long_id_for_edge_case_testing_with_more_than_100_characters_to_verify_db_TEXT_unbounded_handles_it_OK", "phone_number": "+84912999888" }}
]D7.4) New alert rules verify (DevOps P0-D1)
| Test | Verify |
|---|---|
pancake_event_process_duration_seconds p95 > 30s for 10min | Inject slow query → alert fire |
pancake_webhook_latency_seconds p99 > 2s for 5min | Stress test 1000 req/s → alert fire |
pancake_circuit_breaker_state == 2 for 30min | Block Pancake REST 30+ phút → alert escalate Critical |
D7.5) D4 Traceability update
| FR | TC range | Count | Status |
|---|---|---|---|
| FR-001 | TC-001-001..009 | 9 (+1) | Chờ |
| FR-006 | TC-006-001..002 | 2 (+1) | Chờ |
| FR-008 | TC-008-001..006 | 6 (+1) | Chờ |
| FR-009 | TC-009-001..007 | 7 (+1) | Chờ |
| FR-014 | TC-014-001..017 | 17 (+2) | Chờ |
| FR-012 | TC-012-001..021 | 21 (+1) | Chờ |
| Permission | TC-PERM-001..007 | 7 (+2) | Chờ |
| DEC verification | TC-DEC-006, TC-DEC-028 | 2 (NEW) | Chờ |
| TỔNG | 127 TC (+11) |
D7.6) D5 Entry/Exit updates
Entry thêm:
- [ ] PD-008 W1 source list confirmed (handoff blocker)
- [ ] PD-013
first_response_atfield decision (BE+PO) - [ ] DEC-028 pgxpool foundation deployed staging
- [ ] DS-002 v2 realistic data seeded
- [ ] 3 alert rules thêm deployed staging
Exit thêm:
- [ ] Permission audit script chạy auto, không leak
- [ ] K6 baseline benchmark stored (cho regression compare)
- [ ] Rollback drill rehearsal staging pass
D7.7) Pilot exit criteria revisions
- W1 exit: Telesale survey n≥3 (KHÔNG n=1). Source W1 phải có ≥3 telesale active đồng thời.
- W3 exit: Snapshot
pg_stat_user_indexesbefore/after migration (DevOps P0). - W4 exit: Manager survey ≥87% (13/15) — realistic threshold thay vì 100% strict.