[PRD] CDP Contact Enrichment (Q3 2026)
HEADER BLOCK
| Field | Value |
|---|---|
| PM | Zhelia Alifa |
| PRD Version | 2.2 |
| Status | DRAFT — Pilot Planning |
| PRD Type | NEW |
| Epic | TF-3497 |
| Squad | CDP Squad (delivery) + Data/BI (datamart) + Billing (quota) |
| RFC Link | TBD — recommended before build (cross-team data contract) |
| Figma Master | TBD — CSV Upload page + token balance + run history |
| Labels | epic:qontak-cdp | module:customers | feature:contact-enrichment |
| Last Updated | 2026-07-08 |
Grounded reformat (v2.0). This PRD was reformatted into the AI-SDLC template and validated against the real repos (
contact-service,qontak-customer-fe,qontak.com). The load-bearing finding: the delivery mechanics are reuse, but the signals + datamart + cross-CID identity are BI-owned / net-new, and cross-tenant aggregation currently conflicts with the app's own tenant isolation. See §13 Dependencies and §15 Open Questions for the grounded gaps. Original plan-evolution notes retained in the Changelog.
1. One-liner + Problem
One-liner: Give CDP clients a behavioral intelligence layer on their contacts — computed from Qontak's own first-party conversation/campaign/CRM data — delivered as an enriched spreadsheet via a CSV/XLSX upload → email flow, priced per run (tokens).
Problem: CDP contacts today show only static, manually-entered profile data (name, email, phone, custom fields). There is no behavioral layer — an agent cannot tell whether a contact is a promo hunter, what industry/product they lean to, whether their phone is reachable on WhatsApp, or how engaged they are across the Qontak ecosystem. Because the same person (by phone/email) may interact with multiple Qontak clients, signals collected in isolation at one client are incomplete; aggregating cross-client would produce a richer profile no single client can compute alone — but that aggregation is exactly the part the current system cannot do safely today (see §13 / OQ-P1).
2. Why Now — Opportunity
- Qontak already captures the raw behavioral exhaust (Omnichannel rooms/messages, broadcast replies, CRM deals/tickets) — the data exists, it's just not turned into contact-level intelligence.
- The manual enrichment pilot (Apollo.io, skill-driven, external B2B firmographic) proved client appetite but is 3rd-party, costly, and not differentiated. Pivoting to first-party behavioral enrichment is defensible and unique to Qontak.
- The async upload→XLSX→email delivery pipeline already exists in
contact-service(built for bulk import), so the product surface is cheap to ship — the investment concentrates on the data/compute layer.
3. Target Users + Persona Context
| Persona | Role | Goal | Pain | Workaround |
|---|---|---|---|---|
| Primary — Marketing / Campaign Manager | Runs WhatsApp/Email campaigns for a Qontak client | Target the right contacts (promo hunters, high-value, reachable) before a blast | No behavioral signal on contacts; blasts everyone | Blasts the whole list, wastes WA credits, low reply rate |
| Secondary — CS / Sales Lead | Prioritises follow-up | Rank contacts by engagement / monetary value | Only static profile data | Manual guesswork from chat history |
| Buyer — CID Admin / Owner | Owns the Qontak account + billing | Get ROI-justifiable enrichment without a 3rd-party contract | 3rd-party enrichment is expensive + not conversation-aware | Buys Apollo/others separately, or nothing |
4. Non-Goals
- Not real-time enrichment — the datamart refreshes daily at 08:00 WIB; results are up to ~24h stale by design.
- Not an in-product per-contact panel / Metabase embed — MVP delivery is CSV-upload → email only (the embed approach was dropped, 2026-07-08).
- Not 3rd-party firmographic enrichment — Apollo/Clearbit-style external data is out; this is first-party behavioral only (Apollo is a benchmark, not a source).
- Not a real-time WA number verification service — WA validity is inferred from room history first; live WA Business API check is an unresolved fallback (OQ-1).
- Not mobile — web (
qontak-customer-fe) only for MVP. - Not a new billing/payments system — token consumption integrates with the existing Qontak Billing quota service; pack SKUs/pricing are provisioned by Billing/Commercial, not built here.
- Not CRM/contact writeback — enrichment is delivered as a file; MVP does not write signals back onto the CDP contact record.
Scope Changes
Engineering surfaces this initiative touches (controlled vocab: Backend · Frontend · Mobile · Infra · Data · Design · Docs · None).
- Data / BI (largest, cross-team — NOT
contact-service) — build thecustomer_behavioral_enrichmentHologres datamart + the daily 08:00 WIB refresh job + the 8 signal computations. 6+ of the named source tables and Hologres itself have zero app-code connectivity today (BI-owned). Requires a data contract (schema + freshness SLA) back to CDP. - Backend —
contact-service: net-new enrichment upload endpoint + async job (reuse the bulk-import pattern: multipart XLSX →gocraft/workjob → consumer), a datamart lookup/query layer (net-new — the app has no Hologres connection today; Mongo/PG/Redis only), result XLSX via the existingexcelize+ OSS +SendEmailWithAttachment(SendGrid) path, and token check/deduct/refund via the existing Qontak Billing quota API (net-new billing code, e.g.ENRICHMENT-*). - Frontend —
qontak-customer-fe: CSV/XLSX Upload page in CDP › Customers (reuse theBulkUploadPage.vue/MpUploadpattern), token balance display (reuseBillingStore), run history, and top-up/upsell prompt. - Design — Figma for the Upload page, token balance/run-history, edge-case + upsell states.
- Infra — Hologres datamart storage + refresh scheduling (Data/BI infra).
5. Constraints
| Constraint | Value |
|---|---|
| Platform | Web only (qontak-customer-fe) for MVP. |
| Delivery model | Single track — CSV/XLSX upload → email. No in-product panel / Metabase embed. |
| Input format (grounded) | The existing upload path in contact-service is .xlsx-only (import_contact_request.go:34) and there is no CSV parser (encoding/csv NOT FOUND). MVP should be XLSX-only to stay on the reuse path; true CSV input is net-new (OQ-13). |
| Data freshness | Datamart refreshes daily 08:00 WIB; results ≤ ~24h stale. |
| Delivery speed | Enriched XLSX emailed within ≤ 5 min (p95) of a successful submission. |
| Row cap | Enforced by token pack (Free 50 / S 200 / M 1,000 / L 5,000). Note the existing import cap is MaxImportRow = 10000 — the datamart lookup batch path is net-new and must be load-tested at Pack L. |
| Token / quota (grounded) | Consumed per CID on successful submission only (not on rejected/failed uploads). Implemented via the existing Qontak Billing quota (check-quota/deduction/refund) — a new billing code + pack SKUs must be provisioned by the Billing team; do not build a local counter. |
| Tenant isolation (grounded — critical) | Every contact query in contact-service is company_sso_id-scoped (search.go SearchByEmail/SearchByPhone/SearchByBSUID); phone/email indexes are non-unique per-CID. Cross-CID aggregation by phone/email does not exist and breaks the current isolation model — it is net-new and gated on privacy/legal sign-off (OQ-P1). |
| Compute ownership | Signal computation + datamart are BI/Data-owned (Hologres); contact-service has no Hologres connection today. |
| Identifier normalization | Phone formats (+62 / 62 / 0 / spaces/dashes) must be normalized to one standard before matching (OQ-4). |
6. Enrichment Signals (MVP Scope)
8 signals — 2 categories. Status is grounded honestly: 6 are computation-ready; 2 (WA Valid Number, Industry Preferences) are still "In Definition" — so the "8 all P0" framing is aspirational until those two are defined (OQ-1, OQ-2). All source tables below are DWH/Hologres names owned by BI unless noted; the CRM tables (crm_deals, crm_tickets, crm_products) physically live in the legacy Rails qontak.com, not contact-service.
Basic Metrics
| # | Signal | Status | Source (BI datamart) | Logic (summary) | Output |
|---|---|---|---|---|---|
| 1 | WA Valid Number | In Definition (OQ-1) | WA room history + WA Business API check | ≥1 WA room in Qontak → Verified; API check fallback if no history | Verified / Not Verified |
| 2 | Geographic Profile | Ready | contact profile (province/city/address) | Normalize province/city; group to region (Jawa/Sumatera/…) | province, city, region_group |
| 3 | Gender Profile | Ready | contact profile / custom field | Profile or custom-field gender; optional name inference | gender, gender_source |
| 4 | Industry Preferences | In Definition (OQ-2) | CID-level company_industry across CIDs the identifier appears in | Aggregate by frequency → primary; map to group; tie → recency | industry, industry_group, industry_confidence |
Behavior Metrics (cross-company aggregated — see privacy OQ-P1)
| # | Signal | Status | Source (BI datamart) | Logic (summary) | Output |
|---|---|---|---|---|---|
| 5 | Promo Hunter | Ready | broadcast messages/templates, deals | reply-rate to marketing broadcasts; enrich w/ post-broadcast deal | HIGH(≥50%) / MEDIUM(20–49%) / LOW(>0) / NOT_ENGAGED |
| 6 | Price Sensitivity | Ready | broadcast templates/messages, deals, product | reply-rate on price-keyword templates vs non-price; avg deal amount | HIGH_PRICE_SENSITIVE / BUDGET_ORIENTED / PRICE_NEUTRAL |
| 7 | Engagement Rate | Ready | rooms, messages, broadcasts, deals, tickets | 5 dims (breadth/frequency/recency/responsiveness/depth) → weighted 0–100 | engagement_score, engagement_label, + sub-fields |
| 8 | Customer Monetary Value | Ready | deals | SUM(amount) won per contact; avg/first/last | HIGH(≥10jt) / MID(2–10jt) / LOW(>0) / NO_PURCHASE |
Datamart schema (
customer_behavioral_enrichment, PKidentifier) — the ~31-column BI table (identifier, identifier_type, wa_valid_, province/city/region_group, gender/gender_source, industry/industry_group/industry_confidence, promo_hunter/promo_reply_rate, price_sensitivity/avg_deal_amount, engagement_ fields, customer_monetary_value/total_won_deal_value/avg_deal_value/first_purchase_date/last_purchase_date, data_source_cids[], signal_count, computed_at) is defined in the BI data contract (see §13). It is the contract CDP consumes, not a CDP-owned schema.
7. New Features
7.1 CSV/XLSX Upload Enrichment (product surface — CDP-owned)
| Step | Behavior | Grounded reuse |
|---|---|---|
| Upload | User uploads XLSX with one column (phone or email); selects identifier type | Reuse multipart upload (ParseMultipartForm, .xlsx validation) + FE MpUpload |
| Validate | Format, header, row count vs pack limit; reject mixed identifier types, duplicates de-duped; token not consumed on rejection | Reuse excelize.OpenReader/GetRows; row-cap logic net-new per pack |
| Token check | Check per-CID quota; consume on successful submit | Qontak Billing check-quota → deduction (new billing code) |
| Lookup | For each identifier, query customer_behavioral_enrichment; flag is_existing_customer, enrichment_available | Net-new datamart query layer (no Hologres conn today) |
| Result | Build 27-column enriched XLSX; email to registered address ≤5 min | Reuse excelize + OSS + SendEmailWithAttachment |
27 output columns: input_identifier, identifier_type, is_existing_customer, qontak_customer_id, wa_valid_number, province, city, region_group, gender, gender_source, industry, industry_group, industry_confidence, promo_hunter, promo_reply_rate, price_sensitivity, engagement_score, engagement_label, engaged_companies_count, days_since_last_engagement, broadcast_response_rate, has_deal_or_ticket, customer_monetary_value, total_won_deal_value, enrichment_available, suggestion, data_last_updated.
7.2 Token balance + run history (CDP-owned FE)
Token balance (from Billing quota), run history list, upsell/top-up prompt when balance = 0 or upload exceeds pack limit.
7.3 Datamart + 8 signals (BI-owned — dependency, not CDP build)
The datamart, daily refresh, and signal logic are delivered by Data/BI against the §13 data contract.
8. API & Data Behavior
| # | Behavior | Entity | Trigger | Expected | Failure |
|---|---|---|---|---|---|
| 1 | Submit enrichment run | enrichment job | User submits validated XLSX | Multipart upload → validate (format/header/rows/identifier-type) → Billing check-quota → enqueue gocraft/work job → 202 + run id | Validation fail → 4xx, no token consumed; quota=0 → 402 + top-up prompt |
| 2 | Datamart lookup | — | Job runs | Batch-query customer_behavioral_enrichment by phone/email; per row set is_existing_customer, enrichment_available, signals or nulls | Datamart unavailable/stale → job retry/alert; run marked failed → token refunded (Billing refund) |
| 3 | Deliver result | Lookup complete | Build 27-col XLSX (excelize) → OSS → SendEmailWithAttachment to registered email ≤5 min; Billing deduction on success | Email send fail → retry; persistent fail → refund token + surface error | |
| 4 | No data found | — | Identifier absent from datamart | enrichment_available=false, signals null, suggestion="No behavioral data available…" | — (still counts toward the run; token policy for all-null runs = OQ-10) |
| 5 | Guard rails | — | Bad input | Mixed phone+email → reject; duplicates → de-dupe; over pack limit → reject + show balance/upsell | — (no token consumed) |
System Flow
flowchart TD
A[Qontak first-party data<br/>Omnichannel + CRM + Broadcast] --> B[BI: Identity Resolution<br/>phone/email cross-CID key]
B --> C[(Hologres datamart<br/>customer_behavioral_enrichment<br/>daily 08:00 WIB)]
U[User uploads XLSX<br/>phone OR email] --> V{Validate<br/>format/rows/type}
V -- reject --> X[Error + no token consumed]
V -- ok --> Q{Billing check-quota}
Q -- 0 --> T[Top-up / upsell prompt]
Q -- ok --> J[gocraft/work job]
J --> L[Batch lookup datamart]
C -. read .-> L
L --> R[Build 27-col XLSX<br/>excelize + OSS]
R --> E[SendEmailWithAttachment<br/>≤5 min]
E --> D[Billing deduction on success]
9. System Flow + User Stories + ACs
| User Story | Importance | Mockup | Technical Notes | Acceptance Criteria |
|---|---|---|---|---|
| [ENR-S01] Upload + validate identifier file As a marketer, I want to upload a file of phones or emails and have it validated, so bad input is caught before a token is spent. | Must Have | TBD — Figma: upload form + identifier-type selector + validation errors | Reuse: multipart ParseMultipartForm(32<<20) (pkg/http/util.go:56) + .xlsx-only validation (payload/import_contact_request.go:34); parse via excelize.OpenReader+GetRows (service/bulk_import_contact.go:103/115); FE MpUpload accept=".xlsx" (BulkUploadPage.vue). Net-new: identifier-type selector + mixed-type / duplicate / over-pack-limit rejection (existing cap MaxImportRow=10000, bulk_import_contact.go:20). | AC-1: Given a valid XLSX with a single phone or email column within the pack row-limit, when the user selects the identifier type and submits, then the run is accepted (202) and a run id returned.ERR-1: Given a file mixing phone + email, then it is rejected and no token is consumed. ERR-2: Given rows > pack limit, then rejected with the remaining balance + upsell. ERR-3: Given duplicate identifiers, then they are de-duped and only unique ones count toward the limit. ERR-4 (grounded): Given a .csv file, then MVP rejects it (XLSX-only) unless OQ-13 adds CSV parsing. |
| [ENR-S02] Token check / consume via Billing As a CID admin, I want a run to consume exactly one token on success, so billing is fair. | Must Have | N/A — backend (token balance surfaced in ENR-S06) | Reuse: Qontak Billing quota client — check-quota (api/qontak_billing.go:311), deduction (:368), refund (:423); pattern CheckAndDeductCustomFieldQuota (service/qontak_billing/qontak_billing.go:93). Net-new (Billing team): an ENRICHMENT-* billing code + pack SKUs. Order: check-then-deduct-on-success, refund on failure (settle OQ-10 for all-null runs). | AC-1: Given balance ≥ 1, when a run is submitted, then check-quota passes and the run proceeds.AC-2: Given a successful delivery, then deduction consumes 1 token (per CID, shared across users).AC-3: Given a rejected/failed run, then no deduction (or refund if already deducted).ERR-1: Given balance = 0, then submission is blocked with a top-up prompt. |
| [ENR-S03] Datamart lookup + existing-customer flag As a marketer, I want each identifier matched against Qontak's behavioral datamart. | Must Have | N/A — backend / data lookup | Net-new: a Hologres query/connection layer — contact-service connects only to Mongo/PG/Redis today (no Hologres), and customer_behavioral_enrichment appears in zero files. Reuse: set is_existing_customer + qontak_customer_id via the CID-scoped SearchByEmail/SearchByPhone (repository/contact/search.go:41/55). The cross-CID identity match itself is BI-owned (see S07) and privacy-gated (OQ-P1). | AC-1: Given an identifier present in the datamart, then its signals are returned and enrichment_available=true.AC-2: Given the identifier is an existing Qontak contact, then is_existing_customer=true and qontak_customer_id is populated.AC-3: Given no data, then enrichment_available=false, signals null, suggestion set.ERR-1: Given the datamart is unavailable/refresh failed, then the run fails gracefully + token refunded + user notified. |
| [ENR-S04] Enriched XLSX result (27 cols) + edge cases As a marketer, I want a clean enriched spreadsheet. | Must Have | N/A — file output (27-col layout in §7.1) | Reuse: XLSX build via excelize (service/excel_service.go:64) + OSS upload GenerateAndUploadExcel (excel_service.go:92; today writes private/templates/ → use a new prefix e.g. private/enrichment/). Net-new: the 27-column mapping from the datamart contract + Pack-L (5,000×27) size/perf validation (OQ-7). | AC-1: Given a completed run, then a 27-column XLSX is produced with one row per unique input identifier. AC-2: Given data_last_updated, then it reflects the datamart computed_at.ERR-1 (grounded): Given Pack L (5,000 × 27), then file size/perf is validated (OQ-7). |
| [ENR-S05] Email delivery As a marketer, I want the result emailed to me quickly. | Must Have | TBD — email template (§7.1) | Reuse: SendEmailWithAttachment (service/email/email_service.go:98, SendGrid, XLSX MIME :130) + the import email-template pattern (SendEmailImportCustomer{FullSucceeded,PartialSucceeded,FullFailed}, :153-208). Net-new: the enrichment email copy + a token-remaining variable. | AC-1: Given a successful run, then the XLSX is emailed to the user's registered address within 5 min (p95) via SendEmailWithAttachment.AC-2: Given delivery, then the email states data freshness (daily 08:00 WIB) + remaining token balance. ERR-1: Given send failure, then retry; persistent failure → refund + error surfaced. |
| [ENR-S06] Upload page + token balance + run history (FE) As a user, I want to upload, see my balance, and review past runs. | Should Have | TBD — Figma: Enrichment page (upload · token balance · run history · upsell) | Reuse (async): gocraft/work pool (worker/worker_service.go:74, job register :113), enqueue job_enqueuer.go:65, consumer pattern consumer/bulk_import_contact.go:28. Reuse (FE): BulkUploadPage.vue/MpUpload; balance via BillingStore.ts (limit/used/remaining). Net-new: run-history endpoint + store; the Enrichment page route + permission gate (OQ-14). | AC-1: Given the CDP › Customers Enrichment page, then an upload form, current token balance, and run history are shown. AC-2: Given balance = 0 or an over-limit rejection, then an upsell/top-up prompt appears. Permission: gated by the CDP customer view/manage permission (key TBD — OQ-14). |
| [ENR-S07] BI data contract — datamart + 8 signals (cross-team) As CDP, I depend on Data/BI to deliver the datamart + signals to a fixed contract. | Must Have (dependency) | N/A — BI datamart, no UI | BI-owned — not app-buildable. Datamart + 8 signals live in Hologres; 6+ source tables (omnichannel_broadcast_messages, omnichannel_rooms, customer_location, customer_gender, …) are NOT FOUND in any app repo. The CRM tables that exist are in legacy Rails qontak.com (crm_deals schema.rb:1355, crm_tickets :3123, crm_products :2307). contact_custom_fields is really crm_contacts_additional_field (Rails) / a Mongo custom_fields array. App consumes via the data contract only. | AC-1: Given the agreed schema, then customer_behavioral_enrichment exposes all contract columns with types.AC-2: Given the daily 08:00 WIB job, then computed_at advances daily and refresh success is monitored.AC-3: Given the 6 "Ready" signals, then they compute per the defined logic; WA Valid + Industry remain blocked until OQ-1/OQ-2 resolve. AC-4 (privacy): Given cross-CID aggregation, then it proceeds only under the consent/legal model agreed in OQ-P1. |
| [ENR-S08-NEG] Guard rails (from Non-Goals) As the system, I must not leak or overcharge. | Guard Rail | — | Grounded: tenant isolation is already enforced by company_sso_id scoping (repository/contact/search.go); the degraded single-CID path (NEG-2) reuses only those scoped queries. No writeback → do not touch the contact-update path. All-null rows come straight from a datamart miss (no fabrication). | NEG-1: Given a contact with no Qontak history, then all signals null — no fabricated data. NEG-2: Given cross-CID aggregation without legal sign-off (OQ-P1 unresolved), then the cross-company signals (Promo Hunter, Price, Engagement breadth, Industry) are not shipped — feature degrades to single-CID signals only. NEG-3: No signal writeback to the CDP contact record in MVP. |
10. Rollout + Pilot + Decision Gate
| Phase | Target | Deliverable |
|---|---|---|
| Data Foundation | Jul 2026 | Datamart built, 6 Ready signals computing, daily refresh live (BI) |
| CSV Upload MVP | Aug 2026 | Upload UI, validation, datamart lookup, email delivery, Billing token integration |
| Pilot Launch | Aug 2026 | 5 pilot clients (TBD), free tier (3 tokens) active |
| Pilot Eval | Sep 2026 | Decision-gate assessment; WA + Industry finalized; pricing defined |
| GA | Q4 2026 | Full rollout, token packs, all 8 signals |
Decision gate (all must pass before GA): datamart coverage ≥ 60% · signal accuracy ≥ 80% (PM spot-check 50) · email ≤ 5 min p95 · ≥ 3 clients × ≥ 3 runs · no privacy/PII issue raised by Legal (OQ-P1).
Pilot cohort (why selected): Retail/E-commerce (promo hunters, high broadcast volume) · Financial Services (monetary value / upsell) · Healthcare (WA reachability + geo) · Education (engagement / follow-up) · Property/Real-Estate (price sensitivity, high deal amounts). Specific 5 clients TBD (OQ-11).
11. Observability
| Event | Trigger | Properties |
|---|---|---|
cdp_enrichment_run_submitted | Run accepted | company_sso_id, identifier_type, row_count, user_id |
cdp_enrichment_run_rejected | Validation fail | company_sso_id, reason |
cdp_enrichment_run_completed | XLSX emailed | run_id, matched_count, coverage_pct, duration_ms |
cdp_enrichment_run_failed | Lookup/email fail | run_id, stage, reason, token_refunded |
cdp_enrichment_token_consumed / _topup | Billing deduction / pack purchase | company_sso_id, pack, balance_after |
cdp_enrichment_datamart_refresh | Daily job (BI) | status, rows, lag_hours |
Alerts: datamart refresh miss/lag > 24h → BI on-call; run failure rate > 10% → CDP; email p95 > 5 min → CDP.
12. Success Metrics
| Goal | Metric | Target (Pilot) |
|---|---|---|
| ⭐ Coverage | % uploaded contacts with ≥1 signal | ≥ 60% |
| ⭐ Freshness | On-time daily 08:00 WIB refresh | 100% |
| Delivery speed | Email time after submit | ≤ 5 min (p95) |
| Pilot engagement | Runs per client / 30 days | ≥ 3 |
| Accuracy | PM/client spot-check of labels (50 contacts) | ≥ 80% |
| Monetization | Free→paid top-up after 3 free tokens | ≥ 20% (post-pilot) |
13. Dependencies
| Dependency | Owner | Deliverable | Blocking? |
|---|---|---|---|
customer_behavioral_enrichment datamart + 8 signals + daily refresh | Data / BI | Hologres table to the agreed schema/contract; 6 Ready signals computing; refresh SLA | YES — largest risk (zero app connectivity today; 6+ source tables are DWH-only) |
| Cross-tenant privacy/consent model | Legal + PM | Sign-off on aggregating one person's data across client CIDs | YES — gates all cross-company signals (OQ-P1) |
Hologres query layer in contact-service | CDP Backend | Net-new connection/query path (app is Mongo/PG/Redis only today) | YES |
| Token billing code + pack SKUs | Billing / Commercial | New billing code (ENRICHMENT-*) + Free/S/M/L SKUs + prices | YES for paid tier (quota check/deduct/refund plumbing already exists) |
| Upload → job → XLSX → email pipeline | CDP Backend | Adapt the bulk-import scaffold (route→job→consumer→excelize→SendEmailWithAttachment) | No — reuse, low risk |
| Upload page + balance + history | CDP Frontend | Reuse BulkUploadPage.vue / MpUpload / BillingStore | No |
| WA Business API (if used for WA Valid fallback) | Platform/Omnichannel | Per-number check endpoint + cost/latency budget | Conditional (OQ-1) |
Dependency Graph
graph LR
F[CDP Enrichment MVP] -->|BLOCKING| BI[Data/BI: datamart + 8 signals]
F -->|BLOCKING| PRIV[Legal: cross-tenant consent OQ-P1]
F -->|BLOCKING| HOLO[contact-service: Hologres query layer]
F -->|paid tier| BILL[Billing: ENRICHMENT code + SKUs]
F -->|reuse| PIPE[import pipeline: job/XLSX/email]
F -->|reuse| FE[customer-fe: upload/balance/history]
BI -.needs.-> PRIV
14. Key Decisions + Alternatives Rejected
14a — Decisions Made
| ID | Decision | Rationale (grounded) |
|---|---|---|
| D-1 | First-party behavioral, not 3rd-party firmographic. | Differentiated + owns the data; Apollo is a benchmark only. |
| D-2 | Delivery = CSV/XLSX upload → email; no in-product panel/embed. | Simplest MVP; reuses the shipped import→job→XLSX→email pipeline in contact-service. |
| D-3 | Reuse the async bulk-import scaffold (POST /iag/v1/contacts/import pattern: gocraft/work → consumer → excelize → SendEmailWithAttachment). | Grounded reuse; the delivery surface is low-risk. |
| D-4 | Integrate the existing Qontak Billing quota (check-quota/deduction/refund) for tokens, not a local counter. | Grounded — the quota primitive already exists and is Billing-owned. |
| D-5 | Signals + datamart are BI-owned; consumed via a data contract. | Grounded — 6+ source tables + Hologres have no app connectivity; app must not re-implement BI. |
| D-6 | XLSX-only input for MVP. | Grounded — no CSV parser exists; XLSX stays on the reuse path (CSV = OQ-13). |
14b — Alternatives Rejected
| Alternative | Why Rejected |
|---|---|
| In-product per-contact enrichment panel / Metabase embed | Dropped 2026-07-08 — heavier build; upload→email ships the value first. |
App-side signal computation in contact-service | 6+ source tables + Hologres are BI-owned; the app has no connectivity and shouldn't duplicate the DWH. |
Local token counter in contact-service | A per-CID quota system already exists in Billing; a local counter would fork billing truth. |
| Cross-CID aggregation without a consent model | Breaks the app's company_sso_id isolation and is a privacy/PII risk — blocked on Legal (OQ-P1). |
| 3rd-party enrichment (Apollo/Clearbit) | Costly, not conversation-aware, not differentiated. |
15. Open Questions
| # | Type | Question | Default / Mitigation | Owner | Deadline |
|---|---|---|---|---|---|
| OQ-P1 | Risk (blocking) | Cross-tenant consent/privacy: may we aggregate one person's behavior across client CIDs and sell it back? Current app model is strictly CID-scoped. | Block cross-company signals until Legal signs off; MVP can ship single-CID signals only (NEG-2). | Legal + PM | 2026-07-18 |
| OQ-BI | Risk (blocking) | Datamart data contract + ownership: who on BI owns customer_behavioral_enrichment, its schema freeze, and the refresh SLA? App has no Hologres path today. | Sign a data contract before build; add Hologres query layer. | BI + CDP BE | 2026-07-18 |
| OQ-1 | Open | WA Valid Number logic — room history only, or WA Business API fallback (cost/latency)? | Ship room-history-only for pilot; API fallback post-pilot. | PM + Omnichannel | 2026-08-01 |
| OQ-2 | Open | Industry Preferences tie-break when a contact spans 3 industries? | Frequency-weighted, recency tiebreaker. | PM + BI | 2026-08-01 |
| OQ-4 | Decision | Phone normalization standard (+62/62/0/spaces) before matching. | Define one canonical E.164-ish format. | BE + BI | 2026-07-25 |
| OQ-5 | Open | Datamart refresh failure alerting/retry + how staleness is communicated to users. | Alert BI on-call; email notes freshness. | BI | 2026-08-01 |
| OQ-7 | Open | Pack L (5,000 × 27) XLSX size/perf — compress or paginate? | Load-test; compress if needed. | BE | 2026-08-01 |
| OQ-8 | Decision | Token expiry/rollover (free expiry? paid monthly vs lifetime). | Decide in pricing review. | Commercial | 2026-08-01 |
| OQ-9 | Decision | Contact limit per-run vs cumulative per token. | Per-run (1 token = 1 run up to pack cap). | PM | 2026-07-25 |
| OQ-10 | Decision | Charge a token for an all-null run (no Qontak history)? | Lean "don't charge if 0 matched"; confirm w/ Commercial. | PM + Commercial | 2026-07-25 |
| OQ-11 | Open | Which 5 pilot clients, and do we need their consent to use their data for cross-CID compute? (ties to OQ-P1) | Select + obtain consent before pilot. | PM | 2026-07-25 |
| OQ-12 | Decision | Pack S/M/L pricing points. | Commercial input; benchmark Apollo (~USD 79/mo/1,000). | Commercial | 2026-08-15 |
| OQ-13 | Open | Support true CSV input, or XLSX-only? (no CSV parser today) | XLSX-only for MVP. | PM + BE | 2026-07-25 |
| OQ-14 | Open | Which permission key gates the Enrichment page? | Reuse a CDP customers view/manage key. | PM + BE | 2026-07-25 |
PRD CHANGELOG
| Version | Date | Section | Type | Summary |
|---|---|---|---|---|
| 2.2 | 2026-07-08 | §9 Stories | UPDATE | Reconciled the story-table columns to a single canonical layout — User Story · Importance · Mockup · Technical Notes · Acceptance Criteria — matching Confluence. Added the Mockup column (Figma TBD for the FE stories S01/S06; N/A for the backend/data/BI stories) and renamed the grounded technical column to Technical Notes. Removed the duplicate/empty Technical Notes column that had appeared on Confluence. No AC/scope change. |
| 2.1 | 2026-07-08 | §9 Stories | UPDATE | Added a grounded Technical Notes column to the §9 story table — per-story reuse-vs-net-new-vs-BI-owned with file:line evidence (import pipeline ParseMultipartForm/excelize/gocraft/work/SendEmailWithAttachment; Billing quota check-quota/deduction/refund; net-new Hologres query layer; BI-owned signals; company_sso_id isolation for the degraded path). No AC/scope change. |
| 2.0 | 2026-07-08 | All | REFORMAT + GROUNDING | Reformatted the free-form plan into the AI-SDLC NEW template (Header, ToC, §1–15, Scope Changes, story table w/ Gherkin ACs, mermaid flow + dependency graph). Grounded against contact-service / qontak-customer-fe / qontak.com: delivery pipeline (upload→gocraft/work→excelize→SendEmailWithAttachment) + Billing quota (check-quota/deduction/refund) are reuse; datamart + 8 signals + Hologres are BI-owned / no app connectivity; cross-CID aggregation breaks the app's company_sso_id isolation (privacy P0, OQ-P1); input is XLSX-only today (no CSV parser); corrected non-existent names (GenerateAndUploadExcelWithData→GenerateAndUploadExcel, private/exports/→private/templates/, contact_custom_fields→Rails crm_contacts_additional_field). Flagged the 2 "In Definition" signals as blockers to the "8 all P0" claim. |
| 1.x | 2026-06-23 → 2026-07-08 | — | EVOLUTION | Pivot to first-party behavioral (2026-06-23); added CSV upload + token pricing (2026-07-07); simplified to CSV-upload-only, no Metabase embed, 8 metrics Basic+Behavior (2026-07-08). |