Skip to main content

[PRD] CDP Contact Enrichment (Q3 2026)

HEADER BLOCK

FieldValue
PMZhelia Alifa
PRD Version2.2
StatusDRAFT — Pilot Planning
PRD TypeNEW
EpicTF-3497
SquadCDP Squad (delivery) + Data/BI (datamart) + Billing (quota)
RFC LinkTBD — recommended before build (cross-team data contract)
Figma MasterTBD — CSV Upload page + token balance + run history
Labelsepic:qontak-cdp | module:customers | feature:contact-enrichment
Last Updated2026-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

PersonaRoleGoalPainWorkaround
Primary — Marketing / Campaign ManagerRuns WhatsApp/Email campaigns for a Qontak clientTarget the right contacts (promo hunters, high-value, reachable) before a blastNo behavioral signal on contacts; blasts everyoneBlasts the whole list, wastes WA credits, low reply rate
Secondary — CS / Sales LeadPrioritises follow-upRank contacts by engagement / monetary valueOnly static profile dataManual guesswork from chat history
Buyer — CID Admin / OwnerOwns the Qontak account + billingGet ROI-justifiable enrichment without a 3rd-party contract3rd-party enrichment is expensive + not conversation-awareBuys Apollo/others separately, or nothing

4. Non-Goals

  1. Not real-time enrichment — the datamart refreshes daily at 08:00 WIB; results are up to ~24h stale by design.
  2. Not an in-product per-contact panel / Metabase embed — MVP delivery is CSV-upload → email only (the embed approach was dropped, 2026-07-08).
  3. 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).
  4. 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).
  5. Not mobile — web (qontak-customer-fe) only for MVP.
  6. 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.
  7. 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 the customer_behavioral_enrichment Hologres 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.
  • Backendcontact-service: net-new enrichment upload endpoint + async job (reuse the bulk-import pattern: multipart XLSX → gocraft/work job → consumer), a datamart lookup/query layer (net-new — the app has no Hologres connection today; Mongo/PG/Redis only), result XLSX via the existing excelize + OSS + SendEmailWithAttachment (SendGrid) path, and token check/deduct/refund via the existing Qontak Billing quota API (net-new billing code, e.g. ENRICHMENT-*).
  • Frontendqontak-customer-fe: CSV/XLSX Upload page in CDP › Customers (reuse the BulkUploadPage.vue / MpUpload pattern), token balance display (reuse BillingStore), 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

ConstraintValue
PlatformWeb only (qontak-customer-fe) for MVP.
Delivery modelSingle 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 freshnessDatamart refreshes daily 08:00 WIB; results ≤ ~24h stale.
Delivery speedEnriched XLSX emailed within ≤ 5 min (p95) of a successful submission.
Row capEnforced 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 ownershipSignal computation + datamart are BI/Data-owned (Hologres); contact-service has no Hologres connection today.
Identifier normalizationPhone 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

#SignalStatusSource (BI datamart)Logic (summary)Output
1WA Valid NumberIn Definition (OQ-1)WA room history + WA Business API check≥1 WA room in Qontak → Verified; API check fallback if no historyVerified / Not Verified
2Geographic ProfileReadycontact profile (province/city/address)Normalize province/city; group to region (Jawa/Sumatera/…)province, city, region_group
3Gender ProfileReadycontact profile / custom fieldProfile or custom-field gender; optional name inferencegender, gender_source
4Industry PreferencesIn Definition (OQ-2)CID-level company_industry across CIDs the identifier appears inAggregate by frequency → primary; map to group; tie → recencyindustry, industry_group, industry_confidence

Behavior Metrics (cross-company aggregated — see privacy OQ-P1)

#SignalStatusSource (BI datamart)Logic (summary)Output
5Promo HunterReadybroadcast messages/templates, dealsreply-rate to marketing broadcasts; enrich w/ post-broadcast dealHIGH(≥50%) / MEDIUM(20–49%) / LOW(>0) / NOT_ENGAGED
6Price SensitivityReadybroadcast templates/messages, deals, productreply-rate on price-keyword templates vs non-price; avg deal amountHIGH_PRICE_SENSITIVE / BUDGET_ORIENTED / PRICE_NEUTRAL
7Engagement RateReadyrooms, messages, broadcasts, deals, tickets5 dims (breadth/frequency/recency/responsiveness/depth) → weighted 0–100engagement_score, engagement_label, + sub-fields
8Customer Monetary ValueReadydealsSUM(amount) won per contact; avg/first/lastHIGH(≥10jt) / MID(2–10jt) / LOW(>0) / NO_PURCHASE

