Skip to content

Tích hợp Pancake CRM (Pancake CRM Integration)

Phiên bản: 1.0 Ngày: 15/05/2026 Tác giả: PO/BA + AI Tech Lead Loại: Tính năng mới Độ phức tạp: L (Lớn) Module: webhook, crm-api, notification-v2-api (BE) + crm, settings (FE)


Mục đích: Hợp đồng nghiệp vụ — phạm vi, quyết định, FR/AC, rule, công thức, rủi ro ở mức PO/BA/TL/QA. Đọc trước: decision-brief.md → TL;DR → Z (Nhật ký quyết định) → A5 (FR) → A10 (Công thức).

Lịch sử thay đổi

Phiên bảnNgàyTác giảThay đổi
1.015/05/2026PO/BA + AIKhởi tạo từ design doc Mode A v2.2 + Phase 3 evidence + SOURCE_OF_TRUTH §1 (25 DEC)

Tài liệu đầu vào chuẩn

FileVai tròNếu xung đột
SOURCE_OF_TRUTH.mdNguồn sự thật chuẩn + Solution LockƯu tiên cao nhất
EVIDENCE_PACK.mdBằng chứng code/screen/db Phase 3Ưu tiên bằng chứng trước assumption
decision-brief.mdCửa vào packageBrief chỉ tóm tắt; PRD giữ contract chi tiết

Quy tắc công thức: A10 (Công thức nghiệp vụ) là nguồn chuẩn; dev-spec.md C3 chỉ mô tả triển khai SQL/code.

Hướng dẫn đọc

Đối tượngSection cần đọc
PO/BAdecision-brief.md → A0 → Z → A5 → A10 → A9
Sếp/Business Leaddecision-brief.md → TL;DR → A0 → A2 → A3 → A8
Tech Leaddecision-brief.md → A0 → Z → dev-spec C1-C8
UI/UX + FEdecision-brief.md → A5 → ui-spec.md
BE DevA5 + A10 → dev-spec.md C1-C12
QAA5 → qa-test-plan.md D1-D5

Tóm tắt điều hành (TL;DR)

Tích hợp inbound webhook 1 chiều Pancake CRM → Diva CRM để chấm dứt việc copy-paste thủ công 50-200 lead/ngày từ 40+ kênh (FB, Zalo, TikTok, Google Ads...) sang Diva. Khi Pancake gửi event record, hệ thống Diva tự động match khách theo phone E.164, tạo/cập nhật account, tạo ticket follow-up, auto-assign telesale theo round-robin chi nhánh (REUSE ticket_distribute), notify realtime — đảm bảo telesale phản hồi trong window vàng 30 phút. MVP-1 tập trung zero-miss SLO với 4-layer outage recovery (fail-open webhook + adaptive polling + daily reconciliation + DLQ replay). Effort ~37 dev-days, calendar 5.5-6 tuần, pilot 4 tuần (W1=1 source → W4=40+).

Quyết định còn mở (PD vòng đời)

Vòng đời: Open → Resolved (→ DEC-xxx) / Deferred / Rejected. Không close spec khi còn PD critical Open.

PDCâu hỏiLựa chọn / khuyến nghịPhụ tráchHạnTrạng tháiBlock
PD-001Pancake webhook HMAC signature header tên gì?A) X-Pancake-Signature B) Header khác C) Không có HMAC, dùng URL token + IP. Khuyến nghị: test thực, hỏi support 0972273341Ops25/06/2026OpenLaunch W4
PD-002Pancake IP whitelist range chính thức?A) Capture từ logs W1-W3 → tổng hợp B) Hỏi support C) Allow all + monitor. Khuyến nghị: capture + hỏi support song songOps + Dev25/06/2026OpenLaunch W4
PD-003customer_consent.consent_data key cho opt-out marketing?A) marketing B) consent_marketing C) marketing_consent D) Key khác. Khuyến nghị: A (default suy luận từ consent_config)BE + PO22/05/2026OpenPhase 4 SPECIFY FR-017
PD-004VIP tag IDs initial seedSuggest: VIP, Hot Lead, Khách lớn, Khách quay lại. PO confirmPO04/06/2026OpenNon-block
PD-005Pancake retry policy chi tiếtTest trả 500 đo behavior. Assume exponential 5 retriesQAW1 (28/05-04/06)OpenNon-block
PD-006Pancake REST /sources rate limitTest 100 req liên tiếp đo 429. Cache 1hQAW1OpenNon-block
PD-007Pancake future ticket event publish?Monitor crm.pancake.vn/developers quarterlyTLMonitorNon-block
PD-008Pilot 4 tuần exact source listPO chọn theo volume hiện tạiPO + Marketing28/05/2026OpenW1 setup
PD-009Hasura concurrency: 1 cron supported?Verify Hasura docs. Fallback advisory lock trong handlerTLPhase 4OpenNon-block
PD-010Pancake public API roadmap?Track quarterlyPOMonitorNon-block
PD-011KPI baseline manual workflow (response time hiện tại)Manager đo 1 tuần trước W1 launchPO + Manager21/05/2026OpenKPI-measurement
PD-012Notification noti_pancake_outage kênh?A) Push admin in-app (DEC-010) B) Email C) SMS escalate. Khuyến nghị: APOW1OpenNon-block

Backlog giai đoạn 2 (Out of scope MVP-1)

#Tính năngLý do defer
1Outbound Diva → Pancake (POST records/tickets)ROI MVP-1 đã đủ; Pancake chưa có guaranteed write API
2Multi-workspace supportDiva chỉ có 1 workspace Pancake hiện tại
3Pancake ticket event handlingPancake hiện chưa publish event này, roadmap unclear
4Bulk import source routing CSVVolume 40 source quản manual được
5Dashboard analytics realtime (lead by source, conversion by source)Query trực tiếp ticket+event đủ cho MVP-1
6Shopee/TikTok/Lazada tích hợpPattern code extendable
7Conversation/Messenger sync (Pancake POS feature)Sản phẩm khác
8Geo address normalize (chỉ lưu raw text trong pancake_metadata)Geo normalize complexity cao
9ML/AI lead scoringBeyond MVP scope
10Pancake user ↔ Diva user mapping (owner field)Bộ phận trực Pancake ≠ telesale Diva

Z) Nhật ký quyết định

25 DEC canonical. Mỗi DEC ghi: lý do nghiệp vụ + ≥2 alternatives + status. File này là canonical — SOURCE_OF_TRUTH §1 sync với section này.

IDNhómQuyết địnhLý do (≥2 alt)NgàyTrạng thái
DEC-001Nghiệp vụInbound 1 chiều Pancake → Diva (MVP-1). Outbound defer MVP-2Alt A: 2 chiều MVP-1 (ROI thấp, complexity gấp 3). Alt B: 1 chiều inbound (chốt) — đã giải quyết 80% pain point copy-paste, deploy nhanh hơn15/05/2026Locked
DEC-002Kỹ thuậtSplit 2 service: webhook (transport return 200<1s) + crm-api (business logic async qua Hasura event trigger)Alt A: 1 service xử lý sync (risk Pancake suspend 80% error/30 phút). Alt B: Split + async (chốt) — pattern Stringee/iCall reuse15/05/2026Locked
DEC-003Kỹ thuậtWebhook fail-open trả 200 mọi case (auth/IP/parse/dup). Insert raw status='ingested' trước verify → promote 'received' chỉ khi happy pathAlt A: Trả 401/500 chuẩn HTTP (Pancake retry → ramp-up error rate → suspension). Alt B: Fail-open 200 (chốt) — bypass suspension rule, audit đủ qua status enum15/05/2026Locked
DEC-004Kỹ thuậtIdempotency 3-tuple (record_id, modified_on, payload_hash) UNIQUE trong pancake_webhook_eventAlt A: Chỉ record_id (sai vì 1 record update nhiều lần). Alt B: (record_id, modified_on) (sai vì Pancake retry cùng modified_on, body khác). Alt C: 3-tuple (chốt)15/05/2026Locked
DEC-005Kỹ thuậtPhone match canonical = account.normalized_phone E.164 (libphonenumber). Fallback (phone_code, phone_number) trong migration windowAlt A: Match raw phone string (sai format, miss khách). Alt B: E.164 normalize (chốt) — libphonenumber có sẵn ở auth service15/05/2026Locked
DEC-006Kỹ thuậtALTER contact_book ADD primary_phone TEXT + FK account_id → account.id + UNIQUE (account_id). Auto-create contact entry per Pancake accountAlt A: Skip contact_book auto-create, lưu phone trong account.pancake_metadata (rejected vì form Tạo Ticket bắt buộc field Liên hệ *). Alt B: ALTER + auto-create (chốt)15/05/2026Locked
DEC-007Kỹ thuật4 bảng Pancake mới: pancake_webhook_event, pancake_connection, pancake_source_routing, pancake_outage_state. Dùng disabled BOOLEAN (Diva pattern), KHÔNG deleted_atAlt A: Dùng deleted_at (sai pattern Diva). Alt B: disabled BOOLEAN (chốt) — đồng nhất toàn DB15/05/2026Locked
DEC-008Nghiệp vụVIP tag matching: exact match tag NAME (text) case-insensitive trong account.pancake_metadata.pancake_tag_names[] với pancake_connection.vip_tag_names text[]. Admin nhập text vào field multi-lineAlt A: Fetch tag list từ Pancake REST /pages/pancake_tags (extra REST endpoint, tag ID khó nhớ). Alt B: Tag NAME input tay (chốt) — đơn giản hơn, admin tự duy trì15/05/2026Locked
DEC-009Nghiệp vụTest mode event: is_test=true flag trong pancake_webhook_event. KHÔNG count vào KPI. Cron pancake_test_event_cleanup soft-delete (disabled=true) sau 24hAlt A: Hard-delete sau 24h (mất debug data). Alt B: Soft-delete (chốt)15/05/2026Locked
DEC-010UXNotification Loose mode (branch=NULL): in-app push cho Admin role + branch-manager NULL. KHÔNG escalate email/SMSAlt A: Email/SMS escalate (noise nhiều). Alt B: In-app push (chốt) — admin chủ động xem dashboard15/05/2026Locked
DEC-011Nghiệp vụSmart update Q2.b: chỉ tạo ticket mới nếu thay đổi phone HOẶC pancake_source_id mới HOẶC tag VIP mới. Update vô hại (name/address) → CHỈ update account, KHÔNG tạo ticketAlt A: Mỗi webhook = 1 ticket mới (spam, telesale ức chế). Alt B: Smart update (chốt) — giảm ~70% ticket dư15/05/2026Locked
DEC-012Kỹ thuậtAuto-assign telesale REUSE services/crm-api/scheduler/distribute_ticket.go:157 GetTicketUpdates. Refactor thành public helper, gọi sync ngay sau INSERT ticket (KHÔNG qua cron)Alt A: Build counter mới account.open_ticket_count + Hasura counter trigger + index (v1 plan, 1.5d). Alt B: REUSE existing (chốt) — đồng nhất logic, -1.5d15/05/2026Locked
DEC-013UXAuto-assign 1-tier per branch. Branch=NULL → assignee_id=NULL, leader assign. Branch có nhưng không telesale rảnh → assignee_id=NULL cùngAlt A: 3-tier fallback (source→branch RR → toàn hệ thống → pool "Cần phân công") — complexity cao. Alt B: 1-tier (chốt) — 70% đơn giản hơn15/05/2026Locked
DEC-014Nghiệp vụOpt-out marketing: query customer_consent.consent_data->>'marketing'='false' → vẫn update account, KHÔNG tạo ticket. Log status='skipped_opt_out'Alt A: Thêm column account.do_not_contact rời (duplicate state). Alt B: Reuse customer_consent (chốt) — single source consent15/05/2026Locked
DEC-015Kỹ thuật4-layer outage recovery: (1) webhook receiver, (2) Cron 6 adaptive polling fallback, (3) Cron 7 daily reconciliation safety net, (4) DLQ + Admin Replay UIAlt A: Chỉ webhook + retry (miss lead khi outage). Alt B: 4-layer (chốt) — đảm bảo zero-miss SLO15/05/2026Locked
DEC-016UXSettings UI 4 tabs: Kết nối / Map source→branch / Lịch sử+DLQ replay / Sức khỏe. Quyền Admin onlyAlt A: 5 tabs (có Tab Tags) — dropped ở v2. Alt B: 4 tabs (chốt) — bớt complexity15/05/2026Locked
DEC-017UXTicket source mới ticket_source_pancake (slot 9). INSERT 1 row vào crm_master_data (type=ticket_source). FE delta 3 file (types.ts + i18n vi.ts + sourceDescriptions)Alt A: Map Pancake về 1 trong 8 slot existing (tranh slot, future Shopee/TikTok không có slot). Alt B: Slot mới (chốt) — ID semantic15/05/2026Locked
DEC-018Kỹ thuậtPancake REST: chỉ GET /workspaces/{ws}/sources (Cron 3 hourly cache). DROP GET /pages/pancake_tagsAlt A: Sync cả sources + tags (extra complexity, DEC-008 đã drop tag fetch). Alt B: Chỉ sources (chốt)15/05/2026Locked
DEC-019Kỹ thuậtCircuit breaker REST: sony/gobreaker cho Pancake REST call. Default 5 fail/60s → open, half-open sau 30sAlt A: Không circuit breaker (risk cascade fail khi Pancake down). Alt B: gobreaker (chốt) — production-ready15/05/2026Locked
DEC-020Kỹ thuậtAdvisory lock per phone chống race: pg_advisory_xact_lock(hashtext('account_phone_' || normalized_phone)) trong process record STEP 3-5Alt A: SELECT FOR UPDATE (heavy, race vẫn có khi phone chưa exist). Alt B: Advisory lock (chốt)15/05/2026Locked
DEC-021Vận hànhFeature flag 3 mức: app_setting.pancake_integration.enabled (kill switch) / pancake_connection.status enum / pancake_source_routing.is_active (per-source)Alt A: Chỉ 1 kill switch tổng. Alt B: 3 mức (chốt) — pilot rollout linh hoạt15/05/2026Locked
DEC-022Vận hànhPilot 4 tuần: W1=1 source low-vol, W2=5 mid, W3=15 high (load test), W4=40+ all productionAlt A: Big bang launch. Alt B: 4-tuần pilot (chốt) — giảm risk vận hành15/05/2026Locked
DEC-023Vận hànhKPI 5 metric chốt: latency p95 ≤30s, SLA telesale p95 ≤30min, auto-assign correct ≥90% (sau W4), event success ≥99.5%, zero-miss SLO = 0 events/ngàyAlt A: Chỉ event success rate. Alt B: 5 metric đa chiều (chốt)15/05/2026Locked
DEC-024UXNotification 3 template mới: noti_ticket_assigned_pancake (telesale realtime), noti_pancake_unmapped_branch (admin Loose mode), noti_pancake_outage (ops alert)Alt A: Reuse template noti_ticket_reminder_today (sai context). Alt B: 3 template mới (chốt)15/05/2026Locked
DEC-025Kỹ thuậtReconciliation window: Cron 7 daily 02:00 AM, query Pancake records modified trong 25h gần nhất (overlap 1h chống clock skew miss), compare pancake_webhook_event.record_id để detect missAlt A: 24h window (miss khi clock skew). Alt B: 25h overlap (chốt)15/05/2026Locked

