Skip to content

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.md C12. File này là view QA (FR → TC mapping + execution detail).

Đầu vào chuẩn (Canonical Inputs)

FileVai trò
SOURCE_OF_TRUTH.md25 DEC + Solution Lock — ưu tiên cao nhất
decision-brief.mdTóm tắt rủi ro + Pilot W1-W4
prd.mdFR-001..018 + LIFECYCLE-001/002 + 6 FORMULA + A7 RSK
ui-spec.mdPermission Matrix B5 + State Matrix B6 + Tooltip B9
dev-spec.mdC5 flow 15 STEP + C7 migration + C8 security + C12 traceability

D1) Phạm vi kiểm thử

18 FR coverage matrix

FRMô tảĐộ ưu tiên testTest typeSố TC dự kiến
FR-001Webhook receiver endpoint fail-open 200P0Integration + Load8
FR-002Persist raw event audit trailP0Integration3
FR-003Idempotency 3-tuple UNIQUEP0Integration2
FR-004Async processor Hasura event triggerP0Integration10 (15 step coverage)
FR-005Phone E.164 normalize + match/upsertP0Unit + Integration5
FR-006Advisory lock chống raceP0Concurrency stress1
FR-007Auto-create contact_bookP0Integration3
FR-008Round-robin REUSE GetTicketUpdatesP0Integration5
FR-009Smart update Q2.bP0Integration6
FR-010Tạo ticket standardP0Integration4
FR-011Notification (telesale + admin)P0Integration + Notification3
FR-012Settings UI 4 tabsP0 (UI)E2E + Permission20
FR-013REST API consumption /sources + circuit breakerP1Integration + Chaos5
FR-0144-layer outage recovery + DLQ replayP0Chaos test15
FR-015Feature flag 3 mức + pilot rolloutP0E2E3
FR-016Test mode eventP1Integration3
FR-017Opt-out compliance customer_consentP0Integration2
FR-018FE delta dropdown ticket source slot 9P0 (UI)E2E1

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 ticket event 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 đầuKhi thao tácDữ liệu testKỳ vọngĐộ ưu tiên
TC-001-001Pancake 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=truePancake POST webhook payload happy pathJSON {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 telesaleP0

Ngoài luồng thành công (P0/P1 BẮT BUỘC):

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-001-002Âm tínhToken URL saiPancake POST với /api/pancake/record/INVALID_TOKENBất kỳ payloadResponse 200 (KHÔNG 401), pancake_webhook_event.status='auth_failed', alert log nếu accumulate > 10 lần/phútP0
TC-001-003Âm tínhIP không trong whitelist (sau PD-002 enforced W4)Pancake POST từ IP 1.2.3.4 không whitelistHappy payloadResponse 200, status='ip_blocked', source_ip='1.2.3.4' loggedP0 (W4)
TC-001-004Âm tínhJSON schema saiPOST với body {"invalid": "schema"} (thiếu record_id)Response 200, status='parse_error', error_message loggedP0
TC-001-005BiênIdempotency duplicatePOST 2 lần cùng 3-tuple (record_id, modified_on, payload_hash)Cùng payload TC-001-001Lần 1: status='received' rồi 'processed'. Lần 2: status='skipped_duplicate', UPDATE last_received_at. KHÔNG tạo ticket trùngP0
TC-001-006BiênSource disabledpancake_source_routing.is_active=false cho source trong payloadPayload với source.id='src_fb_diva_hcm' đã disabledStatus='skipped_source_disabled', KHÔNG tạo ticketP0
TC-001-007BiênKill switch ONapp_setting.pancake_integration.enabled=falseBất kỳ payloadStatus='skipped_kill_switch', KHÔNG fire triggerP0
TC-001-008LoadSustained 100 req/s × 10 phútK6 load test60000 payload distinct record_idLatency p95 < 1s, error rate < 0.1%, no Pancake suspension (error < 80%)P0 (W3)

TC-002 — FR-002 Persist raw event

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-002-001HappyWebhook nhận request hợp lệINSERT rowTC-001-001 payloadpancake_webhook_event có row với raw_payload jsonb match payload, raw_headers jsonb có HTTP headers, source_ip resolve từ X-Forwarded-ForP0
TC-002-002Âm tínhDB connection errorINSERT khi DB downWebhook trả 500 (Pancake retry), log fatalP1
TC-002-003BiênPayload rất lớn (1MB)POST với payload 1MBRandom 1MB jsonbINSERT thành công, raw_payload lưu nguyênP1

TC-003 — FR-003 Idempotency UNIQUE 3-tuple

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-003-001HappyĐã có row với 3-tuple (rec_A, ts_1, hash_X)INSERT row mới cùng 3-tupleCùng payloadUNIQUE violation → catch → UPDATE last_received_at, status='skipped_duplicate'P0
TC-003-002BiênCùng record_id + modified_on nhưng body khác (Pancake bug)INSERT 2 rowBody khác nhau → hash khác2 row riêng biệt, cả 2 xử lýP1

TC-004 — FR-004 Async processor 15-step

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-004-001Happystatus='received'Trigger fireAccount chưa exist + source mapped branch + telesale activeSTEP 0 pass → 1-14 success → status='processed', ticket có assignee_idP0
TC-004-002Filterstatus='auth_failed'Trigger fire (Hasura fire mọi UPDATE column status)STEP 0 filter ra (NEW.status != 'received') → no-op return 200P0
TC-004-003TX rollbackSTEP 5 exception (DB constraint fail)Trigger firePayload với phone nullTX rollback, status='dead_letter', retry_count=1, error_message loggedP0
TC-004-004Idempotent STEP 1Re-fetch FOR UPDATETrigger fire 2 lần đồng thời (Hasura retry)Cùng eventLần 1 process, lần 2 STEP 1 thấy status đã 'processing' → skip returnP0
TC-004-005DLQSTEP 1-14 fail 3 lầnManual replay từ Tab 3event_id của row dead_letterReset status='received', retry_count=0 → fire lạiP0
TC-004-006Permanently failedretry_count >= 3Auto detectstatus='permanently_failed', alert manual reviewP1
TC-004-007STEP 7 opt-outcustomer_consent.marketing='false'Trigger fireAccount có consent opt-outSTEP 7 detect → status='skipped_opt_out', account update only, KHÔNG tạo ticketP0
TC-004-008STEP 8 smart update no-changeAccount có sẵn, cùng phone/source/tagTrigger fire update payloadSame phone, same source, same tagSTEP 8 detect no important change → status='processed', KHÔNG tạo ticket, chỉ update account.display_nameP0
TC-004-009STEP 8 smart update phone changeAccount có sẵn, phone đổiTrigger firenew phone E.164 khác oldTạo ticket mớiP0
TC-004-010STEP 8 smart update source newAccount có sẵn, source mới chưa mapTrigger firenew source_idTạo ticket mớiP0

TC-005 — FR-005 Phone E.164 normalize

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-005-001Happy local VNWebhook payload phone format 0912345678Phone string 0912345678Normalize thành +84912345678, match/create account với normalized_phone='+84912345678', phone_code=84, phone_number='912345678', phone_enabled=trueP0
TC-005-002Happy E.164Phone đã E.164 +84912345678+84912345678Normalize cùng, KHÔNG đổiP0
TC-005-003Edge formatPhone +84 912 345 678 (có space)Trim space → normalize+84912345678P0
TC-005-004Âm tínhPhone sai format 912345678abcLog warning, normalized_phone=NULL, fallback match by (phone_code, phone_number) rawP1
TC-005-005Edge duplicate2 account legacy cùng normalize thành +84912345678MatchMatch account created_at cũ nhất, log warningP1

TC-006 — FR-006 Advisory lock concurrent

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-006-001Concurrency stressPhone +84912345678 chưa có account10 webhook POST đồng thời cùng phone10 payload distinct record_id cùng phoneCHỈ 1 account được tạo, 10 ticket riêng (mỗi event 1 ticket), không có duplicate accountP0

TC-007 — FR-007 Auto-create contact_book

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-007-001Happy new accountAccount vừa được tạo ở STEP 5 (chưa có contact)STEP 5b query-before-insertaccount_id mới1 row contact_book mới với primary_phone=normalized_phone, FK validP0
TC-007-002Reuse existingAccount đã có contact_book rowSTEP 5b queryaccount_id cũKHÔNG tạo mới, reuseP0
TC-007-003Race2 webhook đồng thời tạo contact cho account mớiAdvisory lock đã serialize ở STEP 3Chỉ 1 contact tạo, không vi phạm UNIQUE constraintP1

TC-008 — FR-008 Round-robin REUSE

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-008-001Round-robin 5 telesaleBranch CN-Q1 có 5 telesale active T1..T5; last assignee T23 webhook đồng thời gửi cho branch CN-Q13 record different3 ticket assign cho T3, T4, T5 (next in rotation từ T2), ticket_distribute INSERT 3 rowP0
TC-008-002Loose mode branch=NULLSource chưa map branchWebhook gửi với source unmappedrecord_idTicket assignee_id=NULL, branch_id=NULL. Notification noti_pancake_unmapped_branch gửi adminP0
TC-008-003No active telesaleBranch CN-Q5 có 0 telesale active (ngoài giờ)Webhook gửirecord_idTicket branch_id set, assignee_id=NULL. Notification gửi manager branchP0
TC-008-004Refactor verify backward compatExisting flow tạo ticket (Stringee/iCall) sau khi refactor GetTicketUpdates thành publicTạo ticket manual qua admin UIRound-robin tiếp tục hoạt động cho ticket existing, no regressionP0
TC-008-005Bulk 100 webhook 1 branchBranch có 10 telesale active100 webhook trong 1 phút100 record100 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

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-009-001New accountaccount chưa existWebhook lần đầurecord_id mớiSTEP 8: account mới → luôn tạo ticketP0
TC-009-002No important changeAccount exist, update name/addressWebhook updatePhone/source/tag không đổiKHÔNG tạo ticket, chỉ update account.display_nameP0
TC-009-003Phone changeAccount existWebhook với phone mớinew phone E.164Tạo ticket mớiP0
TC-009-004Source newAccount exist, đã có source AWebhook 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-005VIP tag newAccount exist, chưa có tag VIPWebhook 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=trueP0
TC-009-006VIP tag exist alreadyAccount đã có had_vip_tag=trueWebhook update với cùng tag VIPtags=["vip"]KHÔNG tạo ticket (đã từng có)P1

TC-010 — FR-010 Tạo ticket standard

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-010-001Happy field mappingSTEP 10 INSERT ticketWebhook happy payloadTicket 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_phoneP0
TC-010-002Input note templateWebhook với tags + address + emailtags=["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-003Due date timezoneServer timezone UTC, customer timezone Asia/Ho_Chi_MinhWebhook trước 17:00 UTC ngày 28/05due_date = '2026-05-28 16:59:59+00' (= 23:59:59 +07)P0
TC-010-004Created_by systemWebhookticket.created_by='system_pancake_webhook'P1

TC-011 — FR-011 Notification

TCLoạiĐiều kiện ban đầuKhi thao tácDữ liệu testKỳ vọngP
TC-011-001Telesale assignedTicket có assignee_idSTEP 12 firetelesale_id=T3Push 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âyP0
TC-011-002Admin Loose modebranch_id=NULLSTEP 12 fireNotification noti_pancake_unmapped_branch gửi Admin role + branch-manager NULLP0
TC-011-003Notification downnotification-v2-api 5xxSTEP 12 fireLog warning, KHÔNG fail STEP 13. Ticket vẫn tạo. Alert ops nếu fail rate > 5%/5minP1

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:

TCLoạiĐiều kiệnThao tácKỳ vọngP
TC-012-001Permission hideManager loginTruy cập /admin/settings/pancake-crmRedirect home, menu Settings/Pancake không hiển thịP0
TC-012-002Permission hide POSPOS-only role loginURL gõ trực tiếpRedirect, no accessP0
TC-012-003Admin tab 1 form saveAdmin loginNhập api_key + click "Lưu"UPDATE pancake_connection, toast successP0
TC-012-004Test connection buttonAdmin tab 1Click "Kiểm tra kết nối"Call Pancake REST /sources. Success → hiển thị list sources detected. Fail → banner đỏ với error codeP0
TC-012-005Tab 2 inline edit branchAdmin tab 2Change branch dropdownAutosave debounce 500ms, UPDATE pancake_source_routing.diva_branch_idP0
TC-012-006Tab 2 toggle is_activeAdmin tab 2Click toggleUPDATE is_active, source ngay lập tức skip/process webhookP0
TC-012-007Tab 2 sync sources manualAdmin tab 2Click "Đồng bộ từ Pancake"Call Pancake /sources → upsert pancake_source_routing → new sources hiển thị với is_active=falseP0
TC-012-008Tab 3 filter comboAdmin tab 3Date range + status + sourceTable render đúng filterP0
TC-012-009Tab 3 drawer detailAdmin tab 3Click row eventDrawer mở với raw_payload formatted JSON, error_message, retry_countP0
TC-012-010Tab 3 replay singleAdmin tab 3Click "Phát lại" trên 1 event status='dead_letter'Call pancake_replay_event(event_id) action, status reset 'received', refetch listP0
TC-012-011Tab 3 bulk replay rate limitAdmin tab 3Select 100 events → "Phát lại hàng loạt"Replay 5 events/giây, progress bar, success toastP0
TC-012-012Tab 3 replay permissionUser thiếu replay_dlq actionClick "Phát lại"Button hiddenP0
TC-012-013Tab 3 replay non-dead-letterEvent 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-014Tab 4 health metrics realtimeAdmin tab 4Page loadCards hiển thị: event success 24h, latency p95, current outage state, last_event_received_atP0
TC-012-015Tab 4 outage historyAdmin tab 4Scroll xuốngList outage history với MTTR per incidentP1
TC-012-016Cross-browser ChromeAdmin Chrome 120+Smoke 4 tabsRender OKP0
TC-012-017Cross-browser SafariAdmin Safari 16+Smoke 4 tabsRender OKP0
TC-012-018Cross-browser FirefoxAdmin Firefox 120+Smoke 4 tabsRender OKP0
TC-012-019Kill switch toggleAdmin tab 1Toggle kill switch ONUPDATE app_setting, toast warning "Đã tắt tất cả webhook Pancake"P0
TC-012-020VIP tag names saveAdmin tab 1Nhậ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

TCLoạiĐiều kiệnThao tácKỳ vọngP
TC-013-001Happy /sourcesCron 3 fire hourlyCall Pancake /sourcesUpsert pancake_source_routing, new sources is_active=falseP0
TC-013-002TimeoutPancake API timeout 30sCron 3 fireCircuit breaker counter+1, log warningP1
TC-013-003Circuit breaker open5 fail/60sCron 3 fire 6 lầnBreaker open, Cron 3 skip dùng cache, log alertP0
TC-013-004Circuit breaker half-openSau 30s breaker openCron 3 nextAllow 1 request → success → close; fail → open lạiP1
TC-013-005Rate limit 429Pancake trả 429Cron 3 fireExponential backoff, log warningP1

TC-014 — FR-014 Outage recovery + DLQ replay (CHAOS TEST P0)

TCLoạiĐiều kiệnThao tácKỳ vọngP
TC-014-001Cron 4 detect outageBlock Pancake webhook 5 phút trong giờ businessCron 4 chạy mỗi phútpancake_outage_state.status='outage_started', outage_started_at=now, notification opsP0
TC-014-002Cron 6 fallback startoutage_startedCron 6 activateCall 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-003Cron 6 interval increaseOutage > 5 phútCron 6 nextInterval tăng 60s → 120s → ... → 900s maxP0
TC-014-004Outage recoveredUnblock Pancake webhookWebhook nhận event mớistatus='outage_recovered', Cron 6 stop sau 5 phút sustain, status='healthy'P0
TC-014-005Cron 7 reconciliation no missDaily 02:00 AM, no outageCron 7 fireQuery Pancake /records?modified_since=NOW()-25h compare DB → miss_count=0, log infoP0
TC-014-006Cron 7 reconciliation miss detectedSimulate webhook drop 3 events trong 24hCron 7 firemiss_count=3, inject vào pancake_webhook_event với event_type='reconciled', alert nếu > 5/ngàyP0
TC-014-007Cron 7 critical threshold100 missing eventsCron 7 fireAlert critical, notify opsP1
TC-014-008DLQ replay singleEvent status='dead_letter', retry_count=2Admin Tab 3 click "Phát lại"Reset status='received', retry_count=0, fire trigger lại, success → status='processed'P0
TC-014-009DLQ replay fail 3 timesEvent status='dead_letter', retry_count=3ReplayReject với error "Đã đạt giới hạn retry. Cần review thủ công." Status='permanently_failed'P0
TC-014-010DLQ bulk replay rate limitSelect 100 events"Phát lại hàng loạt"Replay 5 events/giây, total 20s, no DB lock contentionP0
TC-014-011Chaos: Pancake down 1 giờBlock Pancake API + webhook 1 giờWaitOutage detected → Cron 6 fallback → on recovery, Cron 7 daily catches any miss → DLQ replay nếu lỗiP0
TC-014-012Chaos: Diva crm-api crashKill crm-api pod 5 phútWebhook vẫn nhận, status='received' accumulateKhi crm-api restart, Hasura retry trigger fire, events được process tuần tựP0
TC-014-013Chaos: DB down 1 phútStop PostgreSQL primary 1 phútWebhook trả 500 (Pancake retry), Hasura buffer eventsKhi DB up, replay buffered events, status='processed'P0
TC-014-014Chaos: Hasura down 5 phútStop Hasura 5 phútWebhook receiver vẫn INSERT status='ingested' nhưng KHÔNG fire triggerKhi Hasura up, manual UPDATE status='received' rồi fire lạiP1
TC-014-015Chaos: Pancake gửi 1000 duplicatePancake retry x10 cho 100 event100 event processed, 900 status='skipped_duplicate', no duplicate ticketP0

TC-015 — FR-015 Feature flag 3 mức

TCLoạiĐiều kiệnThao tácKỳ vọngP
TC-015-001Kill switch tổngapp_setting.pancake_integration.enabled=falseWebhook nhậnstatus='skipped_kill_switch'P0
TC-015-002Connection status pausedpancake_connection.status='paused'Webhook nhậnTất cả events skip, status='skipped_kill_switch'P0
TC-015-003Per-source toggle1 source is_active=false trong 10 sources activeWebhook source nàystatus='skipped_source_disabled'. Source khác vẫn xử lýP0

TC-016 — FR-016 Test mode

TCLoạiĐiều kiệnThao tácKỳ vọngP
TC-016-001Test flag from payloadPayload is_test=trueWebhook processevent.is_test=true, account.pancake_metadata.is_test=true, ticket.input_note prefix "[TEST]", KHÔNG fire notificationP1
TC-016-002KPI filterTest events existQuery KPI dashboardis_test=false filter, test events không countP1
TC-016-003Cleanup cronTest event > 24hCron pancake_test_event_cleanup daily 03:00 AMSoft-delete (disabled=true), KHÔNG hard-deleteP1

TC-017 — FR-017 Opt-out

TCLoạiĐiều kiệnThao tácKỳ vọngP
TC-017-001Opt-out detectedaccount_id có customer_consent.consent_data->>'marketing'='false'Webhook processSTEP 7 detect, status='skipped_opt_out', account update only, KHÔNG tạo ticketP0
TC-017-002No consent rowaccount_id chưa có customer_consent rowWebhook processDefault marketing=true, tạo ticket bình thườngP0

TC-018 — FR-018 FE delta dropdown

TCLoạiĐiều kiệnThao tácKỳ vọngP
TC-018-001Dropdown render slot 9Migration INSERT crm_master_data done + FE deployTelesale login → Tickets filter dropdownHiển thị 9 sources gồm "Pancake CRM" cuối cùngP0

Coverage Lifecycle (PRD LIFECYCLE-001 — pancake_webhook_event.status)

TCTừEventGuardSangSide effectsP
TC-ST-001[*]INSERT webhook step 1ingestedINSERT rowP0
TC-ST-002ingestedUPDATE verify passtoken OK, IP OK, schema OK, dedup passreceivedFire Hasura triggerP0
TC-ST-003ingestedUPDATE token failtoken saiauth_failedAudit log, no trigger fire (filter STEP 0)P0
TC-ST-004receivedtrigger fireNEW.status='received' AND record_id NOT NULLprocessingcrm-api STEP 1 begin TXP0
TC-ST-005processingSTEP 7 opt-out detectconsent.marketing='false'skipped_opt_outAccount update only, COMMITP0
TC-ST-006processingSTEP 14 COMMITAll steps OKprocessedprocessed_at=NOW(), notification sentP0
TC-ST-007processingExceptionSTEP 1-14 faildead_letterretry_count++, error_message logged, ROLLBACKP0
TC-ST-008dead_letterAdmin replayretry_count < 3receivedReset retry_count=0, fire trigger lạiP0
TC-ST-009dead_letterCron check retry exceededretry_count >= 3permanently_failedAlert manual reviewP1

Coverage Lifecycle (PRD LIFECYCLE-002 — pancake_outage_state)

TCTừEventGuardSangSide effectsP
TC-ST-010healthyCron 4 detect no event 5 phút business hourslast_event_received_at > NOW()-5minoutage_startedoutage_started_at=NOW, Cron 6 activate, notify opsP0
TC-ST-011outage_startedWebhook nhận event mớioutage_recoveredlast_event_received_at=NOWP0
TC-ST-012outage_recoveredSustain 5 phút healthyNo new outagehealthyCron 6 stop, outage_ended_at=NOW, MTTR computedP0

Coverage Dynamic Permission (Diva RBAC pattern)

TCĐiều kiệnThao tácKỳ vọng UIKỳ vọng APIP
TC-PERM-001Manager không có pancake_crm_integration.accessMở Settings menuMenu "Tích hợp Pancake" ẩnHasura query bảng pancake_* permission denied (no SELECT)P0
TC-PERM-002Admin có fullMở Settings/Pancake4 tabs renderQueries return dataP0
TC-PERM-003Admin có access nhưng KHÔNG có replay_dlqTab 3 view event dead_letterButton "Phát lại" hiddenAction pancake_replay_event rejected: UNAUTHORIZEDP0
TC-PERM-004Telesale có quyền xem ticketXem 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-005POS-only roleMở admin URL Settings/PancakeRedirect/no accessHasura admin metadata không expose cho POS portalP0

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út
  • simulate_crm_api_crash.sh — kill crm-api pod, đo recovery
  • simulate_db_failover.sh — stop primary DB, promote replica
  • simulate_duplicate_retry.sh — Pancake gửi 1000 webhook duplicate

D4) Truy vết (view QA)

Canonical traceability ở dev-spec.md C12. Bảng dưới là QA-side mapping FR → TC count + execution status.

FRTC rangeCountStatus
FR-001TC-001-001..0088Chờ
FR-002TC-002-001..0033Chờ
FR-003TC-003-001..0022Chờ
FR-004TC-004-001..01010Chờ
FR-005TC-005-001..0055Chờ
FR-006TC-006-0011Chờ
FR-007TC-007-001..0033Chờ
FR-008TC-008-001..0055Chờ
FR-009TC-009-001..0066Chờ
FR-010TC-010-001..0044Chờ
FR-011TC-011-001..0033Chờ
FR-012TC-012-001..02020Chờ
FR-013TC-013-001..0055Chờ
FR-014TC-014-001..01515Chờ
FR-015TC-015-001..0033Chờ
FR-016TC-016-001..0033Chờ
FR-017TC-017-001..0022Chờ
FR-018TC-018-0011Chờ
Lifecycle 001TC-ST-001..0099Chờ
Lifecycle 002TC-ST-010..0123Chờ
PermissionTC-PERM-001..0055Chờ
TỔNG116 TC

25 DEC mapping → cách kiểm chứng đầy đủ trong dev-spec.md C12.


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_integration vớ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.marketing key đã verify với PO)
  • [ ] ui-spec.md B0 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 IDFR/DECLoạiMô tảPriority