Datamart schema (customer_behavioral_enrichment, PK identifier) — 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)

StepBehaviorGrounded reuse
UploadUser uploads XLSX with one column (phone or email); selects identifier typeReuse multipart upload (ParseMultipartForm, .xlsx validation) + FE MpUpload
ValidateFormat, header, row count vs pack limit; reject mixed identifier types, duplicates de-duped; token not consumed on rejectionReuse excelize.OpenReader/GetRows; row-cap logic net-new per pack
Token checkCheck per-CID quota; consume on successful submitQontak Billing check-quotadeduction (new billing code)
LookupFor each identifier, query customer_behavioral_enrichment; flag is_existing_customer, enrichment_availableNet-new datamart query layer (no Hologres conn today)
ResultBuild 27-column enriched XLSX; email to registered address ≤5 minReuse 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

#BehaviorEntityTriggerExpectedFailure
1Submit enrichment runenrichment jobUser submits validated XLSXMultipart upload → validate (format/header/rows/identifier-type) → Billing check-quota → enqueue gocraft/work job → 202 + run idValidation fail → 4xx, no token consumed; quota=0 → 402 + top-up prompt
2Datamart lookupJob runsBatch-query customer_behavioral_enrichment by phone/email; per row set is_existing_customer, enrichment_available, signals or nullsDatamart unavailable/stale → job retry/alert; run marked failed → token refunded (Billing refund)
3Deliver resultemailLookup completeBuild 27-col XLSX (excelize) → OSS → SendEmailWithAttachment to registered email ≤5 min; Billing deduction on successEmail send fail → retry; persistent fail → refund token + surface error
4No data foundIdentifier absent from datamartenrichment_available=false, signals null, suggestion="No behavioral data available…"— (still counts toward the run; token policy for all-null runs = OQ-10)
5Guard railsBad inputMixed 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 StoryImportanceMockupTechnical NotesAcceptance 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 HaveTBD — Figma: upload form + identifier-type selector + validation errorsReuse: 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 HaveN/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 HaveN/A — backend / data lookupNet-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 HaveN/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 HaveTBD — 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 HaveTBD — 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 UIBI-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 RailGrounded: 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

PhaseTargetDeliverable
Data FoundationJul 2026Datamart built, 6 Ready signals computing, daily refresh live (BI)
CSV Upload MVPAug 2026Upload UI, validation, datamart lookup, email delivery, Billing token integration
Pilot LaunchAug 20265 pilot clients (TBD), free tier (3 tokens) active
Pilot EvalSep 2026Decision-gate assessment; WA + Industry finalized; pricing defined
GAQ4 2026Full 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

EventTriggerProperties
cdp_enrichment_run_submittedRun acceptedcompany_sso_id, identifier_type, row_count, user_id
cdp_enrichment_run_rejectedValidation failcompany_sso_id, reason
cdp_enrichment_run_completedXLSX emailedrun_id, matched_count, coverage_pct, duration_ms
cdp_enrichment_run_failedLookup/email failrun_id, stage, reason, token_refunded
cdp_enrichment_token_consumed / _topupBilling deduction / pack purchasecompany_sso_id, pack, balance_after
cdp_enrichment_datamart_refreshDaily 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

GoalMetricTarget (Pilot)
⭐ Coverage% uploaded contacts with ≥1 signal≥ 60%
⭐ FreshnessOn-time daily 08:00 WIB refresh100%
Delivery speedEmail time after submit≤ 5 min (p95)
Pilot engagementRuns per client / 30 days≥ 3
AccuracyPM/client spot-check of labels (50 contacts)≥ 80%
MonetizationFree→paid top-up after 3 free tokens≥ 20% (post-pilot)

13. Dependencies