A) PRD

A0) Tổng quan tính năng

Đọc 2 phút nắm hết. Chi tiết xem A1-A12.

Ý tưởng cốt lõi: Pancake CRM (40+ kênh: FB, Zalo, TikTok, Google Ads, Whatsapp, Shopee...) đang chứa 50-200 lead/ngày mà telesale Diva phải copy-paste thủ công. Tích hợp này cho phép Pancake gửi webhook record → Diva tự động match khách theo phone E.164 → tạo/cập nhật account + ticket → auto-assign telesale theo round-robin chi nhánh → notify realtime. Mục tiêu: telesale phản hồi ≤30 phút (p95), zero-miss 0 lead/ngày, event success ≥99.5%.

Kiến trúc tổng thể:

┌─────────────────────────┐
│ Pancake CRM             │
│ workspace=crm.pancake.vn│
│ event=record            │
└───────────┬─────────────┘
            │ HTTP POST (JSON RecordPayload)

┌──────────────────────────────────────────────────────────────┐
│ services/webhook                                             │
│ POST /api/pancake/record/{token}                             │
│  ┌─────────────────────────────────────────────────┐         │
│  │ 1. INSERT raw event → status='ingested'         │         │
│  │ 2. Verify token (URL) + IP whitelist + HMAC?    │         │
│  │ 3. Validate JSON schema                          │         │
│  │ 4. Dedup 3-tuple (record_id, modified_on, hash) │         │
│  │ 5. UPDATE status='received' (PROMOTE)           │         │
│  │ 6. Return 200 OK (< 1s)                          │         │
│  └─────────────────────────────────────────────────┘         │
└──────────┬───────────────────────────────────────────────────┘
           │ Hasura event trigger ON UPDATE column status

┌──────────────────────────────────────────────────────────────┐
│ services/crm-api                                             │
│ POST /events (handler 'pancake_process_record')              │
│  ┌─────────────────────────────────────────────────┐         │
│  │ STEP 0  Filter NEW.status='received'             │         │
│  │ STEP 1  BEGIN TX + re-fetch FOR UPDATE           │         │
│  │ STEP 2-3 Phone normalize E.164 + advisory lock   │         │
│  │ STEP 4  Resolve branch từ pancake_source_routing │         │
│  │ STEP 5  Match/upsert account + contact_book      │         │
│  │ STEP 6  Update pancake_metadata cache            │         │
│  │ STEP 7  Check opt-out (customer_consent)         │         │
│  │ STEP 8  Smart update Q2.b: tạo ticket?           │         │
│  │ STEP 9  Round-robin assignee (REUSE GetTicketUpdates)│     │
│  │ STEP 10 INSERT ticket + ticket_distribute log    │         │
│  │ STEP 11 Update pancake_metadata.last_sync_at     │         │
│  │ STEP 12 Send notification (telesale or admin)    │         │
│  │ STEP 13 UPDATE event.status='processed'          │         │
│  │ STEP 14 COMMIT TX                                 │         │
│  └─────────────────────────────────────────────────┘         │
└──────────┬───────────────────────────────────────────────────┘
           │ + Cron 3 (sources sync hourly)
           │ + Cron 4 (outage detect 1 min)
           │ + Cron 6 (adaptive polling fallback)
           │ + Cron 7 (daily reconciliation 02:00 AM)
           │ + DLQ retry cron
           │ + Test event cleanup cron
           │ + Health alert check cron

┌─────────────────────────┐
│ Pancake CRM REST API    │
│ GET /workspaces/{ws}/sources (Cron 3 cache refresh)
└─────────────────────────┘

Mapping FR ↔ SCR ↔ Dev:

KhốiFRSCR (UI Spec)Dev section
Webhook receiverFR-001/002/003/004(no UI)dev-spec C5 STEP webhook
Process record asyncFR-005/006/007/008/009(no UI)dev-spec C5 STEP 0-14
Ticket creation + assignFR-010(auto-render existing)dev-spec C5 STEP 9-10
NotificationFR-011(existing)dev-spec C5 STEP 12
Settings UIFR-012SCR-01..05ui-spec
REST consumptionFR-013SCR-01 connection paneldev-spec C5 Cron 3
Outage recoveryFR-014SCR-04 health monitordev-spec C6
Pilot + feature flagFR-015SCR-01 toggledev-spec C8
Test modeFR-016SCR-03 audit filterdev-spec C5
Opt-out complianceFR-017(no UI, audit log)dev-spec C5 STEP 7
FE delta dropdownFR-018SCR-06 (delta on existing)dev-spec C2

Quyết định chính (xem Z đầy đủ):

NhómTóm tắtRef
Kiến trúcSplit webhook + crm-api qua Hasura event triggerDEC-002
Kiến trúcFail-open 200 mọi caseDEC-003
Nghiệp vụSmart update Q2.b (tránh spam ticket)DEC-011
Nghiệp vụReuse ticket_distribute round-robinDEC-012
Nghiệp vụOpt-out qua customer_consentDEC-014
UXSettings 4 tabs Admin onlyDEC-016
Vận hànhPilot 4 tuần W1→W4DEC-022

Bảng FR tóm tắt:

FRMô tả (1 dòng)Ưu tiênPhase
FR-001Webhook receiver endpoint return 200<1sMustW1
FR-002Persist raw event auditMustW1
FR-003Idempotency 3-tupleMustW1
FR-004Async processor qua Hasura event triggerMustW1
FR-005Phone E.164 normalize + match/upsert accountMustW1
FR-006Advisory lock per phone chống raceMustW1
FR-007Auto-create contact_book entryMustW1
FR-008Auto-assign REUSE ticket_distribute round-robin 1-tierMustW1
FR-009Smart update Q2.b (chỉ tạo ticket khi đổi quan trọng)MustW1
FR-010Tạo ticket standard source=ticket_source_pancakeMustW1
FR-011Notification telesale (assigned) + admin (Loose mode)MustW1
FR-012Settings UI 4 tabs Admin onlyMustW1
FR-013REST API consumption (chỉ /sources, Cron 3 1h cache)MustW1
FR-0144-layer outage recovery + DLQ replay UIMustW2-W3
FR-015Feature flag 3 mức + pilot rolloutMustW1-W4
FR-016Test mode event tagged is_test=trueMustW1
FR-017Opt-out qua customer_consent.marketingMustW1
FR-018FE delta dropdown ticket source (3 file)MustW1

Mô hình dữ liệu tóm tắt:

BảngLoạiMục đích
pancake_webhook_eventMỚIAudit raw + status lifecycle + retry
pancake_connectionMỚIWorkspace config (api_key, vip_tag_names, kill switch status)
pancake_source_routingMỚIMap Pancake source → Diva branch + is_active toggle
pancake_outage_stateMỚIState machine outage detect (Cron 4)
accountSỬAADD normalized_phone TEXT, pancake_metadata JSONB + index
contact_bookSỬAADD primary_phone TEXT + FK + UNIQUE constraint (DEC-006)
crm_master_dataSEEDINSERT 1 row ticket_source_pancake
app_setting.app_settingsSEEDADD key pancake_integration.enabled
module_permission_actionSEEDINSERT module pancake_crm_integration + 6 actions
notification_templateSEEDINSERT 3 template mới
ticket_distributeKHÔNG ĐỔIReuse round-robin
customer_consentKHÔNG ĐỔIReuse opt-out query
ticketKHÔNG ĐỔIReuse schema (source_id FK)

Ảnh hưởng code hiện tại (high-risk):

  • diva-admin/src/modules/crm/types.ts:155-164 (extend dropdown)
  • services/webhook/handler/route.go:35-48 (add route entry)
  • services/crm-api/scheduler/distribute_ticket.go:157 (refactor public helper)
  • services/crm-api/event/event.go (add handler dispatch)
  • Hasura metadata: 4 bảng mới YAML + 7 cron + 1 event trigger + permission
  • Migration: 4 file mới sequence timestamp 1777870069928+