TC-006-002FR-006Concurrency stress100 concurrent webhook same phone + 100 concurrent distinct phone — measure advisory lock contention, deadlock detection, p95 latency. Verify pgxpool DEC-028 architecture mớiP0 (QA P0-Q1)
TC-001-009FR-001Load burstStress 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 mistakeP0 (QA P0-Q4)
TC-014-016FR-014 DEC-015Chaos network partitionLatency injection 5s vào Hasura trigger call (toxiproxy/iptables) — verify timeout handling + retry behavior + DLQ populationP0 (QA P0-Q5)
TC-014-017FR-014Chaos Cron concurrencyCron 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_idP1 (QA P1-Q3)
TC-PERM-006DEC-027Dynamic Permission overrideAdmin 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-007DEC-027Permission revoke mid-sessionAdmin đang xem SCR-03, admin khác revoke access → next API call 401 → FE refetch + redirect homeP0 (QA P0-Q2)
TC-009-007FR-009Smart update race rapidPancake gửi 3 webhook update cùng record trong <1s — verify ordering preservation theo modified_on DESCP1 (QA P1-Q1)
TC-008-006FR-008Round-robin telesale offlineTelesale offline giữa rotation — verify ticket_distribute skip + next person assignedP1 (QA P1-Q2)
TC-012-021FR-012Tab 3 pagination >5k rowFilter date 30 ngày + status all → 5000+ rows. Verify server-side pagination 20/page response time < 500msP1 (QA P1-Q4)
TC-DEC-006DEC-006 v1.1Migration #3 idempotencyRun migration #3 lần 2 → DROP ADD COLUMN primary_phone đã exist → KHÔNG fail. Verify UNIQUE (account_id) constraint appliedP0 (verify Pass 1 fix)
TC-DEC-028DEC-028Direct pgxpool Pancake handlerVerify 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

