Appearance
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ản | Ngày | Tác giả | Thay đổi |
|---|---|---|---|
| 1.0 | 15/05/2026 | PO/BA + AI | Khở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
| File | Vai trò | Nếu xung đột |
|---|---|---|
SOURCE_OF_TRUTH.md | Nguồn sự thật chuẩn + Solution Lock | Ưu tiên cao nhất |
EVIDENCE_PACK.md | Bằng chứng code/screen/db Phase 3 | Ưu tiên bằng chứng trước assumption |
decision-brief.md | Cửa vào package | Brief 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 C3chỉ mô tả triển khai SQL/code.
Hướng dẫn đọc
| Đối tượng | Section cần đọc |
|---|---|
| PO/BA | decision-brief.md → A0 → Z → A5 → A10 → A9 |
| Sếp/Business Lead | decision-brief.md → TL;DR → A0 → A2 → A3 → A8 |
| Tech Lead | decision-brief.md → A0 → Z → dev-spec C1-C8 |
| UI/UX + FE | decision-brief.md → A5 → ui-spec.md |
| BE Dev | A5 + A10 → dev-spec.md C1-C12 |
| QA | A5 → 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.
| PD | Câu hỏi | Lựa chọn / khuyến nghị | Phụ trách | Hạn | Trạng thái | Block |
|---|---|---|---|---|---|---|
| PD-001 | Pancake 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 0972273341 | Ops | 25/06/2026 | Open | Launch W4 |
| PD-002 | Pancake 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 song | Ops + Dev | 25/06/2026 | Open | Launch W4 |
| PD-003 | customer_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 + PO | 22/05/2026 | Open | Phase 4 SPECIFY FR-017 |
| PD-004 | VIP tag IDs initial seed | Suggest: VIP, Hot Lead, Khách lớn, Khách quay lại. PO confirm | PO | 04/06/2026 | Open | Non-block |
| PD-005 | Pancake retry policy chi tiết | Test trả 500 đo behavior. Assume exponential 5 retries | QA | W1 (28/05-04/06) | Open | Non-block |
| PD-006 | Pancake REST /sources rate limit | Test 100 req liên tiếp đo 429. Cache 1h | QA | W1 | Open | Non-block |
| PD-007 | Pancake future ticket event publish? | Monitor crm.pancake.vn/developers quarterly | TL | — | Monitor | Non-block |
| PD-008 | Pilot 4 tuần exact source list | PO chọn theo volume hiện tại | PO + Marketing | 28/05/2026 | Open | W1 setup |
| PD-009 | Hasura concurrency: 1 cron supported? | Verify Hasura docs. Fallback advisory lock trong handler | TL | Phase 4 | Open | Non-block |
| PD-010 | Pancake public API roadmap? | Track quarterly | PO | — | Monitor | Non-block |
| PD-011 | KPI baseline manual workflow (response time hiện tại) | Manager đo 1 tuần trước W1 launch | PO + Manager | 21/05/2026 | Open | KPI-measurement |
| PD-012 | Notification noti_pancake_outage kênh? | A) Push admin in-app (DEC-010) B) Email C) SMS escalate. Khuyến nghị: A | PO | W1 | Open | Non-block |
Backlog giai đoạn 2 (Out of scope MVP-1)
| # | Tính năng | Lý do defer |
|---|---|---|
| 1 | Outbound Diva → Pancake (POST records/tickets) | ROI MVP-1 đã đủ; Pancake chưa có guaranteed write API |
| 2 | Multi-workspace support | Diva chỉ có 1 workspace Pancake hiện tại |
| 3 | Pancake ticket event handling | Pancake hiện chưa publish event này, roadmap unclear |
| 4 | Bulk import source routing CSV | Volume 40 source quản manual được |
| 5 | Dashboard analytics realtime (lead by source, conversion by source) | Query trực tiếp ticket+event đủ cho MVP-1 |
| 6 | Shopee/TikTok/Lazada tích hợp | Pattern code extendable |
| 7 | Conversation/Messenger sync (Pancake POS feature) | Sản phẩm khác |
| 8 | Geo address normalize (chỉ lưu raw text trong pancake_metadata) | Geo normalize complexity cao |
| 9 | ML/AI lead scoring | Beyond MVP scope |
| 10 | Pancake 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.
| ID | Nhóm | Quyết định | Lý do (≥2 alt) | Ngày | Trạng thái |
|---|---|---|---|---|---|
| DEC-001 | Nghiệp vụ | Inbound 1 chiều Pancake → Diva (MVP-1). Outbound defer MVP-2 | Alt 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ơn | 15/05/2026 | Locked |
| DEC-002 | Kỹ thuật | Split 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 reuse | 15/05/2026 | Locked |
| DEC-003 | Kỹ thuật | Webhook fail-open trả 200 mọi case (auth/IP/parse/dup). Insert raw status='ingested' trước verify → promote 'received' chỉ khi happy path | Alt 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 enum | 15/05/2026 | Locked |
| DEC-004 | Kỹ thuật | Idempotency 3-tuple (record_id, modified_on, payload_hash) UNIQUE trong pancake_webhook_event | Alt 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/2026 | Locked |
| DEC-005 | Kỹ thuật | Phone match canonical = account.normalized_phone E.164 (libphonenumber). Fallback (phone_code, phone_number) trong migration window | Alt A: Match raw phone string (sai format, miss khách). Alt B: E.164 normalize (chốt) — libphonenumber có sẵn ở auth service | 15/05/2026 | Locked |
| DEC-006 | Kỹ thuật | ALTER contact_book ADD primary_phone TEXT + FK account_id → account.id + UNIQUE (account_id). Auto-create contact entry per Pancake account | Alt 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/2026 | Locked |
| DEC-007 | Kỹ thuật | 4 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_at | Alt A: Dùng deleted_at (sai pattern Diva). Alt B: disabled BOOLEAN (chốt) — đồng nhất toàn DB | 15/05/2026 | Locked |
| DEC-008 | Nghiệ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-line | Alt 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/2026 | Locked |
| DEC-009 | Nghiệ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 24h | Alt A: Hard-delete sau 24h (mất debug data). Alt B: Soft-delete (chốt) | 15/05/2026 | Locked |
| DEC-010 | UX | Notification Loose mode (branch=NULL): in-app push cho Admin role + branch-manager NULL. KHÔNG escalate email/SMS | Alt A: Email/SMS escalate (noise nhiều). Alt B: In-app push (chốt) — admin chủ động xem dashboard | 15/05/2026 | Locked |
| DEC-011 | Nghiệ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 ticket | Alt 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/2026 | Locked |
| DEC-012 | Kỹ thuật | Auto-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.5d | 15/05/2026 | Locked |
| DEC-013 | UX | Auto-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ùng | Alt 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ơn | 15/05/2026 | Locked |
| DEC-014 | Nghiệ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 consent | 15/05/2026 | Locked |
| DEC-015 | Kỹ thuật | 4-layer outage recovery: (1) webhook receiver, (2) Cron 6 adaptive polling fallback, (3) Cron 7 daily reconciliation safety net, (4) DLQ + Admin Replay UI | Alt A: Chỉ webhook + retry (miss lead khi outage). Alt B: 4-layer (chốt) — đảm bảo zero-miss SLO | 15/05/2026 | Locked |
| DEC-016 | UX | Settings UI 4 tabs: Kết nối / Map source→branch / Lịch sử+DLQ replay / Sức khỏe. Quyền Admin only | Alt A: 5 tabs (có Tab Tags) — dropped ở v2. Alt B: 4 tabs (chốt) — bớt complexity | 15/05/2026 | Locked |
| DEC-017 | UX | Ticket 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 semantic | 15/05/2026 | Locked |
| DEC-018 | Kỹ thuật | Pancake REST: chỉ GET /workspaces/{ws}/sources (Cron 3 hourly cache). DROP GET /pages/pancake_tags | Alt A: Sync cả sources + tags (extra complexity, DEC-008 đã drop tag fetch). Alt B: Chỉ sources (chốt) | 15/05/2026 | Locked |
| DEC-019 | Kỹ thuật | Circuit breaker REST: sony/gobreaker cho Pancake REST call. Default 5 fail/60s → open, half-open sau 30s | Alt A: Không circuit breaker (risk cascade fail khi Pancake down). Alt B: gobreaker (chốt) — production-ready | 15/05/2026 | Locked |
| DEC-020 | Kỹ thuật | Advisory lock per phone chống race: pg_advisory_xact_lock(hashtext('account_phone_' || normalized_phone)) trong process record STEP 3-5 | Alt A: SELECT FOR UPDATE (heavy, race vẫn có khi phone chưa exist). Alt B: Advisory lock (chốt) | 15/05/2026 | Locked |
| DEC-021 | Vận hành | Feature 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ạt | 15/05/2026 | Locked |
| DEC-022 | Vận hành | Pilot 4 tuần: W1=1 source low-vol, W2=5 mid, W3=15 high (load test), W4=40+ all production | Alt A: Big bang launch. Alt B: 4-tuần pilot (chốt) — giảm risk vận hành | 15/05/2026 | Locked |
| DEC-023 | Vận hành | KPI 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ày | Alt A: Chỉ event success rate. Alt B: 5 metric đa chiều (chốt) | 15/05/2026 | Locked |
| DEC-024 | UX | Notification 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/2026 | Locked |
| DEC-025 | Kỹ thuật | Reconciliation 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 miss | Alt A: 24h window (miss khi clock skew). Alt B: 25h overlap (chốt) | 15/05/2026 | Locked |
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ối | FR | SCR (UI Spec) | Dev section |
|---|---|---|---|
| Webhook receiver | FR-001/002/003/004 | (no UI) | dev-spec C5 STEP webhook |
| Process record async | FR-005/006/007/008/009 | (no UI) | dev-spec C5 STEP 0-14 |
| Ticket creation + assign | FR-010 | (auto-render existing) | dev-spec C5 STEP 9-10 |
| Notification | FR-011 | (existing) | dev-spec C5 STEP 12 |
| Settings UI | FR-012 | SCR-01..05 | ui-spec |
| REST consumption | FR-013 | SCR-01 connection panel | dev-spec C5 Cron 3 |
| Outage recovery | FR-014 | SCR-04 health monitor | dev-spec C6 |
| Pilot + feature flag | FR-015 | SCR-01 toggle | dev-spec C8 |
| Test mode | FR-016 | SCR-03 audit filter | dev-spec C5 |
| Opt-out compliance | FR-017 | (no UI, audit log) | dev-spec C5 STEP 7 |
| FE delta dropdown | FR-018 | SCR-06 (delta on existing) | dev-spec C2 |
Quyết định chính (xem Z đầy đủ):
| Nhóm | Tóm tắt | Ref |
|---|---|---|
| Kiến trúc | Split webhook + crm-api qua Hasura event trigger | DEC-002 |
| Kiến trúc | Fail-open 200 mọi case | DEC-003 |
| Nghiệp vụ | Smart update Q2.b (tránh spam ticket) | DEC-011 |
| Nghiệp vụ | Reuse ticket_distribute round-robin | DEC-012 |
| Nghiệp vụ | Opt-out qua customer_consent | DEC-014 |
| UX | Settings 4 tabs Admin only | DEC-016 |
| Vận hành | Pilot 4 tuần W1→W4 | DEC-022 |
Bảng FR tóm tắt:
| FR | Mô tả (1 dòng) | Ưu tiên | Phase |
|---|---|---|---|
| FR-001 | Webhook receiver endpoint return 200<1s | Must | W1 |
| FR-002 | Persist raw event audit | Must | W1 |
| FR-003 | Idempotency 3-tuple | Must | W1 |
| FR-004 | Async processor qua Hasura event trigger | Must | W1 |
| FR-005 | Phone E.164 normalize + match/upsert account | Must | W1 |
| FR-006 | Advisory lock per phone chống race | Must | W1 |
| FR-007 | Auto-create contact_book entry | Must | W1 |
| FR-008 | Auto-assign REUSE ticket_distribute round-robin 1-tier | Must | W1 |
| FR-009 | Smart update Q2.b (chỉ tạo ticket khi đổi quan trọng) | Must | W1 |
| FR-010 | Tạo ticket standard source=ticket_source_pancake | Must | W1 |
| FR-011 | Notification telesale (assigned) + admin (Loose mode) | Must | W1 |
| FR-012 | Settings UI 4 tabs Admin only | Must | W1 |
| FR-013 | REST API consumption (chỉ /sources, Cron 3 1h cache) | Must | W1 |
| FR-014 | 4-layer outage recovery + DLQ replay UI | Must | W2-W3 |
| FR-015 | Feature flag 3 mức + pilot rollout | Must | W1-W4 |
| FR-016 | Test mode event tagged is_test=true | Must | W1 |
| FR-017 | Opt-out qua customer_consent.marketing | Must | W1 |
| FR-018 | FE delta dropdown ticket source (3 file) | Must | W1 |
Mô hình dữ liệu tóm tắt:
| Bảng | Loại | Mục đích |
|---|---|---|
pancake_webhook_event | MỚI | Audit raw + status lifecycle + retry |
pancake_connection | MỚI | Workspace config (api_key, vip_tag_names, kill switch status) |
pancake_source_routing | MỚI | Map Pancake source → Diva branch + is_active toggle |
pancake_outage_state | MỚI | State machine outage detect (Cron 4) |
account | SỬA | ADD normalized_phone TEXT, pancake_metadata JSONB + index |
contact_book | SỬA | ADD primary_phone TEXT + FK + UNIQUE constraint (DEC-006) |
crm_master_data | SEED | INSERT 1 row ticket_source_pancake |
app_setting.app_settings | SEED | ADD key pancake_integration.enabled |
module_permission_action | SEED | INSERT module pancake_crm_integration + 6 actions |
notification_template | SEED | INSERT 3 template mới |
ticket_distribute | KHÔNG ĐỔI | Reuse round-robin |
customer_consent | KHÔNG ĐỔI | Reuse opt-out query |
ticket | KHÔNG ĐỔI | Reuse 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ường | Giá trị |
|---|---|
| Tính năng | Tích hợp Pancake CRM (inbound webhook) |
| Loại | Tính năng mới |
| Nền tảng | Web Admin (diva-admin) — Settings module + CRM module delta |
| Module FE ảnh hưởng | src/modules/settings (4 tabs mới), src/modules/crm (3 file delta dropdown source) |
| Service BE ảnh hưởng | services/webhook (extend), services/crm-api (extend), services/notification-v2-api (template seed), Hasura controller (metadata + migration) |
| Database | 4 bảng mới + ALTER 2 bảng + seed master data |
| Cron | 7 cron mới + 1 Hasura event trigger mới |
| External API | Pancake CRM REST GET /workspaces/{ws}/sources (Cron 3) |
| Public endpoint | POST /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.vnchứ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ótickettable vớisource_idFK tớicrm_master_data(8 slotticket_source_1..8hiện có),ticket_distributeround-robin per branch,customer_consentopt-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
recordmỗ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-upsource_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ục | Lý do |
|---|---|---|
| 1 | Outbound Diva → Pancake | Defer MVP-2 — ROI chưa đủ |
| 2 | Pancake POS (đơn hàng) tích hợp | Sản phẩm khác Pancake |
| 3 | Pancake Botcake (chatbot Messenger) tích hợp | Sản phẩm khác Pancake (notification-v2-api hiện có actionBotcakeSendContent UNRELATED) |
| 4 | Pancake ticket event | Pancake hiện chưa publish event này |
| 5 | Multi-workspace | Diva chỉ có 1 workspace Pancake |
| 6 | Tag mapping table riêng | DEC-008 dùng input tay tag NAME |
| 7 | Bulk import source routing CSV | Manual quản được volume 40 source |
| 8 | Pancake user → Diva user mapping (owner) | Org structure khác nhau |
| 9 | full_address sync vào geo module | Chỉ lưu raw text trong pancake_metadata |
| 10 | Real-time analytics dashboard | M2/M3 — query trực tiếp ticket+event đủ |
A3) Mục tiêu
| Mục tiêu | Cách đo | Target |
|---|---|---|
| Loại bỏ copy-paste thủ công lead Pancake | Số manager báo cáo "đã ngừng copy-paste" | 100% manager (15/15) sau W4 |
| Telesale phản hồi lead trong window vàng | ticket.first_response_at − created_at p95 (giờ làm việc) | ≤ 30 phút (p95) |
| Latency webhook → ticket assigned | pancake_webhook_event.processed_at − created_at p95 | ≤ 30 giây (p95) |
| Zero-miss lead | Reconciliation report cuối ngày: events Pancake gửi nhưng Diva miss sau 25h | 0 events/ngày |
| Event success rate | processed_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ị suspend | Count 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 W4 | TBD baseline (PD-011) |
A4) Nhân vật (Personas)
| Persona | Vai trò | JTBD | Tần suất |
|---|---|---|---|
| Telesale | Telesale 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ẹn | Mỗi lần có notification (50-200 lần/ngày toàn hệ thống) |
| Manager chi nhánh | Manager 15 chi nhánh | Manual assign telesale cho ticket branch=NULL hoặc telesale rảnh khan; theo dõi KPI lead chi nhánh | Hàng ngày check dashboard ticket |
| Admin Diva (ops) | Admin role | Setup webhook URL trong Pancake admin, manage source→branch mapping, replay event lỗi từ DLQ, monitor health | Setup ban đầu + recovery sự cố |
| Marketing | Marketing manager | Track conversion per Pancake source (page nào ROI cao) — M2/M3 nice-to-have | Hàng tuần / hàng tháng |
| Khách | End user (khách spa/salon) | Chat với Pancake page → đăng ký dịch vụ → mong nhận cuộc gọi telesale Diva nhanh | 1 lần per lead |
| Hệ thống Pancake | External webhook sender | Gửi event record khi nhân viên Pancake tạo/cập nhật record | Auto, theo Pancake retry policy |
| Hệ thống Diva (cron) | Cron 3-7 + DLQ retry | Sync sources / detect outage / reconciliation / retry failed event | Theo 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àopancake_webhook_event(raw_payload jsonb, raw_headers jsonb, source_ip, status enum) - Điều kiện: Pancake gửi event
recordtừ 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) UPDATEstatus='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
- Token URL sai → UPDATE
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_eventvớistatus='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 rowlast_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/abc123tokenvớ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_eventvớ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 đó UPDATEstatustheo 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_payloadlưu nguyên JSON body Pancake gửi (không lossy) - [ ] Field
raw_headerslưu nguyên HTTP headers (debug) - [ ] Field
source_ipresolve qua middleware reverse proxy (X-Forwarded-Fornế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_atauto-trigger; KHÔNG cầncreated_by/updated_bycho 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ênpancake_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 → UPDATElast_received_atcủ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_idNULL (Pancake schema error) → vẫn insert nhưngstatus='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_onnhư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_updatewatch columnstatus, firePOST /eventsvới payload{trigger: {name: 'pancake_process_record'}, event: {data: {old, new}}}. crm-api dispatcher routepancake_process_record→ handlerevent_pancake_process_record.go - Điều kiện: UPDATE column
statustrênpancake_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ì Hasuraupdate.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', incrementretry_count, log error → DLQ replay UI handles - Handler timeout 60s → Hasura mark failed → retry theo Hasura retry config
AC:
- [ ] Khi
pancake_webhook_event.statusUPDATE từingested→received, handler fire trong 1-2 giây - [ ] Khi
statusUPDATE từingested→auth_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ànhprocessed,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.gohelper mới - Điều kiện: STEP 2 trong process record flow
- Hành động: (1) Parse
record.phone_numberbằng libphonenumber với region VN. (2) Nếu hợp lệ → format E.164 (+84...). (3) Queryaccount WHERE normalized_phone = E164. (4) Nếu match → update info (display_name, address, ...). (5) Nếu không match → INSERT new account vớinormalized_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 accountcreated_atcũ nhất, log warning
- Phone parse fail (sai format) → log warning, account
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=truecho 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 ADDprimary_phone TEXT, FKaccount_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_phonecủa contact match vớinormalized_phonecủ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:157thành public). Bảngticket_distribute(audit assignment). 1-tier per branch - Điều kiện: Sau khi tạo ticket ở STEP 10, có
branch_id IS NOT NULLvà 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_branchgử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 ticket | Required | Nguồn | Logic |
|---|---|---|---|
customer_id | ✓ | account.id (đã match/tạo ở STEP 5) | direct |
customer_phone_number | ✓ | account.normalized_phone | direct |
customer_name | ✓ | account.display_name (= record.name) | direct |
source_id | ✓ | 'ticket_source_pancake' | fixed |
target_id | ✓ | 'telesales' | fixed |
branch_id | ✓ | pancake_source_routing.diva_branch_id (lookup) | NULL nếu unmapped |
assignee_id | optional | FR-008 round-robin result | NULL trong Loose mode |
due_date | ✓ | today 23:59:59 Asia/Ho_Chi_Minh | computed |
status_id | ✓ | 'ticket_status_new' (default) | fixed |
result_id | NULL | — | NULL |
input_note | — | Auto-gen text từ payload | template |
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
tickettable 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=NULLLoose 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_notechứ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. Reusenotification-v2-apiHasura actionsendNotifications - Điều kiện: Sau khi tạo ticket ở STEP 10 (FR-010)
- Hành động:
- Nếu ticket có
assignee_id→ gửinoti_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ửinoti_pancake_unmapped_branchtới Admin role + branch-manager - Nếu
branch_id NOT NULLnhưngassignee_id=NULL(không có telesale active) → gửi tới manager branch
- Nếu ticket có
- 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_pancakecó 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_branchcontent: "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
/sourcestest - 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_letterorauth_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)
- 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
- 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_eventaggregate 24h/7d - [ ] Permission
replay_dlqaction: 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 trongpancake_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_keyauth. Parse response. Upsert vàopancake_source_routing(auto-add source mới vớiis_active=false, diva_branch_id=NULL). Send notificationnoti_pancake_unmapped_branchcho 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/gobreakeropen 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
- Pancake API timeout/5xx → circuit breaker
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_routingvớ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 → UPDATEpancake_outage_state.status='outage_started'. Notify opsnoti_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àopancake_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). Comparerecord_idvới DB. Inject missing events vàopancake_webhook_eventvớievent_type='reconciled' - DLQ replay UI (Tab 3): Admin select event status=
dead_letter→ click "Replay" → actionpancake_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_statetable 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_staterealtime + 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.statusENUM (active/paused/suspended_by_pancake/error), (3)pancake_source_routing.is_activeBOOLEAN 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'
- W1: 1 source
- 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. Cronpancake_test_event_cleanupdaily - Điều kiện: Pancake payload có field
is_test=trueHOẶC webhook URL có query param?test=1 - Hành động:
- STEP 1: nếu
is_test=true→ set fieldis_test=truetrong DB - Process flow vẫn run (tạo account/ticket test) NHƯNG flag
account.pancake_metadata.is_test=true,ticket.input_noteprefix "[TEST] " - KPI tracking: KPI calculation filter
WHERE is_test=false - Cron
pancake_test_event_cleanupdaily 03:00 AM: soft-delete (disabled=true) test events + test accounts/tickets sau 24h
- STEP 1: nếu
- 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
FR-017 — Opt-out compliance qua customer_consent
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 jsonbvới keymarketing(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ữ. UPDATEstatus='skipped_opt_out' - Kết quả: Compliance marketing opt-out
- Ngoại lệ:
- Khách chưa có row
customer_consent→ mặc địnhmarketing=true(chưa opt-out), tạo ticket bình thường - PD-003 chưa confirm key name → fallback default key
marketing
- Khách chưa có row
AC:
- [ ] Khách có
customer_consent.consent_data.marketing='false'→ KHÔNG tạo ticket Pancake, logstatus='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
diva-admin/src/modules/crm/types.ts:146-164— ADDexport const TICKET_SOURCE_PANCAKE = "ticket_source_pancake"+ append vào arrayTicketSourcesdiva-admin/src/modules/crm/i18n/vi.ts:130-137— ADD keyticket_source_pancake: "Pancake CRM"diva-admin/src/modules/crm/pages/Tickets.tsx:128-149— ADD entry vào objectsourceDescriptions:ticket_source_pancake: '<span>Lead từ Pancake CRM (40+ kênh: FB, Zalo, TikTok...)</span>'- Migration INSERT
crm_master_datarowticket_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_PANCAKEconst xuất hiện trong types.ts - [ ]
TicketSourcesarray 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 ra | Terminal |
|---|---|---|---|---|
ingested | Webhook handler vừa INSERT raw event | INSERT step 1 webhook | Verify 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 UPDATE | Hasura trigger fire → processing | Không |
processing | crm-api handler đang xử lý STEP 0-14 | Handler started | STEP 14 OK → processed; exception → dead_letter | Không |
processed | Hoàn tất tạo account/ticket/notification | STEP 14 COMMIT | (terminal) | Có |
auth_failed | Token URL sai | Webhook verify step 2 | (terminal — alert ops nếu > 10/min) | Có |
ip_blocked | IP không whitelist | Webhook verify step 2 | (terminal) | Có |
parse_error | JSON schema invalid | Webhook verify step 3 | (terminal) | Có |
skipped_duplicate | 3-tuple đã có | Webhook verify step 4 | (terminal — update last_received_at) | Có |
skipped_source_disabled | pancake_source_routing.is_active=false | Webhook step 5 | (terminal) | Có |
skipped_kill_switch | app_setting kill switch off | Webhook step 5 | (terminal) | Có |
skipped_opt_out | customer_consent.marketing=false | crm-api STEP 7 | (terminal) | Có |
dead_letter | crm-api STEP 1-14 exception | Handler catch | Admin replay → received; retry_count>=3 → permanently_failed | Không |
permanently_failed | DLQ replay fail 3 lần | Cron retry check | (terminal — alert manual review) | Có |
| Từ | Event | Điều kiện | Sang | Giải thích |
|---|---|---|---|---|
ingested | webhook verify pass | token + IP + schema + dedup OK | received | Promote, fire Hasura trigger |
received | Hasura trigger fire | handler STEP 0 filter pass | processing | Handler bắt đầu |
processing | handler exception | STEP 1-14 lỗi | dead_letter | Increment retry_count, log error |
dead_letter | admin replay UI | manual | received | Reset retry_count, fire lại |
dead_letter | retry policy | retry_count >= 3 | permanently_failed | Cần manual review |
LIFECYCLE-002 — Vòng đời pancake_outage_state.status
| Trạng thái | Ý nghĩa | Cron behavior |
|---|---|---|
healthy | Bình thường, webhook nhận đều | Cron 6 không chạy |
outage_started | Cron 4 detect 0 event trong 5 phút giờ làm | Cron 6 adaptive polling kích hoạt |
outage_recovered | Webhook nhận lại event | Cron 6 stop sau 5 phút sustain |
A6) Giả định
| ID | Giả định | Phụ trách xác nhận |
|---|---|---|
| ASM-001 | Pancake gửi webhook có retry tối đa 5 lần exponential backoff (industry default) | QA (PD-005) |
| ASM-002 | Pancake REST /sources rate limit ≥ 100 req/giờ (Cron 3 hourly cache đủ) | QA (PD-006) |
| ASM-003 | customer_consent.consent_data.marketing='false' = opt-out marketing (key name PD-003) | PO + BE |
| ASM-004 | Diva server nhận webhook public HTTPS (TLS termination ở reverse proxy/CDN) | DevOps |
| ASM-005 | Pancake 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-006 | Volume initial ~50-200 lead/ngày, peak ~3.3k/ngày → 100k/tháng. Capacity DB index đủ | Tech Lead capacity model |
| ASM-007 | Pancake 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-008 | Pilot W1 source được PO chọn là low volume (≤ 10 lead/ngày) để verify end-to-end | PO (PD-008) |
| ASM-009 | Diva timezone Asia/Ho_Chi_Minh consistent toàn hệ thống (due_date set theo timezone này) | DevOps |
| ASM-010 | Telesale có app push notification enabled (notification-v2-api phụ thuộc) | All telesale |
A7) Rủi ro
| ID | Rủi ro | Ảnh hưởng | Xác suất | Cách giảm thiểu |
|---|---|---|---|---|
| RSK-001 | Pancake auto-suspend webhook (80% error / 30 phút) → mất lead toàn bộ | Cao | TB | DEC-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-002 | Race condition tạo account trùng khi 2 webhook đồng thời cùng phone | Cao | Cao (nếu không có lock) | DEC-020 pg_advisory_xact_lock per phone + integration test concurrent |
| RSK-003 | Pancake outage / Diva downtime = mất lead | Cao | Thấp | DEC-015 4-layer recovery: webhook + Cron 6 polling + Cron 7 reconciliation + DLQ |
| RSK-004 | Branch=NULL ticket bị bỏ quên trong dashboard admin | TB | TB | DEC-010 push admin in-app realtime + Tab 3 filter status branch_id IS NULL + Daily morning digest email (M2 nice-to-have) |
| RSK-005 | FE delta hardcode source slot 1..8 ngoài TicketSources array (report module có thể hardcode) | TB | TB | F18 grep audit 'ticket_source_' trước deploy + add delta nếu phát sinh |
| RSK-006 | customer_consent.consent_data.marketing key chưa confirm với PO | TB | TB | PD-003 — confirm trước Phase 4 SPECIFY; fallback default key marketing |
| RSK-007 | DLQ + Replay UI scope mới (3d) — chưa có pattern code | TB | TB | Build 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-008 | KPI baseline manual workflow chưa đo → không có baseline so sánh | Thấp | Cao | PD-011 — Manager đo 1 tuần trước W1 launch |
| RSK-009 | Pancake gửi field schema khác (vd format phone mới) | TB | Thấp | JSON schema validation cứng + alert parse_error rate > 5% + extensible schema field event_type cho future event |
| RSK-010 | Pilot W3 high volume (15 source) load test phát hiện performance issue | TB | TB | Scale crm-api horizontally; DB index cho normalized_phone + index status; capacity model 100k events/tháng |
| RSK-011 | Notification-v2-api fail → telesale không nhận lead | Cao | Thấp | STEP 12 KHÔNG block flow (ticket vẫn tạo); cron alert ops nếu notification fail rate > 5%/5 phút |
| RSK-012 | Pancake API key bị compromise (api_key trong DB) | Cao | Thấp | Encrypted at rest (DB column api_key_encrypted với KMS); audit log toggle |
| RSK-013 | Webhook endpoint bị DDoS (no auth public) | TB | Thấp | Rate limit ở reverse proxy (vd Nginx 1000 req/giây/IP); IP whitelist after PD-002 |
| RSK-014 | Migration backfill normalized_phone cho dataset lớn (~1M account) chạy lâu | Thấp | TB | Run as separate migration với batch (10k row/transaction); monitor progress |
| RSK-015 | Pancake gửi record_id rất dài (>100 chars) → DB column overflow | Thấp | Thấp | Column 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 đo | Mục tiêu | Khi 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ây | Realtime 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út | Daily / weekly report |
| Auto-assign correct rate | count(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 rate | count(status='processed') / count(*) 24h window | ≥ 99.5% | Realtime alert < 99% |
| Zero-miss SLO | Reconciliation Cron 7 count missing events injected | 0 events/ngày | Daily 02:00 AM cron report |
| Reconciliation rescue rate | count(event_type='reconciled') / count(*) 7-day | ≤ 0.5% | Weekly review |
| Outage MTTR | outage_ended_at - outage_started_at mean | ≤ 5 phút | Per 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 baseline | Quarterly after W4 |
| Manager time saved | Survey 15 manager: "Bạn còn copy-paste lead Pancake không?" | 100% "không" sau W4 | Survey end of W4 |
| Pancake suspension count | count(*) WHERE pancake_connection.status='suspended_by_pancake' per quarter | 0 | Quarterly |
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ệt | EN | Định nghĩa nghiệp vụ | Phân biệt với | Nguồn |
|---|---|---|---|---|
| Pancake CRM | Pancake CRM | Workspace 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) | Record | Entity 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) | Source | Page/kênh nguồn Pancake (FB page, Zalo OA...). Mapping sang Diva branch qua pancake_source_routing | ≠ ticket_source của Diva (master data ticket) | Pancake /sources API |
| Ticket Pancake | Pancake Ticket Source | Slot 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 event | Webhook Event | Row 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-tuple | Idempotency Triple | UNIQUE (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.b | Smart Update | Logic 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 mode | Loose Mode | Khi 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 branch | Round-Robin | Auto-assign telesale trong branch theo thứ tự xoay vòng. REUSE ticket_distribute.GetTicketUpdates | ≠ Random assignment; ≠ Multi-tier fallback (v1 rejected) | DEC-012 |
| Advisory lock | Advisory Lock | pg_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-open | Fail-Open | Webhook 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 suspension | Pancake Suspension | Auto-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 state | Outage State | Bảng pancake_outage_state tracking healthy/outage_started/outage_recovered | ≠ pancake_connection.status (config kill switch) | DEC-015 |
| Adaptive polling fallback | Adaptive Polling | Cron 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 |
| Reconciliation | Reconciliation | Cron 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 replay | DLQ Replay | Admin manual replay event status='dead_letter' qua Settings Tab 3 | ≠ Reconciliation (automatic cron); ≠ Adaptive polling | DEC-015, FR-014 |
| Webhook receiver | Webhook Receiver | services/webhook/handler/pancake.go — endpoint POST /api/pancake/record/{token} | ≠ Webhook sender (outbound, OUT MVP-1) | A0 |
| Process record flow | Process Record Flow | services/crm-api/event/event_pancake_process_record.go — 15-step async handler | ≠ Webhook receiver flow (sync, <1s) | A0 |
| VIP tag | VIP Tag | Tag 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 event | Test Event | Event với is_test=true, KHÔNG count KPI, auto soft-delete sau 24h | ≠ Production event | DEC-009 |
| Opt-out marketing | Marketing Opt-Out | Khá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 rollout | Pilot Rollout | 4-tuần W1=1 source → W4=40+ all. Feature flag 3 mức (DEC-021) | ≠ Big bang launch (v1 rejected) | DEC-022 |
| Soft delete | Soft Delete | Diva 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 signature | HMAC Signature | Header 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 window | 25-Hour Overlap | Cron 7 reconciliation query 25h thay vì 24h chống clock skew miss | ≠ Standard 24h window (v1 rejected) | DEC-025 |
| Capacity model | Capacity Model | 100k events/tháng peak. Index normalized_phone + status để query nhanh | ≠ Real-time throughput limit | Tech 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ểmpancake_webhook_event.statuschuyểnprocessed— nguồn:pancake_webhook_event.processed_atcreated_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_Minhcreated_at: thời điểm ticket tạo — nguồn:ticket.created_atfirst_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_hoursshift 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:tickettable - Đơ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)
- Mẫu = 0 → hiển thị
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=trueevents: loại khỏi cả tử + mẫu
- Loại trừ
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
- Pancake side: query REST
- Đơ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_eventvớievent_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_statehistory (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.mdC12. Section này chỉ tóm tắt mapping FR → Owner để PO/BA nhanh check.
| FR | Owner doc | Section ref |
|---|---|---|
| FR-001 Webhook receiver | dev-spec.md | C5 STEP webhook + C2 Impact webhook service |
| FR-002 Persist raw event | dev-spec.md | C4 schema pancake_webhook_event |
| FR-003 Idempotency 3-tuple | dev-spec.md | C4 UNIQUE constraint + C5 INSERT logic |
| FR-004 Async processor | dev-spec.md | C5 Hasura event trigger + C6 dispatch |
| FR-005 Phone normalize + match | dev-spec.md | C4 account ALTER + C5 STEP 2-5 |
| FR-006 Advisory lock | dev-spec.md | C5 STEP 3 SQL |
| FR-007 Auto-create contact_book | dev-spec.md | C4 contact_book ALTER + C5 STEP 5b |
| FR-008 Auto-assign round-robin | dev-spec.md | C5 STEP 9-10 + C2 refactor distribute_ticket.go |
| FR-009 Smart update Q2.b | dev-spec.md | C5 STEP 8 logic |
| FR-010 Tạo ticket | dev-spec.md | C5 STEP 10 INSERT |
| FR-011 Notification | dev-spec.md | C5 STEP 12 + C4 notification_template seed |
| FR-012 Settings UI 4 tabs | ui-spec.md | SCR-01..04 + dev-spec C2 FE delta |
| FR-013 REST API consumption | dev-spec.md | C5 Cron 3 + circuit breaker |
| FR-014 Outage recovery + DLQ | dev-spec.md | C6 Cron 4/6/7 + dlq retry + C5 replay action |
| FR-015 Feature flag + pilot | dev-spec.md | C8 Security + C4 app_setting key |
| FR-016 Test mode | dev-spec.md | C5 STEP 1 flag + C6 cleanup cron |
| FR-017 Opt-out compliance | dev-spec.md | C5 STEP 7 query + C4 customer_consent helper |
| FR-018 FE delta dropdown | ui-spec.md | SCR-06 delta + dev-spec C2 FE files |
Mapping DEC → cách kiểm chứng (canonical → dev-spec C12)
| DEC | Triển khai | Cách kiểm chứng |
|---|---|---|
| DEC-002 Split webhook + crm-api | services/webhook/handler/pancake.go + services/crm-api/event/event_pancake_process_record.go | TC-001-* webhook integration tests + TC-004-* handler unit tests |
| DEC-003 Fail-open 200 | webhook 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_book | Migration ADD primary_phone + FK + UNIQUE | TC-007-001 (auto-create contact when account new) |
| DEC-012 REUSE GetTicketUpdates | Refactor public helper + integration test với multi-branch | TC-008-001..003 (round-robin per branch) |
| DEC-015 4-layer recovery | Cron 4/6/7 + DLQ replay | TC-014-001..010 (chaos test outage simulation) |
| DEC-022 Pilot 4 tuần | Feature flag config + rollout plan | go-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 B9canonical — 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)
| ID | Phiên bản | Change |
|---|---|---|
| DEC-006 | v1.1 | 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)
| ID | Nhóm | Quyết định | Lý do (≥2 alt) | Trạng thái |
|---|---|---|---|---|
| DEC-026 | UX | Slot 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 Pancake | Locked |
| DEC-027 | Kỹ thuật | replay_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 enum | Alt 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ần | Locked |
| DEC-028 | Kỹ thuật | Pancake 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 action | Alt 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
| PD | Cũ | Mới | Lý do |
|---|---|---|---|
| PD-003 | Phase-block FR-017 | Phase-block + AC audit data coverage | Nế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-008 | Non-block | Block-W1-start | Không có 1 source low-volume specific → QA không setup test data được |
| PD-011 | Non-block (decision-brief) / Block-pilot-start (handoff mismatch) | Block-pilot-start consistent 3 file | KPI baseline manual workflow required trước W1 |
| PD-013 (NEW) | — | Block-KPI-measure | ticket.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-7 | Pancake 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)
| ID | Rủi ro | Ảnh hưởng | Xác suất | Mitigation |
|---|---|---|---|---|
| RSK-016 | DEC-028 direct pgx pool tạo dependency riêng cho Pancake (không consistent với rest of Diva backend) | TB | Cao | Isolated trong pkg/pancake/ package. Document rõ trong handoff. Pattern reusable cho Shopee/TikTok future |
| RSK-017 | Race condition GetTicketUpdates khi 2+ Pancake webhook đồng thời cùng branch (telesale rotation) | TB | TB | Wrap trong Hasura mutation atomic với row-level lock (TBD), HOẶC dùng Postgres sequence per branch (defer M2) |
| RSK-018 | Effort estimate tăng từ 37d → 43d (+6d) sau Pass 1 — Sprint 1 buffer hẹp | TB | TB | Parallel 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_phonecolumn đã 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_idhoặcis_activenếu source đã exist. CHỈ updatepancake_source_namenế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=truecho 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ăng | Lý do defer |
|---|---|---|
| ... existing items ... | ... | ... |
| 11 (NEW) | Encryption KMS cho api_key_encrypted column | DEC-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 race | RSK-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.mdv1.1 (sync DEC-028 architecture) +ui-spec.mdv1.1 (sync component audit + slot 9 report) +qa-test-plan.mdv1.1 (new TC) +handoff.mdv1.1 (RACI + +6d effort) +go-live-checklist.mdv1.1 (3 alertmanager rules + HMAC spec + deploy split).