→ Chi tiết → dev-spec.md C2 (Impact Map).


A1) Bản thiết kế tóm tắt

TrườngGiá trị
Tính năngTích hợp Pancake CRM (inbound webhook)
LoạiTính năng mới
Nền tảngWeb Admin (diva-admin) — Settings module + CRM module delta
Module FE ảnh hưởngsrc/modules/settings (4 tabs mới), src/modules/crm (3 file delta dropdown source)
Service BE ảnh hưởngservices/webhook (extend), services/crm-api (extend), services/notification-v2-api (template seed), Hasura controller (metadata + migration)
Database4 bảng mới + ALTER 2 bảng + seed master data
Cron7 cron mới + 1 Hasura event trigger mới
External APIPancake CRM REST GET /workspaces/{ws}/sources (Cron 3)
Public endpointPOST /api/pancake/record/{token} (webhook receiver, no auth Hasura — verify token URL + IP + HMAC)

A2) Bối cảnh

Hiện trạng (As-Is):

  • Pancake CRM workspace crm.pancake.vn chứa lead/contact từ 40+ kênh (FB page, Zalo OA, Google Ads, TikTok, Whatsapp, Shopee, ...). Bộ phận trực Pancake (CSKH + sale) tạo record mới hoặc update record sẵn có khi khách chat/mua.
  • Manager 15 chi nhánh đang copy-paste thủ công 50-200 lead/ngày từ Pancake → Diva, mất ~2h/ngày/manager. Ngoài giờ làm việc thì lead nằm chờ → telesale Diva mất window vàng 30 phút.
  • Diva CRM (services/crm-api) đã có ticket table với source_id FK tới crm_master_data (8 slot ticket_source_1..8 hiện có), ticket_distribute round-robin per branch, customer_consent opt-out, libphonenumber phone normalize ở auth service.
  • Diva chưa có endpoint nào nhận webhook từ Pancake. Pattern reuse: Stringee/iCall webhook trong services/webhook/handler/route.go:35-48.

Kỳ vọng (To-Be):

  • Pancake gửi webhook record mỗi khi nhân viên Pancake tạo/cập nhật record → Diva tự động match khách theo phone E.164, tạo/cập nhật account + auto-create contact, tạo ticket follow-up source_id='ticket_source_pancake', auto-assign telesale theo round-robin chi nhánh, notify realtime.
  • Telesale Diva nhận notification realtime + thấy lead trong dashboard ticket → gọi trong window 30 phút.
  • Manager xem audit log + replay event lỗi qua Settings UI.
  • 4-layer outage recovery đảm bảo zero-miss SLO khi Pancake hoặc Diva downtime.

Không thuộc phạm vi (Non-goals):

#MụcLý do
1Outbound Diva → PancakeDefer MVP-2 — ROI chưa đủ
2Pancake POS (đơn hàng) tích hợpSản phẩm khác Pancake
3Pancake Botcake (chatbot Messenger) tích hợpSản phẩm khác Pancake (notification-v2-api hiện có actionBotcakeSendContent UNRELATED)
4Pancake ticket eventPancake hiện chưa publish event này
5Multi-workspaceDiva chỉ có 1 workspace Pancake
6Tag mapping table riêngDEC-008 dùng input tay tag NAME
7Bulk import source routing CSVManual quản được volume 40 source
8Pancake user → Diva user mapping (owner)Org structure khác nhau
9full_address sync vào geo moduleChỉ lưu raw text trong pancake_metadata
10Real-time analytics dashboardM2/M3 — query trực tiếp ticket+event đủ

A3) Mục tiêu

Mục tiêuCách đoTarget
Loại bỏ copy-paste thủ công lead PancakeSố manager báo cáo "đã ngừng copy-paste"100% manager (15/15) sau W4
Telesale phản hồi lead trong window vàngticket.first_response_at − created_at p95 (giờ làm việc)≤ 30 phút (p95)
Latency webhook → ticket assignedpancake_webhook_event.processed_at − created_at p95≤ 30 giây (p95)
Zero-miss leadReconciliation report cuối ngày: events Pancake gửi nhưng Diva miss sau 25h0 events/ngày
Event success rateprocessed_count / total_count từ pancake_webhook_event≥ 99.5%
Auto-assign correct (không cần manager can thiệp)count(ticket source=pancake AND assignee_id IS NOT NULL) / total≥ 90% sau W4
Pancake webhook không bị suspendCount pancake_connection.status='suspended_by_pancake'0 lần / quarter
Conversion 60d (lead Pancake → khách thực sự)Cohort analysis 60 ngày sau W4TBD baseline (PD-011)

A4) Nhân vật (Personas)

PersonaVai tròJTBDTần suất
TelesaleTelesale Diva (60 người, 4-5/chi nhánh)Nhận lead Pancake assign về mình + gọi trong 30 phút window → chốt cuộc hẹnMỗi lần có notification (50-200 lần/ngày toàn hệ thống)
Manager chi nhánhManager 15 chi nhánhManual assign telesale cho ticket branch=NULL hoặc telesale rảnh khan; theo dõi KPI lead chi nhánhHàng ngày check dashboard ticket
Admin Diva (ops)Admin roleSetup webhook URL trong Pancake admin, manage source→branch mapping, replay event lỗi từ DLQ, monitor healthSetup ban đầu + recovery sự cố
MarketingMarketing managerTrack conversion per Pancake source (page nào ROI cao) — M2/M3 nice-to-haveHàng tuần / hàng tháng
KháchEnd user (khách spa/salon)Chat với Pancake page → đăng ký dịch vụ → mong nhận cuộc gọi telesale Diva nhanh1 lần per lead
Hệ thống PancakeExternal webhook senderGửi event record khi nhân viên Pancake tạo/cập nhật recordAuto, theo Pancake retry policy
Hệ thống Diva (cron)Cron 3-7 + DLQ retrySync sources / detect outage / reconciliation / retry failed eventTheo schedule

A5) Yêu cầu chức năng

FR-001 — Webhook receiver endpoint

Tham chiếu: DEC-002, DEC-003 | Ưu tiên: Must | SCR: (no UI)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống Pancake (external)
  • Dữ liệu: Endpoint POST /api/pancake/record/{token}services/webhook. Lưu vào pancake_webhook_event (raw_payload jsonb, raw_headers jsonb, source_ip, status enum)
  • Điều kiện: Pancake gửi event record từ workspace đã setup webhook URL
  • Hành động: (1) Lưu raw event với status='ingested', (2) Verify URL token + IP + HMAC (nếu PD-001 resolved), (3) Validate JSON schema, (4) Dedup, (5) UPDATE status='received' để fire Hasura event trigger, (6) Trả 200 OK trong < 1 giây
  • Kết quả: Pancake nhận 200 → không retry. Hasura event trigger fire → crm-api xử lý async
  • Ngoại lệ:
    • Token URL sai → UPDATE status='auth_failed', trả 200 (KHÔNG 401 để tránh Pancake retry → ramp-up error rate)
    • IP không trong whitelist → status='ip_blocked', trả 200
    • JSON schema sai → status='parse_error', trả 200
    • Idempotency duplicate (3-tuple đã tồn tại) → status='skipped_duplicate', trả 200
    • Source disabled (pancake_source_routing.is_active=false) → status='skipped_source_disabled', trả 200
    • Kill switch (app_setting.pancake_integration.enabled=false) → status='skipped_kill_switch', trả 200

AC:

  • [ ] Khi Pancake POST request hợp lệ tới endpoint với token đúng + IP whitelist + JSON valid + 3-tuple mới, hệ thống INSERT 1 row pancake_webhook_event với status='ingested' rồi UPDATE thành 'received', trả 200 trong < 1 giây
  • [ ] Khi token URL sai, hệ thống vẫn INSERT row với status='auth_failed', trả 200, log alert ops nếu > 10 attempts/phút (signal spoofing)
  • [ ] Khi IP không trong whitelist, hệ thống ghi status='ip_blocked', trả 200, log source_ip
  • [ ] Khi 3-tuple (record_id, modified_on, payload_hash) đã tồn tại → INSERT thất bại do UNIQUE constraint → catch + UPDATE existing row last_received_at=NOW(), return 200
  • [ ] Latency p95 < 1 giây cho 95% request (load test 100 req/giây sustained)
  • [ ] Không trả 4xx/5xx cho bất kỳ trường hợp Pancake gửi (fail-open)

Ví dụ nghiệp vụ:

  • Nhân viên Pancake tạo record mới cho khách "Nguyễn Văn A, 0912xxxxxx" trong page "FB Diva HCM" → Pancake POST tới /api/pancake/record/abc123token với JSON {record_id: "rec_xyz", phone_number: "+84912xxxxxx", source: [{id: "src_fb_hcm", name: "FB Diva HCM"}], ...} → Diva insert raw event, verify pass, UPDATE status='received' → trigger fire → crm-api STEP 1 process.

FR-002 — Persist raw event audit trail

Tham chiếu: DEC-003 | Ưu tiên: Must | SCR: SCR-03

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (auto)
  • Dữ liệu: Bảng pancake_webhook_event với columns: id UUID PK, record_id TEXT, modified_on TIMESTAMPTZ, payload_hash TEXT, raw_payload JSONB, raw_headers JSONB, source_ip INET, status TEXT, event_type TEXT (extensible), error_message TEXT NULL, retry_count INT DEFAULT 0, is_test BOOLEAN DEFAULT false, processed_at TIMESTAMPTZ NULL, last_received_at TIMESTAMPTZ, audit fields
  • Điều kiện: Mọi request Pancake gửi (kể cả invalid) đều phải lưu
  • Hành động: INSERT row đầu tiên với status='ingested', sau đó UPDATE status theo flow verify/process
  • Kết quả: Admin truy vấn được full history; QA debug được issue; reconciliation cron đọc được state
  • Ngoại lệ: DB connection error → log fatal, alert ops; webhook trả 500 (Pancake retry)

AC:

  • [ ] Mọi request hợp lệ hoặc không đều có 1 row trong pancake_webhook_event
  • [ ] Field raw_payload lưu nguyên JSON body Pancake gửi (không lossy)
  • [ ] Field raw_headers lưu nguyên HTTP headers (debug)
  • [ ] Field source_ip resolve qua middleware reverse proxy (X-Forwarded-For nếu có)
  • [ ] Status enum hợp lệ: ingested, received, processing, processed, auth_failed, ip_blocked, parse_error, skipped_duplicate, skipped_source_disabled, skipped_kill_switch, skipped_opt_out, dead_letter
  • [ ] Audit fields created_at, updated_at auto-trigger; KHÔNG cần created_by/updated_by cho webhook event (system-generated)

FR-003 — Idempotency 3-tuple

Tham chiếu: DEC-004 | Ưu tiên: Must | SCR: (no UI)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (auto)
  • Dữ liệu: UNIQUE constraint (record_id, modified_on, payload_hash) trên pancake_webhook_event
  • Điều kiện: Mọi insert raw event
  • Hành động: Compute payload_hash = MD5(raw_payload::text). INSERT với 3 field. Nếu UNIQUE violation → UPDATE last_received_at của row cũ, status='skipped_duplicate'
  • Kết quả: Pancake retry cùng payload → không tạo ticket trùng
  • Ngoại lệ: record_id NULL (Pancake schema error) → vẫn insert nhưng status='parse_error' + log