DependencyOwnerDeliverableBlocking?
customer_behavioral_enrichment datamart + 8 signals + daily refreshData / BIHologres table to the agreed schema/contract; 6 Ready signals computing; refresh SLAYES — largest risk (zero app connectivity today; 6+ source tables are DWH-only)
Cross-tenant privacy/consent modelLegal + PMSign-off on aggregating one person's data across client CIDsYES — gates all cross-company signals (OQ-P1)
Hologres query layer in contact-serviceCDP BackendNet-new connection/query path (app is Mongo/PG/Redis only today)YES
Token billing code + pack SKUsBilling / CommercialNew billing code (ENRICHMENT-*) + Free/S/M/L SKUs + pricesYES for paid tier (quota check/deduct/refund plumbing already exists)
Upload → job → XLSX → email pipelineCDP BackendAdapt the bulk-import scaffold (route→job→consumer→excelizeSendEmailWithAttachment)No — reuse, low risk
Upload page + balance + historyCDP FrontendReuse BulkUploadPage.vue / MpUpload / BillingStoreNo
WA Business API (if used for WA Valid fallback)Platform/OmnichannelPer-number check endpoint + cost/latency budgetConditional (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

IDDecisionRationale (grounded)
D-1First-party behavioral, not 3rd-party firmographic.Differentiated + owns the data; Apollo is a benchmark only.
D-2Delivery = CSV/XLSX upload → email; no in-product panel/embed.Simplest MVP; reuses the shipped import→job→XLSX→email pipeline in contact-service.
D-3Reuse the async bulk-import scaffold (POST /iag/v1/contacts/import pattern: gocraft/work → consumer → excelizeSendEmailWithAttachment).Grounded reuse; the delivery surface is low-risk.
D-4Integrate 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-5Signals + 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-6XLSX-only input for MVP.Grounded — no CSV parser exists; XLSX stays on the reuse path (CSV = OQ-13).

14b — Alternatives Rejected

AlternativeWhy Rejected
In-product per-contact enrichment panel / Metabase embedDropped 2026-07-08 — heavier build; upload→email ships the value first.
App-side signal computation in contact-service6+ source tables + Hologres are BI-owned; the app has no connectivity and shouldn't duplicate the DWH.
Local token counter in contact-serviceA per-CID quota system already exists in Billing; a local counter would fork billing truth.
Cross-CID aggregation without a consent modelBreaks 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

#TypeQuestionDefault / MitigationOwnerDeadline
OQ-P1Risk (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 + PM2026-07-18
OQ-BIRisk (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 BE2026-07-18
OQ-1OpenWA Valid Number logic — room history only, or WA Business API fallback (cost/latency)?Ship room-history-only for pilot; API fallback post-pilot.PM + Omnichannel2026-08-01
OQ-2OpenIndustry Preferences tie-break when a contact spans 3 industries?Frequency-weighted, recency tiebreaker.PM + BI2026-08-01
OQ-4DecisionPhone normalization standard (+62/62/0/spaces) before matching.Define one canonical E.164-ish format.BE + BI2026-07-25
OQ-5OpenDatamart refresh failure alerting/retry + how staleness is communicated to users.Alert BI on-call; email notes freshness.BI2026-08-01
OQ-7OpenPack L (5,000 × 27) XLSX size/perf — compress or paginate?Load-test; compress if needed.BE2026-08-01
OQ-8DecisionToken expiry/rollover (free expiry? paid monthly vs lifetime).Decide in pricing review.Commercial2026-08-01
OQ-9DecisionContact limit per-run vs cumulative per token.Per-run (1 token = 1 run up to pack cap).PM2026-07-25
OQ-10DecisionCharge a token for an all-null run (no Qontak history)?Lean "don't charge if 0 matched"; confirm w/ Commercial.PM + Commercial2026-07-25
OQ-11OpenWhich 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.PM2026-07-25
OQ-12DecisionPack S/M/L pricing points.Commercial input; benchmark Apollo (~USD 79/mo/1,000).Commercial2026-08-15
OQ-13OpenSupport true CSV input, or XLSX-only? (no CSV parser today)XLSX-only for MVP.PM + BE2026-07-25
OQ-14OpenWhich permission key gates the Enrichment page?Reuse a CDP customers view/manage key.PM + BE2026-07-25

PRD CHANGELOG

VersionDateSectionTypeSummary
2.22026-07-08§9 StoriesUPDATEReconciled 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.12026-07-08§9 StoriesUPDATEAdded 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.02026-07-08AllREFORMAT + GROUNDINGReformatted 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→excelizeSendEmailWithAttachment) + 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 (GenerateAndUploadExcelWithDataGenerateAndUploadExcel, 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.x2026-06-23 → 2026-07-08EVOLUTIONPivot 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).