TCMớiLý do
TC-011-003 (notification down)P1P0QA 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)

TestVerify
pancake_event_process_duration_seconds p95 > 30s for 10minInject slow query → alert fire
pancake_webhook_latency_seconds p99 > 2s for 5minStress test 1000 req/s → alert fire
pancake_circuit_breaker_state == 2 for 30minBlock Pancake REST 30+ phút → alert escalate Critical

D7.5) D4 Traceability update

FRTC rangeCountStatus
FR-001TC-001-001..0099 (+1)Chờ
FR-006TC-006-001..0022 (+1)Chờ
FR-008TC-008-001..0066 (+1)Chờ
FR-009TC-009-001..0077 (+1)Chờ
FR-014TC-014-001..01717 (+2)Chờ
FR-012TC-012-001..02121 (+1)Chờ
PermissionTC-PERM-001..0077 (+2)Chờ
DEC verificationTC-DEC-006, TC-DEC-0282 (NEW)Chờ
TỔNG127 TC (+11)

D7.6) D5 Entry/Exit updates

Entry thêm:

  • [ ] PD-008 W1 source list confirmed (handoff blocker)
  • [ ] PD-013 first_response_at field 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_indexes before/after migration (DevOps P0).
  • W4 exit: Manager survey ≥87% (13/15) — realistic threshold thay vì 100% strict.