AC:

  • [ ] Pancake gửi 2 webhook giống nhau hoàn toàn → chỉ 1 row được process, row thứ 2 = skipped_duplicate
  • [ ] Pancake gửi 2 webhook cùng record_id + modified_on nhưng body khác (vd Pancake bug) → hash khác → 2 row riêng biệt (xử lý cả 2)
  • [ ] Pancake gửi 2 webhook cùng record_id, modified_on khác (record update) → 2 row riêng biệt (xử lý cả 2)

FR-004 — Async processor qua Hasura event trigger

Tham chiếu: DEC-002, DEC-003 | Ưu tiên: Must | SCR: (no UI)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (Hasura event trigger + crm-api handler)
  • Dữ liệu: Hasura trigger pancake_webhook_event_status_update watch column status, fire POST /events với payload {trigger: {name: 'pancake_process_record'}, event: {data: {old, new}}}. crm-api dispatcher route pancake_process_record → handler event_pancake_process_record.go
  • Điều kiện: UPDATE column status trên pancake_webhook_event (mọi value, KHÔNG filter ở Hasura)
  • Hành động: STEP 0 handler filter NEW.status='received' AND NEW.record_id IS NOT NULL (filter trong Go, không trong Hasura vì Hasura update.columns:['status'] chỉ filter có-update-không, KHÔNG filter giá trị). Nếu pass → STEP 1-14 process. Nếu fail → return 200 no-op
  • Kết quả: Pancake event được process async, webhook receiver không block
  • Ngoại lệ:
    • STEP 0 fail filter → no-op, không log
    • STEP 1-14 lỗi → UPDATE status='dead_letter', increment retry_count, log error → DLQ replay UI handles
    • Handler timeout 60s → Hasura mark failed → retry theo Hasura retry config

AC:

  • [ ] Khi pancake_webhook_event.status UPDATE từ ingestedreceived, handler fire trong 1-2 giây
  • [ ] Khi status UPDATE từ ingestedauth_failed/parse_error/skipped_*, handler fire NHƯNG STEP 0 filter ra → no-op, không tạo ticket
  • [ ] Khi handler process pass STEP 1-14, status được UPDATE thành processed, processed_at = NOW()
  • [ ] Khi handler fail giữa chừng, status='dead_letter', retry_count+=1, error_message = exception text

FR-005 — Phone E.164 normalize + match/upsert account

Tham chiếu: DEC-005 | Ưu tiên: Must | SCR: (no UI)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (crm-api STEP 2-5)
  • Dữ liệu: account.normalized_phone TEXT (mới), account.phone_code, account.phone_number, account.phone_enabled. Field libphonenumber. pkg/pancake/phone_normalize.go helper mới
  • Điều kiện: STEP 2 trong process record flow
  • Hành động: (1) Parse record.phone_number bằng libphonenumber với region VN. (2) Nếu hợp lệ → format E.164 (+84...). (3) Query account WHERE normalized_phone = E164. (4) Nếu match → update info (display_name, address, ...). (5) Nếu không match → INSERT new account với normalized_phone, phone_code=84, phone_number=<local format>, phone_enabled=true, display_name=record.name
  • Kết quả: 1 account = 1 phone, không trùng
  • Ngoại lệ:
    • Phone parse fail (sai format) → log warning, account normalized_phone=NULL, fallback match by (phone_code, phone_number) raw
    • Match by raw fallback fail → tạo account mới với phone original (không normalize) + flag log
    • 2 account cùng normalized_phone (do legacy data) → match account created_at cũ nhất, log warning

AC:

  • [ ] Pancake gửi phone 0912345678 → normalize +84912345678 → match account cùng phone E.164
  • [ ] Pancake gửi phone +84912345678 → normalize cùng dạng → match
  • [ ] Pancake gửi phone sai format 912345678abc → log warning, fallback match by raw, không tạo account trùng
  • [ ] Khi tạo account mới, các field phone_code=84, phone_enabled=true, normalized_phone được set đúng
  • [ ] Backfill migration: chạy script UPDATE account SET normalized_phone = libphonenumber_format(phone_code, phone_number) WHERE normalized_phone IS NULL AND phone_enabled=true cho dataset hiện có

FR-006 — Advisory lock per phone chống race

Tham chiếu: DEC-020 | Ưu tiên: Must | SCR: (no UI)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (crm-api STEP 3)
  • Dữ liệu: PostgreSQL pg_advisory_xact_lock(hashtext('account_phone_' || normalized_phone))
  • Điều kiện: Trước khi match/upsert account ở STEP 5
  • Hành động: Acquire advisory lock với key = hash của phone E.164. Lock được giữ đến COMMIT/ROLLBACK transaction
  • Kết quả: 2 webhook đồng thời cùng phone không tạo trùng account
  • Ngoại lệ: Lock acquire timeout (rất hiếm) → retry transaction sau 100ms; nếu vẫn fail → DLQ

AC:

  • [ ] Test concurrent: 10 webhook cùng phone gửi đồng thời → chỉ 1 account được tạo, 10 ticket riêng (mỗi event 1 ticket)
  • [ ] Lock được release tự động khi COMMIT/ROLLBACK
  • [ ] Không deadlock với các transaction khác (lock key unique per phone)

FR-007 — Auto-create contact_book entry

Tham chiếu: DEC-006 | Ưu tiên: Must | SCR: (no UI)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (crm-api STEP 5b)
  • Dữ liệu: Bảng contact_book (ALTER ADD primary_phone TEXT, FK account_id → account.id, UNIQUE (account_id)). Pattern query-before-insert
  • Điều kiện: Sau khi match/upsert account ở STEP 5
  • Hành động: Query SELECT id FROM contact_book WHERE account_id=$1. Nếu chưa có → INSERT (account_id, primary_phone=normalized_phone, ...). Nếu đã có → reuse, KHÔNG update
  • Kết quả: Form Tạo Ticket có sẵn field Liên hệ để populate (FR-010)
  • Ngoại lệ: Race condition đã được serialize bởi advisory lock (FR-006)

AC:

  • [ ] Account mới + chưa có contact → 1 row contact_book được tạo
  • [ ] Account đã có contact → KHÔNG tạo mới, reuse existing
  • [ ] primary_phone của contact match với normalized_phone của account

FR-008 — Auto-assign telesale REUSE round-robin

Tham chiếu: DEC-012, DEC-013 | Ưu tiên: Must | SCR: (no UI)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (crm-api STEP 9-10)
  • Dữ liệu: Helper GetTicketUpdates (extract từ scheduler/distribute_ticket.go:157 thành public). Bảng ticket_distribute (audit assignment). 1-tier per branch
  • Điều kiện: Sau khi tạo ticket ở STEP 10, có branch_id IS NOT NULL và có telesale active trong branch
  • Hành động: (1) Tạo ticket unassigned trước (assignee_id=NULL). (2) Call GetTicketUpdates([ticket], arrangedBranchUsers, isTelesales=true) → returns last-assignee-next-in-rotation. (3) UPDATE ticket SET assignee_id, INSERT ticket_distribute log
  • Kết quả: Telesale được assign theo round-robin; load cân tải
  • Ngoại lệ:
    • Branch=NULL (Loose mode) → ticket assignee_id=NULL, notify admin (DEC-010, FR-011)
    • Branch có nhưng không có telesale active (vd: ngoài giờ làm việc) → assignee_id=NULL, leader assign sau, notify admin

AC:

  • [ ] Khi branch có 5 telesale, 5 webhook đồng thời → 5 ticket được assign cho 5 telesale khác nhau (round-robin)
  • [ ] Khi branch=NULL → ticket assignee=NULL, notification noti_pancake_unmapped_branch gửi đến admin
  • [ ] Khi branch có nhưng không telesale active → ticket assignee=NULL, notification gửi đến manager branch

FR-009 — Smart update Q2.b

Tham chiếu: DEC-011 | Ưu tiên: Must | SCR: (no UI)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (crm-api STEP 8)
  • Dữ liệu: So sánh new record (Pancake payload) với old account (Diva DB) qua 3 dimension: phone, source, VIP-tag
  • Điều kiện: Đã match account ở STEP 5
  • Hành động: (1) phone_changed = new.phone != old.normalized_phone. (2) source_new = new.source_id NOT IN old.pancake_metadata.source_ids[]. (3) vip_tag_new = exists(new.tags ∩ pancake_connection.vip_tag_names) AND NOT (old.pancake_metadata.had_vip_tag). (4) Nếu bất kỳ trong 3 = true → tạo ticket. (5) Nếu tất cả false → CHỈ update account info, KHÔNG tạo ticket
  • Kết quả: Giảm ~70% ticket dư cho khách Pancake update liên tục (vd: address update)
  • Ngoại lệ: Account mới (vừa được tạo ở STEP 5) → luôn tạo ticket lần đầu

AC:

  • [ ] Account đã có, Pancake gửi update với cùng phone + cùng source + cùng tag → KHÔNG tạo ticket, chỉ update display_name/address
  • [ ] Account đã có, Pancake gửi update với phone mới → tạo ticket mới
  • [ ] Account đã có, Pancake gửi update với source mới chưa từng map → tạo ticket mới
  • [ ] Account đã có, Pancake gửi update với tag VIP mới (chưa từng có) → tạo ticket mới
  • [ ] Account vừa tạo (STEP 5 tạo mới) → tạo ticket bất kể 3 dimension

FR-010 — Tạo ticket standard

Tham chiếu: DEC-017 | Ưu tiên: Must | SCR: (no UI, render trên Tickets/CustomerTicketManager existing)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (crm-api STEP 10)
  • Dữ liệu: Bảng ticket (KHÔNG đổi schema, reuse). Map field từ Pancake payload:
Field ticketRequiredNguồnLogic
customer_idaccount.id (đã match/tạo ở STEP 5)direct
customer_phone_numberaccount.normalized_phonedirect
customer_nameaccount.display_name (= record.name)direct
source_id'ticket_source_pancake'fixed
target_id'telesales'fixed
branch_idpancake_source_routing.diva_branch_id (lookup)NULL nếu unmapped
assignee_idoptionalFR-008 round-robin resultNULL trong Loose mode
due_datetoday 23:59:59 Asia/Ho_Chi_Minhcomputed
status_id'ticket_status_new' (default)fixed
result_idNULLNULL
input_noteAuto-gen text từ payloadtemplate
created_by'system_pancake_webhook'fixed

Auto-gen input_note template:

Lead từ Pancake CRM
Pancake Record ID: {record_id}
Nguồn (Pancake source): {source_names joined comma}
FB tags: {pancake_tags_text}
Địa chỉ: {full_address}
Email: {email}
  • Điều kiện: STEP 9 quyết định tạo ticket = true
  • Hành động: INSERT row vào ticket table với mapping trên
  • Kết quả: Ticket xuất hiện trong dashboard telesale (auto-render qua existing UI)
  • Ngoại lệ: branch_id=NULL Loose mode → ticket vẫn tạo, leader assign sau

AC:

  • [ ] Ticket được tạo với đầy đủ field bắt buộc theo bảng mapping
  • [ ] source_id='ticket_source_pancake' (DEC-017 — slot 9 mới)
  • [ ] target_id='telesales' fixed
  • [ ] due_date = today 23:59:59 Asia/Ho_Chi_Minh
  • [ ] input_note chứa đầy đủ Pancake metadata (record_id, source names, tags, address, email)
  • [ ] Ticket render được trong filter dropdown (FE delta FR-018)

FR-011 — Notification telesale + admin

