RFC Review: One CID Multiple WABA — Billing V3 Shared Balance Pool (Phase 1)
Companion review for
rfc-one-cid-multiple-waba-phase-1.md, produced by therfc-reviewerskill. Lives beside the RFC; valid only for the RFC revision inreviewed_rfc_last_updated(2026-07-01).
Executive Summary
- Overall Score:
7.0/10(was 6.5 at first pass; +0.5 after REV-1 closed as already-implemented and Decision 5 resolved) - Rating:
Strong - RFC Type:
full-stack - Sub-Type:
enhancement(FE + BE) - Assessment Confidence:
High(all five repos were grounded with file:line evidence; contracts are traceable) - Applied Caps/Gates: No numeric cap triggered (no category
< 5.0). Overall is held below the backend half's strength by the weakest-layer merge rule — the frontend contract layer (now consolidated in Detail 2.F) would force an agent to guess until filled. - Implementation Readiness Verdict:
Backend half → PROCEED(chunks 1–6 executable now);Frontend half → HOLD— gated solely on the FE-owner task (Detail 2.F: frames + component contracts + UI-states + a11y + analytics). No backend or product blockers remain (Decision 1 confirmed; threshold already implemented). - Report Path:
bifrost/one-cid-multiple-waba/rfcs/rfc-one-cid-multiple-waba-phase-1-review.md - RFC Author: Grehasta Mahardika / Syafrizal M. | Reviewed: 2026-07-01
The backend is genuinely agent-ready: every decision is grounded in real code (e.g. the deduction hierarchy at qontak-billing/.../quota_management/deduction.go:169-241, the optimistic-lock UPDATE at organization_package_component_initials.sql:42-51), Decision 1's cardinality is now confirmed, and the DDL is a single, reversible index change. The biggest strength is that the RFC correctly reframes the work as aggregation + surfacing over an already-complete engine rather than greenfield billing. The biggest gap is the frontend contract layer (now consolidated in Detail 2.F): the three net-new components (SharedBalanceTooltip, LowBalanceBanner, and the waba_id filter dropdown) are named and pattern-anchored but carry no prop types, event payloads, or UI-state matrix, and no accessibility or FE-analytics spec — and Design frames are still pending. The one thing that must change before full agentic execution: the FE owner fills Detail 2.F. All backend/product blockers are cleared (Decision 1 confirmed; low-balance threshold already implemented).
Quick Verdict
Why this RFC can be implemented agentically (backend):
- Every backend decision cites a real file+line and a reuse/extend tag; the deduction engine already exists, so the diff surface is small and grounded.
- DDL is one index change with an explicit rollback; migration sequence, backfill, and rollback recipe are concrete and ordered.
- Concurrency, idempotency (
unique_code), and failure paths are specified with real mechanisms, not prose.
Why this RFC will cause agent guessing or rework (frontend only):
- Net-new FE components have no
Propsinterfaces, event names, or 5-state UI matrix — an agent would invent them (now consolidated in Detail 2.F). - Figma frames are pending, so pixel-faithful FE is blocked until Design supplies them (Detail 2.F(a)).
- FE accessibility + analytics event are unspecified (Detail 2.F(d)(e)).
Findings Ledger (carry-forward)
| ID | Severity | Finding (one line) | RFC location | Status | First seen | Resolved in | Evidence / fix |
|---|---|---|---|---|---|---|---|
REV-1 | blocker | Low-balance threshold value undecided | §4.A Config; Decision 5 | wontfix | R1 | R1 (already implemented) | Closed 2026-07-01 — not a concern. Already implemented: billing_components.threshold_running_out DECIMAL(5,2) (qontak-billing migration 20260126000001_*), 40% default when NULL (deduction.go:299-301, quota_management_alert.go:97-98). Per-org override out of Phase 1 scope. |
REV-2 | blocker | Figma frame-level links missing | §1 Design References; Detail 2.F (a) | open | R1 | — | Consolidated into the FE-owner task (Detail 2.F). Design to supply per-surface frames; FE chunks 10–11 blocked until then. |
REV-3 | major | Net-new FE components lack prop-type/event contracts | Detail 2.F (b); §4.C chunks 9–11 | open | R1 | — | Consolidated into Detail 2.F. Props+events for SharedBalanceTooltip, LowBalanceBanner, filter dropdown. |
REV-4 | major | No UI State Matrix (loading/empty/error/partial/success) | Detail 2.F (c) | open | R1 | — | Consolidated into Detail 2.F. Port PRD WABA-S05 UI states into a 5-state matrix. |
REV-5 | major | No FE accessibility spec for new components | Detail 2.F (d) | open | R1 | — | Consolidated into Detail 2.F. Keyboard/focus/ARIA for tooltip + banner + filter. |
REV-6 | major | No FE analytics events | Detail 2.F (e) | open | R1 | — | Consolidated into Detail 2.F. Define tooltip-interaction event (PRD §11). |
REV-7 | major | credited_to value vocabulary mismatch (PRD vs qontak-billing) | §5 non-blocking #8; Detail 2.4 | wontfix | R1 | R1 (owner decision) | Dropped 2026-07-01 by owner — reconciled at implementation time against the live credited_to/quota_type values, not tracked as a spec task. |
REV-8 | minor | /billings/info aggregated response fields not fully typed | Detail 2.4 outbound row 1 | open | R1 | — | Pin field names + types for aggregated wabi/additional/postpaid + billing_version. |
REV-9 | minor | WaConversationLog.status referenced by PRD but absent in schema | §5 non-blocking #7 | open | R1 | — | Confirm billable/failed decided upstream (no column needed) or add column. |
REV-10 | minor | Cross-Layer Rollout Compatibility Matrix described in prose, not tabulated | §4 Rollout | open | R1 | — | Add the 6-scenario old/new matrix; deploy order IS specified so no cap. |
REV-11 | minor | FE performance budget (LCP/INP/CLS, browser matrix) absent | §3 Performance | open | R1 | — | Add Core Web Vitals budget for the Package Usage page delta. |
REV-12 | minor | Gateway path mapping unconfirmed (/internal/qontak/billing/v1 → /iag/v1) | §5 non-blocking #4; Detail 2.4 note | open | R1 | — | Confirm Mekari gateway rewrite; api_spec.yaml authoritative. |
Ledger summary: 10 open (1 blocker [REV-2], 4 major [REV-3/4/5/6], 5 minor), 2 wontfix (REV-1 already-implemented, REV-7 owner-dropped), 0 fixed. REV-2/3/4/5/6 are all consolidated into the single FE-owner task, Detail 2.F. Open findings promoted to the RFC Open-Questions surface by id.
PRD → RFC Traceability Matrix
| PRD Element | RFC Section | Coverage |
|---|---|---|
| §1–4 Problem/Persona/Non-Goals/Constraints | §1 Overview, Out of Scope, §3 Performance | Full |
| §5 CHG-001/002/003 (report columns, aggregated display) | Detail 2.4; Decision 6; Detail 2.A | Full (BE) / Partial (FE contract — REV-3) |
| §6 Shared Balance Tooltip | §4.C chunk 10 | Partial — component contract thin (REV-3), frame pending (REV-2) |
| §7.1–7.7 API & Webhook behavior | Detail 2.4; Decisions 1–5; Detail 2.C | Full |
| §8 Stories WABA-S01..S07 | Detail 1.C (all 7 mapped) | Full |
| §9/§9.1 Rollout/Migration window | §4 Rollout | Full |
| §10/§10.1 Observability | §3 Monitoring | Partial — FE analytics missing (REV-6) |
| §11 Success Metrics | §1 Success Criteria | Full |
| §12 Launch Plan | §4 stages | Full |
| §13 Dependencies | §1 Dependencies | Full |
| §14 Key Decisions | §2 Technical Decisions | Full |
| §15 Open Questions (Q1 threshold, Q2 log granularity, Q4 FK assumption) | §5; Assumption 1/3; REV-1 | Q2/Q4 resolved by grounding; Q1 open (REV-1) |
Summary: 15 of 15 PRD sections covered; all 7 user stories mapped. 2 partials trace to FE-contract thinness (REV-3/4/6). PRD Open Q2 (log granularity) and Q4 (FK assumption) were answered by grounding — a strength. No RFC decision lacks a PRD driver.
Scorecard (Full-Stack, 18 categories)
| # | Category | Source | Score | Evidence-Based Rationale |
|---|---|---|---|---|
| 1 | PRT — PRD Traceability | Merged | 9.0 | FE+BE forward/reverse matrices (Detail 1.A), all 7 stories in Detail 1.C, PRD §-coverage table; even answers PRD Open Q2/Q4. |
| 2 | TDC — Technical Decisions | Merged | 8.0 | 6 ADR blocks w/ options+reversibility; BE strong. FE decisions limited to Decision 6; tooltip/banner not ADR'd → weaker layer drags. |
| 3 | CNT — Contract Specificity | FE | 5.0 | Net-new components (SharedBalanceTooltip/LowBalanceBanner/filter) have no prop types or event payloads (Detail 2.A not filled for FE). REV-3. |
| 4 | SCB — Scope Boundaries | FE | 7.5 | §4.C chunks 7–11 name exact files to create/modify; non-goals explicit. |
| 5 | DEP — Dependencies | FE | 8.0 | §1 Dependencies table tags availability (exists/needs-building/blocked) incl. BE endpoints. |
| 6 | NFS — Non-Functional Specificity | FE | 5.0 | Page/query latency targets present; no LCP/INP/CLS, a11y, or browser matrix. REV-5/REV-11. |
| 7 | TPS — Test Plan Specificity | FE | 7.0 | vitest commands sourced from package.json:15; per-chunk AC. No explicit cross-API integration test. |
| 8 | DMS — Data Model & Schema | BE | 8.5 | Index DDL + rollback; existing tables cited w/ columns + migration files; ER diagram; retention/PII. |
| 9 | ACV — API Contract & Versioning | BE | 7.0 | Outbound+inbound tables, reuse tags, unique_code idempotency. /billings/info aggregated fields untyped (REV-8); credited_to mismatch (REV-7). |
| 10 | DIC — Data Integrity & Consistency | BE | 8.5 | Data Integrity Matrix + Concurrency Collision Map; optimistic lock cited at line level; tx scope explicit. |
| 11 | FMC — Failure Mode Coverage | Merged | 7.0 | BE strong (retry/requeue/timeout/DLQ catalogs, error catalog). FE lacks 5-state UI matrix (REV-4) → weakest link. |
| 12 | CSS — Concurrency & Scaling | BE | 7.5 | Collision map + optimistic lock + bounded retry; QPS/pool numbers absent (advisory). |
| 13 | SAS — Security & Authorization | BE | 8.0 | Role × Endpoint matrix, company_id tenancy, sqlc parameterization, secrets, static analysis tools named. |
| 14 | ROL — Rollout & Rollback | Merged | 7.5 | Deploy order specified (5 ordered steps), rollback recipe, flags, backfill. Compat matrix in prose not tabulated (REV-10). Deploy order present → no 6.0 cap. |
| 15 | OBS — Observability | Merged | 7.0 | BE events/metrics/alerts named (PRD §10 mirror). FE analytics missing → merge caps at 7.0 (REV-6). |
| 16 | SBC — Service Boundary & Coupling | BE | 8.5 | Responsibility Boundary Matrix; in-process gem vs HTTP-gateway boundary explicit; sync/async labeled. |
| 17 | CPA — Consistency & Pattern Alignment | Merged | 8.0 | Patterns-to-Follow table per layer w/ reference files (Go handler/sqlc, Grape, Pinia, pixel3). |
| 18 | CDG — Compliance & Data Governance | BE | 8.0 | Triggered (PII: customer_name/phone_number Lockbox-encrypted; financial). Detail 3.C: classification, legal basis, retention, encryption. |
Resource & Cost Advisory
- Detail 4.E present and reasonable (no new pods; +1 Redis key/company; log growth unchanged). Advisory only — does not affect score.
Decision Closure Assessment
Decision Index
| # | Decision | Status | Critical Gaps |
|---|---|---|---|
| 1 | Shared-pool cardinality (single OrganizationPackage; N WABAs → 1) | Resolved | none — cardinality confirmed 2026-07-01; grounded; reversibility stated |
| 2 | Concurrency on shared pool (optimistic lock + requeue) | Resolved | minor — "align to 3 retries" not pinned to a concrete max_fails number |
| 3 | Aggregated balance read + cache | Partial | cache TTL value not specified ("short TTL"); invalidation trigger set |
| 4 | WABI monthly reset (reuse) | Resolved | none |
| 5 | Company-level low-balance + below-zero notification | Resolved | threshold already implemented (40% default, billing_components.threshold_running_out); below-zero branch + dedup key specified |
| 6 | waba_id column gating data-driven → preference-driven | Resolved | FE contract detail thin (ties to REV-3) but decision itself closed |
Aggregate: 5 Resolved, 1 Partial (Decision 3 cache TTL), 0 Dangling.
Decision 1 — Shared-pool cardinality
Status: Resolved. Decided: single OrganizationPackage per company; drop the unique index on whatsapp_packages.organization_package_id so N WABAs share it. Alternatives: Option B (many packages + SUM aggregation) documented and rejected for concentrating risk on the money-critical hot path. Grounding: hub_core/.../20260113000000_*.rb, wa_package.rb:213-240, organization_package_components.sql:87-137. Failure/reversibility: gated behind existing waba_save_organization_id; reversibility rated Medium with mitigation. Challenge: holds at scale (single-row optimistic lock); consistent with company-scoped deduction. No gap.
Decision 3 — Aggregated balance read + cache
Status: Partial. Missing: the Redis TTL value (RFC says "short TTL"), and the exact cache-key invalidation ordering vs invalidate-cache. Suggested resolution: pin TTL (e.g. 30–60s given the ≤500ms budget and refresh-based display) and state that deduction/top-up call PUT .../invalidate-cache before returning. Open question: what staleness is acceptable for balance display between a deduction and the next read?
Decision 5 — Low-balance + below-zero notification
Status: Resolved (2026-07-01). The threshold is already implemented — billing_components.threshold_running_out with a 40% default (deduction.go:299-301, quota_management_alert.go:97-98); Phase 1 reuses it (REV-1 closed). The only net-new parts (distinct balance_below_zero event + per-org/per-cycle dedup key) are specified. Minor follow-up: document the dedup window length. A per-org threshold override is explicitly out of Phase 1 scope.
Cross-Layer Contract Verification
| Endpoint | Backend Response Schema | Frontend Expected Schema | Match? | Gaps |
|---|---|---|---|---|
GET /billings/info | + billing_version, aggregated wabi/additional/postpaid (untyped in RFC) | PackageInfo.billing_version; BillingInfo (needs billing_version) | Partial | field names/types not pinned (REV-8); FE BillingStore must add billing_version |
GET /mcc_logs?waba_id= | rows incl. waba_id (pref ON) | WhatsAppBalanceLog.waba_id? (exists) | Yes | none — param + optional field already present both sides |
GET /download_broadcast_deduction | CSV WABA ID col pref-gated | blob download (exists) | Yes | none |
POST /iag/.../deduction | credited_to = component_code + quota_type | PRD/report expect wa_balance_initial/... | Partial | vocabulary mismatch (REV-7) |
Checks: casing (snake_case BE ↔ camelCase FE) — FE customFetch transform not explicitly stated for new fields; nullability ok; error shapes differ by service (Grape vs qontak-billing envelope) — acceptable across the gateway. Mismatches found: 1 material (credited_to vocabulary) → does not cap ROL (it's a data-vocabulary, not an endpoint-shape, mismatch) but is a major (REV-7).
Cross-Layer Rollout Compatibility Matrix
| Scenario | Frontend | Backend | Works? | Notes |
|---|---|---|---|---|
| Pre-deploy | Old | Old | Yes | baseline |
| Backend first | Old | New | Yes | new fields additive; old FE ignores billing_version; deduction pool-correct |
| Frontend first | New | Old | Partial | FE gates on billingM1Version/pref; if BE lacks billing_version in payload, tooltip stays hidden (fail-safe) — acceptable |
| Both | New | New | Yes | target |
| Backend rollback | New | Old | Partial | same as "frontend first" — FE degrades safely |
| Frontend rollback | Old | New | Yes | additive BE, old FE unaffected |
Deploy order: specified — backend first (§4 Rollout steps 1→5). No unaddressed "No". Tabulating this (REV-10) would make it explicit in the RFC.
End-to-End Data Flow — outbound message deduction
Agent sends billable msg
→ hub_service Grape → hub_core Broadcasts::BillingDeduction (stamp waba_id)
→ QuotaManagement HTTP client → Mekari gateway → qontak-billing POST /iag/v1/.../deduction (company_id)
→ GetQuotaForUpdate (pool) → OPC*DeductionQuota (lock_version) → CreateBillingLog (credited_to, quota_type, waba_id)
→ Side effects: enqueue alert if remaining ≤ threshold
→ Response value_before/after/credited_to → hub_core Success → delivery not blocked
Gaps: the credited_to value returned (component_code) vs what reports render (REV-7). Otherwise fully traceable across all five services — a strength.
Strengths
- Grounded to the line, across 5 repos. Every anchor in Detail 2.0 has file:line evidence (e.g.
deduction.go:169-241,billings.rb:489/559,useTopupStore.ts:115-117), and the Source Verification table is complete — an agent can navigate directly. - Correct problem reframing. The RFC proves most of the PRD is already built and scopes the true delta (one aggregation change + FE surfacing), and it answers two of the PRD's own Open Questions (Q2 log granularity, Q4 FK assumption) from code.
- Backend rigor. Decision 1's cardinality is confirmed; DDL is a single reversible index change; concurrency/idempotency/failure paths use real mechanisms (optimistic lock + requeue,
unique_code), and the rollback recipe is ordered and flag-gated.
Biggest Gaps
- FE component contracts absent (REV-3/4/5).
SharedBalanceTooltip,LowBalanceBanner, and thewaba_idfilter dropdown have no prop types, event payloads, UI-state matrix, or a11y — an agent building the FE would guess all of it. This is what drags the merged score to Needs Work. - Two blocking Open Questions (REV-1/REV-2). Threshold value blocks WABA-S06's warning branch; Figma frames block pixel-faithful FE. Both are external decisions the RFC correctly flags but cannot self-resolve.
credited_tovocabulary unreconciled (REV-7). The report/FE key on strings that differ between the PRD and the actual engine output; unresolved, thewaba_id/bucket columns risk showing wrong or empty values.
Priority Actions
- Detail 2.F — FE-owner task (REV-2/3/4/5/6) — the single remaining blocker, all frontend. Design supplies frame-level Figma links per surface; the FE owner then pins
Props+events for the three net-new components, a 5-state UI matrix per report surface, a11y (keyboard/focus/ARIA), and the tooltip-interaction analytics event. Unblocks FE chunks 9–11. - Detail 2.4 (REV-8) — pin the
/billings/infoaggregated field names/types (the exact JSON the FEBillingStorewill read). - Decision 3 — pin the balance-cache TTL value + invalidation ordering.
- Backend chunks 1–6 — no blocker; can start now in parallel with the FE-owner task above.
Backend Contract Addendum
Endpoint Contract Details
| Endpoint | Method/Path | AuthZ | Request | Response | Error | Idempotency/Versioning | Status |
|---|---|---|---|---|---|---|---|
| Billing info | GET /api/core/v1/billings/info | OAuth2 :admin,:owner,:supervisor,:agent,:member | none (org from token) | + billing_version, aggregated buckets | timeout → cached + log | n/a (read) | Missing typed aggregated fields (REV-8) |
| Deduction | POST /iag/v1/quota-managements/deduction | internal SSO + X-Api-Key | company_id,billing_code,deduction_code,unique_code,quantity,extra_attrs{waba_id} | value_before/after,credited_to | quota_exceeded; 400 envelope | unique_code | Complete (vocabulary note REV-7) |
Database Changes Details
| Change | Table | DDL / Shape Diff | Migration Plan | Rollback | Compatibility | Status |
|---|---|---|---|---|---|---|
| Drop unique, add non-unique index | whatsapp_packages | organization_package_id index unique→non-unique | backfill FK on V3 rows; verify 100% | re-add unique only after re-split (documented) | non-unique is a superset; additive | Complete |
Implementation Readiness Checklist
Unblocked (backend)
- PRD→RFC traceability complete
- Backend decisions resolved with alternatives
- Backend failure modes + error catalog
- Schema at DDL precision + rollback
- Transaction boundaries + idempotency (
unique_code) - Concurrency collision points + resolution
- Security: tenancy (
company_id), validation, injection, secrets - Migration/backfill/rollback recipe
- Observability (BE metrics/alerts)
- Task decomposition with per-chunk AC
Blocked (must fix first) — all in the single FE-owner task (Detail 2.F)
- Figma frames per surface — REV-2 (Design)
- FE component contracts (props/events) — REV-3
- 5-state UI matrix — REV-4
- FE a11y — REV-5
- FE analytics event — REV-6
Closed since first pass: REV-1 (threshold already implemented), REV-7 (vocabulary, owner-dropped).
Verdict: One consolidated FE-owner task (Detail 2.F) blocks the frontend half. Backend half: Ready to implement now.
Task Manifest
The RFC's Detail 4.C already provides an 11-chunk ordered plan with files + commands + verifiable AC — verified as sound for the backend chunks (1–6) and directionally correct for FE chunks (7–11), which become executable once REV-3/REV-1/REV-2 close. No replacement manifest needed; chunks 1–6 can start now, 7–11 after the FE-contract gaps close.
Dangling Decisions Log
| # | Decision | Location | Owner | Deadline |
|---|---|---|---|---|
| — | none dangling — 2 Partial (Decision 3 TTL, Decision 5 threshold) tracked as REV-1 / Decision-3 note | §2 | Bifrost Eng / PM+Finance | before Ready:yes |
Open Questions
| # | Question | Category | Severity |
|---|---|---|---|
| 1 | threshold_running_out, 40% default) | FMC (REV-1, wontfix) | — |
| 2 | FE-owner task (Detail 2.F): frames + component contracts + UI-states + a11y + analytics | CNT/NFS (REV-2/3/4/5/6) | Blocking (FE only) |
| 4 | credited_to string vocabulary | ACV (REV-7, wontfix) | — |
| 5 | Typed /billings/info aggregated response shape? | ACV (REV-8) | Important |
| 6 | Cache TTL + invalidation ordering for aggregated balance? | TDC (Decision 3) | Important |
| 7 | Is WaConversationLog.status needed, or is billable decided upstream? | DMS (REV-9) | Nice-to-have |
| 8 | Gateway path rewrite /internal/qontak/billing/v1 → /iag/v1 confirmed? | SBC (REV-12) | Nice-to-have |
Evidence Notes
- Detail 2.0 Source Verification — complete, per-line evidence across 5 repos; drove PRT/CPA/DMS high and confidence to High.
- Detail 2.A (FE) — the full-stack template's FE UI Contract / UI-State matrix was not filled for the net-new components; drove CNT/FMC down (REV-3/4).
- §3 (FE) — no a11y/analytics/CWV for FE; drove NFS/OBS down (REV-5/6/11).
- Decision blocks — 6 ADRs with reversibility; 2 Partial on undecided TTL/threshold.
- Mermaid — 10/10 blocks parse (validated with
mmdc); one;-in-sequenceDiagram issue was found and fixed pre-review.
Review History
| Cycle | Date | Reviewed RFC revision | Score | Verdict | Findings open → fixed | Notes |
|---|---|---|---|---|---|---|
R1 | 2026-07-01 | last_updated: 2026-07-01 / working tree (uncommitted) | 7.0 | BE PROCEED / FE HOLD | 10 open (1B/4M/5m), 2 wontfix | Initial review (6.5), then owner refinements same day: REV-1 closed (threshold already implemented), REV-7 dropped (vocabulary), REV-2/3/4/5/6 consolidated into FE-owner task (Detail 2.F) → 7.0. BE chunks 1–6 executable now. Mermaid 10/10. |