[PRD] Export Customer Data with Layout
Supersedes: PRD — Export Customer Data with Selected Fields (page 49861459975). Why v2.0: v1.0 was validated against
contact-serviceandqontak-customer-fe. The entire async export pipeline, all required backend jobs/handlers/structs, the FE export button, the field-selection modal, and format-selection UI are all absent from the codebase today. This version scopes the feature correctly as net-new work, documents reusable infrastructure, resolves critical open questions, and provides grounded technical references. See Appendix A. v2.2 change: Export now supports XLSX or CSV (user-selected), and an in-app notification (via the Qontak Unified Notification Service) is delivered alongside the email when an export completes. Grounded againstcontact-service,qontak-customer-fe, andmobile-qontak-crm. Section 8.2 User Stories is final and unchanged; only supporting sections were adjusted. See Changelog. v2.4 change: Tweaked the "Select all first 10,000" shortcut to follow the user's current sort (created_atorupdated_at/edited_at, chosen direction) instead of a fixedcreated_at desc, shown only when total ≥ 10,000; it still sends a server-side selection criterion (not 10,000 IDs) to bound the FE payload, plus (from v2.3) company/org-level export history by reusingchat.qontak.com/reports/export(kept async). §8.2 User Stories remain final/unchanged — new behaviors are specified in §6/§7. See Changelog.
HEADER BLOCK
| Field | Value |
|---|---|
| PM | @Zhelia Alifa |
| PRD Version | 2.6 |
| Status | DRAFT |
| PRD Type | NEW |
| Epic | TF-3181 |
| Squad | CDP Squad |
| RFC Link | TBD |
| Figma Master | https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=9132-240403&t=zobKJGo6eX162Qdk-0 |
| Anchor | No — standalone BaU feature |
| Labels | epic:qontak-cdp | module:customers | feature:export-customer-data |
| Last Updated | 2026-06-24 |
1. One-liner + Problem
One-liner: Let CDP users export up to 10,000 customer records with only the fields they need — delivered asynchronously via email and an in-app notification as an XLSX or CSV file.
Problem: CDP has no customer data export capability today. CRM export is one of the most-used operational features (895 export events / 3 months across 122 CIDs). When clients migrate to CDP, they lose the ability to extract customer data for reporting, campaign targeting, and system integrations.
Technical baseline (grounded):
- The permission constant
CustomersCustomersExportKeyalready exists inpermission_handler.go:120but is not checked on any live endpoint. - A
downloadSelectedCustomers()function exists atListPage.vue:317–328and its event listener is wired atListPage.vue:67— but no UI button emits the event, making the entire flow unreachable. - The existing
downloadCustomerFE call usesGET /api/core/v1/{org_id}/contacts/download/contact— a different namespace and HTTP method from the new export endpoint. gocraft/work,excelize,SendEmailWithAttachment(), and theDownloadTemplateModal.vuefield-selection pattern are all reusable infrastructure.- CSV is net-new but cheap:
encoding/csvis Go stdlib — no newgo.moddependency. No CSV path exists anywhere today; it slots into the same OSS-upload/signed-URL flow, branching only the serializer + extension + content-disposition onformat. - In-app notification is net-new in contact-service: there is no notification-publish mechanism today (only OS-signal
Notify). It must publish to the Qontak Unified Notification Service (/notif/v1/notifications) — either via the existing Kafka publisher (job_enqueuer.go:85) or the heimdall HTTP-client pattern (internal/app/api/iag_mekari.go). On mobile the One Notification V2 center already renders this service (General tab + download/upload category); on web the notification center is owned by the launchpad host shell (customer-fe holds only a stub). - The entire async export pipeline is net-new — none of it exists today.
2. What Happens If We Don't Build This
- CRM migrating clients cannot replace CRM export workflows for as long as CDP lacks export — they must maintain parallel CRM access indefinitely until this ships, blocking full Qontak One transition for their migration cohort.
- Sales ops, marketing ops, and CRM admins lose the ability to extract contact data for campaigns, data cleaning, and integrations — forcing manual workarounds immediately from day 1 of their CDP migration.
- CDP loses a table-stakes feature that every competing CRM product (HubSpot, Zoho, Intercom) provides — directly weakening the enterprise value proposition from the first day of a sales demo and reducing client confidence in CDP as a CRM replacement.
- Internal ops teams cannot extract migrated customer data for QA and validation post-migration, slowing down every migration cycle until this is available.
3. Target Users + Persona Context
| Persona | Role | Goal | Pain | Workaround |
|---|---|---|---|---|
| Primary — CRM Admin / Sales Ops | Admin configuring data pipelines and reporting | Export specific customer fields for external reporting, CRM sync, or campaign targeting | CDP has no export; must stay in CRM for all data extraction | Keep CRM login active; manually pull reports from CRM after migrating to CDP |
| Secondary — Marketing Ops | Marketer building audience lists | Extract filtered, field-specific customer data for broadcast/campaign setup | Cannot get clean customer lists out of CDP | Request data from engineering or export from CRM module |
4. Non-Goals
- Real-time / synchronous export — all exports are async (background job → email + in-app notification delivery).
- Export for non-customer objects — Customers module only.
- Bulk export > 10,000 records — hard cap at 10,000.
- No NEW in-app download center / live progress bar — completion is signalled via email and an in-app notification (Unified Notification Service), and export history + re-download is surfaced by reusing the existing company/org-level export-history page at
chat.qontak.com/reports/export(§6.6). v1 does not build a CDP-local download manager or a live progress bar. - No new notification surface — the in-app notification reuses the existing Qontak Unified Notification Service + Notification Center (web host shell + mobile One Notification V2); this PRD does not build a notification center.
- Org-level export template persistence — "save last selected fields" is per-user.
- Export via OpenAPI / programmatic trigger — v1 is UI-triggered only.
Scope Changes
Engineering surfaces this PRD touches (controlled vocab: Backend · Frontend · Mobile · Infra · Data · Design · Docs · None). Kept in sync with the scope_changes frontmatter above.
- Backend —
contact-service: net-new async export pipeline (route → handler →gocraft/workjob → consumer → service);GenerateAndUploadExcelWithData()(XLSX) and a CSV serializer (encoding/csv, stdlib) behind theformatbranch; OSS storage (private/exports/, 48h TTL); 3 export email methods; a net-new in-app notification publish to the Qontak Unified Notification Service (/notif/v1/notifications); durable export job-status store; a server-sidefirst_10k_sortedselection mode (resolve the first 10,000 in the user's current sort order —created_atorupdated_at, chosen direction, with a deterministicidtie-breaker — avoids 10k IDs in the request body); and registering each export in the company/org-level export-history consumed bychat.qontak.com/reports/export(§6.6). - Frontend —
qontak-customer-fe: "Export Selected" button,ExportCustomerPage(forkDownloadTemplateModal.vue), XLSX/CSV format radio, GET→POST refactor for large ID sets; a "Select all first 10,000" shortcut (shown only when total ≥ 10,000; selects the first 10,000 in the current sort order — created_at/updated_at; sends a selection criterion + sort, not IDs; 10,001+ auto-disabled, no manual unselect); the export-ready in-app notification surfaces via the launchpad host notification center (customer-fe holds only a stub); a new export entry point opens a right-side export-configuration panel (per the Chat-Panel·Reports Figma) hosting the layout/field/format config + last-config persistence (§6.7, refining the full-pageExportCustomerPage); and the company/org export-history is viewed via the reusedchat.qontak.com/reports/exportpage (§6.6, EXP-S08). - Mobile —
mobile-qontak-crm: the export-ready in-app notification renders in the existing One Notification V2 center (General tab, download/upload category) when emitted; tap-through requiresclick_action=OPEN_URL+click_action_url. Gated byflag_one_notification(OFF by default). - Design — Figma for the Export configuration page, the XLSX/CSV format selector, and the email + in-app notification states.
5. Constraints
| Constraint | Value |
|---|---|
| Platform | Export is triggered on Website only (mobile cannot trigger an export). However, the export-ready in-app notification surfaces on both web and mobile — web via the launchpad host notification center, mobile via the mobile-qontak-crm One Notification V2 center. |
| Selection cap | 10,000 records maximum per export job. Enforced at both FE (selectedCustomerIds.size) and API (MaxExportRow = 10000). Mirrors existing MaxImportRow = 10000 in bulk_import_contact.go. |
| Async delivery | Export is always async: background job → file to CDP storage → email with download link + in-app notification (Unified Notification Service) + the export is registered in the company/org-level export-history page at chat.qontak.com/reports/export (reused, see §6.6). |
| Selection modes (FE payload) | Two ways to select: (a) explicit IDs — FE sends contact_ids[] (≤10,000) in the POST body; (b) "Select all first 10,000" shortcut (shown only when the list total is ≥ 10,000) — FE sends a selection criterion (selection_mode = first_10k_sorted + order_by/order_direction + current filter context), not 10,000 explicit IDs, so the backend resolves the set server-side in the user's current sort order (created_at or updated_at, chosen direction). This bounds the FE→BE payload (10,000 × 36-char UUIDs ≈ 360KB+ body) — an explicit engineering concern. The backend must apply a deterministic tie-breaker (e.g. secondary sort on id) so the first 10,000 are stable and match the rows the user saw. In shortcut mode the first 10,000 are selected and 10,001+ are auto-disabled; the user cannot manually unselect or swap a customer (shortcut only). See D-14, OQ-14. |
| Email recipient | Always the logged-in user's registered email — no override. The in-app notification targets the same triggering user. |
| Feature flag | cdp_export_customer_enabled | default: OFF — enabled per company by Ops. (Mobile in-app notification additionally depends on flag_one_notification, which is OFF by default — see "In-app notification" and OQ-12.) |
| Layout resolution | User selects a layout during the field-selection step. IsHidden fields excluded from selection. |
| Filtered export cap | Applies equally. Filter matches >10,000 → FE error: "Your filter matches {N} customers. Please narrow your filter to 10,000 or fewer." No silent truncation. |
| Permission | Guarded by existing CustomersCustomersExportKey (permission_handler.go:120). |
| Rate limiting | Export endpoint applies rate-limiting middleware (same pattern as import at rest_router.go:141) — max 5 export jobs per company per hour. Attempts beyond this limit return 429 EXPORT_RATE_LIMIT_EXCEEDED. |
| Plan scope | All Qontak One clients. |
| File format (XLSX or CSV) | User selects XLSX or CSV at export time. XLSX via excelize (go.mod). CSV via Go stdlib encoding/csv — no new dependency; net-new serializer (no CSV path exists today). The worker branches on ExportContactRequest.Format: serializer, file extension (.xlsx/.csv), and OSS content-disposition vary by format; the OSS upload + signed-URL (172800s) flow is shared. Rich type formatting (multi-select arrays, line breaks, currency — see EXP-S05) needs CSV escaping rules (OQ-11). |
| Endpoint namespace | The new export endpoint lives under /iag/v1/ (internal IAG auth) — not /api/core/v1/ (the org-scoped external path used by the existing downloadCustomer call at ListPage.vue:442). The existing FE function must be refactored from GET with query-param contact_ids[] to POST /iag/v1/customers/export with IDs in the request body — 10,000 × 36-char UUIDs in a query string would exceed the 8KB URL length limit. |
| In-app notification | On job completion the worker publishes an in-app notification to the Qontak Unified Notification Service (/notif/v1/notifications). contact-service has no notification mechanism today — net-new (publish via Kafka job_enqueuer.go:85 or a new heimdall HTTP client per iag_mekari.go; base URL/secrets via config/load.go). Notification payload should be notif_type = general, notif_category = download/upload, click_action = OPEN_URL, click_action_url = {download link} so mobile tap-through routes correctly. RFC refs: Unified Notification Service, Notification Center on Unified Component, One Notification — Mobile. |
| XLSX/CSV buffer → email handling | GenerateAndUploadExcel() returns a signed OSS URL string (excel_service.go:113), but SendEmailWithAttachment() accepts an *os.File (email_service.go:98). The export worker must: (1) write the file bytes.Buffer to a temp os.File, (2) pass it to the email method, (3) delete the temp file after email sends. This buffer-handling flow is net-new (applies to both XLSX and CSV). |
| New XLSX generation method required | A new GenerateAndUploadExcelWithData() method must be created. The existing GenerateAndUploadExcel() hardcodes private/templates/ path, bulk_upload_template_customer.xlsx ContentDisposition, and 3600s TTL — all wrong for customer data export. Export needs private/exports/{company_sso_id}/export_{timestamp}.xlsx, different ContentDisposition, and 172800s (48h) TTL. A parallel CSV serializer follows the same OSS pattern. |
| OSS file path for export | Export files stored under private/exports/{company_sso_id}/export_{timestamp}_{random}.{xlsx|csv} — not private/templates/. Enables targeted 48h cleanup and prevents PII exposure via template download flows. |
| Export job status store | Export job status must be persisted in a durable MongoDB store. Two options: (a) reuse bulk_upload_jobs collection with job_type = "export" discriminator, or (b) new export_jobs collection. Decision needed before RFC — see OQ-7. |
| Backward compat | download_template endpoint for bulk-upload templates is unchanged. Existing downloadSelectedCustomers() in ListPage.vue is refactored (GET → POST, endpoint namespace), not replaced. |
5.1 Data Lifecycle
| Artifact | Retention | Visibility |
|---|---|---|
| Export job status | 7 days | Internal (Ops + user) |
| Generated XLSX or CSV file in CDP storage | 48 hours (auto-deleted) — OSS signed URL TTL = 172800s | Download link in email + in-app notification only |
| Export audit log | 90 days | Internal / compliance |
6. New Features
All features below are net-new.
6.1 Export Button in Bulk-Actions Popover (ListTable.vue)
| Element | Before | After |
|---|---|---|
Bulk-actions popover (ListTable.vue:56-61) | Contains only "Delete selected" | Add "Export Selected" button that emits downloadSelectedCustomers (listener already wired at ListPage.vue:67) |
| Selection counter on floating component | - | Shows total selected customers count |
| "Select all first 10,000" shortcut (shown only when total ≥ 10,000) | - | Visible only when the list total is ≥ 10,000 customers. A shortcut that selects the first 10,000 customers in the current sort order — i.e. whatever the user sorted by (created_at or updated_at/edited_at, in the chosen direction). Customers ranked 10,001+ are auto-disabled (cannot be selected). Shortcut only: the user cannot manually unselect a customer or swap in another while this mode is active. FE sends a selection criterion (selection_mode = first_10k_sorted + order_by/order_direction + current filter), not 10,000 IDs — bounding the request payload (engineering concern). Requires a deterministic backend tie-breaker (e.g. secondary sort on id) so the 10,000 are stable and match the rows the user saw. See D-14, OQ-14. |
6.2 Export Configuration Page (ExportCustomerPage.vue — new)
URL: /customers/export (or full-screen modal over /customers — confirm with Figma, see OQ-8).
→ Surface update (v2.5): the configuration UI is delivered as a right-side panel opened from a new entry point (see §6.7, per the Chat-Panel·Reports Figma) — refining the full-page/modal approach described here. The field-selection contract below is unchanged.
| Section | Description |
|---|---|
| Header | Shows total selected customers count |
| Layout selector | Dropdown of org's layouts; pre-selects default. |
| Field selection | Three groups: Customer Info (id always required, created_at, updated_at), Default Fields, Custom Fields. IsHidden fields greyed out and excluded. |
| Format selection | Radio: XLSX | CSV (both selectable). format is sent in the request body. |
| Export button | On submit → "Export started — check your email at {email}." |
Reference implementation: Fork
DownloadTemplateModal.vue(DownloadTemplateModal.vue:228-236). Do not build from scratch. It already has a "File format" radio scaffold (FILE_FORMAT_OPTIONSatDownloadTemplateModal.vue:136,fileFormatref + localStorage persistence) — currently locked to XLSX only. Adding CSV = add{ label: 'CSV', value: 'CSV' }to that array and threadformatinto the export request body (the template flow currently posts onlycolumn_headers).⚠️ Field-properties API clarification:
DownloadTemplateModal.vue:132callsPOST /v1/download_templatefor field data — notGET /iag/v1/customers/field-properties. Engineering must confirm whether a standaloneGET /iag/v1/customers/field-properties?layout_id={id}endpoint exists, or whetherExportCustomerPageuses a different path. See OQ-9.
4 UI States:
- Loading: skeleton while fetching field_properties.
- Empty: "No fields available for the selected layout."
- Error: "Could not load fields. Please try again."
- Success: Banner: "Export started — check your email at {email}."
6.3 Email Notifications (3 variants)
| Variant | Subject | Content |
|---|---|---|
| Full success | "Your CDP customer export is ready" | Exported count + XLSX/CSV download link (48h expiry) |
| Partial success | "Export completed with partial success" | Success count + failed count + reason summary + download link |
| Full failure | "Export failed" | Error explanation + retry suggestion. No attachment. |
Reuse:
SendEmailWithAttachment()atemail_service.go:98-151. Add 3 new methods:SendEmailExportCustomerSucceeded,SendEmailExportCustomerPartialSucceeded,SendEmailExportCustomerFailed. Note: the method takes*os.File— worker must write the file buffer to a temp file before calling.
6.4 Persist Last Export Configuration for Customer Data Export
| Field | Detail |
|---|---|
| Before state | Users must reselect fields every time they export customer data. No persistence of previous configuration. |
| After delta | System saves the last export configuration (selected fields, format, layout, etc.) per user (or per organization), and auto-loads it on next visit. |
6.5 In-App Notification (Qontak Unified Notification Service)
| Field | Detail |
|---|---|
| What | When an export job finishes, the worker publishes an in-app notification to the Qontak Unified Notification Service (/notif/v1/notifications), in addition to the email. It appears in the user's notification center under the General tab with an unread indicator (red dot), cleared on open. Mirrors EXP-S04 AC-2/AC-3. |
| Web | Surfaced by the launchpad host shell notification center (the Unified Component). customer-fe only holds a non-functional stub (TheNotification.vue, unified-notifications-popover) — the live center is host-owned, so no customer-fe rendering work; integration is via the host + the notification API. |
| Mobile | mobile-qontak-crm already renders this service via One Notification V2 (GET /notif/v1/notifications, General/Approval tabs, download/upload category, unread + mark-as-read) — gated by flag_one_notification (OFF by default) + profile useQontakOneNotif. The notification appears for free if emitted; the only mobile gap is tap-through (must be click_action = OPEN_URL with a valid click_action_url, else no navigation). |
| Payload | notif_type = general, notif_category = download/upload, click_action = OPEN_URL, click_action_url = {48h download link}, plus title/description per success / partial / failure. |
| Backend | Net-new in contact-service (no notification mechanism today). Publish via Kafka (job_enqueuer.go:85 KafkaPublish) if the service consumes Kafka, or a new heimdall HTTP client (internal/app/api/, per iag_mekari.go) with base URL/secrets in config/load.go. Ingest mechanism = OQ-10. |
| RFC refs | Unified Notification Service · Notification Center on Unified Component · One Notification — Mobile (Qontak Chat) |
6.6 Export History (company/org level) — reuse chat.qontak.com/reports/export
The export stays async, but each completed export is also registered in the existing company/org-level export-history page at https://chat.qontak.com/reports/export (the "Quota usage / Export history" surface) — reused, not rebuilt in CDP.
| Element | Detail |
|---|---|
| Surface | Existing chat.qontak.com/reports/export history table (per company/organization). Columns: File name, Export type (e.g. "Customer Data"; mirrors the existing "Quota type"), Exporter (the user who triggered it), Export date, Status (Active → downloadable; Expired → download disabled after the 48h TTL). |
| Visibility | Company/org-level — users in the company can see the history; download depends on export options (per the page's own rule — "each exporter can download depends on export options") + the 48h link TTL. |
| Registration | On job completion the export worker registers the export (file name, type, exporter, date, download link, status) into the store backing reports/export. Keeps async — this is visibility + re-download, not a live progress tracker. |
| Reuse vs build | Reuse the existing page (owned by the Chat/Omnichannel reports surface) — confirm it supports a CDP/"Customer Data" export type + company/org listing (D-15, OQ-15). |
6.7 "Download all customers" entry point + right-side configuration panel
Figma: Chat Panel · Reports (export menu) — https://www.figma.com/design/9fUk4MHn6KriVf5YT5H1MS/%F0%9F%93%8A-Chat-Panel---Reports?node-id=1705-42216&p=f&t=TbY3yQp8RsJsdHKV-0
A new entry point — "Download all customers" — opens a right-side configuration panel docked to the customer list, so the user filters and triggers an export (top 10,000) without leaving the list. Refines §6.2's full-page/modal approach.
| Control | Detail |
|---|---|
| Info banner | "If you have more than 10,000 customers, only the top 10,000 will be downloaded." |
| File format | Radio — XLSX (CSV per §5 / §6.2). |
| Timezone (required) | Timezone selector, pre-filled with the company/user timezone (e.g. GMT+07:00 Jakarta). Export timestamps are generated in this timezone. Cannot download without it. |
| Period | Date-range filter on last update; default "All time". Presets: Today, Last 7 days, Last 30 days, Per day, Per week, Per month, Custom (calendar). Scopes which customers are included by last-updated date. |
| Source | Multi-select channel filter; default "All source". Options = all contact sources (Advertisement, Email, Event, Facebook messenger, Google My Business, Instagram DM, Instagram comment, …). Scopes customers by source. |
| Layout (required) | Layout selector that auto-fills the default layout ("Default view" — a predefined layout by Mekari Qontak) on open (tooltip explains it). Determines the field set; changing it reloads the field groups. |
| Select data to download | "N of N selected — the number of selected data affects the download duration." Collapsible groups: Customers info, Default fields, Custom fields (counts per group). IsHidden fields excluded. |
| Persist last config | Persist-last-export-configuration (§6.4 / EXP-S07) applies to the panel — pre-loads the last timezone / period / source / layout / fields / format on open; saves on download. |
| Request params | The panel sends timezone, period (last-update range or all_time), source[], layout_id, selected_fields[], format to POST /iag/v1/customers/export (top-10,000 cap applied server-side). |
| Download | Disabled until required fields (Timezone, Layout) are valid; on submit → the same async flow (job → email + in-app notification + export-history registration) as §6.1. |
| Error / retry | If the field data (layout / default / custom fields) fails to load, the data section shows "Unable to load data — please click retry to reload data" with a Retry button; Download stays disabled until it loads. |
7. API & Webhook Behavior
| # | Behavior | Entity | Triggered By | Expected Behavior | Failure Behavior |
|---|---|---|---|---|---|
| 1 | Trigger export job | export_job (new, durable store — see OQ-7) | POST /iag/v1/customers/export (new, /iag/v1/ internal IAG namespace, guarded by CustomersCustomersExportKey). Note: existing FE uses GET /api/core/v1/{org_id}/contacts/download/contact — different namespace + method. FE downloadCustomer() at ListPage.vue:436-445 must be refactored to POST /iag/v1/customers/export with IDs in request body (not query params — URL length limit). | Validate: flag ON, selection ≤10,000, user has permission, format ∈ {xlsx, csv}, selection_mode ∈ {ids, first_10k_sorted}. For ids, read contact_ids[] from the body; for first_10k_sorted, resolve the first 10,000 by the request's order_by (created_at | updated_at) + order_direction + a deterministic id tie-breaker, within the filter, server-side (no IDs in the body — bounds payload, D-14). Enqueue ExportContactJobName via gocraft/work. Return {job_id, status: "queued", email}. |
| 2 | Execute export job | Contact records + field_properties + temp file | ExportContactConsumer.ProcessExportContactJob() (new gocraft/work consumer) | Fetch contacts by contact_ids[] with field projection. Resolve custom field values. Serialize by format: XLSX via new GenerateAndUploadExcelWithData(); CSV via a new encoding/csv serializer (stdlib, same OSS pattern). Buffer handling: write bytes.Buffer → temp *os.File → pass to SendEmailExportCustomer*() → delete temp file. Upload to OSS at private/exports/{company_sso_id}/export_{ts}.{xlsx|csv} with 172800s TTL. | Partial row failure: log, continue; partial-success email + notification. Generation / storage fail: failure email + notification. Temp file write fail: abort job, failure email + notification. |
| 3 | Get export status | export_job (durable store) | GET /iag/v1/customers/export/status/{job_id} | Returns {job_id, status, total_records, success_count, failed_count} | 404 if job not found. |
| 4 | Fetch field list for layout | layout_properties | FE calls field-properties endpoint with layout_id. Note: DownloadTemplateModal.vue:132 calls POST /v1/download_template for this — engineering must confirm whether a standalone GET /iag/v1/customers/field-properties?layout_id={id} exists separately. See OQ-9. | Returns ordered fields with IsHidden flag. FE renders selectable checkboxes (IsHidden = excluded). | API unavailable: error state "Could not load fields." Export button disabled. |
| 5 | Publish export-ready in-app notification | notification record (Unified Notification Service) | ExportContactConsumer on job completion (success / partial / failure) | Net-new. Publish to the Unified Notification Service (/notif/v1/notifications) — via Kafka (KafkaPublish) or a new heimdall HTTP client — with notif_type=general, notif_category=download/upload, click_action=OPEN_URL, click_action_url={download link}, targeted at the triggering user. Surfaces in web host center + mobile One Notification V2. | Notification publish fails: log + alert; do not fail the export job (email remains the primary channel). Retry per publisher policy. |
| 6 | Register export in company/org export-history | export-history record (chat.qontak.com/reports/export store) | ExportContactConsumer on job completion | Register the export (file name, export type = "Customer Data", exporter = triggering user, export date, download link, status Active) in the store backing the existing reports/export page so it appears at company/org level. Status flips to Expired after the 48h TTL (download disabled). Keeps async — visibility + re-download only. | Registration fails: log + alert; do not fail the export job (email + in-app notification remain primary). See D-15, OQ-15. |
Export job payload (ExportContactRequest — new struct):
type ExportContactRequest struct {
SelectionMode string `json:"selection_mode"` // "ids" | "first_10k_sorted"
ContactIDs []string `json:"contact_ids"` // mode "ids": IDs in request BODY (not query params)
Filter any `json:"filter,omitempty"` // mode "first_10k_sorted": current list filter context
OrderBy string `json:"order_by,omitempty"` // mode "first_10k_sorted": "created_at" | "updated_at" (user's current sort)
OrderDirection string `json:"order_direction,omitempty"` // mode "first_10k_sorted": "asc" | "desc"; BE adds an id tie-breaker
LayoutID string `json:"layout_id"`
SelectedFields []string `json:"selected_fields"`
Format string `json:"format"` // "xlsx" | "csv"
}
8. System Flow + User Stories + ACs
8.1 System Flow
- User selects ≤10,000 customers — either manually / by filter, or (when total ≥ 10,000) via the "Select all first 10,000" shortcut that takes the first 10,000 in the current sort order (
created_at/updated_at; 10,001+ auto-disabled; no manual unselect — FE sends a selection criterion + sort, not IDs). - Clicks "Export Selected" in bulk-actions popover (
ListTable.vue). - FE navigates to
ExportCustomerPage.vue. - User selects layout, checks fields, format (XLSX or CSV) → clicks "Export".
- FE calls
POST /iag/v1/customers/export(/iag/v1/namespace; body:selection_mode+ eithercontact_ids[](modeids) orfilter+order_by/order_direction(modefirst_10k_sorted),layout_id,selected_fields[],format). - API validates → enqueues
ExportContactJobName→ returns{job_id, status: "queued"}. - FE shows: "Export started — check your email at {email}."
- Worker: fetch contacts → resolve custom fields → serialize by format (XLSX via
GenerateAndUploadExcelWithData()/ CSV viaencoding/csv) → write buffer to temp*os.File→ upload to OSS (private/exports/..., 172800s TTL) → email viaSendEmailExportCustomer*()→ publish in-app notification (Unified Notification Service) → delete temp file. - User receives email + in-app notification with download link (valid 48h); the export is also registered in the company/org export-history (
chat.qontak.com/reports/export). - Partial failure: some rows fail → continue remaining → partial-success email + notification.
- Critical failure: temp file / storage / serializer error → failure email + notification; job status =
failed.
📊 System Flow Diagram
sequenceDiagram
participant User
participant FE as customer-fe
participant API as contact-service (/iag/v1/)
participant Worker as gocraft/work consumer
participant OSS as CDP Storage (private/exports/)
participant Email as Email Service
participant Notif as Unified Notification Service
User->>FE: Select ≤10K customers + click Export
FE->>FE: Navigate to ExportCustomerPage.vue
User->>FE: Select layout, fields, format (XLSX/CSV) → click Export
FE->>API: POST /iag/v1/customers/export {contact_ids in body, layout_id, selected_fields, format}
API->>API: Validate (cap, permission, flag, rate limit, format)
alt >10K / no permission / flag OFF / rate limit
API-->>FE: 422 / 403 / 429
FE-->>User: Error message
else valid
API->>Worker: Enqueue ExportContactJobName
API-->>FE: {job_id, status: "queued"}
FE-->>User: "Export started — check your email"
Worker->>Worker: Fetch contacts + serialize (XLSX excelize / CSV encoding/csv)
Worker->>Worker: Write bytes.Buffer → temp *os.File
Worker->>OSS: Upload file (private/exports/{co_id}/..., 172800s TTL)
alt All rows success
Worker->>Email: SendEmailExportCustomerSucceeded(*os.File)
Worker->>Notif: Publish export-ready (general / download-upload / OPEN_URL + link)
Worker->>Worker: Delete temp file
Email-->>User: Email + download link (48h)
Notif-->>User: In-app notification (web host center + mobile)
else Partial failure
Worker->>Email: SendEmailExportCustomerPartialSucceeded(*os.File)
Worker->>Notif: Publish export-ready (partial)
Worker->>Worker: Delete temp file
else Critical failure
Worker->>Email: SendEmailExportCustomerFailed()
Worker->>Notif: Publish export-failed
Worker->>Worker: Delete temp file (if exists)
end
end
8.2 User Stories
| User Story | Importance | Mockup | Technical Notes | Acceptance Criteria |
|---|---|---|---|---|
| [EXP-S01] — Export manually selected customers As a Sales Ops, I want to select specific customers on the Index page and export their data to XLSX or CSV, so that I can use the data for reporting and integrations. | Must Have | Figma (Happy Path): https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=16492-601704&t=AfuhodsrnjpgdAHC-4 image-20260617-022708.png Figma (Unhappy Path): https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=19429-139509&t=oRXkulRRzet0jHwF-4 image-20260618-012856.png | Data Fields: • contact_ids[] (array, required, in request body) — source: UI selection • layout_id (string, required) — source: layout selector • selected_fields[] (array, required) — source: field checkboxes • format = "xlsx" or "csv" Before-After: Before: bulk-actions popover has only "Delete selected" (ListTable.vue:56-61); downloadSelectedCustomers() at ListPage.vue:317-328 exists but no UI trigger. FE uses GET /api/core/v1. After: "Export Selected" button added; FE calls POST /iag/v1/customers/export with IDs in request body. | — Happy Path — • AC-1: Given 1–10,000 customers selected and user has CustomersCustomersExportKey, when they click "Export Selected", then ExportCustomerPage opens with selected count displayed. • AC-2: Given user selects layout, checks fields, and clicks "Export", when POST /iag/v1/customers/export is called with IDs in request body (not query params), then system returns {job_id, status: "queued"} and FE shows "Customer download started" success message and “Your customer data is downloading. Data is generated in GMT (+07:00) timezone. Please check your email to download the file.” banner. — Error / Unhappy Path — • ERR-1: Given the system has a maximum selection limit of 10,000 customers, when the user selects customers exceeding this limit, then the system should automatically cap the selection to 10,000 customers only, and the remaining customers should be automatically unselected, and the system should display message via tooltip (e.g., "You can only select up to 10,000 customers") • ERR-2: Given user has no CustomersCustomersExportKey, when they access Customer Index, then Export button is not visible • ERR-3: Given export job fails completely (temp file / storage / excelize error), when worker catches error, then user receives email "Export failed" with retry suggestion. • ERR-4: Given feature flag cdp_export_customer_enabled is OFF, when the export endpoint is called, then API returns 403 FLAG_DISABLED and no job is created. • ERR-5: Given user has triggered 5 export jobs in the last hour, when they attempt a 6th, then API returns 429 EXPORT_RATE_LIMIT_EXCEEDED and FE shows "Too many export requests. Please wait before trying again." — Permission Model — • CAN: Users with CustomersCustomersExportKey and flag ON. • Unauthorized: API returns 403. |
| [EXP-S02] — Export from filtered result As a CRM Admin, I want to apply filters, select all filtered results (≤10,000), and export, so that I can get a targeted customer list. | Must Have | Figma: Figma (Happy Path): https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=16492-601704&t=AfuhodsrnjpgdAHC-4 image-20260617-022708.png Figma (Unhappy Path): https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=19429-139509&t=oRXkulRRzet0jHwF-4 image-20260618-012856.png | Data Fields: same as EXP-S01 (FE resolves filtered IDs client-side, passes contact_ids[] in request body) Before-After: Before: "Select all" with filter resolves IDs but export is unreachable. After: filtered IDs passed as POST body to export API. | — Happy Path — • AC-1: Given user applies filter and result count is ≤10,000, when they select all filtered customers and click "Export Selected", then ExportCustomerPage opens with filtered count displayed. • AC-2: Given user selects layout, checks fields, and clicks "Export", when POST /iag/v1/customers/export is called with IDs in request body (not query params), then system returns {job_id, status: "queued"} and FE shows "Customer download started" success message and “Your customer data is downloading. Data is generated in GMT (+07:00) timezone. Please check your email to download the file.” banner. — Error / Unhappy Path — • ERR-1: Given active filter matches >10,000 contacts and the system has a maximum selection limit of 10,000 customers, when the user selects customers exceeding this limit, then the system should automatically cap the selection to 10,000 customers only, and the remaining customers should be automatically unselected, and the system should display message via tooltip (e.g., "You can only select up to 10,000 customers") * ERR-2: Given user has no CustomersCustomersExportKey, when they access Customer Index, then Export button is not visible * ERR-3: Given export job fails completely (temp file / storage / excelize error), when worker catches error, then user receives email "Export failed" with retry suggestion. * ERR-4: Given feature flag cdp_export_customer_enabled is OFF, when the export endpoint is called, then API returns 403 FLAG_DISABLED and no job is created. * ERR-5: Given user has triggered 5 export jobs in the last hour, when they attempt a 6th, then API returns 429 EXPORT_RATE_LIMIT_EXCEEDED and FE shows "Too many export requests. Please wait before trying again." — Permission Model — • CAN: Users with CustomersCustomersExportKey and flag ON. • Unauthorized: API returns 403. |
| [EXP-S03] — Configure export fields by layout As a CRM Admin, I want to select which fields to include in my export organized by the org's layout, so that I get only relevant columns without manual cleanup. | Must Have | Figma: https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=12224-219006&t=oRXkulRRzet0jHwF-4 ExportCustomerPage (fork from DownloadTemplateModal.vue:228-236) image-20260608-090001.png | Data Fields: • Layout list: field-properties endpoint with layout_id (confirm exact path — see OQ-9) • Field groups: Customer Info (id required), Default Fields, Custom Fields • IsHidden fields excluded Before-After: Before: no field selection UI for export. After: ExportCustomerPage with layout selector + field checkboxes. | — Happy Path — • AC-1: Given user opens ExportCustomerPage, when page loads, then layout selector shows all org layouts; default layout is pre-selected. • AC-2: Given user selects a layout, when fields load, then they are grouped: Customer Info (id always checked), Default Fields, Custom Fields — ordered per layout. • AC-3: Given a field has IsHidden = true, when field list renders, then that field is greyed out and cannot be checked. • AC-4: Given user checks desired fields and clicks Export, when API is called, then selected_fields[] contains exactly the checked field keys. • AC-5: Given user returns to export later, when ExportCustomerPage opens, then last used field selection for that layout is pre-loaded from localStorage (optional). — Error / Unhappy Path — • ERR-1: Given field-properties API fails, when ExportCustomerPage loads, then error state: "Could not load fields. Please try again." Export button disabled. |
| [EXP-S04] — Receive export email with XLSX or CSV As a CRM Admin, I want to receive an email with the exported XLSX or CSV (or failure notice) so that I can use the data without staying on the page. | Must Have | Figma (Happy Path) - Email Notification: https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=19216-734654&t=oRXkulRRzet0jHwF-4 image-20250530-130605.png Figma (Happy Path) - In-App Notification: https://www.figma.com/design/9fUk4MHn6KriVf5YT5H1MS/%F0%9F%93%8A-Chat-Panel---Reports?node-id=5409-58952&t=TbY3yQp8RsJsdHKV-4 image-20260618-021135.png Figma (Unhappy Path): https://www.figma.com/design/YtCkrXaXMD63i8diuXJ4QC/Customer-Data-Platform?node-id=19216-734655&t=oRXkulRRzet0jHwF-4 image-20250604-055859.png | Data Fields: user_email, total_count, success_count, failed_count, download_url (48h expiry) Before-After: Before: no export email methods in email_service.go. After: 3 new methods reusing SendEmailWithAttachment() base (worker writes temp *os.File for attachment). | — Happy Path — • AC-1: Given export job completes with 0 failures, then user receives “Customers data export completed" with total count and XLSX / CSV download link via email * AC-2: Given export job completes with 0 failures, then user receives an in-app notification and the notification should appear under the General tab * AC-3: Given the notification is newly created, when the user has not interacted with it, then the system should display an unread indicator (e.g., red dot), and when the user clicks or opens the notification, then the notification should be marked as read, and the unread indicator should be removed — Error / Unhappy Path — • ERR-1: Given export job fails completely (excelize / storage / temp file error), then user receives "Failed to export Customers data" email with explanation and retry suggestion. No file attached. • ERR-2: Given export job completes with some failed rows, then user receives "Failed to export Customers data" email with explanation and retry suggestion. No file attached. |
| [EXP-S05] — XLSX or CSV field formatting by type As a CRM Admin, I want exported fields to be correctly formatted per field type so that I don't need to reformat the file manually. | Must Have | Figma: N/A — backend serialization | — | Scenario 1: Single Line Text Field * Given a contact has a single line text field filled * When the user exports the data * Then the field should be exported as plain text (e.g., High) Scenario 2: Multi Line Text Field * Given a contact has a multi-line text field filled with line breaks * When the user exports the data * Then the field should be exported as long text and preserve line breaks example: Had a call with the client on May 5. They are interested in upgrading to the Enterprise package. Follow-up scheduled for next Monday. Scenario 3: Dropdown Field * Given a contact has selected a value from a dropdown field * When the user exports the data * Then the field should be exported as a single text value (e.g., Qualified) Scenario 4: Multi Select Field * Given a contact has selected multiple values from a multi-select field * When the user exports the data * Then the field should be exported as an array string (e.g., ["CRM", "Chat", "Omnichannel"]) Scenario 5: URL Field * Given a contact has a valid URL entered in the field * When the user exports the data * Then the field should be exported as a valid URL string (e.g., https://www.mekari.com)/) Scenario 6: GPS Field * Given a contact’s GPS/location data is available * When the user exports the data * Then the field should be exported as a human-readable location text or coordinates (e.g., Jl. Jendral Sudirman No. 10, Jakarta or -6.2146, 106.8451) Scenario 7: File Upload / Attachment Field * Given a contact has uploaded a file * When the user exports the data * Then the field should be exported as a file URL (e.g., https://crm-media.qontak.com/property/235920398/Row_Count.png) Scenario 8: Signature Field * Given a contact has submitted a signature * When the user exports the data * Then the field should be exported as a signature image URL (e.g., https://crm-media.qontak.com/property/235920398/Signature.png) Scenario 9: Number - Numeric Field * Given a contact has entered a numeric field format * When the user exports the data * Then the field should be exported as a numeric value with number formatting if any (e.g., 100, 85.9) Scenario 10: Number - Percentage Field * Given a contact has entered a percentage field format * When the user exports the data * Then the field should be exported as a number value with number formatting if any (e.g., 55.55) Scenario 11: Number - Currency Field * Given a contact has entered a currency field format * When the user exports the data * Then the field should be exported with currency formatting (e.g., IDR 1,000,000) |
| [EXP-S06-NEG] — No export without permission, over cap, or with flag OFF (Guard Rail) As the system, when a user without export permission, with >10K selection, or with flag OFF attempts to export, the action is blocked. | Must Have | — | — | • NEG-1: User without CustomersCustomersExportKey → button hidden; API 403. • NEG-2: Feature flag OFF → API 403 FLAG_DISABLED; no job created. |
| [EXP-S07] — Persist Last Export Configuration for Customer Data Export As a user, I want the system to remember my last export configuration (selected fields, format, and layout), So that I don’t need to reconfigure the export setup every time I download customer data. | Must Have | — | — | AC-1: Save Export Configuration After Download Given a user configures export settings (file format, layout, selected fields, associations), When the user clicks Download, Then the system should: * Save the configuration as the latest export configuration * Store: + File format (XLSX / CSV) + Timezone + Layout + Selected fields: - Customer info - Default fields - Custom fields - Associations AC-2: Auto-Load Last Configuration on Page Open Given a user has previously exported customer data, When the user opens the Export Customer page, Then the system should: * Automatically pre-fill all fields based on the last saved configuration * Reflect: + Previously selected checkboxes + Selected file format + Layout & timezone AC-3: Default Configuration for First-Time Users Given a user has never exported customer data before, When the user opens the export page, Then the system should: * Load default configuration: + Default layout selected + Standard fields pre-selected * Not show any previously saved configuration AC-4: Configuration Scope (User-Level Persistence) Given multiple users exist within the same organization, When User A saves an export configuration, Then: * Only User A should see their saved configuration And: * User B should see their own configuration (or default if none exists) AC-5: Update Saved Configuration Given a user modifies the export configuration, When the user performs another export, Then the system should: * Overwrite the previous configuration with the latest selection AC-6: Handle Missing Fields (Edge Case) Given previously saved fields are no longer available (e.g., custom field deleted), When the export page is opened, Then the system should: * Ignore unavailable fields * Load only valid fields * Not break the UI * (Optional) Show subtle indicator: “Some previously selected fields are no longer available” AC-7: Reset to Default Option Given a user wants to reset configuration, When the user clicks Reset to Default, Then the system should: * Clear saved configuration * Reload default field selection AC-8: No Auto-Download Trigger Given the system loads the last configuration, When the export page is opened, Then the system should: * NOT trigger download automatically * Only prepare the configuration for user review AC-9: Consistency Across Format Change Given a user changes file format (e.g., XLSX → CSV), When the configuration is saved, Then: * Field selections should remain unchanged * Only format-related settings are updated |
| [EXP-S08] — View export history (company/org level) As a CRM Admin / Sales Ops, I want a company/org-level history of customer-data exports, so that I can re-download recent files and see who exported what — without re-running the export. | Should Have | Figma: Chat Panel · Reports (export menu) — https://www.figma.com/design/9fUk4MHn6KriVf5YT5H1MS/%F0%9F%93%8A-Chat-Panel---Reports?node-id=1705-42216&p=f&t=TbY3yQp8RsJsdHKV-0 | Reuse: chat.qontak.com/reports/export (§6.6) — not rebuilt in CDP. Data Fields: file name, export type ("Customer Data"), exporter, export date, status (Active/Expired), download link (48h). | — Happy Path — • AC-1: Given completed customer-data exports exist for the company, when the user opens the export history (chat.qontak.com/reports/export), then a company/org-level list shows each export's file name, export type ("Customer Data"), exporter, export date, and status. • AC-2: Given an export whose 48h link is still valid (status Active), when the user clicks download, then the file downloads (download visibility follows the reports page's export-options rule). • AC-3: Given an export whose 48h TTL has passed, when the user views it, then status shows Expired and download is disabled. • AC-4: Given a CDP customer-data export completes, when the worker registers it (§6.6), then it appears in reports/export with export type "Customer Data". — Error / Unhappy Path — • ERR-1: Given chat.qontak.com/reports/export does not yet support a CDP "Customer Data" export type or company/org listing, then confirm support before build (D-15 / OQ-15). — Permission Model — • CAN: users in the company per the reports page's visibility rules. |
| [EXP-S09] — Download all customers via the right-side configuration panel As a Sales Ops / CRM Admin, I want a "Download all customers" entry point that opens a right-side panel where I set timezone, period, source, and layout, so that I can download a filtered customer export (top 10,000) without leaving the list. | Must Have | Figma: Chat Panel · Reports — https://www.figma.com/design/9fUk4MHn6KriVf5YT5H1MS/%F0%9F%93%8A-Chat-Panel---Reports?node-id=1705-42216&p=f&t=TbY3yQp8RsJsdHKV-0 Right panel reuses the §6.2 field config + adds Timezone / Period / Source filters + Layout auto-fill (see §6.7), forked from DownloadTemplateModal.vue. | Data Fields: timezone (req), period (last-update range or all_time), source[], layout_id (req, defaults to the default layout), selected_fields[], format. | — Happy Path — • AC-1: Given the user is on the Customers list, when they open "Download all customers" (per Figma), then a right-side configuration panel opens without navigating away, showing the 10,000-cap info banner. • AC-2: Given the panel loads, then Timezone is required and pre-filled with the company/user timezone (e.g. GMT+07:00 Jakarta), and Layout auto-fills the default layout ("Default view") with its tooltip. • AC-3: Given Period defaults to "All time", when the user picks a preset (Today / Last 7 days / Last 30 days / Per day / week / month / Custom), then the export is scoped to customers by last-update date range. • AC-4: Given Source defaults to "All source", when the user selects one or more channels, then the export is scoped to those sources. • AC-5: Given a selected layout, then the field groups (Customers info / Default fields / Custom fields) render with counts + "N of N selected" + the duration note; IsHidden excluded. • AC-6: Given the user has exported before, when the panel opens, then it pre-loads the last timezone / period / source / layout / fields / format (persist — EXP-S07); on download it saves them. • AC-7: Given Timezone + Layout are valid, when the user clicks Download, then POST /iag/v1/customers/export runs the same async flow (job → email + in-app notification + export-history registration) as EXP-S01; otherwise Download is disabled. — Error / Unhappy Path — • ERR-1: Given the field data (layout / default / custom fields) fails to load, then the data section shows "Unable to load data — please click retry to reload data" with a Retry button, and Download stays disabled until it loads. • ERR-2: Given the user lacks CustomersCustomersExportKey, then the "Download all customers" entry point is not shown. — Permission Model — • CAN: users with CustomersCustomersExportKey and flag ON. |
🧪 Test Coverage Matrix — [EXP-S01]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ⚠️ partial | AC-1 covers 1–10K; ⚠️ QA: exactly 10,000, 0 selected, 1 selected, exactly at rate limit (5th job) |
| State transitions | ✅ defined | AC-2 (queued), AC-3 (completed), ERR-3 (failed), ERR-4 (flag OFF), ERR-5 (rate limit) |
| Data validation | ⚠️ partial | ERR-1 covers cap; ⚠️ QA: contact IDs deleted between selection and job execution; IDs from different companies |
| Concurrency | ⚠️ TBD | ⚠️ QA: 5 export jobs triggered simultaneously for same company |
| Network/timeout | ✅ defined | ERR-3 covers worker failure → email |
9. Rollout
| Field | Detail |
|---|---|
| Flag | cdp_export_customer_enabled | default: OFF |
| Stage 0 (prerequisites) | Add ExportContactJobName constant + register in worker_service.go. Add MaxExportRow = 10000. Add export REST endpoint. Add ExportContactRequest struct (incl. format). Build ExportContactConsumer. Build GenerateAndUploadExcelWithData() + a CSV serializer (encoding/csv) behind the format branch. Implement buffer→temp file→email flow. Add 3 email methods. Add export-ready notification publish to the Unified Notification Service (Kafka or new heimdall client). Refactor FE downloadCustomer GET→POST + add CSV to the format radio. Confirm OSS quota for private/exports/. No user-visible change. |
| Stage 1 — Internal QA | 2 internal test companies. Validate XLSX + CSV output, email + in-app notification (web host center; mobile with flag_one_notification ON). |
| Stage 2 — GA | Progressive per-company enable. |
10. Observability
| Event Name | Trigger | Properties |
|---|---|---|
cdp_export_customer_triggered | User submits export job | job_id, company_sso_id, user_sso_id, contact_count, field_count, layout_id, format |
cdp_export_customer_completed | Job completes (all rows) | job_id, company_sso_id, success_count, failed_count, file_size_bytes, duration_seconds, format |
cdp_export_customer_partial | Job completes with partial failures | job_id, company_sso_id, success_count, failed_count, failure_reasons[] |
cdp_export_customer_failed | Job fails completely | job_id, company_sso_id, failure_reason, error_code |
cdp_export_customer_cap_exceeded | User attempts >10K export | company_sso_id, attempted_count |
cdp_export_customer_email_sent | Export email dispatched | job_id, company_sso_id, email_type (success/partial/failed) |
cdp_export_customer_notification_sent | Export in-app notification published | job_id, company_sso_id, user_sso_id, notif_category, result (success/partial/failed) |
cdp_export_history_registered | Export registered in the company/org export-history (reports/export) | job_id, company_sso_id, user_sso_id, export_type, result |
Dashboard owner: CDP Engineering Squad. Alerts: job failure rate >5% rolling 1h → Slack #cdp-ops; email delivery failure → Slack #cdp-ops; notification publish failure → Slack #cdp-ops; job duration >10min for ≤10K → Slack #cdp-ops. Cadence: daily first 2 weeks of pilot; weekly first month of GA; monthly post-GA.
11. Success Metrics
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| ⭐ Feature adoption | % of companies (flag ON) triggering ≥1 export within 30 days | 0% | ≥60% within 30 days of GA |
| Export job success rate | completed / (completed + failed) | N/A | ≥98% |
| Email delivery rate | Emails sent / jobs completed | N/A | ≥99.5% |
| Adoption by CRM-migrating clients | % of migrating accounts using export within 14 days post-migration | N/A | ≥70% |
| Cap-exceeded rate | Cap-exceeded triggers / total export triggers | N/A | <15% |
12. Launch Plan & Stage Gates
| Stage | Audience | Duration | Success Gate | Owner |
|---|---|---|---|---|
| Stage 0 — Prerequisites | Engineering only | 1–2 sprints | All backend deliverables (endpoint, job, struct, consumer, new GenerateAndUploadExcelWithData() + CSV serializer, buffer flow, email methods, notification publish, OSS path). FE refactored (GET→POST) + CSV in format radio. ExportCustomerPage + Export button in staging. | CDP Eng |
| Stage 1 — Internal QA | 2 internal test companies | 1 week | 100% XLSX + CSV correct; all 3 email variants; in-app notification (web + mobile); 10K cap; 429 rate limit; 403 flag OFF; field formatting (11 types); 48h link expiry; OSS path private/exports/. | QA |
| Stage 2 — Pilot | 5–10 CSM-approved CRM-migrating clients | 2 weeks | ≥98% job success; zero email failures; zero critical bugs. | PM + CSM |
| Stage 3 — GA | All CDP companies (progressive) | Ongoing | ≥60% adoption in 30 days; failure rate <5%. | PM + Ops |
13. Dependencies
| Dependency | Owner | Deliverable | Blocking? |
|---|---|---|---|
ExportContactJobName constant + worker registration | CDP Backend | New constant in job_enqueuer_disabled.go; registerJob() in worker_service.go:101-122. gocraft/work v0.5.1 already in go.mod. | YES |
POST /iag/v1/customers/export endpoint | CDP Backend | New handler + service + ExportContactRequest struct (IDs in body, format field). Guarded by CustomersCustomersExportKey. Rate-limited. | YES |
| Export service layer (record fetch + field projection) | CDP Backend | Bulk-fetch contacts by IDs with selected field projection + custom field value resolution. | YES |
GenerateAndUploadExcelWithData() method (NEW) | CDP Backend | New method in excel_service.go. The existing GenerateAndUploadExcel() is template-only (empty headers, private/templates/ path, 3600s TTL). Export needs data rows, private/exports/{company_sso_id}/... path, 172800s TTL. | YES |
| CSV serializer (NEW) | CDP Backend | New CSV path using Go stdlib encoding/csv (no go.mod change). Shares the OSS upload + signed-URL flow; branches extension + content-disposition on format. CSV escaping rules for arrays/line-breaks/currency — see OQ-11. | YES |
| XLSX/CSV buffer → temp file → email flow | CDP Backend | Export worker writes bytes.Buffer → temp *os.File → SendEmailExportCustomer*() → delete temp file. SendEmailWithAttachment() at email_service.go:98 requires *os.File. | YES |
| 3 export email methods | CDP Backend | SendEmailExportCustomerSucceeded, SendEmailExportCustomerPartialSucceeded, SendEmailExportCustomerFailed in email_service.go. Reuse SendEmailWithAttachment() base. | YES |
| In-app notification publish (NEW) | CDP Backend | Publish export-ready notification to the Qontak Unified Notification Service (/notif/v1/notifications) on job completion — net-new (no notification mechanism in contact-service today). Via Kafka (KafkaPublish) or a new heimdall HTTP client (internal/app/api/, base URL/secrets in config/load.go). Payload: notif_type=general, notif_category=download/upload, click_action=OPEN_URL, click_action_url={download link}. Ingest mechanism = OQ-10. | YES |
| Web notification center (host-owned) | Launchpad / Host shell | The Unified Notification Center on web is owned by the launchpad host shell (customer-fe holds only the TheNotification.vue stub). Confirm CDP export notifications render in the host's General tab with unread indicator (EXP-S04 AC-2/AC-3). | YES (confirm) |
| Mobile notification (One Notification V2) | Mobile (mobile-qontak-crm) | Export-ready notif renders for free in the existing One Notification V2 center (/notif/v1/notifications, General tab, download/upload category) if emitted. Gated by flag_one_notification (OFF by default) + profile useQontakOneNotif. Tap-through gap: needs click_action=OPEN_URL + valid click_action_url, else no navigation; or a CDP origin→route mapping. | YES (confirm) |
FE downloadCustomer refactor (GET → POST) + CSV format | CDP FE | Refactor downloadCustomer() at ListPage.vue:436-445 from GET /api/core/v1/... to POST /iag/v1/customers/export with IDs in body; add { label: 'CSV', value: 'CSV' } to FILE_FORMAT_OPTIONS and send format. | YES |
ExportCustomerPage.vue (FE) | CDP FE | New page/modal. Fork DownloadTemplateModal.vue:228-236 (format radio scaffold already present). Add layout selector, field groups, format picker (XLSX/CSV), submit flow. | YES |
Export button in ListTable.vue | CDP FE | Add "Export Selected" to bulk-actions popover (ListTable.vue:56-61). | YES |
| 10K cap enforcement (FE) | CDP FE | Cap selectedCustomerIds Set; disable export if filtered >10K; inline error message. | YES |
| Field-properties API for ExportCustomerPage | CDP Backend | Confirm whether GET /iag/v1/customers/field-properties?layout_id={id} exists separately from POST /v1/download_template. See OQ-9. | YES (confirm) |
| CDP Storage (file upload + quota) | CDP Infra | OSS already used for bulk import. Confirm quota for export volumes (~5–15MB per file) and 48h retention at private/exports/. | YES |
Company/org export-history page (chat.qontak.com/reports/export) | Chat/Omnichannel (reports surface) | Confirm the existing export-history page can register + list a CDP/"Customer Data" export type at company/org level (File name, type, Exporter, date, Status, Download). Reuse — do not rebuild (D-15). See OQ-15. | YES (confirm) |
Server-side first_10k_sorted selection mode | CDP Backend | Resolve the first 10,000 contacts in the user's current sort order (order_by = created_at|updated_at, order_direction) within the current filter when selection_mode = first_10k_sorted, with a deterministic id tie-breaker for stability (avoids 10k IDs in the body). Confirm the ordering fields + tie-breaker + filter passing. See OQ-14. | YES |
14. Key Decisions + Alternatives Rejected
14a — Decisions Made
Decisions dated 2026-06-03; v2.2 additions dated 2026-06-18 (grounded code review).
| ID | Decision | Rationale (grounded) |
|---|---|---|
| D-1 | XLSX and CSV in v1. | v2.2. encoding/csv is Go stdlib — no new go.mod dependency. No CSV path exists today, but it slots into the existing excelize/OSS flow, branching only the serializer, extension, and content-disposition on format. Low marginal cost vs the value of CSV for downstream integrations. |
| D-2 | Async export via gocraft/work; no synchronous download. | 10,000 records is too slow for synchronous HTTP. gocraft/work is already in use. |
| D-3 | Email recipient = always logged-in user's email; no override. | Simplest auth-safe approach. Existing FE pattern preserved. |
| D-4 | FE resolves contact IDs client-side for filtered export. | Simpler v1 implementation. 10K cap bounds the payload. Server-side filter re-execution deferred to v2. |
| D-5 | Layout selector determines available fields; IsHidden fields excluded. | Respects org layout configuration; prevents exporting hidden/internal fields. |
| D-6 | Fork DownloadTemplateModal.vue for field selection UI. | Already implements Default/Custom field grouping and a File-format radio scaffold (FILE_FORMAT_OPTIONS, DownloadTemplateModal.vue:136). Saves significant FE effort. |
| D-7 | MaxExportRow = 10000 enforced at both API and FE. | Mirrors MaxImportRow = 10000 in bulk_import_contact.go. Defense in depth. |
| D-8 | Save last field selection to new API Endpoint. | Needs a new API endpoint to persist per-user config (EXP-S07). |
| D-9 | Rate limiting: max 5 export jobs per company per hour. | Mirrors import rate limiting pattern (rest_router.go:141). |
| D-10 | Create new GenerateAndUploadExcelWithData() method; do NOT extend existing GenerateAndUploadExcel(). | The existing method hardcodes template-specific assumptions (path, ContentDisposition, 3600s TTL). Extending creates conflicting responsibilities and risks breaking template generation. |
| D-11 | Worker writes file buffer to temp *os.File, passes it to email method, then deletes temp file. | SendEmailWithAttachment() at email_service.go:98 requires *os.File — not a URL. Writing to a temp file is lower-risk and consistent with the existing pattern. |
| D-12 | Deliver an in-app notification via the Qontak Unified Notification Service, in addition to email. | v2.2. Mobile already renders this service (One Notification V2: General tab + download/upload category); web is served by the launchpad host notification center. contact-service has no notification mechanism today, so publishing is net-new — reuse Kafka or a heimdall HTTP client. Email stays the primary, always-on channel; notification is additive. |
| D-13 | In-app notification uses notif_type=general, notif_category=download/upload, click_action=OPEN_URL with the 48h download link. | v2.2. Grounded in mobile One Notification V2: the download/upload category exists, and tap-through only routes OPEN_URL (or known origins). Using OPEN_URL + click_action_url makes the notification actionable on mobile without a new origin/route mapping. |
| D-14 | The "Select all first 10,000" shortcut (shown only when total ≥ 10,000) sends a server-side selection criterion, not 10,000 IDs. In this mode the first 10,000 in the user's current sort order (created_at or updated_at, chosen direction) are selected, 10,001+ are auto-disabled, and the user cannot manually unselect/swap (shortcut only). | Engineering raised that a 10,000-ID body (10,000 × 36-char UUID ≈ 360KB+) is a heavy FE→BE payload. Sending selection_mode = first_10k_sorted + order_by/order_direction + the current filter lets the backend resolve the set, bounding the payload. The backend needs a deterministic id tie-breaker so the set is stable and matches the rows the user saw. The trade-off (no manual unselect in this mode) is acceptable for a bulk shortcut; explicit-ID mode remains for manual/filtered selections. |
| D-15 | v2.3. Reuse the existing chat.qontak.com/reports/export page for company/org-level export history + re-download — do not build a CDP-local export history. | The reports/export "Quota usage / Export history" surface already lists exports per company (File name, type, Exporter, date, Status, Download with expiry). Registering CDP exports there gives company-level visibility + re-download for free and keeps the async model; building a second history surface in CDP would fork the experience. |
14b — Alternatives Rejected
Dated 2026-06-03; v2.2 additions 2026-06-18.
| Alternative | Why Rejected |
|---|---|
| Synchronous XLSX/CSV download | 10K records × field resolution + rendering takes 10–60s; HTTP timeout risk. |
| Server-side filter re-execution for export | More complex for v1. 10K cap bounds the client-side approach. Deferred to v2. |
| Build field selection / format UI from scratch | DownloadTemplateModal.vue:228-236 already implements the field-group UX and a format radio. Fork saves effort. |
| Email to any recipient | Adds auth complexity. Logged-in user is always the correct recipient. Configurable is v2. |
Extend GenerateAndUploadExcel() for export data | Template-specific hardcoded assumptions would conflict with export needs. New method is cleaner. |
Pass OSS signed URL to SendEmailWithAttachment() | Method requires *os.File (email_service.go:98). Changing signature would break existing import email calls. |
Store export files under private/templates/ | Templates are permanent; export files contain PII and must expire in 48h. Sharing paths makes targeted cleanup impossible. |
| Add a third-party CSV library | Unnecessary — Go stdlib encoding/csv covers it with no new dependency. |
| Build the in-app notification center in customer-fe | The Unified Notification Center is host-owned (launchpad MFE); customer-fe's TheNotification.vue is only a stub. Building a second center would fork the surface. Integrate with the host + publish to the shared notification service instead. |
| In-app notification only (drop email) | Email is the always-on, auditable channel and works regardless of flag_one_notification (OFF by default on mobile). Notification is additive, not a replacement. |
| Send all 10,000 contact IDs in the export request body (shortcut) | A 10,000-ID body (~360KB+) is a heavy FE→BE payload (engineering concern). The first_10k_sorted shortcut sends a selection criterion the backend resolves instead (D-14). Explicit-ID mode stays for ≤10,000 manual/filtered selections. |
| Build a CDP-local export-history / download center | chat.qontak.com/reports/export already provides company/org-level export history + re-download with expiry. Reusing it (D-15) avoids forking the surface; a new one is unjustified for v1. |
15. Open Questions
| # | Type | Question | Mitigation / Default | Owner | Deadline |
|---|---|---|---|---|---|
| OQ-1 | Risk | What happens if a contact ID no longer exists at job execution time? | Default: skip the deleted record; count as "failed row" in partial-success email. No halt. | CDP Eng | 2026-06-25 |
| OQ-2 | Decision | For partial-success email — expose full technical errors or user-friendly summaries? | Default: user-friendly summary only. Log full error internally. | PM + CDP Eng | 2026-06-25 |
| OQ-3 | Open Question | XLSX/CSV header row: field display names (NameAlias) or internal field keys? | Default: display names from layout_properties/base.go NameAlias field. | CDP Eng | 2026-06-25 |
| OQ-4 | Risk | Estimated file size for 10K contacts × 30 fields — confirm CDP storage quota and 48h retention capacity. | Estimate: ~5–15MB per export. Confirm quota before Stage 0. | CDP Infra | 2026-06-25 |
| OQ-5 | Decision | Should user be able to trigger a new export while a previous one is still in progress? | Default: allow (each job has its own job_id). Rate limit (5/hour) is the backstop. | CDP Eng | 2026-06-25 |
| OQ-6 | Open Question | "Save last field selection" — persist per layout or globally? | Default: per layout. localStorage key: export_field_config_{layout_id}. | CDP FE | 2026-06-25 |
| OQ-7 | Decision | Export job status store: reuse bulk_upload_jobs collection (add job_type = "export" discriminator) or create a new export_jobs collection? | Default: reuse bulk_upload_jobs with job_type discriminator. Confirm with backend lead before RFC. | CDP Backend | 2026-06-25 |
| OQ-8 | Open Question | Should ExportCustomerPage be a dedicated route (/customers/export) or a full-screen modal over /customers? | Default: full-screen modal (consistent with bulk-upload template selection). Confirm with Figma/UX. | CDP FE + Figma | 2026-06-25 |
| OQ-9 | Open Question | Does a standalone GET /iag/v1/customers/field-properties?layout_id={id} endpoint exist separately from POST /v1/download_template? | DownloadTemplateModal.vue:132 uses POST — confirm the correct path for ExportCustomerPage. | CDP Backend | 2026-06-25 |
| OQ-10 | Decision | Does the Unified Notification Service ingest via Kafka event or synchronous HTTP (/notif/v1/notifications)? Determines whether contact-service reuses KafkaPublish or adds a new heimdall HTTP client. | Default: confirm with the One Notification team; lean to the same channel mobile/chat already use. | CDP Backend + Notif team | 2026-06-25 |
| OQ-11 | Open Question | CSV field formatting (EXP-S05): how to represent multi-select arrays, multi-line text (line breaks), and currency in CSV (which has no cell typing)? | Default: RFC-4180 quoting; arrays as "[\"a\",\"b\"]" or comma-joined in a quoted cell; preserve line breaks inside quotes; currency as a plain formatted string. Confirm in RFC. | CDP Eng | 2026-06-25 |
| OQ-12 | Risk | Mobile in-app notification depends on flag_one_notification (OFF by default) + profile useQontakOneNotif. Is mobile in-app notif gated until One Notification GA, with email as the guaranteed channel meanwhile? | Default: yes — email is always-on; mobile in-app notif lights up as One Notification rolls out. | PM + Mobile | 2026-06-25 |
| OQ-13 | Open Question | The download link expires after 48h. What does tapping/opening an expired notification show? | Default: route to a "link expired — re-export" message; do not 404. Confirm UX. | PM + CDP Eng | 2026-06-25 |
| OQ-14 | Risk | For first_10k_sorted: the set must match the rows the user saw, in their current sort (created_at or updated_at, chosen direction). The list has no default sort and no tie-breaker on the FE (order_by/order_direction are sent only when a column is actively sorted — grounded in ListPage.vue), so a stable "first 10,000" needs a backend deterministic tie-breaker (e.g. secondary sort on id) plus a defined behavior when no column is sorted. | Default: pass order_by/order_direction + active filter; backend adds an id tie-breaker; if no sort is active, default to created_at desc. Confirm in RFC. | CDP Backend + FE | 2026-06-25 |
| OQ-15 | Open Question | Does chat.qontak.com/reports/export support registering/listing a CDP "Customer Data" export type at company/org level, and what are its auth/visibility rules (who can download — "depends on export options")? Who owns that surface? | Confirm with the Chat/Omnichannel reports owner; reuse the existing schema/type if possible (D-15). | PM + Chat/Omni squad | 2026-06-25 |
Appendix A — Grounded Code References
contact-service (CDP, Go/MongoDB)
- Export permission constant (exists, unused):
CustomersCustomersExportKey—permission_handler.go:120(alsointernal/pkg/consts/const.go:28). - No export endpoint, handler, job, or payload: grep for
/export,ExportRequest,ExportContactJob→ no matches. - Worker pattern (reusable):
gocraft/work v0.5.1ingo.mod; job constants injob_enqueuer_disabled.go;registerJob()atworker_service.go:101-122. - XLSX infrastructure (partial reuse — new method required):
excelizeingo.mod:35;excel_service.go:35-141GenerateAndUploadExcel()writes headers only, hardcodesprivate/templates/path,bulk_upload_template_customer.xlsxContentDisposition, and 3600s TTL (excel_service.go:113). A newGenerateAndUploadExcelWithData()is required — different path, ContentDisposition, 172800s TTL. - CSV (net-new, stdlib):
encoding/csvis not used anywhere in the repo (grep → 0) and is Go stdlib — nogo.modchange. The OSS upload + signed-URL flow (excel_service.go:95-112) is format-agnostic; only serialization, file extension, and content-disposition branch onformat. - Email base method (reusable — requires
*os.File):SendEmailWithAttachment(ctx, toEmail, subject, plainText, html, *os.File, fileName)at email_service.go:98-151. Worker must write the filebytes.Buffer→ temp*os.Filebefore calling. - In-app notification (net-new): no notification-publish mechanism exists (
Notifyhits are OS-signal handlers only). Reusable plumbing: Kafka publisherJobEnqueuer.KafkaPublish(job_enqueuer.go:85, used byPublishContactEvent), and the heimdallhttpclient.Clientexternal-call pattern (internal/app/api/iag_mekari.goNewIagClient). Base URL/secrets viaconfig/load.go(getStringOrPanic/getString). - Rate limiting pattern (reusable):
rest_router.go:141— apply same to export endpoint. - Cap constant pattern:
MaxImportRow = 10000inbulk_import_contact.go→ defineMaxExportRow = 10000.
qontak-customer-fe (Nuxt 3)
- Invisible export entry point:
downloadSelectedCustomers()atListPage.vue:317-328; listener atListPage.vue:67; no button inListTable.vue:56-61emits it. - Existing FE download uses GET + query params:
downloadCustomer()atListPage.vue:436-445callsGET /api/core/v1/{org_id}/contacts/download/contactwithcontact_ids[]as query params. Must be refactored to POST /iag/v1/customers/export with IDs in request body. - Format radio already scaffolded:
DownloadTemplateModal.vuehas a "File format"MpRadiogroup bound toFILE_FORMAT_OPTIONS(:136, currently[{XLSX}]) +fileFormatref + localStorage persistence — butfileFormatis not yet sent to the backend (handleDownloadTemplateposts onlycolumn_headers). Add CSV + threadformatinto the body. - No field selection for export:
DownloadTemplateModal.vueis the reference to fork (:228-236). Note: it callsPOST /v1/download_templateat line 132 — notGET /field-properties. - Web notification center is a STUB / host-owned:
layouts/TheNavbar/TheNotification.vueis a hardcoded empty popover (notifications = ref([]), "Inbox" tab only, no API, no unread logic, no "General" tab). The real Unified Notification Center is the launchpad host shell — customer-fe runs as a micro-frontend (__mfeHostPinia,__mfePixelToast,unified-notifications-popover,MFE_CONTACT_HOST,MEKARI_LAUNCHPAD_URL). Integrate with the host; do not extend the stub. (toastNotifyinutils/toast.tsis an ephemeral toast, not the center.) - localStorage pattern (reusable):
ListPage.vue:173, 251— same pattern forexport_field_config_{layout_id}.
mobile-qontak-crm (Flutter)
- One Notification V2 center EXISTS:
crm_miscpackage —presentation/screens/notification_v2/notification_v2_screen.dartwith General/Approval tabs (notif_typegeneral=1) and sub-categories incl. download/upload (5) (util/notif_category.dart), unread filter + badges + mark-as-read. - Unified service endpoints:
config/constant/endpoint/unified_notification_endpoint.dart—GET /notif/v1/notifications,/unread/count,PUT /mark_all_as_read,PUT /mark_as_read/{id}. Modelunified_notification_responsecarriesorigin,notif_type,notif_category,click_action(OPEN_URL/OPEN_APP),click_action_url,read_at. - Flag-gated:
flag_one_notification(feature_flag_constant.dart:78-82, default OFF) + profileuseQontakOneNotif(bottom_navigation_screen_mixin.dart:64-68). - Tap-through gap:
notification_item_v2_mixin.dart:44-124routes only known origins orOPEN_URL+ validclick_action_url; otherwise shows a "cannot redirect" toast. No CDP/Contact360 origin/route mapping exists today (grep contact360/cdp/exportin notif layer → ABSENT).
PRD CHANGELOG
| Version | Date | By | Section | Type | Summary |
|---|---|---|---|---|---|
| 2.6 | 2026-06-24 | Review fixes — panel breakdown + In-App Figma | S6.7, S8.2 (EXP-S09), EXP-S04 (Figma) | UPDATED | Per design review of the "Download all customers" panel: broke down §6.7 + EXP-S09 to the real panel controls — Timezone (required; GMT+07:00 default), Period (last-update date-range presets + Custom), Source (multi-select channels), Layout auto-fill (default "Default view" + tooltip), the field groups (Customers info / Default / Custom + "N of N selected" + duration note), and the field-load error → Retry edge case ("Unable to load data"). Repointed the EXP-S04 In-App Notification Figma link → Chat-Panel·Reports node 5409-58952. (On Confluence, also moved the EXP-S08/EXP-S09 narrative from the Mockup column to the User Story column.) |
| 2.5 | 2026-06-24 | New scope — export menu/panel + history story | Scope Changes, S6.2 (note), S6.7 (new), S8.2 (EXP-S08, EXP-S09) | UPDATED | Added new scope per the Chat-Panel · Reports Figma (node 1705-42216): (1) new entry point + right-side export-configuration panel (new §6.7; §6.2 noted as refined to a panel) hosting the layout/field/format config + last-config persistence (EXP-S07 surfaced on the panel). (2) EXP-S08 — View export history (company/org level, reuse chat.qontak.com/reports/export, §6.6) — previously only in §6 New Features, now a user story. (3) EXP-S09 — new-entry-point + right-panel export story. Existing EXP-S01–S07 unchanged; Scope Changes Frontend bullet extended. |
| 2.4 | 2026-06-22 | Shortcut tweak (grounded) | intro, S4, S5, S6.1, S7 (+struct), S8.1, S13, S14, S15 | UPDATED | Tweaked the "Select all first 10,000" shortcut to follow the user's current sort instead of a fixed created_at desc: it now takes the first 10,000 in the active order_by (created_at or updated_at/edited_at) + order_direction. Renamed mode first_10k_recent → first_10k_sorted; added order_by/order_direction to ExportContactRequest. Made the ≥10,000 visibility gate explicit (button shown only when the list total ≥ 10,000). Added the deterministic id tie-breaker requirement (stable "first 10,000") + a no-active-sort default — grounded in qontak-customer-fe (ListPage.vue: created_at/updated_at sortable, single-column, no FE default sort/tie-breaker, MAX_SELECTION=500, button gate via pagination.total). Updated §5 selection-modes, §6.1 row, §7 #1 + payload struct, §8.1 flow, D-14, dependency, OQ-14 (now a Risk). Payload concern unchanged (criterion, not 10k IDs). §8.2 User Stories unchanged (final). |
| 2.3 | 2026-06-22 | Behavior adjustments | S4, S5, Scope Changes, S6.1, S6.6 (new), S7, S8.1, S10, S13, S14, S15 | UPDATED | Two adjustments. (1) "Select all first 10,000 (recently created)" shortcut: selects the first 10k by created_at desc; 10,001+ auto-disabled; no manual unselect/swap (shortcut only). FE sends a selection criterion (selection_mode = first_10k_sorted + filter), not 10k IDs — bounding the request payload (engineering concern) — §6.1 row, §5 Selection-modes constraint, §7 #1 + ExportContactRequest (selection_mode/filter), D-14, OQ-14, rejected "send 10k IDs". (2) Company/org export history (reuse chat.qontak.com/reports/export): keep async, but register each export in the existing reports/export history page (File name, type, Exporter, date, Status incl. Expired, Download) for company-level visibility + re-download — new §6.6, §7 #6, Non-Goal #4 revised, §5 async-delivery row, D-15, OQ-15, dependency, cdp_export_history_registered event, rejected "CDP-local history center". Section 8.2 User Stories unchanged (final). |
| 2.2 | 2026-06-18 | CSV format + in-app notification (grounded) | S1, S4, Scope Changes, S5, S6.2, S6.5 (new), S7, S8.1, S9, S10, S13, S14, S15, App.A | UPDATED | XLSX or CSV export — encoding/csv is Go stdlib (no new dep), branches on format (D-1 rationale filled; S5 file-format constraint; S6.2 radio XLSX|CSV; S7 #2 serializer branch + payload "xlsx"|"csv"; CSV escaping OQ-11). In-app notification via the Qontak Unified Notification Service alongside email (new S6.5; S7 behavior #5; D-12/D-13; constraint + Platform updated for web host center + mobile One Notification V2; Non-Goals #1/#4/#5 reconciled; observability cdp_export_customer_notification_sent; dependencies for backend publish + host web center + mobile; OQ-10/12/13; Appendix grounding for contact-service net-new publish, customer-fe host-owned stub, mobile One Notification V2). Added ## Scope Changes section + scope_changes: [Backend, Frontend, Mobile, Design]. 8.1 diagram adds the notification step. Section 8.2 User Stories unchanged (final). |
| 2.1 | 2026-06-03 | Score fixes (9 grounded gaps) | S1b, S4, S6, S7, S8, S13, S14, S15 | UPDATED | CANNOT reversibility added to EXP-S01/S02 Permission Model (Q2 fix). ERR-4 (flag OFF 403) + ERR-5 (rate limit 429) added to EXP-S01. Constraints: endpoint namespace /iag/v1/ added; GET→POST refactoring documented; XLSX buffer→temp file→email flow specified; new GenerateAndUploadExcelWithData() method requirement; OSS path private/exports/{co_id}/...; OSS TTL 172800s (48h); plan scope; export job status store spec. S6: /customers/export URL path; field-properties endpoint clarification. S7: B1/B2/B4 fixed. S8 diagram: /iag/v1/ namespace + buffer→OSS→email flow. Dependencies + Key Decisions (D-10/D-11) + OQ-7/8/9 added. |
| 2.0 | 2026-06-03 | Grounded rewrite | All | REWRITE | Full rewrite grounded in contact-service and qontak-customer-fe. |
| 1.0 | 2026-05-28 | (prior) | All | CREATED | Original PRD (superseded). |