Tham chiếu: DEC-024, DEC-010 | Ưu tiên: Must | SCR: SCR-04 (health)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (crm-api STEP 12)
  • Dữ liệu: 3 template mới: noti_ticket_assigned_pancake, noti_pancake_unmapped_branch, noti_pancake_outage. Reuse notification-v2-api Hasura action sendNotifications
  • Điều kiện: Sau khi tạo ticket ở STEP 10 (FR-010)
  • Hành động:
    • Nếu ticket có assignee_id → gửi noti_ticket_assigned_pancake (in-app push + có thể email/SMS theo telesale preference) tới user_id=assignee_id
    • Nếu branch_id=NULL (Loose mode) → gửi noti_pancake_unmapped_branch tới Admin role + branch-manager
    • Nếu branch_id NOT NULL nhưng assignee_id=NULL (không có telesale active) → gửi tới manager branch
  • Kết quả: Telesale nhận realtime trong window vàng
  • Ngoại lệ: Notification service down → log warning, KHÔNG fail process (ticket vẫn tạo); cron alert ops nếu fail rate > 5%/5 phút

AC:

  • [ ] Template noti_ticket_assigned_pancake có nội dung Việt: "Bạn vừa nhận lead Pancake CRM: [Khách: {name}, Phone: {phone}, Nguồn: {source}]. Phản hồi trong 30 phút."
  • [ ] Template noti_pancake_unmapped_branch content: "Ticket Pancake mới chưa có chi nhánh map. Click để gán chi nhánh."
  • [ ] Template noti_pancake_outage (cho FR-014 Cron 4): "Pancake webhook offline {duration} phút. Đã chuyển sang adaptive polling fallback."
  • [ ] Telesale nhận notification trong 5 giây sau khi ticket assigned

FR-012 — Settings UI 4 tabs

Tham chiếu: DEC-016, DEC-021 | Ưu tiên: Must | SCR: SCR-01..05

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Admin Diva
  • Dữ liệu: pancake_connection, pancake_source_routing, pancake_webhook_event, pancake_outage_state, app_setting.pancake_integration
  • Điều kiện: Permission pancake_crm_integration.access (default Admin role)
  • Hành động: 4 tabs tại Settings/Pancake CRM:
    • Tab 1 Kết nối: Form workspace_id, api_key (encrypted), webhook_url display (copy-button), webhook token (copy-button), kill switch toggle, VIP tag names (multi-line text input), button "Kiểm tra kết nối" → call Pancake /sources test
    • Tab 2 Map nguồn → chi nhánh: Table inline-edit. Cột: Pancake source name (từ cache /sources), Diva branch (dropdown), is_active toggle. Bulk action: "Sync sources từ Pancake" manual button
    • Tab 3 Lịch sử event + DLQ: Filter: date range, status, source, is_test. Table list event với drawer detail (raw_payload, raw_headers, processed_at, error_message, retry_count). Button "Replay event" (single + bulk select) — chỉ enable cho status=dead_letter or auth_failed
    • Tab 4 Sức khỏe: Metrics card: event success rate 24h/7d, latency p95, current outage state, last successful event time. Outage history list. Alert config (TBD MVP-2)
  • Kết quả: Admin tự quản, không cần dev can thiệp
  • Ngoại lệ:
    • Non-admin user truy cập route → hidden (no menu, redirect home)
    • Pancake REST API call fail → hiển thị banner "Pancake không phản hồi. Sources hiển thị từ cache."

AC:

  • [ ] Settings/Pancake CRM hidden cho non-admin (permission check FE + Hasura)
  • [ ] Tab 1 button "Kiểm tra kết nối" call Pancake /sources → hiển thị list source detected hoặc error message
  • [ ] Tab 1 VIP tag names: multi-line text, mỗi dòng 1 tag, save trim whitespace + lowercase
  • [ ] Tab 2 inline-edit branch dropdown autosave (debounce 500ms)
  • [ ] Tab 2 is_active toggle autosave
  • [ ] Tab 3 filter combo: date range + status + source. Default: last 7 days, status=all
  • [ ] Tab 3 drawer detail hiển thị raw_payload formatted JSON
  • [ ] Tab 3 button "Replay event" call action pancake_replay_event(event_id) → mark status=received để fire trigger lại
  • [ ] Tab 3 bulk replay: select multi → call bulk action với rate limit (5 events/giây)
  • [ ] Tab 4 metrics: query pancake_webhook_event aggregate 24h/7d
  • [ ] Permission replay_dlq action: button "Replay" hidden nếu thiếu

FR-013 — REST API consumption Pancake

Tham chiếu: DEC-018, DEC-019 | Ưu tiên: Must | SCR: SCR-01 (test connection)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (Cron 3 + manual via Tab 1 "Kiểm tra kết nối")
  • Dữ liệu: Pancake REST endpoint GET https://crm.pancake.vn/api/workspaces/{ws}/sources?api_key={key}. Cache local trong pancake_source_routing (auto-create row khi source mới detect)
  • Điều kiện: Cron 3 chạy mỗi giờ HOẶC admin click "Kiểm tra kết nối" manual
  • Hành động: Call REST với api_key auth. Parse response. Upsert vào pancake_source_routing (auto-add source mới với is_active=false, diva_branch_id=NULL). Send notification noti_pancake_unmapped_branch cho admin nếu có source mới
  • Kết quả: Admin biết source mới Pancake, có thể map sang branch
  • Ngoại lệ:
    • Pancake API timeout/5xx → circuit breaker sony/gobreaker open sau 5 fail/60s. Cron 3 skip, dùng cache cũ. Log alert nếu open > 30 phút
    • HTTP 429 rate limit → exponential backoff, log warning
    • api_key sai → status pancake_connection.status='error', alert ops

AC:

  • [ ] Cron 3 hourly schedule 0 * * * * config trong cron_triggers.yaml
  • [ ] Circuit breaker config: 5 fail/60s → open, half-open sau 30s
  • [ ] Source mới detect → auto INSERT pancake_source_routing với is_active=false
  • [ ] Source bị xóa khỏi Pancake → KHÔNG xóa khỏi DB (giữ lịch sử), chỉ log warning
  • [ ] Notification admin khi có source mới chưa map

FR-014 — 4-layer outage recovery + DLQ replay UI

Tham chiếu: DEC-015 | Ưu tiên: Must | SCR: SCR-03 (DLQ), SCR-04 (health)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (Cron 4/6/7 + DLQ replay UI)
  • Dữ liệu: Bảng pancake_outage_state (status enum, outage_started_at, outage_ended_at, last_check_at, last_event_received_at, current_polling_interval_sec)
  • Điều kiện: Outage detection + recovery
  • Hành động:
    • Layer 1 webhook receiver: Đã có FR-001
    • Layer 2 Cron 4 outage detect (mỗi 1 phút): query pancake_webhook_event WHERE created_at > NOW() - 5 minutes. Nếu count = 0 trong 5 phút liên tiếp + nhánh thời gian là business hours → UPDATE pancake_outage_state.status='outage_started'. Notify ops noti_pancake_outage
    • Layer 3 Cron 6 adaptive polling fallback (active khi outage_started): poll Pancake REST /records?modified_since={last_event_received_at} với interval tăng dần (start 1 phút, max 15 phút). Inject events vào pancake_webhook_event để trigger fire xử lý. Khi nhận event mới qua webhook → exit fallback, status='outage_recovered'
    • Layer 4 Cron 7 daily reconciliation (02:00 AM): query Pancake /records?modified_since=NOW()-25h (25h overlap chống clock skew). Compare record_id với DB. Inject missing events vào pancake_webhook_event với event_type='reconciled'
    • DLQ replay UI (Tab 3): Admin select event status=dead_letter → click "Replay" → action pancake_replay_event(event_id) reset status=received để fire trigger lại + reset retry_count
  • Kết quả: Zero-miss SLO = 0 events/ngày bị miss
  • Ngoại lệ:
    • Cron 6 polling rate limit → reduce interval
    • Cron 7 reconciliation phát hiện > 100 missing events → alert critical (Pancake/Diva có vấn đề nghiêm trọng)
    • DLQ replay fail lần thứ 3 → mark status=permanently_failed, alert manual review

AC:

  • [ ] pancake_outage_state table tracking: status, outage_started_at, outage_ended_at, last_check_at
  • [ ] Cron 4: schedule * * * * * mỗi phút, query window 5 phút
  • [ ] Cron 6: chỉ active khi outage_state.status='outage_started'. Interval start 60s, max 900s (15 phút)
  • [ ] Cron 7: schedule 0 2 * * * daily 02:00 AM
  • [ ] DLQ replay: action handler GraphQL pancake_replay_event(event_id: UUID!): SuccessOutput
  • [ ] Bulk replay: rate limit 5 events/giây, batch tối đa 100 event/lần
  • [ ] Tab 4 health: hiển thị outage_state realtime + last_event_received_at + current polling interval

FR-015 — Feature flag + pilot rollout

Tham chiếu: DEC-021, DEC-022 | Ưu tiên: Must | SCR: SCR-01

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Admin + Ops
  • Dữ liệu: 3 mức flag: (1) app_setting.app_settings.pancake_integration.enabled (BOOLEAN), (2) pancake_connection.status ENUM (active/paused/suspended_by_pancake/error), (3) pancake_source_routing.is_active BOOLEAN per row
  • Điều kiện: Pilot 4 tuần W1=1 source → W4=40+
  • Hành động:
    • W1: 1 source is_active=true (low volume FB test page)
    • W2: 5 source mid volume
    • W3: 15 source high volume (load test)
    • W4: 40+ all production
    • Kill switch tổng app_setting.pancake_integration.enabled=false → webhook receiver vẫn nhận nhưng status='skipped_kill_switch'
  • Kết quả: Pilot rollout linh hoạt, rollback nhanh khi sự cố
  • Ngoại lệ: pancake_connection.status='suspended_by_pancake' → toàn bộ flow paused đến manual re-enable

AC:

  • [ ] Tab 1 kill switch toggle: UPDATE app_setting.pancake_integration.enabled + log audit ai/khi nào toggle
  • [ ] Tab 2 inline is_active toggle per source autosave
  • [ ] Webhook receiver flow: check kill switch ngay STEP 1 (trước INSERT), nếu tắt → skip với status
  • [ ] Status suspended_by_pancake (auto từ Cron 4 detect HTTP fail từ Pancake) → manual re-enable qua Tab 1 button "Re-enable webhook"

FR-016 — Test mode event

Tham chiếu: DEC-009 | Ưu tiên: Must | SCR: SCR-03

Mô tả nghiệp vụ phần mềm:

  • Vai trò: QA + Dev test
  • Dữ liệu: Field pancake_webhook_event.is_test BOOLEAN DEFAULT false. Cron pancake_test_event_cleanup daily
  • Điều kiện: Pancake payload có field is_test=true HOẶC webhook URL có query param ?test=1
  • Hành động:
    • STEP 1: nếu is_test=true → set field is_test=true trong DB
    • Process flow vẫn run (tạo account/ticket test) NHƯNG flag account.pancake_metadata.is_test=true, ticket.input_note prefix "[TEST] "
    • KPI tracking: KPI calculation filter WHERE is_test=false
    • Cron pancake_test_event_cleanup daily 03:00 AM: soft-delete (disabled=true) test events + test accounts/tickets sau 24h
  • Kết quả: QA test không pollute production data
  • Ngoại lệ: Test event không trigger notification (skip STEP 12)

AC:

  • [ ] Pancake payload với is_test=true được flag đúng
  • [ ] Test ticket có prefix "[TEST]" trong input_note
  • [ ] KPI dashboard filter is_test=false
  • [ ] Cron cleanup soft-delete sau 24h, KHÔNG hard-delete
  • [ ] Tab 3 filter cho phép xem/ẩn test events

