RFC Review: Export Customer Data with Layout (XLSX/CSV, async, email + in-app notification)
Companion review for
rfc-export-customer-data.md, produced by therfc-reviewerskill. Current cycle: R2 — 2026-06-30, against RFClast_updated: 2026-06-30(PRD v2.6 scope). The body below reflects the R2 (current) state; the Findings Ledger and Review History carry the full audit trail from R1 (2026-06-18). R1 reviewed the PRD-v2.2-era RFC and surfaced 10 findings (6 fixed same-day); R2 re-scores the post-fix RFC plus the v2.6 additions (first_10k_sortedselection mode, EXP-S08 export-history reuse, EXP-S09 right-side panel) and the 2026-06-30 banner-copy fix.
Executive Summary
- Overall Score:
8.5/10(was8.0at R1 — pre-fix snapshot) - Rating:
Agentic-Ready - RFC Type:
full-stack - Sub-Type:
new-feature(FE: new-feature · BE: new-feature; Mobile is render-only / flag-gated) - Assessment Confidence:
High - Applied Caps/Gates: None triggered. NFS
7.0is not the performance sub-type so the NFS<4.0 cap is inert; no category < 5.0; ACV9.0, DIC8.5, SAS8.5, CDG8.0, FMC8.5, ROL8.5all clear their floors; no cross-layer contract mismatch (the single §2.Gpartialonfield_propertiesis resolved by an in-page FE mapping); fewer than 4 categories below 5.0. The9.0+tier is unreachable only because NFS7.0< 7.5 (no FE Core-Web-Vitals budget) — every other gate for 9.0 is met (ACV/DIC/FMC/CNT ≥ 8.5; zero dangling decisions; no mismatch). - Agentic Readiness Verdict:
PROCEED— the core pipeline (EXP-S01–S07, S09) is agent-executable now. One story is specified-but-not-buildable: EXP-S08 (export-history reuse) is blocked on a net-new cross-team path (OQ-17); it ships behind a non-fatalExportHistoryRegistrarinterface and is a PRD Should-Have, so it does not gate the core release. Confirm OQ-10 (notif channel) before chunk 7, OQ-20 (timezone value format) before chunks 18/21, and OQ-21 (source enum) before chunk 21 — all carry adopted, reversible defaults. - RFC Author: Zhelia Alifa | Reviewed (R2): 2026-06-30 | R1: 2026-06-18
The R1 blocker and majors are now closed: the rate-limit contract (REV-5/OQ-14) that capped R1 has moved from Dangling to a decisive v1 design — extend IRateLimiterService + RateLimitMiddleware with an additive per-route (key, max, window) param (("export", 5, 3600)), grounded against the real middleware (rate_limit_middleware.go:16-55, which today takes only the service, keys on companySsoID, and hardcodes limit=1 + an "import" message), with a documented fallback (inherit the shared limiter + amend PRD §5). The custom-field value resolution (REV-2) is now a concrete algorithm (§2.4 — SearchWithFilters + _id $in + pagination; default field → top-level struct, custom → CustomFields[].Key). The success-banner copy (REV-11) now carries the PRD AC-2 GMT-timezone sentence. Decision discipline expanded from 13 → 17 ADR decisions (Decisions 9–11, 14–17 added for first_10k_sorted, export-history, and the panel) with no dangling decisions remaining. The remaining work is honest cross-team / confirm-item surface introduced by v2.6 — none of it dangling, all chunk-scoped.
R2 Delta vs R1
| Class | Findings | Detail |
|---|---|---|
| Confirmed fixed (carried) | REV-1, REV-2, REV-3, REV-4, REV-6 | The 5 R1 same-day fixes are present and intact in the 2026-06-30 RFC (EXP-S05 Decision-6 table; contact fetch/field-resolution algorithm; FE /v1 base-path rule; field_properties {data,pagination} envelope; chunk-1 file naming). |
| Newly resolved in R2 | REV-5, REV-11 | REV-5 rate-limit upgraded from "fixed-as-decision" to a decisive, grounded v1 design with a fallback (OQ-14, chunk 9). REV-11 success-banner GMT-timezone copy added (§2.C/§3.C; 2026-06-30 edit). |
| Still open (carried) | REV-7, REV-8, REV-9, REV-10 | EXP-S07 durable persistence (OQ-15, PM sign-off); NotificationPublisher signature + notif timeout/retry (OQ-10/REV-8); OSS cross-border residency (OQ-16); OSS quota (OQ-4, infra). None regressed; none newly closed. |
| Newly found in R2 (v2.6 scope) | REV-12, REV-13, REV-14, REV-15, REV-16, REV-17 | EXP-S08 not buildable / cross-team (OQ-17, major); first_10k_sorted _id tie-breaker + get-by-IDs batch net-new (OQ-18, minor); FE MAX_SELECTION=500 vs 10K export cap (OQ-19, minor); EXP-S09 timezone value format ambiguity + net-new BE tz formatting (OQ-20, major); source enum gap Advertisement/Email (OQ-21, minor); 2 edited mermaid blocks not machine-validated post-edit (minor). |
| Score movement | 8.0 → 8.5 | Two R1 majors/blocker closed + banner fixed + 4 new grounded ADRs + zero dangling decisions, net of broadened (but defaulted, chunk-scoped) v2.6 confirm-surface. |
Mermaid validity: 10 / 10 blocks pass a rule-based pitfall scan (no ; in sequence text; no </>/>= in stateDiagram-v2 transition labels; the only slash-in-[…] cases are a mid-label slash and a quoted parallelogram — both safe). The automated mmdc parser could not run in this sandbox (Chromium launch blocked — exit 1, empty stderr); 8 blocks were machine-validated at R1 and the 2 edited blocks (branch/skip flowchart + happy-path sequence) pass the manual scan — a mmdc/Docusaurus render is the pre-merge TODO the RFC comment log already records (REV-17).
Quick Verdict
Why this RFC can be implemented agentically:
- §4.D Agent Execution Plan now gives 21 ordered chunks with exact files, repo-sourced commands (
make test,pnpm test -- features/customers/export), and a verifiable "done when" per chunk; §2.4 + §3.B pin the full API + error contract (now incl.EXPORT_SELECTION_MODE_INVALIDand thelayouts/defaultreuse); §2.D/§2.E/§2.F pin transactions, collisions, retry/DLQ/idempotency. - Decisions are closed, not deferred: Detail 1.B lists 17 decisions each with chosen option, rejected alternatives, and reversibility; the v2.6 additions (Decision 9
first_10k_sorted, Decision 10 export-history, Decision 11 panel) are grounded againstSearchContacts/SortBy,hub-chat/IAG billing, and the existingExportCustomerDrawer.vue. - The two R1 implementability gaps (rate-limit mechanism, custom-field resolution) are now specified; an agent can build chunks 1–9 (BE) and 11–16 (FE) without a clarification meeting.
Why this RFC will still cause agent guessing or rework:
- EXP-S08 (export-history) is not buildable today (OQ-17): the
chat.qontak.com/reports/exportsurface ishub-chat→ IAG billing (/report/v1/billings/logs/export), billing-only quota types, no CDP "Customer Data" type and no service-to-service register path. The RFC isolates this behind a non-fatal interface and gates chunk 19, but an agent cannot complete EXP-S08 until billing/IAG + Chat/Omni deliver the path. - EXP-S09 timezone formatting (OQ-20) is under-pinned for the implementer: the
ExportContactRequest.Timezoneexample carries a label string ("(GMT+07:00) Asia/Jakarta") but Go'stime.LoadLocationneeds an IANA id ("Asia/Jakarta"); the BE timezone-aware timestamp formatter is "net-new" with no parse contract. An agent building chunk 18/21 would guess the parse + which cells are tz-rendered.
Findings Ledger (carry-forward)
Stable, never-renumbered ids (
REV-n). Cycles: R1 = 2026-06-18 (PRD-v2.2-era RFC), R2 = 2026-06-30 (PRD-v2.6 RFC). "Was this captured before?" = the id exists here; "did we improve?" =open → fixedtransitions across cycles.
| ID | Severity | Finding (one line) | RFC location | Status | First seen | Resolved in | Evidence / fix |
|---|---|---|---|---|---|---|---|
| REV-1 | blocker | EXP-S05 per-type formatting unimplementable: no per-scenario table; number/currency/percentage all share field_type="number"; date omitted | Decision 6, §2.1 ER | fixed | R1 | R1 | Decision 6 formatting table (10 repo types + number subtypes + date + default → XLSX/CSV); type source corrected to field_properties.Validation.Number.NumberType + CustomField.{Value,CurrencyCode}. R2: intact. |
| REV-2 | major | Custom-field value resolution + bulk-fetch by IDs unspecified (no GetByIDs; default-vs-custom split missing) | §2.4 fetch algorithm, §2.H, chunk 8/9 | fixed | R1 | R1 | §2.4 "Contact fetch & field-resolution algorithm" (SearchWithFilters + _id $in + pagination; default → top-level struct, custom → CustomFields[].Key). R2: intact + extended for selection_mode. |
| REV-3 | major | FE export path double-prefixes /iag → 404 | §1 Assumptions, §2.B, §2.H | fixed | R1 | R1 | FE paths use /v1/contacts/export + /v1/contacts/field_properties; backend stays /iag/v1/...; §2.B base-path rule. R2: intact. |
| REV-4 | major | field_properties response shape wrong (key/group don't exist) | §2.4 row 3, §2.G | fixed | R1 | R1 | {data,pagination} envelope of FieldPropertiesSerializer; name (not key); group derived from is_default/type; FE pages. R2: intact. |
| REV-5 | major | Rate-limit "5/company/hour" unenforceable: middleware is one global per-company window (default 1/60s) shared with import | §3 Security, OQ-14, chunk 9, §4.B | fixed (decision) | R1 | R2 | R2 upgrade: OQ-14 is now a decisive v1 design grounded in rate_limit_middleware.go:16-55 — extend IRateLimiterService+middleware with an additive per-route (key,max,window) = ("export",5,3600), distinct counter; fallback = inherit shared + amend PRD §5. No longer dangling; chunk-9 AC asserts the chosen limit. Implementation still in chunk 9. |
| REV-6 | minor | Chunk 1 named a directory, not a file, for constants | §4.D chunk 1 | fixed | R1 | R1 | internal/app/service/export_contact.go (+ ExportContactJobName beside existing job consts). R2: intact. |
| REV-7 | major | EXP-S07 durable per-user persistence deferred (v1 = FE localStorage); PRD marks all EXP-S07 ACs "Must Have" | §2.B, Detail 1.C, OQ-15 | open (accepted-risk, PM sign-off) | R1 | — | v1 ships localStorage. R2 update: AC-1 Timezone now covered via the EXP-S09 panel; Associations still not persisted (§2.B note added — no grounded "associations" field group in repo); AC-4 cross-device scope still deferred. Tracked OQ-15; needs PM sign-off before GA. |
| REV-8 | major | NotificationPublisher Go interface signature + notif HTTP timeout/retry budget not pinned; OQ-10 channel unconfirmed | Decision 7, chunk 7, §4.B | open (partial) | R1 | — | Interface isolation + non-fatal policy present; config keys pinned (§4.B). R2: unchanged — the explicit Go signature Publish(ctx, ExportNotification) error and a timeout/retry number are still not written; OQ-10 channel still cross-team. |
| REV-9 | minor | OSS bucket region / cross-border data-residency for the PII export body not addressed | §3.D, OQ-16 | open | R1 | — | InfoSec to confirm OSS region vs UU PDP cross-border rules. R2: unchanged (OQ-16). |
| REV-10 | minor | OSS quota/headroom for export volumes unconfirmed | §1 Assumptions, OQ-4 | open (infra, Stage-0) | R1 | — | CDP Infra to confirm ~5–15 MB/file at 48 h before Stage 0. R2: unchanged (OQ-4). |
| REV-11 | minor | Success-banner copy dropped the PRD AC-2 "Data generated in GMT (+07:00) timezone" sentence | §2.C, §3.C, sequence | fixed | R1 | R2 | 2026-06-30 edit: banner now reads "Customer download started — your data is generated in {timezone} (default GMT +07:00). Please check your email at {email}…", {timezone} interpolating the EXP-S09 selection. Was an R1 traceability Partial + OQ-11. |
| REV-12 | major | EXP-S08 export-history reuse not buildable today — depends on a net-new cross-team s2s path on the IAG billing surface (no CDP quota type, UI-only row creation) | Decision 10, §2.4 #6, §2.F.1 step 4b, OQ-17, chunk 19 | open (cross-team; mitigated) | R2 | — | Grounded against hub-chat + IAG /report/v1/billings/logs/export. Isolated behind a non-fatal ExportHistoryRegistrar iface; chunk 19 blocked on OQ-17. PRD Should-Have → does not gate the core release. |
| REV-13 | minor | first_10k_sorted needs a net-new deterministic _id tie-breaker + a get-by-IDs/criteria batch (no $in-by-_id today) | Decision 9, §2.4 resolution, OQ-18, chunk 9/20 | open (decide in chunk 9; mostly resolved) | R2 | — | Decision 9 specifies the resolution via SearchContacts+SortBy (handler already defaults created_at desc) + added _id secondary sort, limit 10K. Remaining: confirm the _id tie-breaker + add the batch path. Low risk. |
| REV-14 | minor | FE MAX_SELECTION = 500 conflicts with the 10,000 export cap (net-new FE work) | Decision 9, §2.I, OQ-19, chunk 20 | open (decide in chunk 20) | R2 | — | ListPage.vue:220 caps list selection at 500; explicit-ID export needs up to 10K and the shortcut needs the criterion path. Default: raise/replace MAX_SELECTION for the export flow only; prefer the criterion over collecting 10K IDs client-side. |
| REV-15 | major | EXP-S09 timezone value format ambiguous (label "(GMT+07:00) Asia/Jakarta" vs IANA "Asia/Jakarta") + BE timezone-aware timestamp formatting is net-new with no parse contract | Decision 11, §2.4 struct, §2.A.1, OQ-20, chunks 18/21 | open (confirm before chunk 18/21) | R2 | — | The Timezone example carries a display label; time.LoadLocation needs IANA. RFC flags OQ-20 ("IANA vs label") but the formatter contract (parse path + which cells are tz-rendered) is unspecified — agent would guess in chunk 18. |
| REV-16 | minor | EXP-S09 Source filter enum gap — PRD lists "Advertisement"/"Email" but FE sources.ts + contact Source lack them | Decision 11, §2.A.1, OQ-21, chunk 21 | open (confirm before chunk 21) | R2 | — | Default: reconcile the vocabulary (extend the enum or scope the panel's Source options to existing channels) before build. |
| REV-17 | minor | The 2 mermaid blocks edited for v2.6/banner (branch-skip flowchart, happy-path sequence) were not machine-validated post-edit | §2.1 branch/skip, §2.2 happy-path; Comment log | open (pre-merge TODO) | R2 | — | Both pass a rule-based pitfall scan; mmdc was unavailable at R2 (Chromium launch blocked) and at the 2026-06-26 edit. Run mmdc/Docusaurus render before publish — already noted in the RFC comment log. |
Ledger summary: 17 findings — 7 fixed (REV-1..6 + REV-11), 10 open (REV-7/8 major-but-mitigated, REV-9/10/13/14/16/17 minor, REV-12/15 major-new). All open material findings are promoted to the RFC Open-Questions table: REV-7→OQ-15, REV-8→OQ-10, REV-9→OQ-16, REV-10→OQ-4, REV-12→OQ-17, REV-13→OQ-18, REV-14→OQ-19, REV-15→OQ-20, REV-16→OQ-21; REV-17 is tracked in the RFC Comment log (pre-merge mmdc TODO).
PRD → RFC Traceability Matrix
Evidence normalization applied: §6 Comment logs were excluded from scoring evidence as non-authoritative discussion context. Where the comment log references fixes, those fixes are independently present in the authoritative body and were scored from there.
Standard format (PRD v2.6 — 9 stories)
| PRD Element | RFC Section | Coverage |
|---|---|---|
| EXP-S01 Export manually selected | §2.A button + §2.4 row 1 + Detail 1.C | Full |
| EXP-S02 Export from filtered result (+ "Select all first 10,000" shortcut) | §2.A + Decision 9 + §2.4 selection_mode resolution | Full — shortcut gated on total ≥ 10,000; sends first_10k_sorted criterion not IDs; _id tie-breaker finalised in chunk 9 (OQ-18, REV-13) |
| EXP-S03 Configure fields by layout | §2.A field groups + §2.4 field_properties envelope + §2.B | Full |
| EXP-S04 Receive email + in-app notification | §2.F (3 email methods) + §2.4 #5 + §2.F.2 | Full |
| EXP-S05 XLSX/CSV formatting by type | Decision 6 + §4.D chunks 4–5 (24 table cases) | Full |
| EXP-S06-NEG guard rails | §3 Role × Endpoint + §3.B (403/422/403 FLAG_DISABLED) | Full |
| EXP-S07 Persist last export config (AC-1..AC-9) | §2.B localStorage + §2.A.1 panel persist + Detail 1.C | Partial — v1 = FE localStorage (+ panel timezone/period/source); durable per-user store (PRD D-8) + AC-4 cross-device + AC-1 Associations deferred (OQ-15, REV-7) |
| EXP-S08 View export history (company/org) | Decision 10 + §2.4 #6 + §2.F.1 step 4b | Partial — specified behind a non-fatal ExportHistoryRegistrar iface but not buildable: no CDP quota type / no s2s register path on the IAG billing surface (OQ-17, REV-12). PRD Should-Have |
| EXP-S09 Download-all via right-side panel | §2.A.1 + Decision 11 + §2.4 struct (timezone/period/source[]) | Full (with confirms) — extend ExportCustomerDrawer.vue; Timezone+Layout gate Download; Period/Source server-side; tz value format (OQ-20, REV-15) + source enum (OQ-21, REV-16) to confirm before chunks 18/21 |
| PRD §5 rate limit "5/company/hour" | §3 Security + §4.B + OQ-14 | Full (as decision) — decisive v1 design (per-route param) grounded in rate_limit_middleware.go; implementation in chunk 9 (REV-5) |
| PRD §5 selection cap 10,000 (FE + API) | Decision 12 + §2.4 422 EXPORT_LIMIT_EXCEEDED + §2.C tooltip; FE MAX_SELECTION raise (OQ-19) | Full |
| PRD §5.1 data lifecycle (status 7d / file 48h / audit 90d) | §2.3 per-status lifecycle + §3.D | Full |
| AC-1 success banner copy ("Data generated in GMT (+07:00) timezone") | §2.C / §3.C export.started | Full — banner now carries the GMT-timezone sentence, {timezone}-parameterised (REV-11 fixed R2) |
| PRD §7 #1–#6 API behavior | §2.4 (4 endpoints incl. export-history register) + §2.F | Full (export-history #6 gated OQ-17) |
| PRD §8.1 system flow + diagram | §2.2 sequence (happy + 2 failure paths) | Full |
| PRD §10 observability (8 events) | §3 Monitoring — names preserved exactly (incl. cdp_export_history_registered) | Full |
| PRD §11 / §12 metrics + stage gates | §1 Success Criteria + §3 SLO + §4 Rollout | Full |
| PRD §13 dependencies | §1 Dependencies (+ v2.6 deps) + §2.F.1 | Full |
| RFC decision with no PRD driver | — | Assessment |
|---|---|---|
GET /iag/v1/contacts/export/status/{job_id} | — | Self-flagged new-with-justification (out-of-band status; v1 FE doesn't poll). Justified. |
CSV formula-injection guard (= + - @) | — | Sound security addition (§3 Security). Justified. |
Summary: of 9 PRD stories — 7 Full (S01–S06, S09-with-confirms), 2 Partial (S07 durable persistence, S08 not-buildable). Supporting PRD elements all Full; the R1 banner-copy and rate-limit Partials are now closed. Bidirectional matrices (Detail 1.A forward + reverse) cover all 9 stories; PRD §-coverage table present.
Scorecard
Full-Stack Scorecard (18 categories) — R2
| # | Category | Source | R1 | R2 | Evidence-Based Rationale (R2) |
|---|---|---|---|---|---|
| 1 | PRT — PRD Traceability | Merged | 8.5 | 9.0 | Detail 1.A forward+reverse covers all 9 stories; PRD-to-Schema maps every entity/rule → field + endpoint + enforcement. R1 Partials closed (banner, rate-limit, AC-1 timezone); only EXP-S07 durable + EXP-S08 buildability remain Partial. |
| 2 | TDC — Technical Decisions | Merged | 8.5 | 9.0 | Detail 1.B = 17 decisions, each chosen option + rejected alternatives + reversibility; Decisions 1–11 in ADR format. Zero dangling (OQ-14 now decided). Docked: Decision 7 (notif channel) is a default-pending-confirm (OQ-10). |
| 3 | CNT — Contract Specificity (FE) | FE | 8.0 | 8.5 | ExportCustomerPageProps TS interface; §2.A.1 panel control table (required/default/maps-to-request); §2.B fetch + localStorage; §2.G request body + casing. Docked: field-mapping transform still prose ("mirror DownloadTemplateModal"). |
| 4 | SCB — Scope Boundaries (FE) | FE | 9.0 | 9.0 | §2.I enumerates FE create/modify/NOT-touched + shared read-only; chunks 11–16, 20–21 name exact files incl. ExportCustomerDrawer.vue. |
| 5 | DEP — Dependencies (FE) | FE | 8.5 | 8.5 | §1 table: each dep with availability + path:line + blocking flag; v2.6 deps added (SearchContacts, FilterCheckbox/InputPeriod/timezones.ts, default-layout, ExportCustomerDrawer, hub-chat history). The hub-chat dep is a YES-confirm blocker (OQ-17). |
| 6 | NFS — Non-Functional Specificity (FE) | FE | 7.0 | 7.0 | §3 endpoint p99 < 300ms, worker < 10min; §3.E a11y. Docked (unchanged): no bundle-size budget, no browser-matrix versions, no LCP/INP/CLS. This single 7.0 is what blocks the 9.0+ overall tier. |
| 7 | TPS — Test Plan Specificity (FE) | FE | 8.0 | 8.0 | §4.C maps layer→command→proof incl. first_10k_sorted, panel (Timezone/Layout gate, Period/Source, Retry), tz formatting. Docked: no named a11y/axe automation. |
| 8 | DMS — Data Model & Schema (BE) | BE | 8.0 | 8.0 | §2.3 extends bulk_upload_jobs field-by-field (+failure_reason), erDiagram, example row, PII per field, retention per status. Docked (unchanged): index still "optional" (ship/no-ship ambiguous); failure_reason/list_failed_rows shapes thin. |
| 9 | ACV — API Contract & Versioning (BE) | BE | 8.5 | 9.0 | §2.4: 4 endpoints, method/path/auth/request/response/codes/idempotency; the field-resolution algorithm + selection_mode resolution; field_properties now a documented envelope (grounded serializer.go:7-32); EXPORT_SELECTION_MODE_INVALID added; example payloads for all 3 selection paths. |
| 10 | DIC — Data Integrity & Consistency (BE) | BE | 8.5 | 8.5 | §2.D per-write-path transaction scope, idempotency (job_id + terminal-status guard), email single-fire; export-history register added as a non-fatal side effect (consistent policy). Crash-window-between-email-and-status-write caveat persists. |
| 11 | FMC — Failure Mode Coverage | Merged | 8.5 | 8.5 | §2.C UI matrix (+ panel Retry state) + §3.C i18n catalog; §2.F retry/DLQ/poison; §3.A FE codes = BE codes (incl. EXPORT_SELECTION_MODE_INVALID). Docked: notif/OSS timeout + circuit-breaker values still unpinned (REV-8). |
| 12 | CSS — Concurrency & Scaling (BE) | BE | 6.5 | 7.5 | Up: the rate-limit mechanism is now a decided, grounded per-route design (REV-5); §2.E collision map intact; first_10k_sorted adds a bounded 10K-row server-side resolution. Docked: worker pool count still "default" (no number); concurrency QA item (5 simultaneous) flagged not resolved. |
| 13 | SAS — Security & Authorization (BE) | BE | 8.5 | 8.5 | §3 threat model (5 vectors), IAG JWT + permission key, company-scope on every query + OSS path, cross-company rejection, CSV formula guard, SSRF analysis, secrets via config/load.go. |
| 14 | ROL — Rollout & Rollback (merged) | Merged | 8.5 | 8.5 | BE-first deploy order with rationale; §4.A matrix covers all 6 scenarios; flag default OFF + mobile flag; new config keys (notif, export-history, default timezone) in §4.B. No cap triggered. |
| 15 | OBS — Observability (merged) | Merged | 8.0 | 8.0 | 8 Mixpanel events (+ cdp_export_history_registered), names exact; slog fields; 5 alert thresholds → #cdp-ops; SLOs. Docked (unchanged): no distributed trace FE→API→worker. |
| 16 | SBC — Service Boundary & Coupling (BE) | BE | 8.5 | 8.5 | §2 per-service diagram + table; notif call sync HTTP non-fatal behind interface; export-history registrar s2s behind ExportHistoryRegistrar iface, non-fatal, owner named (OQ-17). |
| 17 | CPA — Pattern Alignment (merged) | Merged | 9.0 | 9.0 | §2.0 Patterns table maps each concern → repo file → deviation; v2.6 components (extend ExportCustomerDrawer, reuse FilterCheckbox/InputPeriod) all grounded; snake↔camel boundary explicit. Strongest category. |
| 18 | CDG — Compliance & Data Governance | BE | 8.0 | 8.0 | Triggered. §3.D classification/legal basis/retention/encryption/audit/right-to-delete. Docked (unchanged): OSS cross-border residency not addressed (OQ-16); right-to-delete relies on file TTL. |
Overall: 8.5 — Agentic-Ready. No score cap triggered. The 9.0+ tier is gated only by NFS 7.0 (< 7.5); all other 9.0 conditions (ACV/DIC/FMC/CNT ≥ 8.5, zero dangling decisions, no cross-layer mismatch) are met.
Resource & Cost Advisory (non-blocking)
- §4.F: +M worker pods bounded by 5/h/company; few hundred status docs/day + one index; OSS ~5–15 MB/job auto-expiring 48h → near-zero steady-state growth; no new infra. OQ-4 (OSS quota) routed to CDP Infra as a Stage-0 gate. Adequate advisory note.
Decision Closure Assessment (R2)
Decision Index
| # | Decision | Status | Critical Gaps |
|---|---|---|---|
| 1 | XLSX + CSV in v1 (stdlib encoding/csv) | Resolved | none |
| 2 | Async via gocraft/work | Resolved | none |
| 3 | Reuse bulk_upload_jobs + job_type discriminator | Resolved | backfill conditional ("only if") — agent audits callers |
| 4 | New GenerateAndUploadExcelWithData() | Resolved | none |
| 5 | Buffer → temp *os.File → email → delete | Resolved | crashed-pod temp-file reclamation relies on OS reaping (acknowledged) |
| 6 | Type-aware serializer + CSV RFC-4180 | Resolved | custom-field value resolution now specified (REV-2) |
| 7 | Notif publish via HTTP heimdall (default) | Partial | channel HTTP-vs-Kafka unconfirmed (OQ-10); NotificationPublisher Go signature + timeout/retry still unwritten (REV-8); mitigated by interface + non-fatal policy |
| 8 | Endpoint namespace /iag/v1/contacts/export | Resolved | none (grounded PRD correction) |
| 9 | first_10k_sorted server-side criterion (not 10K IDs) | Resolved | net-new _id tie-breaker + get-by-IDs batch to add in chunk 9 (OQ-18, REV-13) |
| 10 | Reuse chat.qontak.com/reports/export (register s2s) | Resolved-with-blocked-dependency | the decision is made (iface, non-fatal, ship-without) but the path does not exist — needs cross-team work (OQ-17, REV-12); chunk 19 blocked |
| 11 | Extend ExportCustomerDrawer.vue; BE timezone formatting net-new | Partial | timezone value format (label vs IANA) + tz formatter contract unspecified (OQ-20, REV-15); source enum gap (OQ-21, REV-16) |
| 12 | Cap at both FE + API | Resolved | FE MAX_SELECTION=500 raise needed (OQ-19, REV-14) |
| 13 | Notification job non-fatal | Resolved | none |
| 14 | OQ-14 per-route rate-limit (extend middleware) | Resolved (was Dangling at R1) | implementation in chunk 9; fallback documented |
| 15 | (= Decision 10 export-history) | see #10 | — |
| 16 | (= Decision 11 panel) | see #11 | — |
| 17 | Export timestamp timezone (BE renders in request tz) | Partial | parse contract / which cells rendered unspecified (OQ-20, REV-15) |
Aggregate: of the 17 decisions, 13 Resolved, 3 Partial (Decision 7 notif channel, Decision 11/17 timezone formatting), 1 Resolved-with-blocked-dependency (Decision 10 export-history), 0 Dangling. The R1 Dangling item (OQ-14) is now resolved. No decision blocks chunks 1–9 (BE core) or 11–16 (FE core); the Partials gate chunks 7, 18, 19, 21.
Decision: 7 — Notification publish channel (HTTP heimdall vs Kafka) — Partial (carried, unchanged)
What was decided Option A (heimdall HTTP POST /notif/v1/notifications) as the default, pending One Notification team confirmation (OQ-10); non-fatal (Decision 13).
Still missing (REV-8): (a) confirmed ingest channel; (b) the NotificationPublisher Go signature (type NotificationPublisher interface { Publish(ctx, ExportNotification) error }); (c) HTTP timeout + retry budget on the notif POST.
Suggested resolution: write the interface signature + a timeout (mirror iag_mekari.go WithHTTPTimeout) + single retry into the RFC; confirm OQ-10 before chunk 7. Reversible behind the interface.
Decision: 10 — Reuse chat.qontak.com/reports/export for export history — Resolved-with-blocked-dependency (new R2)
What was decided Register each completed export into the existing surface via an ExportHistoryRegistrar interface, non-fatal, gated on OQ-17; ship the pipeline without it.
Grounding (strong, honest): the surface is hub-chat → IAG billing /report/v1/billings/logs/export; billing-only quota types; rows created only by the hub-chat UI; no CDP quota type and no s2s register path today (verified, §2.0 source table).
Why this is not Dangling but is blocked: the RFC chooses the path and isolates it; the cross-team prerequisite (billing/IAG adds a CDP quota_type + an s2s create accepting a CDP file/link + company/org visibility) does not exist. EXP-S08 is a Should-Have, so chunk 19 is blocked without gating the release.
Open question: OQ-17 (binding for EXP-S08 only).
Decision: 11 / 17 — EXP-S09 panel + BE timezone formatting — Partial (new R2)
What was decided Extend the existing ExportCustomerDrawer.vue; reuse FilterCheckbox/InputPeriod/timezones.ts; resolve the set server-side from Period/Source; BE renders timestamps in the request timezone (net-new).
Missing (REV-15/REV-16): (a) the timezone value format — the struct example uses a display label "(GMT+07:00) Asia/Jakarta" but time.LoadLocation needs IANA "Asia/Jakarta"; (b) which cells/fields are timezone-rendered and the formatter contract; (c) the Source enum reconciliation (PRD Advertisement/Email absent from sources.ts).
Suggested resolution: pin the request timezone as an IANA id (map the FE label → IANA on send), name the tz-rendered fields (Decision 6 date row), and reconcile the source vocabulary — all before chunks 18/21.
Cross-Layer Contract Verification (R2)
| Endpoint | Backend Response Schema | Frontend Expected Schema | Match? | Gaps |
|---|---|---|---|---|
POST /iag/v1/contacts/export | {job_id, status, email} snake | {job_id, status, email} | Yes | banner reads email; FE renders {timezone} from panel state / default (REV-11) — FE copy only, no BE field |
GET .../export/status/{job_id} | {job_id,status,total_records,success_count,failed_count} | (v1 doesn't poll) | Yes (partial use) | FE ignores in v1 (no progress bar) — acceptable |
GET .../field_properties?layout_id= | {data:[{name,name_alias,field_type,is_hidden,is_default,type,validation…}], pagination} | {label,value,group,disabled} per checkbox | Partial | FE maps name_alias→label, name→value, derives group, pages the envelope — in-page mapping, no contract change |
GET .../layouts/default | default layout object | pre-selected "Default view" | Yes | reused for EXP-S09 auto-fill |
| Request body (panel) | {…, timezone, period/start_date/end_date, source[]} | same | Partial | timezone value format (label vs IANA) unresolved (OQ-20, REV-15); source[] enum gap (OQ-21, REV-16) |
Mismatches found: 0 hard contract mismatches. The field_properties partial is resolved by the in-page FE mapping (no 6.5 cap). The panel timezone/source items are value-format / vocabulary gaps (confirm-before-chunk), not response-shape mismatches.
End-to-End Data Flow (R2)
EXP-S01/S02 (ids) | EXP-S02 shortcut (first_10k_sorted) | EXP-S09 panel (period/source/timezone)
→ FE: ListTable/Drawer → POST /v1/contacts/export {selection_mode, ids|criterion, layout_id, selected_fields, format, timezone?}
→ API: ExportHandler (flag/perm/cap/format/selection_mode/rate) → resolve set (ids in body | SearchContacts+SortBy+_id tie-breaker)
→ DB: insert bulk_upload_jobs {job_type:export, status:queued} → Enqueue ExportContactJobName → 200 {job_id, queued, email}
→ FE: success banner (with {timezone})
--- async ---
→ Worker: fetch contacts (SearchWithFilters + _id $in) → resolve default/custom fields → formatValue per type (tz-aware for date/timestamp)
→ serialize (excelize / encoding/csv) → temp *os.File → OSS PutObject + SignURL 172800
→ email → publish notification (origin=external_url) → register export-history row (non-fatal, OQ-17) → delete temp → status=completed
Gaps in flow (R2): (1) the timezone formatting step has no parse/render contract (REV-15); (2) the export-history register step targets a path that does not exist yet (REV-12). Everything else is traceable end-to-end from the RFC alone — the R1 custom-field-resolution gap is closed.
Strengths
- Closed-loop decision discipline at scale: 17 ADR decisions, zero dangling, each with chosen option + rejected alternatives + reversibility; the v2.6 additions are as rigorously grounded as the original scope.
- Honest cross-team grounding (the EXP-S08 treatment): rather than asserting "reuse for free," the RFC verifies the export-history surface is billing-owned with no s2s path, isolates it behind a non-fatal interface, and gates the chunk — exactly the right way to ship a Should-Have with a real dependency.
- The R1 blocker is genuinely closed: rate-limit moved from "we could do X or Y" to a grounded, decided per-route middleware extension with a named fallback; an agent can now implement chunk 9.
Biggest Gaps
- EXP-S08 not buildable (OQ-17, REV-12): the cross-team s2s register path on the IAG billing surface does not exist. Mitigated (interface + non-fatal + Should-Have) but a real dependency to drive with billing/IAG + Chat/Omni.
- EXP-S09 timezone contract under-pinned (OQ-20, REV-15): label-vs-IANA ambiguity + an unspecified BE timezone formatter would make an agent guess in chunk 18/21.
- Notification interface still unwritten (OQ-10, REV-8): the
NotificationPublisherGo signature + notif timeout/retry remain unspecified — an agent would invent them in chunk 7.
Priority Actions (before merge / before the named chunk)
- OQ-20 / chunks 18 & 21 — pin the timezone contract. Specify the
timezonerequest value as an IANA id (FE maps its label → IANA on send), name which cells are tz-rendered (Decision 6daterow), and define the formatter. Without this chunk 18 guesses. - OQ-10 / chunk 7 — write the
NotificationPublisherinterface signature + notif HTTP timeout/retry, and confirm HTTP-vs-Kafka with the One Notification team. - OQ-17 / chunk 19 — drive the export-history cross-team path (CDP quota type + s2s create + company/org visibility) with billing/IAG + Chat/Omni, or formally defer EXP-S08; the interface lets the core pipeline ship meanwhile.
- OQ-19 / chunk 20 + OQ-21 / chunk 21 — confirm the FE
MAX_SELECTIONraise for export and reconcile the Source enum (Advertisement/Email). Both have adopted defaults; just confirm. - Pre-merge — run
mmdcon the 2 edited mermaid blocks (REV-17) once a renderer is available.
Open Questions (mirror of the RFC §5 table — current)
| # | Question | Category | Severity | Maps to |
|---|---|---|---|---|
| OQ-10 | Notif ingest HTTP or Kafka? + NotificationPublisher signature + timeout/retry | SBC / FMC | Important | REV-8 |
| OQ-14 | Per-route rate-limit mechanism (decided: extend middleware; fallback: shared) | CSS | Decide in chunk 9 | REV-5 (resolved) |
| OQ-15 | EXP-S07 localStorage v1 vs durable per-user store (Timezone covered; Associations not) | PRT | Sign-off before GA | REV-7 |
| OQ-16 | OSS bucket region / cross-border residency for PII export body | CDG | Confirm Stage 0 | REV-9 |
| OQ-17 | EXP-S08 export-history: CDP quota type + s2s register path on IAG billing | SBC / DMS | Blocking (EXP-S08 only) | REV-12 |
| OQ-18 | first_10k_sorted _id tie-breaker + get-by-IDs batch | TDC / DMS | Decide in chunk 9 | REV-13 |
| OQ-19 | FE MAX_SELECTION=500 vs 10,000 export cap | CNT | Decide in chunk 20 | REV-14 |
| OQ-20 | EXP-S09 timezone value format (label vs IANA) + tz formatter contract | TDC / ACV | Confirm before chunk 18/21 | REV-15 |
| OQ-21 | EXP-S09 Source enum gap (Advertisement/Email) | CNT | Confirm before chunk 21 | REV-16 |
| OQ-4 | OSS quota for private/exports/ (~5–15 MB/job, 48h) | RCS (advisory) | Stage-0 infra gate | REV-10 |
Review History
One row per review cycle. Append, never overwrite — the audit trail of how the RFC improved (complementary to git history).
| Cycle | Date | Reviewed RFC revision | Score | Verdict | Findings (open → fixed) | Notes |
|---|---|---|---|---|---|---|
| R1 | 2026-06-18 | last_updated: 2026-06-18 (working tree) | 8.0 — Strong | PROCEED with notes | 10 raised → 6 fixed same-day (REV-1..6); 4 open (REV-7..10) | Initial full review (full-stack rubric) + hostile cross-review against PRD v2.2. Scorecard was the pre-fix snapshot. |
| R2 | 2026-06-30 | last_updated: 2026-06-30 (working tree, uncommitted) | 8.5 — Agentic-Ready | PROCEED | fixed: REV-5, REV-11 (+ confirmed REV-1..4,6 intact); still open: REV-7,8,9,10; new: REV-12..17 | Delta re-review against PRD v2.6 + the 2026-06-30 banner fix. Rate-limit (REV-5) decided → CSS 6.5→7.5; banner (REV-11) fixed; 4 new ADRs (Decisions 9–11,14–17) → TDC/PRT/ACV up; zero dangling decisions. New v2.6 surface: EXP-S08 not-buildable/cross-team (OQ-17), timezone contract (OQ-20), source enum (OQ-21), _id tie-breaker (OQ-18), MAX_SELECTION (OQ-19). Mermaid 10/10 rule-based pass; mmdc unavailable in env → pre-merge render TODO (REV-17). Score 8.0→8.5; 9.0+ blocked only by NFS 7.0. |