Qontak | Billing & Subscription | One CID Multiple WABA — Phase 1: Shared Balance Pool
Product Requirements Document · NEW PRD v1.0
HEADER BLOCK
| Field | Value |
|---|---|
| PM | Addo Hernando, Ega Javier Harwenda |
| PRD Version | 1.0 |
| Status | DRAFT |
| PRD Type | NEW |
| Epic | BIF-6428 |
| Squad | Bifrost |
| RFC Link | TBD — pending PRD approval |
| Figma Master | Figma — Subscription |
| Anchor | No — standalone, single-squad |
| Labels | epic:billing-platform | module:billing-subscription | feature:one-cid-multiple-waba |
| Last Updated | 2026-06-29 |
| Source | PRD Implementation of One CID Multiple Waba ID - Billing V3 |
Table of Contents
- HEADER BLOCK
- 1. One-liner + Problem
- 2. Target Users + Persona Context
- 3. Non-Goals
- Scope Changes
- 4. Constraints
- 5. Feature Changes
- 6. New Features
- 7. API & Webhook Behavior
- 8. System Flow + User Stories + ACs
- 9. Rollout
- 10. Observability
- 11. Success Metrics
- 12. Launch Plan & Stage Gates
- 13. Dependencies
- 14. Key Decisions + Alternatives Rejected
- 15. Open Questions
- PRD CHANGELOG
1. One-liner + Problem
One-liner: Enable Billing V3 companies with multiple WABA IDs to share a single aggregated WhatsApp balance pool, eliminating per-WABA balance confusion under Meta's Coexistence model.
Problem:
Companies using Meta's new Coexistence model can register multiple WhatsApp Business Account IDs (WABA IDs) under a single company, but Qontak's current billing system tracks and displays balance at the individual WABA level — each WABA appears to have an independent credit pool. This forces Company Admins and Qontak Finance teams to manually reconcile per-WABA balances, making it impossible to see total credit availability at a glance, and creating a high risk of unexpected service interruptions when one WABA exhausts its individual allocation while others still have credit. The Billing V3 component-based quota architecture (organization_package_component_initials, _additionals, _postpaid_limits) was designed for a shared-pool model but is not yet surfaced to the user; without the aggregation layer, the multi-WABA value proposition of Meta Coexistence is blocked for Qontak clients.
2. Target Users + Persona Context
| Persona | Role | Goal | Pain | Workaround |
|---|---|---|---|---|
| Primary — Company Admin (V3) | Owner or Admin managing a Billing V3 account with ≥2 WABA IDs registered under Meta Coexistence | View a single consolidated WhatsApp balance and understand how it is consumed across all WABA IDs without doing manual math | Must add up individual per-WABA balances to estimate total credit; balance resets and deductions feel unpredictable because the pool logic is opaque; risk of unexpected service blockage when one WABA exhausts quota while the company overall still has credit | Checks each WABA's balance separately in Package Usage page; maintains a side spreadsheet to sum totals; contacts Qontak Finance support to confirm available credits before large campaigns |
| Secondary — Qontak Finance Team | Internal Finance/Activation ops user managing credit top-ups, postpaid limits, and contract renewals for V3 clients | Configure and monitor a single aggregated WhatsApp credit pool per company — not per WABA — so that limit and top-up operations are performed once per company | Must mentally aggregate across WABAs when setting postpaid limits or confirming a top-up amount; reconciliation between Metabase CID/WABA dashboards and per-WABA billing records is slow and error-prone | Uses internal Metabase dashboards (see check_cid_and_waba_id and check_billing_version in moderator-be) to cross-reference CID vs WABA usage; manually aggregates before making top-up decisions |
3. Non-Goals
- Balance isolation per WABA — each WABA ID will NOT have its own independent spending cap or sub-quota in this phase; all WABAs share the single company pool with no per-WABA allocation rules.
- Billing V1 and V2 clients — the shared pool model applies only to
billing_version = "3.0.0"organizations; V1/V2 behavior (per-WABAwa_credit/wa_balancefields onWhatsappPackage) is entirely unchanged. - Mobile app changes — this feature is web-only; no changes to the Qontak mobile app in this phase.
- Real-time balance push updates — balance displayed in the UI updates on page refresh or explicit reload, not via WebSocket live push.
- Per-WABA spending limit configuration — Admins cannot set individual spending caps per WABA ID in this phase; that requires a future allocation-rules initiative.
- Multi-currency support — balance is denominated in a single currency; multi-currency conversion is out of scope.
- Historical per-WABA usage data migration — existing
WaConversationLogrecords are preserved as-is; only the balance aggregation model changes for new deductions going forward. - Automatic top-up trigger — low balance notifications alert the user, but no auto-recharge is implemented in this phase.
Scope Changes
Engineering surfaces this PRD touches (controlled vocab). Kept in sync with the scope_changes frontmatter above.
- Backend —
qontak-billing: aggregate balance query across allWhatsappPackagerows linked to the sameorganization_package_id; 3-tier deduction logic usingorganization_package_component_initials→additionals→postpaid_limits; monthly WABI reset job (is_initial_monthly_reset = trueonBillingComponent); carry-over on renewal (is_carry_over_contract = true); low balance notification events;billing_report_show_waba_idpreference gateswaba_idcolumn inWaConversationLogreport queries.hub-service:/api/core/v1/billings/inforeturns aggregated balance for V3 orgs;GET /mcc_logsadds optionalwaba_idfilter;GET /download_broadcast_deductionincludeswaba_idcolumn whenbilling_report_show_waba_idis enabled. - Frontend —
hub-chat: shared balance tooltip on Package Usage page (/subscriptions/packages);waba_idcolumn in Broadcast deduction table and Conversation/MCC log table;waba_idfield in CSV export for both report types; low balance banner driven bybillingM1Versionflag. - Data —
WhatsappPackage.organization_package_idFK already added (migration20260113000000);billing_report_show_waba_idpreference entry inOrganizationPreference; monthly WABI reset cron job configuration inqontak-billingscheduler.
4. Constraints
| Field | Value |
|---|---|
| Platform | Web only — no mobile changes in this phase. |
| Performance | Aggregated balance query ≤ 500ms p95. Package Usage page (with tooltip) loads ≤ 2s. Report table with waba_id column renders ≤ 2s for up to 500 rows. CSV export completes ≤ 10s for up to 10,000 rows. |
| Data limits | Max WABA IDs per organization: no Qontak-imposed hard cap in this phase (follows Meta Coexistence limits). Balance values displayed to 4 decimal places (matching existing wa_balance precision in OrganizationPackage). |
| Plan scope | Applies exclusively to organizations with billing_version = "3.0.0". V1 (1.0.0) and V2 (2.0.0) organizations are entirely unaffected. |
| Feature flag | billing_report_show_waba_id | default: OFF — enabled per CID to surface waba_id column in Broadcast and Conversation reports. Shared balance pool and deduction logic are active for ALL V3 orgs without a separate flag (tied to billing_version check). |
| Read/write | Company Admin: read aggregated balance, view tooltip, view waba_id in reports, configure postpaid limit. Qontak Finance Team: configure postpaid limit, add WAB-Additional credits via Modpanel. CS Agent: read-only access to waba_id column in reports they have permission to view. |
4.1. Data Lifecycle
| Artifact Type | Retention Period | Cleanup Trigger | User-Visible Effect |
|---|---|---|---|
WaConversationLog records (with waba_id) | 90 days from created_at | Existing nightly TTL cleanup job in qontak-billing | None — logs older than 90 days no longer appear in report queries |
WhatsappUsageComparison records (CID vs Meta reconciliation) | 90 days from created_at | Nightly cleanup job | None — internal reconciliation data only |
Component quota snapshots (organization_package_component_initials at reset) | Duration of contract + 6 months | Contract lifecycle cleanup | None — audit trail only |
| Low balance notification event log | 30 days | Nightly event log cleanup | None — internal observability only |
5. Feature Changes
Change ID: CHG-001
| Field | Detail |
|---|---|
| Change Type | Modified component |
| Page | Package Usage — Broadcast deduction report table (path TBD by engineering; currently at /subscriptions/packages or /billing/usage) |
| Page Intent | Admin or Finance user reviews broadcast message usage and associated balance deductions per period. |
| Before | • Broadcast deduction table has no waba_id column. Usage rows are not identifiable by which WABA sent them. |
| After | • A waba_id column is added to the broadcast deduction table when billing_report_show_waba_id preference is ON for the CID. • CSV export includes the waba_id field on every row when the column is visible. |
| Element | Before | After |
|---|---|---|
| Broadcast deduction table columns | date, type, messages, deducted_amount (approximate) | Same + waba_id column (gated by billing_report_show_waba_id) |
| CSV export fields | Same columns as table | Same + waba_id field when preference is ON |
Figma: Figma — Subscription (frame-level link TBD)
Change ID: CHG-002
| Field | Detail |
|---|---|
| Change Type | Modified component |
| Page | Package Usage — Conversation / MCC Logs table (/subscriptions/packages or equivalent) |
| Page Intent | Admin or Finance user reviews per-conversation billing deductions to reconcile usage and understand credit consumption. |
| Before | • MCC log table has no waba_id column. Conversation records cannot be attributed to a specific WABA ID at a glance. • No per-WABA filter is available on the conversation log view. |
| After | • A waba_id column is added to the MCC log table when billing_report_show_waba_id preference is ON. • A waba_id filter dropdown is added to allow filtering conversation logs by a specific WABA ID. • CSV export includes waba_id on every row when the column is visible. |
| Element | Before | After |
|---|---|---|
| MCC log table columns | date, conversation_id, type, credited_to, amount (approximate) | Same + waba_id column (gated by preference) |
| Filter options | No WABA filter | waba_id filter dropdown added |
| CSV export | Same columns as table | Same + waba_id field when preference ON |
Figma: Figma — Subscription (frame-level link TBD)
Change ID: CHG-003
| Field | Detail |
|---|---|
| Change Type | Modified component |
| Page | Package Usage page — WhatsApp balance display section |
| Page Intent | Admin views current WhatsApp credit availability to decide on top-ups or campaign scheduling. |
| Before | • For V3 orgs with multiple WABAs, balance is displayed at the individual WhatsappPackage level (per WABA). Each WABA appears to have a separate pool. |
| After | • For V3 orgs (billing_version = "3.0.0"), balance is displayed as a single aggregated company-level pool. The WABI (initial), WAB-Additional, and Postpaid buckets each show a single consolidated figure summed across all linked WABA IDs. |
| Element | Before | After |
|---|---|---|
| WABI balance display | Per WABA (e.g., WABA-1: 3,000 / WABA-2: 2,000) | Single company total (e.g., 5,000) |
| WAB-Additional display | Per WABA | Single company total |
| Postpaid Limit display | Per WABA | Single company total |
Figma: Figma — Subscription (frame-level link TBD)
6. New Features
Feature: Shared Balance Tooltip
| Field | Detail |
|---|---|
| URL | /subscriptions/packages (Package Usage page — existing page) |
| Access | Company Admin (all roles who can view the Package Usage page) |
Component Tree:
| Component | Parent | Purpose |
|---|---|---|
SharedBalanceTooltip | WhatsApp balance section header | Tooltip trigger icon displayed next to the WhatsApp balance heading; visible only when billing_version = "3.0.0" |
TooltipContent | SharedBalanceTooltip | Popover content explaining that the displayed balance is shared across all WABA IDs registered under the company |
UI States:
| State | Description |
|---|---|
| Hidden | Tooltip icon not rendered for non-V3 organizations (billing_version ≠ "3.0.0") |
| Idle | Info icon (ⓘ) displayed next to "WhatsApp Balance" heading |
| Active | On hover/click: tooltip popover appears with text: "This balance is shared across all your WhatsApp numbers (WABA IDs). Any message sent by any connected WABA ID draws from this single pool." |
| Error | N/A — tooltip content is static, no API dependency |
Figma: Figma — Subscription (Low Balance example) (frame-level link TBD)
7. API & Webhook Behavior
| # | Behavior | Entity Affected | Triggered By | Expected Behavior | Failure Behavior |
|---|---|---|---|---|---|
| 1 | Aggregate balance query | OrganizationPackage + linked WhatsappPackage rows (via organization_package_id FK) | Frontend loads Package Usage page or calls /api/core/v1/billings/info | For billing_version = "3.0.0" orgs: sum wa_balance_initial, wa_balance, and wa_credit across all WhatsappPackage rows sharing the same organization_package_id. Return a single aggregated WABI, WAB-Additional, and Postpaid Limit value. V1/V2 orgs return per-WABA values as before — no change. | If query times out (> 500ms p95): return last-known cached value and log billing_aggregate_balance_timeout. If no WhatsappPackage rows exist for the org: return zeros. |
| 2 | Multi-bucket deduction hierarchy | organization_package_component_initials → organization_package_component_additionals → organization_package_component_postpaid_limits (V3 quota tables) | Outbound message sent by any WABA ID linked to the OrganizationPackage | Deduct from WABI (initial quota) first. When WABI is exhausted (remaining_quota = 0), continue deduction from WAB-Additional. When WAB-Additional is exhausted, deduct from Postpaid Limit. Record credited_to in WaConversationLog for each deduction event with the waba_id of the sending channel. Failed messages (non-billable status): no deduction made. | If deduction fails due to optimistic lock conflict: retry up to 3 times. If all retries fail: log billing_deduction_failed with organization_id, waba_id, conversation_id; do not block message delivery but flag for manual reconciliation. |
| 3 | Monthly WABI reset | organization_package_component_initials.remaining_quota | Scheduled job runs at the start of each billing cycle (1st day of month or contract anniversary date) | Reset remaining_quota in organization_package_component_initials to the subscription-defined initial_quota for all active V3 organizations. Previous remaining WABI is discarded (no carry-over — is_carry_over_monthly = false, is_initial_monthly_reset = true on the WhatsApp BillingComponent). Log wabi_reset_completed per organization with organization_id, old_remaining, new_initial_quota. | If reset job fails for an org: retry up to 3 times. If still failing: log wabi_reset_failed with organization_id; alert Bifrost on-call via configured Slack channel. Do not skip or partially reset. |
| 4 | WAB-Additional carry-over on contract renewal | organization_package_component_additionals.remaining_quota | New contract ID generated on subscription renewal | Transfer existing remaining_quota from the expiring contract's additionals row to the new contract's row. is_carry_over_contract = true on the WhatsApp additional BillingComponent ensures this logic is applied. Log wab_additional_carried_over with old_contract_id, new_contract_id, carried_amount. | If carry-over fails: log wab_additional_carry_over_failed; alert on-call. Fail loudly — do not silently discard carry-over balance. |
| 5 | MCC logs with WABA filter | WaConversationLog query | Admin or Finance user applies waba_id filter on the MCC log view, or API calls GET /api/core/v1/billings/mcc_logs?waba_id=<id> | Filter conversation log results to rows where waba_id matches the provided value. waba_id is optional — omitting it returns all logs for the org (existing behavior). billing_report_show_waba_id preference must be ON for the CID; if OFF, filter parameter is ignored and waba_id column is not returned. | If waba_id does not match any log for the org: return empty result set with 200. Do not return 404. |
| 6 | Broadcast deduction report with WABA column | WaConversationLog export query for broadcast type | Admin clicks "Download CSV" on broadcast deduction report | When billing_report_show_waba_id preference is ON for the CID, include waba_id as a column in the CSV. Column maps to WaConversationLog.waba_id for each row. Column is absent from CSV when preference is OFF. | If export fails (query timeout for large datasets): return HTTP 504 with message "Export is taking too long. Try reducing the date range." Log broadcast_report_export_timeout. |
| 7 | Low balance notification trigger | OrganizationPackage aggregate balance | Deduction event causes aggregated balance to cross a configured threshold; separate check for balance dropping below 0 | When aggregated balance (WABI + WAB-Additional + available Postpaid) falls below the configured low-balance threshold: emit low_balance_warning event → send email to Admin + show in-app banner. When aggregated balance drops below 0: emit balance_below_zero event → send a distinct "below zero" email and banner. Same delivery mechanism as current per-WABA notifications. | If notification delivery fails: retry up to 3 times with exponential backoff. Log low_balance_notification_failed with organization_id, balance_at_trigger, threshold. Do not block message deduction on notification failure. |
HTTP methods, endpoint paths, request/response JSON schemas, and error codes to be resolved by Engineering in the RFC.
8. System Flow + User Stories + ACs
8.1. System Flow
Flow: One CID Multiple WABA — Shared Balance Pool Lifecycle Type: State Lifecycle / System Journey
- Billing V3 organization is created with
billing_version = "3.0.0"(set via Flipper flagsm1_create_subscription_chat/m1_create_subscription_crmin moderator-be). - Company registers one or more WABA IDs via Meta Coexistence embedded signup. Each WABA is stored as a
WhatsappPackagerow withorganization_package_idFK pointing to the sameOrganizationPackagerecord. - Monthly billing cycle starts: WABI reset job runs →
organization_package_component_initials.remaining_quotais reset to the subscription-defined max (e.g., 5,000). Previous remaining WABI is discarded. - Agent sends a WhatsApp message from any WABA ID:
- System identifies the
OrganizationPackageviawaba_id→organization_package_idlookup. - Billing checks if message is billable (non-failed status).
- If billable: deduct from WABI first (
organization_package_component_initials). - If WABI exhausted: deduct from WAB-Additional (
organization_package_component_additionals). - If WAB-Additional exhausted: deduct from Postpaid Limit (
organization_package_component_postpaid_limits). - If Postpaid Limit also exhausted (0): block further messages; return "Quota Exceeded" error.
WaConversationLogrecord created withwaba_id,organization_package_id,credited_to(which bucket),total_price.
- System identifies the
- Admin views Package Usage page:
GET /billings/inforeturns aggregated balance (sum of all WABAs' buckets for this org).- Shared Balance Tooltip is visible; hovering it explains the pool model.
- Report tables (Broadcast, MCC Logs) show
waba_idcolumn whenbilling_report_show_waba_idpreference is ON.
- Balance drops below low-balance threshold:
- System detects aggregated balance crossing threshold post-deduction.
low_balance_warningevent emitted → email sent to Admin + in-app banner displayed.- If balance < 0:
balance_below_zeroevent emitted → distinct below-zero notification sent.
- Admin tops up WAB-Additional balance (via Finance PI or self top-up flow):
organization_package_component_additionals.remaining_quotaincreases by the top-up amount.- Low balance banner dismissed on next page load.
- Contract renewal:
- New contract ID generated.
- WAB-Additional
remaining_quotais carried over to the new contract period. - WABI resets to new subscription max per the monthly reset job.
Failure branches:
- Postpaid Limit fully consumed (0) → message blocked;
quota_exceedederror returned to hub-service. - Deduction optimistic lock conflict → retry up to 3 times; if all fail → log
billing_deduction_failed, flag for manual reconciliation, message delivery not blocked. - Monthly reset job failure → retry 3 times; if still failing → on-call alert; do not silently skip reset.
flowchart TD
Create[V3 Org created\nbilling_version = 3.0.0] --> RegisterWABA[Register WABA IDs via\nMeta Embedded Signup\nWhatsappPackage rows\nlinked via organization_package_id]
RegisterWABA --> CycleStart[Billing Cycle Start:\nWABI Reset Job runs\ninitial_quota restored\nno carry-over]
CycleStart --> Send[Agent sends message\nfrom any WABA ID]
Send --> Billable{Billable\nmessage?}
Billable -- No --> NoDeduct[No deduction]
Billable -- Yes --> CheckWABI{WABI\nremaining > 0?}
CheckWABI -- Yes --> DeductWABI[Deduct from WABI\ncredited_to=wa_balance_initial]
CheckWABI -- No --> CheckAdditional{WAB-Additional\nremaining > 0?}
CheckAdditional -- Yes --> DeductAdditional[Deduct from WAB-Additional\ncredited_to=wa_balance]
CheckAdditional -- No --> CheckPostpaid{Postpaid Limit\n> 0?}
CheckPostpaid -- Yes --> DeductPostpaid[Deduct from Postpaid Limit\ncredited_to=wa_credit]
CheckPostpaid -- No --> Block[Block message\nQuota Exceeded error]
DeductWABI & DeductAdditional & DeductPostpaid --> LogConv[WaConversationLog created\nwaba_id, credited_to, total_price]
LogConv --> ThresholdCheck{Aggregated balance\nbelow threshold?}
ThresholdCheck -- Yes --> LowBal[Emit low_balance_warning\nEmail + in-app banner]
ThresholdCheck -- Below 0 --> BelowZero[Emit balance_below_zero\nDistinct notification]
ThresholdCheck -- No --> Done[Continue]
8.2. User Stories
| User Story | Importance | Mockup / Technical Notes | Acceptance Criteria |
|---|---|---|---|
| [WABA-S01] — WABI Monthly Reset & Deduction Priority As a System, I want to reset the WhatsApp Initial Balance (WABI) to the subscription-defined max at the start of each billing cycle and always deduct from WABI before any other bucket, so that clients receive their allocated free credits each month and use them first. | Must Have | Figma: N/A — backend logic only Data Fields: • organization_package_component_initials.remaining_quota (decimal) — resets to initial_quota value on billing cycle start• BillingComponent.is_initial_monthly_reset (bool) — must be true for WhatsApp Initial component• BillingComponent.is_carry_over_monthly (bool) — must be false (no WABI carry-over)• credited_to in WaConversationLog — set to "wa_balance_initial" when deducting from WABIBefore-After Behavior: Before: WABI reset logic exists but was scoped to single-WABA organizations. After: Reset applies to the shared organization_package_component_initials pool for the entire V3 org; all WABA IDs drawing from the same pool benefit from the reset. | — Happy Path — • AC-1: Given it is the 1st day of the billing cycle for a V3 organization, when the WABI reset job runs, then organization_package_component_initials.remaining_quota is set to the subscription-defined initial_quota (e.g., 5,000) for that org.• AC-2: Given the reset completes, when the result is logged, then wabi_reset_completed event is emitted with organization_id, old_remaining, and new_initial_quota.• AC-3: Given a V3 org has 300 WABI remaining at cycle end, when the reset runs, then the 300 remaining credits are discarded and remaining_quota is set to initial_quota — no carry-over.• AC-4: Given a billable message is sent and WABI remaining_quota > 0, when the deduction runs, then the amount is deducted from organization_package_component_initials (WABI) first; WAB-Additional and Postpaid are not touched.— Error / Unhappy Path — • ERR-1: Given the WABI reset job fails for an organization after 3 retries, when the failure persists, then wabi_reset_failed is logged with organization_id and an on-call alert is triggered — the reset is not silently skipped.• ERR-2: Given a V1 or V2 organization exists, when the reset job runs, then the job does NOT process that organization — only billing_version = "3.0.0" orgs are in scope.— Permission Model — • CAN: System (scheduled job); no user action triggers this • CANNOT: No user can manually trigger or suppress the WABI reset • Unauthorized: N/A — internal cron job — UI States — • N/A — backend job; balance update reflected on next page load of Package Usage |
| [WABA-S02] — WAB-Additional Purchase & Carry-Over As a Company Admin, I want to top up "WAB-Additional" balance via Finance PI or self top-up, and have unused WAB-Additional carry over when my contract renews, so that I never lose credits I've paid for. | Must Have | Figma: Figma — Subscription (frame-level TBD) Data Fields: • organization_package_component_additionals.remaining_quota (decimal) — increases by top-up amount• BillingComponent.is_carry_over_contract (bool) — must be true for WhatsApp Additional component• credited_to in WaConversationLog — set to "wa_balance" when deducting from WAB-AdditionalBefore-After Behavior: Before: WAB-Additional top-up and carry-over existed for single-WABA orgs. After: Top-up increases the shared organization_package_component_additionals pool for the V3 org; carry-over on renewal transfers the remaining pool — not per-WABA balances — to the new contract period. | — Happy Path — • AC-1: Given Finance creates a Paid Invoice with +10,000 credits for a V3 org, when the payment is processed, then organization_package_component_additionals.remaining_quota increases by 10,000.• AC-2: Given a V3 admin completes a self top-up for +10,000 WAB-Additional credits, when the Mekari Pay webhook is received, then organization_package_component_additionals.remaining_quota increases by 10,000.• AC-3: Given a contract renewal occurs with 6,500 WAB-Additional remaining, when the new contract ID is generated, then the 6,500 balance transfers to the new contract's organization_package_component_additionals.remaining_quota and wab_additional_carried_over is logged with old_contract_id, new_contract_id, carried_amount = 6,500.• AC-4: Given WABI remaining_quota = 0 and a billable message is sent, when the deduction runs, then credits are taken from organization_package_component_additionals (WAB-Additional) — not from Postpaid.— Error / Unhappy Path — • ERR-1: Given the carry-over job fails after 3 retries, when the failure is confirmed, then wab_additional_carry_over_failed is logged and on-call is alerted — the remaining balance is never silently discarded.— Permission Model — • CAN: Qontak Finance Team (Paid PI), Company Admin (self top-up via existing self-topup flow) • CANNOT: CS Agents cannot initiate top-ups • Unauthorized: Top-up attempt by non-authorized role is rejected at the application level — UI States — • N/A — balance update reflected on next Package Usage page load after fulfillment — Negative Scenarios — • NEG-1: Given a V1 or V2 org Admin, when a top-up is processed, then the top-up does NOT flow through the V3 component quota tables — it uses the existing wa_balance field on OrganizationPackage unchanged. |
| [WABA-S03] — Postpaid Limit Configuration & Blocking As a Qontak Finance Team member, I want to configure a Postpaid Limit for a V3 company, and have the system block messages when that limit is fully consumed, so that clients never exceed their agreed credit ceiling. | Must Have | Figma: N/A — Modpanel configuration; no client-facing UI change Data Fields: • organization_package_component_postpaid_limits.remaining_quota (decimal) — decreases with each postpaid deduction• organization_package_component_postpaid_limits.initial_quota (decimal) — the configured postpaid ceiling set by Finance• credited_to in WaConversationLog — set to "wa_credit" when deducting from Postpaid LimitBefore-After Behavior: Before: Postpaid limit was configured at WABA level ( WhatsappPackage.postpaid_limit). After: Postpaid limit is configured once at the OrganizationPackage component level, applying to the aggregated pool shared by all WABA IDs of the V3 org. | — Happy Path — • AC-1: Given Finance configures a Postpaid Limit of 3,000 for a V3 org, when WABI and WAB-Additional are both 0 and a billable message is sent, then organization_package_component_postpaid_limits.remaining_quota decreases by the message cost.• AC-2: Given 2,500 units remain in the Postpaid Limit after deductions, when 500 billable units are consumed, then the Postpaid Limit decreases to 2,000. • AC-3: Given Postpaid Limit remaining_quota = 0 (fully consumed) and WABI and WAB-Additional are both 0, when a message is attempted, then the API returns a "Quota Exceeded" error — the message is blocked.— Error / Unhappy Path — • ERR-1: Given a multi-bucket deduction spans WABI (500), WAB-Additional (400), and Postpaid (100) for a single billing event of 1,000 units, when the deduction runs, then each bucket is reduced by the corresponding amount in a single atomic-like operation — partial deduction from wrong buckets must not occur. — Permission Model — • CAN: Qontak Finance Team (configure via Modpanel) • CANNOT: Company Admin cannot modify Postpaid Limit directly • Unauthorized: Modpanel access required; unauthorized access to limit configuration returns 403 — UI States — • N/A — Modpanel configuration; client-facing display covered by CHG-003 (aggregated balance display) |
| [WABA-S04] — Multi-Bucket Balance Deduction Hierarchy As a System, I want to deduct usage from the correct bucket in strict order (WABI → WAB-Additional → Postpaid Limit), and never deduct for failed messages, so that clients are billed accurately and in the correct priority. | Must Have | Figma: N/A — backend deduction logic Data Fields: • WaConversationLog.credited_to (string) — one of "wa_balance_initial", "wa_balance", "wa_credit"; identifies which bucket was used• WaConversationLog.waba_id (string) — WABA ID that originated the conversation• WaConversationLog.is_auto_deduct (bool) — true for system-triggered deductions• Conversation status field — deduction only occurs for billable statuses (not "failed")Before-After Behavior: Before: Deduction hierarchy was applied at per-WABA WhatsappPackage level using wa_credit/wa_balance/wa_balance_initial fields. After: Deduction hierarchy operates on shared component quota tables (initials → additionals → postpaid_limits) for V3 orgs, applying the same priority logic across all WABAs sharing the pool. | — Happy Path — • AC-1: Given a V3 org has WABI = 500, WAB-Additional = 400, Postpaid = 100, and a billable event of 1,000 units occurs, when deduction runs, then 500 is taken from WABI, 400 from WAB-Additional, and 100 from Postpaid — all three buckets are updated correctly in that order. • AC-2: Given a conversation has status = "failed", when the billing job evaluates it, then no balance is deducted from any bucket and the WaConversationLog record is created with total_price = 0.• AC-3: Given the deduction crosses a bucket boundary (WABI partially consumed, remainder from WAB-Additional), when the event is logged, then two WaConversationLog entries are created — one per bucket — or a single entry with credited_to reflecting the primary bucket (Engineering to determine granularity in RFC).• AC-4: Given a V3 org sends messages from WABA-1 and WABA-2 concurrently, when both deductions run simultaneously, then the optimistic lock mechanism in quota_management/deduction.go ensures the shared pool is not over-deducted — total deducted ≤ available quota.— Error / Unhappy Path — • ERR-1: Given an optimistic lock conflict occurs during deduction, when the retry mechanism activates, then the deduction is retried up to 3 times before logging billing_deduction_failed and flagging for manual reconciliation — message delivery is not blocked.• ERR-2: Given Postpaid Limit is fully consumed, when a message is attempted, then the system returns quota_exceeded error; the WaConversationLog record is NOT created for the blocked message.— Permission Model — • CAN: System (automated deduction triggered per message event) • CANNOT: No user can manually trigger or override the deduction hierarchy • Unauthorized: N/A — internal system behavior — UI States — • N/A — backend only; balance changes visible on next Package Usage page load |
| [WABA-S05] — WABA ID Column in Usage Reports As a Company Admin, I want to see which specific WABA ID consumed balance in the Broadcast and Conversation reports, so that I can track per-phone-number usage even though the balance pool is shared. | Must Have | Figma: Figma — Subscription (frame-level TBD — see sample Google Sheet) Data Fields: • WaConversationLog.waba_id (string) — sourced from the sending channel• billing_report_show_waba_id (bool) — per-CID preference; default OFF• hub-service GET /mcc_logs — accepts optional waba_id query param when preference is ON• hub-service GET /download_broadcast_deduction — includes waba_id in CSV when preference is ONBefore-After Behavior: Before: No waba_id column in Broadcast or Conversation report tables; no per-WABA filter available. After: waba_id column visible in both tables and CSV exports when billing_report_show_waba_id = ON; MCC log table gains a waba_id filter dropdown. | — Happy Path — • AC-1: Given billing_report_show_waba_id = ON for a V3 CID, when the Package Usage page loads the Broadcast deduction table, then a waba_id column is visible and populated for every row.• AC-2: Given billing_report_show_waba_id = ON, when the Admin downloads the Broadcast deduction CSV, then every row includes a waba_id field populated with the sending WABA ID.• AC-3: Given billing_report_show_waba_id = ON, when the Admin views the Conversation/MCC log table, then a waba_id column is visible and a waba_id filter dropdown is available.• AC-4: Given the Admin filters the MCC log by a specific waba_id, when the filter is applied, then only rows matching that WABA ID are returned.• AC-5: Given billing_report_show_waba_id = OFF for a CID, when the Admin views Broadcast or Conversation reports, then no waba_id column or filter is shown — the table renders as today.— Error / Unhappy Path — • ERR-1: Given the Admin applies a waba_id filter with a value that has no matching logs for the org, when the query returns, then an empty table with "No records found" message is shown — not an error state.• ERR-2: Given the CSV export times out (> 10s for > 10,000 rows), when the timeout occurs, then the download fails with "Export is taking too long. Try reducing the date range." and broadcast_report_export_timeout is logged.— Permission Model — • CAN: Any role with Package Usage page access (Admin, Finance Team, Supervisor where permitted) • CANNOT: Access is not granted to roles that cannot view Package Usage today — no change to existing role restrictions • Unauthorized: billing_report_show_waba_id = OFF → column and filter silently absent— UI States — • Loading: Skeleton rows while report data fetches • Empty (no data): "No records found for this period." • Empty (filter applied, no match): "No records found for WABA ID [id]." • Error: "Could not load report. Please refresh." + Retry button • Success: Table with waba_id column populated— Negative Scenarios — • NEG-1: Given a V1 or V2 org Admin views the Package Usage report, when the report loads, then no waba_id column or filter is shown regardless of any preference state — V1/V2 UI is unchanged. |
| [WABA-S06] — Company-Level Low Balance Notifications As a Company Admin, I want to be notified by email and in-app banner when the aggregated company WhatsApp balance is low, empty, or below zero, so that I can top up before service is interrupted. | Must Have | Figma: Figma — Subscription (Low Balance) (frame-level TBD) Data Fields: • Aggregated balance = WABI remaining_quota + WAB-Additional remaining_quota + available Postpaid Limit• low_balance_threshold — configurable per org (TBD — see Open Questions); triggers low_balance_warning event• balance_below_zero event — separate event when aggregated balance < 0• Notification recipients: Admin role on the company account (same logic as current per-WABA notification recipients) Before-After Behavior: Before: Low balance notifications were sent per WABA based on individual WABA-level balance. After: Notifications evaluate the aggregated company-level balance — one notification per threshold crossing per company, not per WABA. Delivery mechanism (email + in-app banner) is unchanged. | — Happy Path — • AC-1: Given the aggregated balance drops below the configured low-balance threshold after a deduction event, when the system evaluates the threshold, then a low_balance_warning event is emitted and the Admin receives an email notification and sees a low-balance banner in the dashboard.• AC-2: Given the aggregated balance drops below 0 (due to postpaid settlement), when the balance update occurs, then a balance_below_zero event is emitted and a distinct "balance below zero" email and banner are triggered — separate from the standard low-balance notification.• AC-3: Given the Admin receives a low-balance notification and tops up, when the top-up is fulfilled and the balance rises above the threshold, then the low-balance banner is cleared on the next page load. — Error / Unhappy Path — • ERR-1: Given the notification delivery fails after 3 retry attempts, when the failure is logged, then low_balance_notification_failed is recorded with organization_id, balance_at_trigger, and threshold — message deduction is never blocked by notification failure.• ERR-2: Given the same threshold is crossed multiple times within a single billing cycle due to repeated top-ups and depletions, when the event fires, then the notification must not be duplicate-sent — a cooldown or deduplication mechanism is applied (Engineering to specify in RFC). — Permission Model — • CAN: System (emits notification); Admin (receives email and sees banner) • CANNOT: CS Agents do not receive low balance email notifications (same as today) • Unauthorized: N/A — system-driven notification — UI States — • Banner hidden: Balance above threshold or V1/V2 org • Low balance banner: Yellow/warning banner displayed in dashboard header area • Below zero banner: Red/critical banner with "Your WhatsApp balance is below 0" message • Banner dismissed: Auto-dismissed on next load after balance is restored above threshold |
| [WABA-S07] — Shared Balance Tooltip on Package Usage Page As a Company Admin, I want to see an informational tooltip on the Package Usage page explaining that the displayed balance is shared across all my WABA IDs, so that I understand the pool model and don't mistake the total for a single-WABA balance. | Must Have | Figma: Figma — Subscription (frame-level TBD) Data Fields: • billing_version (string) — gating condition; tooltip rendered only when billing_version = "3.0.0"• billingM1Version (bool) — frontend computed flag in hub-chat's PackageInfo type: billing_version === "3.0.0"• Tooltip content is static — no API call needed to render it Before-After Behavior: Before: WhatsApp balance section on Package Usage page shows no contextual explanation of the pool model. After: A static info icon (ⓘ) is displayed next to the WhatsApp balance section heading; on hover/click it shows a tooltip explaining the shared pool model. Visible only for V3 orgs. | — Happy Path — • AC-1: Given a V3 org Admin ( billing_version = "3.0.0") is on the Package Usage page, when the WhatsApp balance section renders, then an info icon (ⓘ) is displayed next to the balance heading.• AC-2: Given the info icon is visible, when the Admin hovers or clicks it, then a tooltip appears with text explaining: "This balance is shared across all your WhatsApp numbers (WABA IDs). Any message sent by any connected WABA draws from this single pool." • AC-3: Given a V1 or V2 org Admin is on the Package Usage page, when the WhatsApp balance section renders, then no info icon or tooltip is displayed — the section appears exactly as it does today. — Error / Unhappy Path — • ERR-1: Given the billingM1Version flag cannot be resolved (billing info API fails), when the page renders, then the tooltip icon defaults to hidden — fail-safe to avoid rendering confusing UI for non-V3 users.— Permission Model — • CAN: Any role with Package Usage page access on a V3 org • CANNOT: V1/V2 org users — tooltip not rendered • Unauthorized: N/A — display-only component — UI States — • Hidden: billing_version ≠ "3.0.0" or billing info not loaded• Idle: Info icon displayed next to balance heading • Active: Tooltip popover visible with explanatory text • Error: Tooltip icon hidden (fail-safe when billing version unknown) — Negative Scenarios — • NEG-1: Given a V1 or V2 org Admin views the Package Usage page, when the page renders, then no tooltip icon is shown — per Non-Goal 2 (V1/V2 not in scope). |
AC ids are numbered per story (each story restarts at AC-1). Composite ids minted here:
WABA-S01/AC-1throughWABA-S01/AC-4,WABA-S01/ERR-1,WABA-S01/ERR-2;WABA-S02/AC-1throughWABA-S02/AC-4,WABA-S02/ERR-1;WABA-S03/AC-1throughWABA-S03/AC-3,WABA-S03/ERR-1;WABA-S04/AC-1throughWABA-S04/AC-4,WABA-S04/ERR-1,WABA-S04/ERR-2;WABA-S05/AC-1throughWABA-S05/AC-5,WABA-S05/ERR-1,WABA-S05/ERR-2;WABA-S06/AC-1throughWABA-S06/AC-3,WABA-S06/ERR-1,WABA-S06/ERR-2;WABA-S07/AC-1throughWABA-S07/AC-3,WABA-S07/ERR-1.
9. Rollout
| Field | Value |
|---|---|
| Feature flag | billing_report_show_waba_id — default: OFF. Enabled per CID by Bifrost ops to surface the waba_id column in reports. The shared balance pool, deduction hierarchy, and tooltip are active for all V3 orgs (billing_version = "3.0.0") without a separate toggle. |
| Billing version gate | billing_version = "3.0.0" is a hard gate for all shared-pool behaviors. V1/V2 orgs are entirely unaffected — no rollout risk to existing clients. |
| Stage 1 | Internal Bifrost test V3 CIDs only (≤5 accounts, manually enabled) — validate deduction hierarchy, WABI reset, and aggregated balance display |
| Stage 2 | Closed beta: 5–10 selected Billing V3 clients with multiple WABA IDs registered — validate tooltip UX, report columns, and low balance notifications with real data |
| Stage 3 | All Billing V3 clients in batches (25% → 50% → 100%) — enable billing_report_show_waba_id per batch |
| GA | All Billing V3 clients — billing_report_show_waba_id = ON by default for new V3 CIDs; existing V3 CIDs migrated in batch |
| Backward compat | Yes — V1/V2 orgs are completely unaffected. All per-WABA wa_credit/wa_balance/wa_balance_initial fields on WhatsappPackage remain unchanged and functional for non-V3 orgs. |
| Migration | WhatsappPackage.organization_package_id FK already exists (migration 20260113000000). Remaining migration: seed billing_report_show_waba_id preference row for all V3 CIDs (default OFF). No balance data migration required — the component quota tables are already populated for active V3 orgs. |
9.1. Migration Transition Window
| Field | Detail |
|---|---|
| Old record behavior | WaConversationLog records created before this PRD ships do not have meaningful waba_id population for multi-WABA aggregation. These records appear in reports with whatever waba_id was stored at creation time — no backfill is performed. |
| New record behavior | All WaConversationLog records created after GA have waba_id correctly populated and linked to the shared organization_package_id pool. |
| Coexistence period | From Stage 1 until all V3 CIDs are migrated to GA (estimated 4–6 weeks). During this window, some V3 orgs have the new pool model while others still see old per-WABA display — gated by billing_report_show_waba_id preference. |
| End state | All V3 CIDs on shared pool model with billing_report_show_waba_id = ON. Historical logs pre-GA remain as-is with no retrospective waba_id enrichment. |
10. Observability
Key Events:
| Event Name | Trigger | Properties |
|---|---|---|
wabi_reset_completed | Monthly WABI reset job completes for an org | organization_id, old_remaining, new_initial_quota, timestamp |
wabi_reset_failed | WABI reset job fails after 3 retries | organization_id, error, timestamp |
wab_additional_carried_over | WAB-Additional carry-over on contract renewal | organization_id, old_contract_id, new_contract_id, carried_amount |
wab_additional_carry_over_failed | Carry-over fails after 3 retries | organization_id, error, timestamp |
billing_deduction_completed | Successful multi-bucket deduction | organization_id, waba_id, conversation_id, credited_to, amount, buckets_used (array) |
billing_deduction_failed | Deduction fails after all retries | organization_id, waba_id, conversation_id, error |
quota_exceeded | Message blocked due to all buckets exhausted | organization_id, waba_id |
low_balance_warning | Aggregated balance crosses low-balance threshold | organization_id, aggregated_balance, threshold |
balance_below_zero | Aggregated balance drops below 0 | organization_id, aggregated_balance |
low_balance_notification_failed | Notification delivery fails after retries | organization_id, balance_at_trigger, threshold, error |
broadcast_report_export_timeout | CSV export times out | organization_id, row_count_estimate, date_range |
billing_aggregate_balance_timeout | Aggregate balance query exceeds 500ms p95 | organization_id, query_duration_ms |
| Field | Detail |
|---|---|
| Dashboard owner | Bifrost team — existing Datadog billing dashboard; add panels for wabi_reset_*, billing_deduction_failed, quota_exceeded, low_balance_* event rates |
| Alert 1 | billing_deduction_failed rate > 0.1% of deduction events in 5-minute window → PagerDuty: Bifrost on-call |
| Alert 2 | wabi_reset_failed for any org → Slack: #bifrost-billing-alerts within 15 minutes of job run |
| Alert 3 | billing_aggregate_balance_timeout rate > 1% of requests in 5-minute window → PagerDuty: Bifrost on-call |
10.1. Post-Launch Monitoring Cadence
| Field | Detail |
|---|---|
| Review cadence | Weekly for first 4 weeks post-GA, then monthly |
| Owner | Bifrost PM + Bifrost Eng Lead |
| Review scope | wabi_reset_completed/failed counts; billing_deduction_failed rate; quota_exceeded rate; low_balance_warning and balance_below_zero event volume; billing_aggregate_balance_timeout rate |
| Trigger threshold 1 | billing_deduction_failed rate > 0.5% in any week → immediate investigation; PM disables billing_report_show_waba_id preference for affected CIDs pending root cause |
| Trigger threshold 2 | wabi_reset_failed for > 3 organizations in a single cycle → escalate to engineering sprint priority; affected orgs receive manual reset within 24 hours |
| Rollback consideration | If billing_deduction_failed or quota_exceeded rates exceed thresholds and cannot be resolved within 4 hours, PM disables the shared-pool deduction path for affected CIDs by reverting billing_version check in the deduction service — decision made jointly with Eng Lead. |
11. Success Metrics
Stability & Accuracy:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| ⭐ Billing deduction accuracy | % of billable conversations with correct bucket deduction (verified by WaConversationLog.credited_to audit vs. expected order) | N/A — new multi-bucket model for V3 | 100% accurate within 30 days of GA; zero deduction order violations reported |
| Zero critical incidents | Count of P0/P1 incidents related to balance deduction, WABI reset, or notification in first 30 days | N/A | 0 P0/P1 incidents in first 30 days post-GA |
| WABI reset reliability | % of V3 orgs for which WABI reset completes successfully on billing cycle start | N/A | ≥ 99.9% successful resets per cycle |
Adoption & Engagement:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
Report waba_id column visibility | % of V3 multi-WABA CIDs with billing_report_show_waba_id = ON | 0% (new feature) | ≥ 70% of eligible V3 CIDs enabled within 60 days of GA |
| Tooltip interaction rate | % of V3 Admin sessions on Package Usage page that interact with the Shared Balance Tooltip (hover/click) | 0% (new feature) | ≥ 20% of V3 sessions within 30 days of GA (signals comprehension of pool model) |
Business Impact:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| Finance reconciliation time | Avg time for Finance team to confirm total WhatsApp credit availability for a multi-WABA client | TBD (currently manual; ≥ 5 min estimated per client) | ≤ 1 minute (single aggregated view replaces multi-WABA lookup) |
12. Launch Plan & Stage Gates
| Stage | Audience | Duration | Success Gate to Advance | Owner |
|---|---|---|---|---|
| Internal Alpha | ≤5 Bifrost internal V3 test CIDs | 2 weeks | 0 P0 bugs. WABI reset runs correctly on cycle start. Multi-bucket deduction order verified (WABI → Additional → Postpaid). Aggregated balance display matches manual sum of per-WABA balances. Tooltip renders for V3 and is absent for V1/V2. | Bifrost PM + QA |
| Closed Beta | 5–10 V3 clients with ≥2 registered WABA IDs | 2 weeks | Billing deduction accuracy = 100% (verified via WaConversationLog audit). Low balance notification received within 5 minutes of threshold crossing. waba_id column visible and correctly populated in both report types. 0 P0/P1 incidents. | Bifrost PM |
| Staged Rollout | All V3 CIDs in batches (25% → 50% → 100%) enabling billing_report_show_waba_id | 3 weeks | billing_deduction_failed rate < 0.1%. wabi_reset_failed rate = 0. V1/V2 org behavior verified unchanged by sampling ≥20 non-V3 CIDs. | Bifrost PM + Eng Lead |
| GA | All V3 CIDs | Ongoing | All staged rollout gates sustained for 2 consecutive weeks. Tooltip interaction rate ≥ 20%. Finance team confirms reconciliation time ≤ 1 min for multi-WABA clients. PMM acknowledgement received. | Bifrost PM |
13. Dependencies
| Dependency | Owning Team | Deliverable Needed | Blocking? |
|---|---|---|---|
WhatsappPackage.organization_package_id FK populated for all V3 orgs — migration 20260113000000 adds the column but all existing V3 WhatsappPackage rows must have this FK correctly set before the shared-pool deduction logic runs | Bifrost Eng | Data backfill script to set organization_package_id on all existing V3 WhatsappPackage rows; verification query confirming 100% FK coverage before Stage 1 | YES — deduction and aggregation blocked without FK |
billing_report_show_waba_id preference available in OrganizationPreference — new preference key must be seedable per CID before Staged Rollout begins | Bifrost Eng | Preference key defined in hub-service preference registry; Modpanel UI or script to enable per CID | YES for WABA-S05 — report column gating blocked without preference |
Low balance threshold configuration — the specific numeric threshold per org (or a global default) for triggering low_balance_warning must be decided and configurable before notification events can be emitted in production | Bifrost PM + Finance Team | Decision on default threshold value and whether it's configurable per org or global (see Open Questions Q1) | YES for WABA-S06 — notification logic blocked without threshold definition |
| Figma frame-level designs — existing Figma file linked in Header is present; frame-level links for each UI change (CHG-001–003, tooltip, report columns) required before RFC | Wulan Febyazzahra Putri (Bulan) | Frame-level Figma URLs for all new/modified UI components | YES for RFC — Engineering needs component specs before RFC |
Meta Coexistence embedded signup — WhatsappEmbeddedSignup in hub-service must correctly link each new WABA to the org's organization_package_id; the multi-WABA onboarding flow (POST /core/v1/whatsapp_embedded_signup/waba) must set this FK on creation | Bifrost / hub-service Eng | Confirm that POST /waba in embedded signup correctly sets organization_package_id; fix if not | YES — without this, newly registered WABAs won't share the pool |
14. Key Decisions + Alternatives Rejected
14a — Decisions Made
| Date | Decision | Rationale |
|---|---|---|
| 2026-03-11 | Shared Pool Model — all WABA IDs under a company draw from a single aggregated balance, not individual per-WABA pools | Meta Coexistence requires companies to treat all WABAs as part of one account; per-WABA isolation would contradict the coexistence model and force clients to manually rebalance credits across WABAs |
| 2026-03-11 | Billing V3 only — shared pool behavior is gated on billing_version = "3.0.0"; V1/V2 orgs are entirely unaffected | V1/V2 organizations use a different billing architecture (wa_credit/wa_balance flat fields); migrating them would require a separate initiative with higher risk |
| 2026-03-11 | Deduction priority: WABI → WAB-Additional → Postpaid | WABI is the subscription-included free allocation and must be consumed first; WAB-Additional is prepaid and should be used before incurring postpaid credit; Postpaid is the overdraft safety net of last resort |
| 2026-06-29 | waba_id column gated by billing_report_show_waba_id preference | Not all V3 orgs have multiple WABAs yet; showing the column by default when it has only one value adds noise. Preference allows controlled rollout and opt-in for orgs that need per-WABA visibility |
14b — Alternatives Rejected
| Alternative | Why Rejected | Date |
|---|---|---|
| Per-WABA balance isolation with manual rebalancing — give each WABA its own sub-quota that Finance can redistribute | Adds operational complexity (Finance must manage N quotas per company instead of 1), contradicts the Coexistence pooling intent, and creates service interruption risk when one WABA runs out while others have surplus | 2026-03-11 |
| Display both aggregated and per-WABA breakdown simultaneously — show total + per-WABA split table on Package Usage page | Overwhelming for Admins; per-WABA breakdown is available via the waba_id column in reports, which is the appropriate granular view; the balance page should show actionable credit total, not a breakdown that implies per-WABA control | 2026-03-11 |
| Apply shared pool to V1/V2 orgs via migration — migrate all existing clients to the new architecture in one go | Too high risk; V1/V2 billing uses different fields (wa_credit, wa_balance on WhatsappPackage); a forced migration without a standalone initiative for V1→V3 migration would break existing billing flows and require significant rollback planning | 2026-03-11 |
| No low balance notification change (keep per-WABA notifications) — continue sending one notification per WABA instead of one per company | Per-WABA notifications become meaningless when balance is pooled — receiving "WABA-1 is low" when the shared pool is fine is a false alarm; aggregated notification aligns with the shared-pool mental model | 2026-03-11 |
15. Open Questions
| # | Type | Question | Owner | Deadline |
|---|---|---|---|---|
| 1 | Open Question | What is the default low-balance threshold for triggering low_balance_warning? Is it a fixed global value (e.g., 10% of subscription WABI max), a Finance-configurable value per org, or a fixed amount (e.g., 500 credits)? This decision blocks WABA-S06 implementation. | Bifrost PM + Finance Team | TBD — before RFC |
| 2 | Open Question | For deductions that span multiple buckets in a single billing event (e.g., 500 from WABI + 300 from WAB-Additional), should the WaConversationLog create one record per bucket or a single record with the primary bucket in credited_to? The answer determines the reporting granularity and log schema. | Bifrost PM + Eng | TBD — before RFC |
| 3 | Open Question | Should the billing_report_show_waba_id preference be enabled by default for all new V3 CIDs, or remain OFF until explicitly enabled? If ON by default, does this require a customer communication/changelog entry? | Bifrost PM + PMM | TBD — before GA |
| 4 | Assumption | The WhatsappEmbeddedSignup flow in hub-service (POST /core/v1/whatsapp_embedded_signup/waba) correctly sets organization_package_id on new WhatsappPackage rows. If this FK is not being set at WABA registration time, the shared pool deduction will silently fail to aggregate new WABAs. This assumption must be verified by engineering before Stage 1. | Bifrost Eng | Before Stage 1 |
| 5 | Risk | The notification deduplication mechanism for low_balance_warning (preventing duplicate sends when threshold is crossed multiple times within a billing cycle) is not specified. Without this, Finance teams could receive spam notifications during volatile balance periods. Mitigation: Engineering to define a per-org per-cycle deduplication window in the RFC. | Bifrost Eng | Before RFC — must have mitigation before READY |
| 6 | Assumption | WhatsappUsageComparison table (CID vs Meta reconciliation) already tracks cid + waba_id per the existing migration 20260223000000. This table is used for Meta reconciliation only and does not need changes to support the shared pool model. To be confirmed by engineering. | Bifrost Eng | Before RFC |
PRD CHANGELOG
| Version | Date | By | Section | Type | Summary |
|---|---|---|---|---|---|
| 1.0 | 2026-06-29 | Claude | All | CREATED | Initial NEW PRD generated from Confluence source (BIF-6428) with full technical context from qontak-billing, moderator-be, hub-service, and hub-chat repositories |