Tham chiếu: DEC-014 | Ưu tiên: Must | SCR: (no UI, log in audit)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: Hệ thống (crm-api STEP 7)
  • Dữ liệu: customer_consent.consent_data jsonb với key marketing (PD-003 confirm)
  • Điều kiện: Sau khi match/upsert account ở STEP 5
  • Hành động: Query SELECT consent_data->>'marketing' FROM customer_consent WHERE account_id=$1. Nếu = 'false' → SKIP STEP 8-10 (KHÔNG tạo ticket). Update account info vẫn giữ. UPDATE status='skipped_opt_out'
  • Kết quả: Compliance marketing opt-out
  • Ngoại lệ:
    • Khách chưa có row customer_consent → mặc định marketing=true (chưa opt-out), tạo ticket bình thường
    • PD-003 chưa confirm key name → fallback default key marketing

AC:

  • [ ] Khách có customer_consent.consent_data.marketing='false' → KHÔNG tạo ticket Pancake, log status='skipped_opt_out'
  • [ ] Account vẫn được update info (display_name, address)
  • [ ] Audit log Tab 3 lọc được status='skipped_opt_out'

FR-018 — FE delta dropdown ticket source

Tham chiếu: DEC-017 | Ưu tiên: Must | SCR: SCR-06 (delta on existing)

Mô tả nghiệp vụ phần mềm:

  • Vai trò: FE Dev (1 lần delta khi deploy)
  • Dữ liệu: 3 file delta + 1 migration master data
    1. diva-admin/src/modules/crm/types.ts:146-164 — ADD export const TICKET_SOURCE_PANCAKE = "ticket_source_pancake" + append vào array TicketSources
    2. diva-admin/src/modules/crm/i18n/vi.ts:130-137 — ADD key ticket_source_pancake: "Pancake CRM"
    3. diva-admin/src/modules/crm/pages/Tickets.tsx:128-149 — ADD entry vào object sourceDescriptions: ticket_source_pancake: '<span>Lead từ Pancake CRM (40+ kênh: FB, Zalo, TikTok...)</span>'
    4. Migration INSERT crm_master_data row ticket_source_pancake
  • Điều kiện: Deploy MVP-1
  • Hành động: PR FE + migration BE
  • Kết quả: Dropdown filter Tickets list + TicketMultipleAdd form + CustomerTicketManager drawer + report module filters tự render slot 9
  • Ngoại lệ: Nếu report module hardcode 1..8 → grep audit + sửa thêm (estimate +0.5d nếu phát sinh)

AC:

  • [ ] TICKET_SOURCE_PANCAKE const xuất hiện trong types.ts
  • [ ] TicketSources array có 9 phần tử
  • [ ] i18n vi.ts có key, label = "Pancake CRM"
  • [ ] sourceDescriptions có entry với HTML span
  • [ ] Migration INSERT crm_master_data run trước deploy FE (tránh dropdown render text undefined)
  • [ ] Dropdown filter Tickets list hiển thị "Pancake CRM" sau deploy
  • [ ] Customer-service report + telesales report filter source: verify auto-render OR add delta nếu hardcode

LIFECYCLE-001 — Vòng đời pancake_webhook_event.status

Trạng tháiÝ nghĩaĐiều kiện vàoĐiều kiện raTerminal
ingestedWebhook handler vừa INSERT raw eventINSERT step 1 webhookVerify pass → received; fail → auth_failed/ip_blocked/parse_error/skipped_*Không
receivedĐã verify pass, sẵn sàng cho crm-api xử lýWebhook handler UPDATEHasura trigger fire → processingKhông
processingcrm-api handler đang xử lý STEP 0-14Handler startedSTEP 14 OK → processed; exception → dead_letterKhông
processedHoàn tất tạo account/ticket/notificationSTEP 14 COMMIT(terminal)
auth_failedToken URL saiWebhook verify step 2(terminal — alert ops nếu > 10/min)
ip_blockedIP không whitelistWebhook verify step 2(terminal)
parse_errorJSON schema invalidWebhook verify step 3(terminal)
skipped_duplicate3-tuple đã cóWebhook verify step 4(terminal — update last_received_at)
skipped_source_disabledpancake_source_routing.is_active=falseWebhook step 5(terminal)
skipped_kill_switchapp_setting kill switch offWebhook step 5(terminal)
skipped_opt_outcustomer_consent.marketing=falsecrm-api STEP 7(terminal)
dead_lettercrm-api STEP 1-14 exceptionHandler catchAdmin replay → received; retry_count>=3 → permanently_failedKhông
permanently_failedDLQ replay fail 3 lầnCron retry check(terminal — alert manual review)
TừEventĐiều kiệnSangGiải thích
ingestedwebhook verify passtoken + IP + schema + dedup OKreceivedPromote, fire Hasura trigger
receivedHasura trigger firehandler STEP 0 filter passprocessingHandler bắt đầu
processinghandler exceptionSTEP 1-14 lỗidead_letterIncrement retry_count, log error
dead_letteradmin replay UImanualreceivedReset retry_count, fire lại
dead_letterretry policyretry_count >= 3permanently_failedCần manual review

LIFECYCLE-002 — Vòng đời pancake_outage_state.status

Trạng tháiÝ nghĩaCron behavior
healthyBình thường, webhook nhận đềuCron 6 không chạy
outage_startedCron 4 detect 0 event trong 5 phút giờ làmCron 6 adaptive polling kích hoạt
outage_recoveredWebhook nhận lại eventCron 6 stop sau 5 phút sustain

A6) Giả định

IDGiả địnhPhụ trách xác nhận
ASM-001Pancake gửi webhook có retry tối đa 5 lần exponential backoff (industry default)QA (PD-005)
ASM-002Pancake REST /sources rate limit ≥ 100 req/giờ (Cron 3 hourly cache đủ)QA (PD-006)
ASM-003customer_consent.consent_data.marketing='false' = opt-out marketing (key name PD-003)PO + BE
ASM-004Diva server nhận webhook public HTTPS (TLS termination ở reverse proxy/CDN)DevOps
ASM-005Pancake gửi phone_number chuẩn quốc tế (+84...) hoặc local format (09xx, 03xx). Không gửi format khác (vd US +1)QA verify W1
ASM-006Volume initial ~50-200 lead/ngày, peak ~3.3k/ngày → 100k/tháng. Capacity DB index đủTech Lead capacity model
ASM-007Pancake KHÔNG gửi event record cho khách bị xóa (no DELETE webhook). Khách bị Pancake xóa → vẫn còn ticket Diva.PO (acceptable)
ASM-008Pilot W1 source được PO chọn là low volume (≤ 10 lead/ngày) để verify end-to-endPO (PD-008)
ASM-009Diva timezone Asia/Ho_Chi_Minh consistent toàn hệ thống (due_date set theo timezone này)DevOps
ASM-010Telesale có app push notification enabled (notification-v2-api phụ thuộc)All telesale

A7) Rủi ro

IDRủi roẢnh hưởngXác suấtCách giảm thiểu
RSK-001Pancake auto-suspend webhook (80% error / 30 phút) → mất lead toàn bộCaoTBDEC-003 fail-open 200 mọi case + alert 1 phút khi error rate > 10% (config alertmanager) + Cron 4 detect outage chuyển adaptive polling
RSK-002Race condition tạo account trùng khi 2 webhook đồng thời cùng phoneCaoCao (nếu không có lock)DEC-020 pg_advisory_xact_lock per phone + integration test concurrent
RSK-003Pancake outage / Diva downtime = mất leadCaoThấpDEC-015 4-layer recovery: webhook + Cron 6 polling + Cron 7 reconciliation + DLQ
RSK-004Branch=NULL ticket bị bỏ quên trong dashboard adminTBTBDEC-010 push admin in-app realtime + Tab 3 filter status branch_id IS NULL + Daily morning digest email (M2 nice-to-have)
RSK-005FE delta hardcode source slot 1..8 ngoài TicketSources array (report module có thể hardcode)TBTBF18 grep audit 'ticket_source_' trước deploy + add delta nếu phát sinh
RSK-006customer_consent.consent_data.marketing key chưa confirm với POTBTBPD-003 — confirm trước Phase 4 SPECIFY; fallback default key marketing
RSK-007DLQ + Replay UI scope mới (3d) — chưa có pattern codeTBTBBuild mới theo design doc P1.2, ref UI pattern từ audit_log của module crm (nếu có) hoặc external notification queue
RSK-008KPI baseline manual workflow chưa đo → không có baseline so sánhThấpCaoPD-011 — Manager đo 1 tuần trước W1 launch
RSK-009Pancake gửi field schema khác (vd format phone mới)TBThấpJSON schema validation cứng + alert parse_error rate > 5% + extensible schema field event_type cho future event
RSK-010Pilot W3 high volume (15 source) load test phát hiện performance issueTBTBScale crm-api horizontally; DB index cho normalized_phone + index status; capacity model 100k events/tháng
RSK-011Notification-v2-api fail → telesale không nhận leadCaoThấpSTEP 12 KHÔNG block flow (ticket vẫn tạo); cron alert ops nếu notification fail rate > 5%/5 phút
RSK-012Pancake API key bị compromise (api_key trong DB)CaoThấpEncrypted at rest (DB column api_key_encrypted với KMS); audit log toggle
RSK-013Webhook endpoint bị DDoS (no auth public)TBThấpRate limit ở reverse proxy (vd Nginx 1000 req/giây/IP); IP whitelist after PD-002
RSK-014Migration backfill normalized_phone cho dataset lớn (~1M account) chạy lâuThấpTBRun as separate migration với batch (10k row/transaction); monitor progress
RSK-015Pancake gửi record_id rất dài (>100 chars) → DB column overflowThấpThấpColumn type TEXT (unbounded); test với edge case length 1000+

A8) Chỉ số đo lường sau phát hành

Chỉ sốCách đoMục tiêuKhi nào đo
Latency webhook → ticket assigned (p95)SQL: percentile_cont(0.95) WITHIN GROUP (ORDER BY processed_at - created_at) FROM pancake_webhook_event WHERE status='processed' AND created_at > NOW() - 24h≤ 30 giâyRealtime dashboard, daily summary
SLA telesale phản hồi (p95)SQL: percentile_cont(0.95) ... FROM ticket WHERE source_id='ticket_source_pancake' AND first_response_at IS NOT NULL (filter business hours)≤ 30 phútDaily / weekly report
Auto-assign correct ratecount(ticket WHERE source=pancake AND assignee_id IS NOT NULL) / count(ticket WHERE source=pancake) 7-day window≥ 90% (sau W4)Weekly review
Event success ratecount(status='processed') / count(*) 24h window≥ 99.5%Realtime alert < 99%
Zero-miss SLOReconciliation Cron 7 count missing events injected0 events/ngàyDaily 02:00 AM cron report
Reconciliation rescue ratecount(event_type='reconciled') / count(*) 7-day≤ 0.5%Weekly review
Outage MTTRoutage_ended_at - outage_started_at mean≤ 5 phútPer incident
Conversion 60d (lead Pancake → khách thực)Cohort: count(account WHERE created_by='system_pancake_webhook' AND first_order_at < created_at + 60d) / count(account created_by Pancake)TBD baselineQuarterly after W4
Manager time savedSurvey 15 manager: "Bạn còn copy-paste lead Pancake không?"100% "không" sau W4Survey end of W4
Pancake suspension countcount(*) WHERE pancake_connection.status='suspended_by_pancake' per quarter0Quarterly

A9) Bảng thuật ngữ (Glossary)

BẮT BUỘC từ profile S. Cặp gần nghĩa có cột Phân biệt với.

Tiếng ViệtENĐịnh nghĩa nghiệp vụPhân biệt vớiNguồn
Pancake CRMPancake CRMWorkspace crm.pancake.vn quản lý lead/contact từ 40+ kênh. Sản phẩm CRM của công ty Pancake≠ Pancake POS (đơn hàng, sản phẩm khác); ≠ Pancake Botcake (chatbot Messenger, có actionBotcakeSendContent trong notification-v2-api UNRELATED đến tích hợp này)SOURCE_OF_TRUTH §1
Record (Pancake)RecordEntity lead/contact của Pancake CRM. Map sang Account + Ticket của Diva khi nhận webhook≠ Ticket Pancake (Pancake hiện chưa publish event này, OUT scope)Pancake document.yaml
Source (Pancake)SourcePage/kênh nguồn Pancake (FB page, Zalo OA...). Mapping sang Diva branch qua pancake_source_routingticket_source của Diva (master data ticket)Pancake /sources API
Ticket PancakePancake Ticket SourceSlot mới ticket_source_pancake trong crm_master_data (DEC-017). Mọi ticket Diva tạo từ webhook có source_id='ticket_source_pancake'≠ Pancake Ticket Event (Pancake CRM ticket entity, OUT scope)DEC-017
Webhook eventWebhook EventRow trong pancake_webhook_event lưu raw payload + audit lifecycle status≠ Hasura event trigger (cơ chế fire crm-api handler)A5 FR-002
Idempotency 3-tupleIdempotency TripleUNIQUE (record_id, modified_on, payload_hash) chống duplicate retry≠ Simple record_id dedup (sai vì 1 record update nhiều lần)DEC-004
Smart update Q2.bSmart UpdateLogic chỉ tạo ticket khi phone/source/VIP-tag đổi. Tránh spam ticket cho khách Pancake update info liên tục≠ Full update (mỗi webhook = 1 ticket)DEC-011
Loose modeLoose ModeKhi branch_id=NULL (source chưa map) — ticket vẫn được tạo với assignee_id=NULL, leader assign sau. Notify admin in-app≠ Strict mode (reject ticket nếu thiếu branch — v1 design rejected)DEC-013
Round-robin per branchRound-RobinAuto-assign telesale trong branch theo thứ tự xoay vòng. REUSE ticket_distribute.GetTicketUpdates≠ Random assignment; ≠ Multi-tier fallback (v1 rejected)DEC-012
Advisory lockAdvisory Lockpg_advisory_xact_lock per phone chống race tạo account trùng≠ SELECT FOR UPDATE (heavy, race vẫn có nếu phone chưa exist)DEC-020
Fail-openFail-OpenWebhook receiver trả 200 mọi case (auth/IP/parse/dup) để tránh Pancake suspension rule≠ Fail-closed (trả 401/500 chuẩn HTTP)DEC-003
Pancake suspensionPancake SuspensionAuto-suspend webhook khi 80% error / 300 fails / 30 phút. Manual re-enable qua Pancake admin≠ Diva-side kill switch (DEC-021)Pancake document webhook.yaml
Outage stateOutage StateBảng pancake_outage_state tracking healthy/outage_started/outage_recoveredpancake_connection.status (config kill switch)DEC-015
Adaptive polling fallbackAdaptive PollingCron 6 — polling Pancake REST /records?modified_since=X khi outage. Interval tăng dần 1-15 phút≠ Regular cron (Cron 3 sources sync hourly)DEC-015
ReconciliationReconciliationCron 7 daily 02:00 AM — query Pancake records modified 25h gần nhất, detect miss, inject lại≠ Adaptive polling (real-time outage); ≠ DLQ replay (manual admin)DEC-025
DLQ replayDLQ ReplayAdmin manual replay event status='dead_letter' qua Settings Tab 3≠ Reconciliation (automatic cron); ≠ Adaptive pollingDEC-015, FR-014
Webhook receiverWebhook Receiverservices/webhook/handler/pancake.go — endpoint POST /api/pancake/record/{token}≠ Webhook sender (outbound, OUT MVP-1)A0
Process record flowProcess Record Flowservices/crm-api/event/event_pancake_process_record.go — 15-step async handler≠ Webhook receiver flow (sync, <1s)A0
VIP tagVIP TagTag NAME (text) match case-insensitive giữa Pancake payload và pancake_connection.vip_tag_names text[]. Trigger Smart update Q2.b≠ Pancake tag ID (DEC-008 rejected sync tag list)DEC-008
Test mode eventTest EventEvent với is_test=true, KHÔNG count KPI, auto soft-delete sau 24h≠ Production eventDEC-009
Opt-out marketingMarketing Opt-OutKhách có customer_consent.consent_data.marketing='false' → KHÔNG tạo ticket Pancake. Account info vẫn update≠ Account disabled (account.disabled=true); ≠ Customer consent treatment_photo (key khác)DEC-014
Pilot rolloutPilot Rollout4-tuần W1=1 source → W4=40+ all. Feature flag 3 mức (DEC-021)≠ Big bang launch (v1 rejected)DEC-022
Soft deleteSoft DeleteDiva pattern dùng disabled BOOLEAN, KHÔNG deleted_at≠ Hard delete (rare in Diva); ≠ deleted_at (legacy pattern, không dùng cho bảng pancake_*)DEC-007
HMAC signatureHMAC SignatureHeader verify webhook nguồn từ Pancake (PD-001 TBD). Default trust IP + URL token cho W1-W3≠ URL token (auth path)PD-001
25h overlap window25-Hour OverlapCron 7 reconciliation query 25h thay vì 24h chống clock skew miss≠ Standard 24h window (v1 rejected)DEC-025
Capacity modelCapacity Model100k events/tháng peak. Index normalized_phone + status để query nhanh≠ Real-time throughput limitTech Lead

A10) Công thức nghiệp vụ

Section này canonical. Dev Spec C3 chỉ ref + thêm SQL implementation delta.

FORMULA-001: Latency webhook → ticket assigned (p95)

  • Mô tả: Thời gian từ Pancake gửi webhook đến khi ticket Diva được tạo + assign telesale. Đo trên 95% request trong 24h gần nhất.
  • Công thức: latency_p95 = percentile_cont(0.95) WITHIN GROUP (ORDER BY (processed_at - created_at))
  • Biến số:
    • processed_at: thời điểm pancake_webhook_event.status chuyển processed — nguồn: pancake_webhook_event.processed_at
    • created_at: thời điểm webhook handler INSERT row — nguồn: pancake_webhook_event.created_at
  • Đơn vị: Giây (1 chữ số thập phân, format 12,5s)
  • Ví dụ: 100 event trong 24h, p95 = 18.7s → hiển thị "18,7s ✅" (target ≤ 30s)
  • Trường hợp cá biệt:
    • processed_at IS NULL (status != 'processed') → loại khỏi tính
    • Không event nào trong 24h → hiển thị , không hiện 0s
    • Outlier > 5 phút → loại khỏi p95 (data quality)

FORMULA-002: SLA telesale phản hồi (p95, giờ làm việc)

  • Mô tả: Thời gian từ khi ticket Pancake tạo đến khi telesale phản hồi lần đầu (gọi/note/status change). Filter trong giờ làm việc 08:00-22:00.
  • Công thức: sla_p95 = percentile_cont(0.95) WITHIN GROUP (ORDER BY business_hours(created_at, first_response_at))
  • Biến số:
    • business_hours(start, end): hàm tính thời gian trong giờ làm 08:00-22:00 Asia/Ho_Chi_Minh
    • created_at: thời điểm ticket tạo — nguồn: ticket.created_at
    • first_response_at: thời điểm phản hồi đầu tiên — nguồn: ticket.first_response_at (hoặc TBD logic compute từ ticket_log)
  • Đơn vị: Phút (2 chữ số thập phân)
  • Ví dụ: 100 ticket, p95 = 25.4 phút → hiển thị "25,40 phút ✅" (target ≤ 30 phút)
  • Trường hợp cá biệt:
    • first_response_at IS NULL → loại khỏi tính (chưa phản hồi)
    • Ticket tạo ngoài giờ làm (vd 23:00) → business_hours shift sang 08:00 hôm sau

FORMULA-003: Auto-assign correct rate

  • Mô tả: Tỷ lệ ticket Pancake được auto-assign telesale (không phải manager intervention) trên tổng ticket Pancake. Window 7 ngày.
  • Công thức: correct_rate = count(ticket WHERE source_id='ticket_source_pancake' AND assignee_id IS NOT NULL AND created_by='system_pancake_webhook') / count(ticket WHERE source_id='ticket_source_pancake') × 100
  • Biến số: source_id, assignee_id, created_by — nguồn: ticket table
  • Đơn vị: % (2 chữ số thập phân, dấu phẩy)
  • Ví dụ: 1000 ticket Pancake 7 ngày, 920 có assignee từ system → 920/1000 × 100 = 92,00% ✅ (target ≥ 90%)
  • Trường hợp cá biệt:
    • Mẫu = 0 → hiển thị
    • Manager manual edit assignee sau auto-assign → vẫn count = correct (assignee_id NOT NULL)

FORMULA-004: Event success rate

  • Mô tả: Tỷ lệ event xử lý thành công trên tổng event nhận trong 24h.
  • Công thức: success_rate = count(pancake_webhook_event WHERE status='processed') / count(pancake_webhook_event WHERE status NOT IN ('skipped_*')) × 100
  • Biến số: status — nguồn: pancake_webhook_event
  • Đơn vị: % (3 chữ số thập phân, ví dụ 99,950%)
  • Ví dụ: 10000 event 24h, 9985 processed, 10 dead_letter, 5 skipped_* → success = 9985/(9985+10) × 100 = 99,900% ✅ (target ≥ 99,5%)
  • Trường hợp cá biệt:
    • Loại trừ skipped_* (vì là intentional, không phải fail)
    • is_test=true events: loại khỏi cả tử + mẫu

FORMULA-005: Zero-miss SLO

  • Mô tả: Số events Pancake gửi nhưng Diva miss (không nhận webhook) trong 25h gần nhất. Đo qua Cron 7 reconciliation.
  • Công thức: miss_count = count(Pancake records modified > NOW()-25h) - count(pancake_webhook_event.record_id matching same window)
  • Biến số:
    • Pancake side: query REST /records?modified_since=NOW()-25h
    • Diva side: pancake_webhook_event WHERE record_id IS NOT NULL AND created_at > NOW()-25h
  • Đơn vị: Số events (integer)
  • Ví dụ: Pancake report 1500 records modified 25h, Diva có 1500 event records → miss = 0 ✅
  • Trường hợp cá biệt:
    • Diva count > Pancake count → có duplicate (đáng nghi, alert)
    • Pancake API down → Cron 7 skip, log + retry hour sau
    • Missing events detected → auto-inject vào pancake_webhook_event với event_type='reconciled', không alert escalate trừ khi > 5/ngày

FORMULA-006: Outage MTTR

  • Mô tả: Mean Time To Recovery — thời gian trung bình từ outage_started_at đến outage_ended_at.
  • Công thức: mttr = AVG(outage_ended_at - outage_started_at) WHERE outage_ended_at IS NOT NULL
  • Biến số: outage_started_at, outage_ended_at — nguồn: pancake_outage_state history (cần audit trail riêng hoặc time-series)
  • Đơn vị: Phút (2 chữ số thập phân)
  • Ví dụ: 5 outage trong tháng, total 22 phút → MTTR = 4,40 phút ✅ (target ≤ 5 phút sau Pancake recovery)
  • Trường hợp cá biệt: Outage chưa recover → loại khỏi tính (chưa có outage_ended_at)

A11) Truy vết (Traceability)

Profile L — canonical Traceability ở dev-spec.md C12. Section này chỉ tóm tắt mapping FR → Owner để PO/BA nhanh check.

FROwner docSection ref
FR-001 Webhook receiverdev-spec.mdC5 STEP webhook + C2 Impact webhook service
FR-002 Persist raw eventdev-spec.mdC4 schema pancake_webhook_event
FR-003 Idempotency 3-tupledev-spec.mdC4 UNIQUE constraint + C5 INSERT logic
FR-004 Async processordev-spec.mdC5 Hasura event trigger + C6 dispatch
FR-005 Phone normalize + matchdev-spec.mdC4 account ALTER + C5 STEP 2-5
FR-006 Advisory lockdev-spec.mdC5 STEP 3 SQL
FR-007 Auto-create contact_bookdev-spec.mdC4 contact_book ALTER + C5 STEP 5b
FR-008 Auto-assign round-robindev-spec.mdC5 STEP 9-10 + C2 refactor distribute_ticket.go
FR-009 Smart update Q2.bdev-spec.mdC5 STEP 8 logic
FR-010 Tạo ticketdev-spec.mdC5 STEP 10 INSERT
FR-011 Notificationdev-spec.mdC5 STEP 12 + C4 notification_template seed
FR-012 Settings UI 4 tabsui-spec.mdSCR-01..04 + dev-spec C2 FE delta
FR-013 REST API consumptiondev-spec.mdC5 Cron 3 + circuit breaker
FR-014 Outage recovery + DLQdev-spec.mdC6 Cron 4/6/7 + dlq retry + C5 replay action
FR-015 Feature flag + pilotdev-spec.mdC8 Security + C4 app_setting key
FR-016 Test modedev-spec.mdC5 STEP 1 flag + C6 cleanup cron
FR-017 Opt-out compliancedev-spec.mdC5 STEP 7 query + C4 customer_consent helper
FR-018 FE delta dropdownui-spec.mdSCR-06 delta + dev-spec C2 FE files

Mapping DEC → cách kiểm chứng (canonical → dev-spec C12)

DECTriển khaiCách kiểm chứng
DEC-002 Split webhook + crm-apiservices/webhook/handler/pancake.go + services/crm-api/event/event_pancake_process_record.goTC-001-* webhook integration tests + TC-004-* handler unit tests
DEC-003 Fail-open 200webhook handler trả 200 mọi case + status enum đầy đủTC-001-001 (auth fail), TC-001-002 (IP block), TC-001-003 (parse error)
DEC-006 ALTER contact_bookMigration ADD primary_phone + FK + UNIQUETC-007-001 (auto-create contact when account new)
DEC-012 REUSE GetTicketUpdatesRefactor public helper + integration test với multi-branchTC-008-001..003 (round-robin per branch)
DEC-015 4-layer recoveryCron 4/6/7 + DLQ replayTC-014-001..010 (chaos test outage simulation)
DEC-022 Pilot 4 tuầnFeature flag config + rollout plango-live-checklist E1 W1-W4 gates

Detail traceability matrix (FR → file → test case) → dev-spec.md C12.


A12) Từ điển tooltip

Profile L dùng ui-spec.md B9 canonical — không duplicate ở đây. Section này skip.

→ Xem ui-spec.md B9 cho tooltip dictionary chi tiết của 4 tabs Settings + dropdown delta + audit log fields.


Z') Decision Log v1.1 — Pass 1 Resolutions (Phase 5.2)

Trigger: Multi-Perspective Review (PO/BA + Tech Lead BLOCK + QA/DevOps + FE/UX). Section này OVERRIDE conflict với Z gốc + thêm DEC-026..028 + PD-013..014 + RSK-016..018.

Z'.1) DEC updates (revision)

IDPhiên bảnChange
DEC-006v1.1ALTER contact_book ADD primary_phone TEXT + FK + UNIQUEprimary_phone ĐÃ EXIST (migration 1693889973118 line 12, Go struct pkg/store/contact_book.go:17). Chốt: Migration #3 CHỈ ADD UNIQUE (account_id) constraint sau cleanup duplicate. KHÔNG ADD FK tới account.id (giữ Hasura manual_configuration tới ecommerce_user)

Z'.2) DEC new (DEC-026..028)

IDNhómQuyết địnhLý do (≥2 alt)Trạng thái
DEC-026UXSlot 9 ticket_source_pancake hiển thị trong filter Customer-Service report + Telesales report ngang hàng 8 slot existing (auto-render qua TicketSources.map tại TelesaleReportFilter.tsx:140 + CustomerServiceReportFilter.tsx:195)Alt A: Filter slot 9 ra khỏi report (KPI báo cáo telesales không include Pancake lead) — KHÔNG hợp lý vì Pancake leads = telesale leads. Alt B: Include slot 9 (chốt) — KPI revenue + conversion per source PHẢI count ticket PancakeLocked
DEC-027Kỹ thuậtreplay_dlq permission: Reuse update action của module pancake_crm_integration cho replay DLQ event. KHÔNG add new action replay_dlq vào canonical enumAlt A: Add new action replay_dlq (đụng 5 chỗ shared infra: enum + labels + matrix + DB master + role seed). Alt B: Reuse update (chốt) — MVP-1 giản hơn. M2 add granular nếu cầnLocked
DEC-028Kỹ thuậtPancake event handler dùng direct pgxpool.Pool (env PANCAKE_DB_URL), bypass Hasura GraphQL cho 15-step flow. Notification dispatch SAU tx.Commit() qua Hasura actionAlt A: Direct pgx (chốt) — atomic TX + advisory lock + FOR UPDATE khả thi. Alt B: Saga pattern through Hasura mutations (complexity gấp 3, advisory lock impossible). Alt C: Hasura action RPC backend (tương đương A nhưng thêm layer)Locked

Z'.3) PD updates + new PDs

PDMớiLý do
PD-003Phase-block FR-017Phase-block + AC audit data coverageNếu <50% account có row customer_consent → MVP-1 không thực sự enforce opt-out. Add AC FR-017 audit + Legal/PO RACI
PD-008Non-blockBlock-W1-startKhông có 1 source low-volume specific → QA không setup test data được
PD-011Non-block (decision-brief) / Block-pilot-start (handoff mismatch)Block-pilot-start consistent 3 fileKPI baseline manual workflow required trước W1
PD-013 (NEW)Block-KPI-measureticket.first_response_at field exist? Nếu chưa, derive từ đâu (call_log, ticket_log status change, note added)? Owner BE + PO, hạn 22/05/2026
PD-014 (NEW)Block-Cron-7Pancake REST /records?modified_since endpoint exist + schema + pagination + rate limit? Owner Ops + QA, hạn W1 test 28/05/2026

Z'.4) New Risks (RSK-016..018)

IDRủi roẢnh hưởngXác suấtMitigation
RSK-016DEC-028 direct pgx pool tạo dependency riêng cho Pancake (không consistent với rest of Diva backend)TBCaoIsolated trong pkg/pancake/ package. Document rõ trong handoff. Pattern reusable cho Shopee/TikTok future
RSK-017Race condition GetTicketUpdates khi 2+ Pancake webhook đồng thời cùng branch (telesale rotation)TBTBWrap trong Hasura mutation atomic với row-level lock (TBD), HOẶC dùng Postgres sequence per branch (defer M2)
RSK-018Effort estimate tăng từ 37d → 43d (+6d) sau Pass 1 — Sprint 1 buffer hẹpTBTBParallel 2 BE: BE-1 webhook+handler+migration, BE-2 cron+REST+DLQ. FE +2d cho XJsonViewer/XMetricCard build. Monitor sprint 1 cuối tuần 2

Z'.5) FR updates (corrections)

FR-007 (contact_book auto-create) REVISED

Spec cũ: "ALTER contact_book ADD primary_phone + FK + UNIQUE. Pattern query-before-insert".

Revised:

  • primary_phone column đã exist (verified)
  • Migration #3 chỉ ADD UNIQUE (account_id) sau cleanup duplicate nếu có
  • Pattern query-before-insert vẫn giữ (defense layer dù đã advisory lock)

FR-013 (REST API consumption) REVISED — Cron 3 race fix

Add AC mới:

  • AC-FR-013-NEW-1: Cron 3 ON CONFLICT (pancake_source_id) DO NOTHING. KHÔNG overwrite diva_branch_id hoặc is_active nếu source đã exist. CHỈ update pancake_source_name nếu Pancake đổi tên.

FR-014 (4-layer outage recovery) REVISED — Cron 6 fixed schedule

Spec cũ STEP Cron 6: "Dynamic schedule via pancake_outage_state.current_polling_interval_sec".

Revised: Cron 6 fixed schedule * * * * * (every 1 min). Handler đọc current_polling_interval_sec → nếu chưa đủ delta time từ last_polled_at → return early. Tăng frequency CHECK thay vì giảm schedule.

FR-017 (Opt-out compliance) REVISED — audit data coverage AC

Add AC mới:

  • AC-FR-017-NEW-1: Trước launch W4, audit dataset: bao nhiêu % active account có row customer_consent?
  • AC-FR-017-NEW-2: Nếu <50%, document policy "default marketing=true cho legacy account" trong handoff + RACI ghi rõ Legal/PO accountability cho compliance gap.
  • AC-FR-017-NEW-3: PD-003 resolve trước Phase 4 SPECIFY (22/05/2026) — confirm key name marketing.

Z'.6) A8 KPI dependency on PD-013

FORMULA-002 SLA telesale phản hồi (p95) phụ thuộc ticket.first_response_at — PD-013 phải resolve trước launch.

Default fallback nếu PD-013 chưa resolve: derive first_response_at từ event nào sớm nhất trong: (a) ticket_log status change new → in_progress, (b) call_log first outbound call to customer, (c) ticket.input_note append manual. BE confirm 1 trong 3.

Z'.7) A10 FORMULA additions

FORMULA-007 (NEW): Lead duplicate rate (M2 metric — PO/BA improvement I-1)

  • Mô tả: Tỷ lệ Pancake gửi cùng phone trong cùng ngày — đo chất lượng Smart update Q2.b
  • Công thức: dup_rate = count(events WHERE record_id same AND created_at same day) / count(events) × 100
  • Đơn vị: % (1 chữ số thập phân)
  • Defer M2 — không block MVP-1

Z'.8) Update Backlog Phase 2

#Tính năngLý do defer
... existing items .........
11 (NEW)Encryption KMS cho api_key_encrypted columnDEC-019 cũ. MVP-1 lưu plain text với column comment "TODO encrypt M2". Permission Hasura exclude api_key_encrypted cho mọi role không phải admin
12 (NEW)Permission action replay_dlq granular (tách khỏi update)DEC-027 reuse update cho MVP-1. M2 add nếu Marketing/non-admin role cần view-only mà không có replay
13 (NEW)Hasura concurrency: 1 cron support OR Postgres sequence per-branch cho GetTicketUpdates raceRSK-017 — defer M2

Hết PRD v1.1 (Pass 1 Resolutions).

Pass 1 Resolutions OVERRIDE conflicts với Z gốc. DEC-006 revised, DEC-026/027/028 mới, PD-003/008/011 escalated, PD-013/014 mới, RSK-016/017/018 mới. Sẵn sàng cho dev-spec.md v1.1 (sync DEC-028 architecture) + ui-spec.md v1.1 (sync component audit + slot 9 report) + qa-test-plan.md v1.1 (new TC) + handoff.md v1.1 (RACI + +6d effort) + go-live-checklist.md v1.1 (3 alertmanager rules + HMAC spec + deploy split).