Skip to main content

Task Breakdown — One CID Multiple WABA — Billing V3 Shared Balance Pool (Phase 1)

Source RFC: rfc-one-cid-multiple-waba-phase-1.md · Review: …-review.md Slicing: vertical, aligned to the RFC's 11 execution chunks (Detail 4.C) · Blocked tasks included (full picture) · Excluded: WABA-S02 carry-over. Repos (all checked out under /Users/mekari/Documents/repos/): qontak-billing (Go, master), hub_core (Ruby gem, production), hub_service (Ruby Grape, production), moderator-be (Ruby, master), hub-chat (Nuxt 3/Vue 3, master).

Definition of Done (from RFC §1 Success Criteria): deduction accuracy 100% across the shared pool (no over-deduction across WABAs); aggregated balance query p95 ≤ 500ms; V1/V2 orgs verifiably unchanged; WABI reset reliability ≥ 99.9%.

Grounding note that shapes these estimates: with one company = one organization_package confirmed, the organization_package_component_* quota tables (keyed by organization_package_id) are already a single shared pool, and the qontak-billing deduction is already company-scoped (company_id+billing_code). So the qontak-billing tasks are verify/harden + net-new event, and the functional core of "shared pool" is the hub_core change that lets N WABAs point at the one package (drop the unique FK index) plus the backfill.


Effort Summary

#TaskLayerFEBEQATotal
1Verify/harden company-scoped deduction for multi-WABA pool (qontak-billing)BE2.00.52.5
2Bounded retry + billing_deduction_failed on exhaustion (qontak-billing)BE1.50.52.0
3Drop unique FK index + many-WABA package creation (hub_core)BE2.50.53.0
4Backfill organization_package_id for V3 + verify (hub_core)BE1.50.52.0
5/billings/info aggregated balance + billing_version + cache (hub_core/hub_service)BE3.01.04.0
6Net-new balance_below_zero event + dedup (qontak-billing)BE2.00.52.5
7billing_version in BillingStore + show_waba_id in AppConfig (hub-chat)FE1.00.51.5
8Re-gate waba_id column to preference on both tables (hub-chat)FE1.00.51.5
9waba_id filter dropdown on MCC view — net-new (hub-chat)FE1.50.52.0
10SharedBalanceTooltip + aggregated balance display (hub-chat)FE2.00.52.5
11LowBalanceBanner (warning + below-zero) (hub-chat)FE1.50.52.0
Grand total7.012.56.025.5

Confidence: medium. Two unknowns can move this: (1) whether qontak-billing truly needs zero deduction-logic change under the confirmed 1-company-1-package model (could shrink Task 1) vs. needing a waba_id→conversation-log propagation fix (could grow it); (2) FE Tasks 9–11 are blocked on the FE-owner task (RFC Detail 2.F) — their estimates are provisional until component contracts + frames land. Backend Tasks 1–6 are well-grounded and independently sizable.


Task 1: [BE] Verify & harden company-scoped deduction for the multi-WABA shared pool (WABA-S04) — qontak-billing

A message sent from any of a company's WABAs draws from the single shared pool in WABI→Additional→Postpaid order, and total deductions never exceed available quota.

Status: ✅ Actionable

What to build

Confirm (and harden with tests) that GetQuotaForUpdate — filtered by company_id — returns the company's single pool regardless of which WABA triggered the deduction, that the WABI→Additional→Postpaid hierarchy is unchanged, and that the originating waba_id (passed in extra_attrs) is persisted for reporting. No schema change: cardinality (1 company = 1 organization_package) is confirmed, so the existing query is already pool-correct.

Implementation Plan

ActionFileWhat changes
verifyinternal/app/usecase/quota_management/deduction.goconfirm hierarchy L169-241 + requeue L71-90 unchanged; ensure req.ExtraAttrs.waba_id flows to the log write L264-284
verifydb/queries/organization_package_components.sqlGetQuotaForUpdate L87-137 returns the single pool for company_id (no per-WABA branch)
extendinternal/app/usecase/quota_management/deduction_test.goadd cases: 2+ WABAs on one company deduct from one pool; 500/400/100 split across buckets; failed msg → no row; -race concurrent multi-WABA never over-deducts

Implementation steps

  1. Explore — open internal/app/usecase/quota_management/deduction.go; re-read the bucket branch (L169-241) and the CreateBillingLog call (L264-284); confirm whether waba_id is written (via ExtraAttrs or a column). If not persisted for reporting, add it to the log write.
  2. Red — add table-driven tests in deduction_test.go for: multi-WABA same-company pool draw, cross-bucket split, non-billable skip, and a concurrent (-race) two-WABA deduction asserting remaining never goes below what a serial run would give.
  3. Confirm query — verify GetQuotaForUpdate in db/queries/organization_package_components.sql has no waba_id predicate and keys on company_id; regenerate sqlc only if a query is touched (sqlc generate).
  4. Greenmake test until the new cases pass under -race.
  5. Quality gatemake test clean; staticcheck per staticcheck.conf.

Acceptance criteria

  • Two WABAs under one company deducting concurrently draw from the same pool; total deducted ≤ available (verified under go test -race).
  • A single 1000-unit event with WABI=500/Add=400/Postpaid=100 deducts 500/400/100 in order (WABA-S04/AC-1).
  • A status=failed (non-billable) event writes no billing_log row / total_price=0 (WABA-S04/AC-2).
  • The originating waba_id is persisted on the deduction record for reporting.

Test strategy

Table-driven Go tests against the test Postgres pool (internal/app/repository/main_test.go harness); key mock is the seeded quota rows; key assertion is post-deduction remaining_quota per bucket and the -race no-over-deduction invariant.

Effort estimate

DisciplineDays
Frontend
Backend2.0
QA0.5
Total2.5

Assumptions: no deduction-logic rewrite needed under confirmed cardinality; grows to ~3 BE if waba_id propagation to the conversation log turns out to be missing.

Run to verify

cd /Users/mekari/Documents/repos/qontak-billing && make test

Depends on

  • None (can start immediately)

Task 2: [BE] Bounded retry + billing_deduction_failed on exhaustion (WABA-S04/ERR-1) — qontak-billing

When two WABAs collide on the shared pool, the deduction retries a bounded number of times and, if it still can't apply, is logged for manual reconciliation without ever blocking message delivery.

Status: ✅ Actionable

What to build

Make the existing requeue-on-lock-conflict path (rowAffected == 0 → re-enqueue) bounded to an explicit attempt count (align to PRD's "3"), and on exhaustion emit a named billing_deduction_failed log/metric with organization_id, waba_id, conversation_id.

Implementation Plan

ActionFileWhat changes
extendinternal/app/usecase/quota_management/deduction.gobound the requeue (L71-90) to N attempts via job max_fails; on exhaustion slog + metric billing_deduction_failed
verifyjob/worker config (deduction worker registration)confirm/adjust max_fails for the deduction job
extendinternal/app/usecase/quota_management/deduction_test.goassert requeue on conflict, and billing_deduction_failed after N failures

Implementation steps

  1. Explore — open deduction.go L71-90; find where EnqueueQuotaManagementDeduction is called and where the deduction worker's retry policy (max_fails) is configured.
  2. Red — add a test forcing repeated rowAffected == 0 and assert the job stops after N attempts and logs billing_deduction_failed.
  3. Implement — thread an attempt count (or rely on max_fails) and add the exhaustion slog.ErrorContext + metric.
  4. Greenmake test.
  5. Quality gatemake test + staticcheck.

Acceptance criteria

  • Lock conflict re-enqueues the deduction (existing behavior preserved).
  • After the bounded attempts, billing_deduction_failed is logged with organization_id, waba_id, conversation_id.
  • Message delivery is never blocked by a deduction failure (no synchronous error surfaced to the caller).

Test strategy

Go test injecting a stale lock_version to force rowAffected == 0; assert requeue count and the terminal log/metric; key assertion is the attempt bound.

Effort estimate

DisciplineDays
Frontend
Backend1.5
QA0.5
Total2.0

Assumptions: the job queue (gocraft/work) already supports max_fails; only the exhaustion log/metric is net-new.

Run to verify

cd /Users/mekari/Documents/repos/qontak-billing && make test

Depends on

  • Task 1 (shared test scaffolding for deduction) — soft; can run in parallel.

Task 3: [BE] Drop unique FK index + let N WABAs share one OrganizationPackage (WABA-S04, Decision 1) — hub_core

Registering a 2nd/3rd WABA under a company links it to that company's single balance pool instead of getting its own — the schema-level foundation of the shared pool.

Status: ✅ Actionable

What to build

A migration that drops the unique index on whatsapp_packages.organization_package_id and recreates it non-unique, plus a change to the WABA-creation path so a company's additional WABAs create their own WhatsappPackage row against the same organization_package_id (gated by the existing waba_save_organization_id preference), rather than short-circuiting on "package already exists".

Implementation Plan

ActionFileWhat changes
createdatabase/billing/db/migrate/2026XXXXXXXXXX_drop_unique_org_package_id_index_on_whatsapp_packages.rbdrop index_whatsapp_packages_on_organization_package_id (unique), re-add non-unique
extendapp/core/domains/services/billing/wa_package.rbin create (L38-76): stop returning early when a package exists for the org so an additional WABA creates a new WhatsappPackage on the same organization_package_id (L213-240 create_whatsapp_package) — gated by fetch_waba_by_package_id_enabled?
verifyapp/core/domains/repositories/billings/helpers.rbfind_whatsapp_package L217-232 still resolves by organization_package_id
create/extendapp/core/domains/services/billing/wa_package_spec.rb2nd WABA on same org → new row, same organization_package_id; unique-violation no longer raised

Implementation steps

  1. Explore — open app/core/domains/services/billing/wa_package.rb; read create (L38-76) and create_whatsapp_package (L213-240) and the waba_save_organization_id gate in helpers.rb.
  2. Red — in wa_package_spec.rb, add a spec: given a V3 org with one WABA package, registering a second WABA (feature ON) creates a second WhatsappPackage row with the same organization_package_id and does not raise.
  3. Migration — write the drop+recreate-non-unique migration under database/billing/db/migrate/.
  4. Logic — adjust the early-return in create so additional WABAs proceed to create_whatsapp_package when use_org_package_id is true.
  5. GreenRAILS_ENV=test bundle exec rails app:db:migrate then bundle exec rspec app/core/domains/services/billing/wa_package_spec.rb.
  6. Quality gate — full rspec app --tag ~@is_skip_pipeline for the billing area; confirm migrate down works.

Acceptance criteria

  • Migration up drops the unique index and adds a non-unique index; migrate down restores it.
  • With waba_save_organization_id ON, a company's 2nd WABA creates a new WhatsappPackage row sharing the existing organization_package_id.
  • With the flag OFF, legacy per-WABA behavior is unchanged (V1/V2 safe).

Test strategy

RSpec against the billing DB; key assertion is two WhatsappPackage rows with the same organization_package_id after a second registration, and no ActiveRecord::RecordNotUnique.

Effort estimate

DisciplineDays
Frontend
Backend2.5
QA0.5
Total3.0

Assumptions: the waba_save_organization_id preference already gates the new flow (verified); no consumer relies on the 1:1 uniqueness (verify via grep before merge).

Run to verify

cd /Users/mekari/Documents/repos/hub_core && RAILS_ENV=test bundle exec rspec app/core/domains/services/billing/wa_package_spec.rb

Depends on

  • None (critical-path foundation).

Task 4: [BE] Backfill organization_package_id for existing V3 WhatsappPackages + verify (Dependency, WABA-S04) — hub_core

Every existing V3 company's WABAs are linked to its shared pool so deduction/aggregation work for current customers, not just newly-registered ones.

Status: ✅ Actionable

What to build

An idempotent backfill (rake task or script) that sets organization_package_id on existing V3 WhatsappPackage rows that lack it, plus a verification query asserting 100% coverage before Stage 1.

Implementation Plan

ActionFileWhat changes
createlib/tasks/billing/backfill_organization_package_id.rakebatched, idempotent backfill for V3 rows; logs progress + final coverage count
createapp/core/.../backfill_organization_package_id_spec.rb (or rake spec)asserts rows get the FK; re-run is a no-op

Implementation steps

  1. Explore — confirm how V3 orgs are identified (billing_v3? / is_billing_v3) and how OrganizationPackageWhatsappPackage relate (organization_package.rb).
  2. Red — spec: seed V3 WhatsappPackage rows without organization_package_id; run task; assert all populated; second run changes nothing.
  3. Implement — batched update (respect production traffic); emit a verification count (WhatsappPackage V3 rows with NULL organization_package_id == 0).
  4. Green — run the spec; dry-run on staging.
  5. Quality gaterspec clean; document the verification query in the task output.

Acceptance criteria

  • All V3 WhatsappPackage rows have a non-NULL organization_package_id after the run.
  • The task is idempotent (safe to re-run).
  • A verification query returns 0 uncovered V3 rows (gate for Stage 1).

Test strategy

RSpec seeding uncovered rows; assert coverage post-run and no-op on re-run.

Effort estimate

DisciplineDays
Frontend
Backend1.5
QA0.5
Total2.0

Assumptions: the target organization_package_id per WABA is derivable from existing org→package linkage; if ambiguous for multi-package legacy data, escalate before running (should not occur under confirmed cardinality).

Run to verify

cd /Users/mekari/Documents/repos/hub_core && RAILS_ENV=test bundle exec rspec spec/**/backfill_organization_package_id_spec.rb

Depends on

  • Task 3 (non-unique index must exist before N rows can share a package).

Task 5: [BE] /billings/info returns aggregated balance + billing_version + Redis cache (CHG-003, WABA-S07, Decision 3) — hub_core / hub_service

The Package Usage page shows a single company-level WhatsApp balance (WABI/Additional/Postpaid) for V3, and the frontend can tell a V3 org apart via billing_version.

Status: ✅ Actionable

What to build

Extend the billings/info response (built in hub_core, served by hub_service) to include billing_version and the aggregated pool balance, read-through from qontak-billing's quota info, cached in Redis with a short TTL and invalidated on deduction/top-up. On read timeout, return last-known cache and log billing_aggregate_balance_timeout.

Implementation Plan

ActionFileWhat changes
extendapp/core/domains/interactors/billings/billing_info.rb [unverified — confirm exact path; class Interactors::Billings::BillingInfo]add billing_version + aggregated buckets to the result
extendapp/core/domains/builders/billings/billing_info.rb [unverified — class Builders::Billings::BillingInfo]shape aggregated WABI/Additional/Postpaid; read via qontak-billing quota info client
extendapp/apps/billings/services/quota_management.rbadd a read of /iag/v1/quota-managements/info (company-scoped) if not already present
extendapp/services/api/core/v1/billings/resources/billings.rb (hub_service)/info L607-629 — expose new fields (Grape entity/presenter)
extendbilling info rspec (spec/.../billings_spec.rb in hub_service)assert billing_version:"3.0.0" + aggregated fields for V3; timeout → cached

Implementation steps

  1. Explore — trace GET /info (billings.rb:607-629) → Interactors::Billings::BillingInfoBuilders::Billings::BillingInfo in hub_core; confirm exact file paths (the grounding referenced them by class name — verify before editing).
  2. Red — hub_service rspec: V3 org /billings/info returns billing_version + aggregated WABI/Additional/Postpaid; V1/V2 unchanged; simulated qontak-billing timeout returns last-known cache + logs billing_aggregate_balance_timeout.
  3. Implement read — add/confirm a company-scoped read of qontak-billing /iag/v1/quota-managements/info in quota_management.rb; aggregate in the builder.
  4. Cache — Redis key billing_info:{company} short TTL; invalidate via qontak-billing PUT .../{company_id}/invalidate-cache (or key delete) on deduction/top-up.
  5. GreenRAILS_ENV=test bundle exec rspec for the billing resources spec.
  6. Quality gate — full billing-area rspec.

Acceptance criteria

  • For a V3 org, /billings/info includes billing_version:"3.0.0" and a single aggregated WABI/Additional/Postpaid figure.
  • V1/V2 responses are byte-for-byte unchanged.
  • Aggregated read p95 ≤ 500ms via cache; on upstream timeout, last-known value returned + billing_aggregate_balance_timeout logged.

Test strategy

RSpec with a stubbed qontak-billing quota-info client; assert response shape + cache hit path + timeout fallback. Follow-up (RFC REV-8/Decision 3): pin the exact aggregated field names/types and the cache TTL value in review before merge.

Effort estimate

DisciplineDays
Frontend
Backend3.0
QA1.0
Total4.0

Assumptions: qontak-billing /iag/v1/quota-managements/info returns per-bucket remaining for the company; exact interactor/builder paths confirmed in step 1.

Run to verify

cd /Users/mekari/Documents/repos/hub_service && RAILS_ENV=test bundle exec rspec spec/services/api/core/v1/billings/resources/billings_spec.rb

Depends on

  • Tasks 3 + 4 (the single pool must exist and be populated).

Task 6: [BE] Net-new balance_below_zero event + low-balance dedup (WABA-S06, Decision 5) — qontak-billing

A company gets one low-balance warning when the shared pool crosses the threshold, and a distinct "below zero" alert when it goes negative — without duplicate spam within a cycle.

Status: ✅ Actionable

What to build

Extend the existing threshold-alert path (already fires at threshold_running_out, default 40%) with a distinct balance_below_zero branch when aggregated remaining < 0, and a per-org/per-cycle dedup key (low_balance:{org}:{cycle} in Redis) so repeated crossings don't re-notify.

Implementation Plan

ActionFileWhat changes
extendinternal/app/usecase/worker/quota_management_alert.goadd balance_below_zero branch (aggregated remaining < 0) distinct from the ≤threshold warning (L97-98)
extendinternal/app/usecase/quota_management/deduction.godedup key check before enqueuing the alert (L99-103)
extendalert worker testassert below-zero emits the distinct event; duplicate crossing within a cycle is suppressed

Implementation steps

  1. Explore — open quota_management_alert.go (threshold calc L90-102) and deduction.go L99-103 / L293-306 (where shouldTriggerAlert is computed).
  2. Red — tests: aggregated remaining < 0 emits balance_below_zero (not the plain warning); second crossing within the same cycle is deduped.
  3. Implement — add the below-zero branch + a Redis dedup key with a per-cycle TTL.
  4. Greenmake test.
  5. Quality gatemake test + staticcheck.

Acceptance criteria

  • Crossing the threshold (40% default) emits low_balance_warning once per crossing (existing, verified).
  • Aggregated remaining < 0 emits a distinct balance_below_zero event.
  • Repeated crossings within one cycle do not re-notify (dedup key present).
  • Notification failure never blocks deduction.

Test strategy

Go tests over the alert worker with seeded balances above/at/below zero; key assertion is event type + dedup suppression.

Effort estimate

DisciplineDays
Frontend
Backend2.0
QA0.5
Total2.5

Assumptions: threshold reuse needs no product decision (40% default already implemented); dedup window length is a minor follow-up.

Run to verify

cd /Users/mekari/Documents/repos/qontak-billing && make test

Depends on

  • Task 1 (deduction test scaffolding) — soft.

Task 7: [FE] billing_version in BillingStore + show_waba_id in AppConfig (WABA-S07, Decision 6) — hub-chat

The frontend can reliably detect a V3 org and whether the waba_id reporting preference is enabled — the data foundation for all other FE work.

Status: ✅ Actionable (needs no design; consumes Task 5's payload)

Design reference: n/a — store/type plumbing, no UI.

What to build

Add billing_version to BillingStore.BillingInfo (currently only in useTopupStore's PackageInfo), and add billing_reports.show_waba_id to AppConfigStore.AppConfig, so a single shouldShowWabaId = pref && billingM1Version gate can be derived.

Implementation Plan

ActionFileWhat changes
extendcommon/store/BillingStore.tsadd billing_version to the BillingInfo interface (L4-19) + populate from /billings/info (L137)
extendcommon/store/AppConfigStore.tsadd show_waba_id?: boolean under billing_reports (L38-43)
extendcommon/store/__tests__/BillingStore.spec.ts [unverified — confirm test path]assert billing_version parsed; billingM1Version-style gate derivable

Implementation steps

  1. Explore — open common/store/BillingStore.ts (L4-19, L122-158) and AppConfigStore.ts (L38-43); mirror the existing billing_reports flag pattern.
  2. Red — store spec: given a /billings/info payload with billing_version:"3.0.0", billingInfo.billing_version is set; given appConfig.billing_reports.show_waba_id=true, the flag reads true.
  3. Implement — extend the interfaces + parsing.
  4. Greenpnpm test.
  5. Quality gatepnpm lint && pnpm build.

Acceptance criteria

  • BillingStore.billingInfo.billing_version is populated from /billings/info.
  • AppConfig.billing_reports.show_waba_id is available to components.
  • A shared shouldShowWabaId = show_waba_id && (billing_version === "3.0.0") computed is derivable.

Test strategy

Vitest store test with a mocked $customFetch /billings/info payload; assert parsed fields.

Effort estimate

DisciplineDays
Frontend1.0
Backend
QA0.5
Total1.5

Assumptions: /billings/info returns billing_version (delivered by Task 5); client_configs/config can carry the new billing_reports.show_waba_id flag.

Run to verify

cd /Users/mekari/Documents/repos/hub-chat && pnpm test && pnpm lint

Depends on

  • Task 5 (BE must return billing_version); backend seeding of billing_report_show_waba_id.

Task 8: [FE] Re-gate waba_id column to preference on both report tables (WABA-S05, Decision 6) — hub-chat

The waba_id column shows only for V3 orgs with the reporting preference ON — not whenever data happens to contain a waba_id — and never for V1/V2.

Status: ✅ Actionable (no new design; swaps existing gate)

Design reference: existing tables — no new frame required (column already renders). Styling per @mekari/pixel3@1.0.12.

What to build

Replace the data-driven hasWabaId visibility on both report tables with the preference-driven shouldShowWabaId = show_waba_id && billingM1Version gate from Task 7.

Implementation Plan

ActionFileWhat changes
extendfeatures/subscriptions/usages/TableComponentCampaignBroadcast.vuereplace hasWabaId (L308-310) in the column defs (L311-320) with shouldShowWabaId
extendfeatures/subscriptions/usages/TableComponentWhatsappBalance.vuereplace hasWabaId (L395-397) in column defs (L370-392) + cell v-if (L74)
extendfeatures/subscriptions/usages/__tests__/TableComponentCampaignBroadcast.spec.tscolumn shown iff pref ON AND V3; hidden for V1/V2 or pref OFF
extendfeatures/subscriptions/usages/__tests__/TableComponentWhatsappBalance.spec.ts [unverified — confirm test path]same assertions

Implementation steps

  1. Explore — open both table components; find hasWabaId computed and its use in the header/cell templates.
  2. Red — update specs: assert column visibility keyed to shouldShowWabaId (pref ON + V3), not to data presence.
  3. Implement — import the gate from Task 7; swap the computed.
  4. Greenpnpm test for both specs.
  5. Quality gatepnpm lint && pnpm build.

Acceptance criteria

  • Column visible iff billing_report_show_waba_id ON and billing_version === "3.0.0" (WABA-S05/AC-1,3,5).
  • V1/V2 orgs never show the column regardless of data (WABA-S05/NEG-1).
  • CSV column continues to be gated by the preference (BE already does this).

Test strategy

Vitest with mocked store flags across the four combinations (pref × version); assert header + cell presence.

Effort estimate

DisciplineDays
Frontend1.0
Backend
QA0.5
Total1.5

Assumptions: both tables already render the column; only the gate changes.

Run to verify

cd /Users/mekari/Documents/repos/hub-chat && pnpm test -- features/subscriptions/usages && pnpm lint

Depends on

  • Task 7 (the gate/flags).

Task 9: [FE] waba_id filter dropdown on the MCC/Conversation log view — net-new (WABA-S05/AC-3,4) — hub-chat

An admin can filter the conversation log to a single WABA ID and see only that number's usage.

Status: 🚫 Blocked — net-new UI control; needs the design frame + component contract in RFC Detail 2.F(a)(b). Unblocks when Design supplies the filter frame and the FE owner pins the dropdown's props/options/@change contract.

Design reference: n/a — design pending (RFC Detail 2.F / REV-2). File-level Figma: Subscription; DS @mekari/pixel3@1.0.12; Design QA: Bulan (TBD).

What to build

A waba_id filter dropdown on the MCC view whose selection sets the waba_id query param on GET /mcc_logs, with a no-match empty state.

Implementation Plan

ActionFileWhat changes
extendpages/subscriptions/usages/index.vueadd the filter control to the MCC view's filter section
extendfeatures/subscriptions/usages/TableComponentWhatsappBalance.vuepass selected waba_id into the /mcc_logs request (L523); no-match empty state
extendfeatures/subscriptions/usages/__tests__/TableComponentWhatsappBalance.spec.tsfilter sets waba_id param; empty state on no match

Implementation steps

  1. Unblock — obtain the filter frame + confirm the dropdown contract (options source = distinct waba_ids; selected value; @change) per Detail 2.F.
  2. Explore — open pages/subscriptions/usages/index.vue for the existing filter pattern and TableComponentWhatsappBalance.vue for the /mcc_logs call (L523).
  3. Red — spec: selecting a waba_id adds ?waba_id= to the request; no-match shows "No records found for WABA ID [id]".
  4. Implement — add the dropdown (pixel3) + wire the query param.
  5. Greenpnpm test.
  6. Quality gatepnpm lint && pnpm build.

Acceptance criteria

  • Selecting a WABA filters the MCC log to matching rows (WABA-S05/AC-4).
  • No-match shows the empty state, not an error (WABA-S05/ERR-1).
  • Filter only present when shouldShowWabaId is true.

Test strategy

Vitest asserting the request param on selection and the empty-state render; mock $customFetch.

Effort estimate

DisciplineDays
Frontend1.5
Backend
QA0.5
Total2.0

Assumptions: options come from distinct waba_ids already available client-side or a lightweight lookup; estimate firms once Detail 2.F lands.

Run to verify

cd /Users/mekari/Documents/repos/hub-chat && pnpm test -- features/subscriptions/usages/TableComponentWhatsappBalance

Depends on

  • Task 7; RFC Detail 2.F (design frame + dropdown contract) — external blocker.

Task 10: [FE] SharedBalanceTooltip + aggregated balance display (WABA-S07, CHG-003) — hub-chat

A V3 admin sees one consolidated WhatsApp balance and an info tooltip explaining it's shared across all their WABA numbers.

Status: 🚫 Blocked — net-new component + display change; needs the design frames + component contract in RFC Detail 2.F(a)(b).

Design reference: n/a — design pending (RFC Detail 2.F / REV-2). File-level Figma: Subscription; DS @mekari/pixel3@1.0.12; Design QA: Bulan (TBD).

What to build

A SharedBalanceTooltip.vue (info icon + popover, V3-only) next to the WhatsApp balance heading, and the aggregated single-figure balance display on the Package Usage page for V3.

Implementation Plan

ActionFileWhat changes
createfeatures/subscriptions/packages/SharedBalanceTooltip.vueinfo icon + v-mp-tooltip/<mp-tooltip> popover; rendered only when billingM1Version
extendfeatures/subscriptions/packages/UsageDetail.vueshow aggregated WABI/Additional/Postpaid single figures for V3 (L43-47 area)
extendfeatures/subscriptions/packages/PackageDetails.vuemount the tooltip in the balance section
createfeatures/subscriptions/packages/__tests__/SharedBalanceTooltip.spec.tsrenders for V3 only; hidden when billing info missing (fail-safe)

Implementation steps

  1. Unblock — obtain the balance + tooltip frames and confirm the tooltip's props/copy per Detail 2.F.
  2. Explore — open UsageDetail.vue (balance rows) and InvoicesDetailPage.vue:159-167 (existing <mp-tooltip> pattern).
  3. Red — spec: tooltip renders for billing_version==="3.0.0" only; hidden when billing info unresolved (WABA-S07/ERR-1).
  4. Implement — build the tooltip; switch UsageDetail to the aggregated figure for V3.
  5. Greenpnpm test.
  6. Quality gatepnpm lint && pnpm build.

Acceptance criteria

  • Info icon + tooltip render only for V3 (WABA-S07/AC-1,2); absent for V1/V2 (AC-3).
  • Tooltip hidden when billing info fails to load (fail-safe, ERR-1).
  • Balance shows a single aggregated figure per bucket for V3 (CHG-003).

Test strategy

Vitest mounting with V3/non-V3 store states; assert conditional render + aggregated figure.

Effort estimate

DisciplineDays
Frontend2.0
Backend
QA0.5
Total2.5

Assumptions: aggregated figure comes from /billings/info (Task 5); copy + placement from Detail 2.F.

Run to verify

cd /Users/mekari/Documents/repos/hub-chat && pnpm test -- features/subscriptions/packages

Depends on

  • Tasks 5 + 7; RFC Detail 2.F — external blocker.

Task 11: [FE] LowBalanceBanner — warning + below-zero (WABA-S06) — hub-chat

A V3 admin sees a warning banner when the shared balance is low and a distinct critical banner when it drops below zero, clearing once topped up.

Status: 🚫 Blocked — net-new component; needs the two banner variants' design + contract in RFC Detail 2.F(a)(b).

Design reference: n/a — design pending (RFC Detail 2.F / REV-2). File-level Figma: Subscription — Low Balance; DS @mekari/pixel3@1.0.12; Design QA: Bulan (TBD).

What to build

A LowBalanceBanner.vue with warning and below-zero variants, driven by the aggregated balance vs threshold, mounted on the Package Usage page, dismissable and auto-clearing when balance is restored.

Implementation Plan

ActionFileWhat changes
createfeatures/subscriptions/packages/LowBalanceBanner.vue<MpBanner> warning + below-zero variants; state prop; dismiss event
extendfeatures/subscriptions/packages/PackageDetails.vuemount banner above the quota section; bind to aggregated balance/threshold
createfeatures/subscriptions/packages/__tests__/LowBalanceBanner.spec.tswarning below threshold; below-zero variant; cleared when restored

Implementation steps

  1. Unblock — obtain both banner variants' frames + confirm the state/dismiss contract per Detail 2.F.
  2. Explore — open BannerRingGroup.vue for the existing <MpBanner> pattern.
  3. Red — spec: warning when balance below threshold; distinct below-zero when < 0; hidden when restored.
  4. Implement — build the banner; wire to aggregated balance from /billings/info.
  5. Greenpnpm test.
  6. Quality gatepnpm lint && pnpm build.

Acceptance criteria

  • Warning banner shows when aggregated balance is below threshold (WABA-S06/AC-1).
  • Distinct below-zero banner when balance < 0 (WABA-S06/AC-2).
  • Banner clears on next load after top-up restores balance (WABA-S06/AC-3).

Test strategy

Vitest with mocked balance states; assert variant + clear behavior.

Effort estimate

DisciplineDays
Frontend1.5
Backend
QA0.5
Total2.0

Assumptions: banner reacts to /billings/info aggregated balance + threshold; copy/variants from Detail 2.F.

Run to verify

cd /Users/mekari/Documents/repos/hub-chat && pnpm test -- features/subscriptions/packages/LowBalanceBanner

Depends on

  • Tasks 5 + 7; RFC Detail 2.F — external blocker.

Ordering rationale

  • Critical path is hub_core, not qontak-billing. Because one company = one org package is confirmed, the shared pool is created by Task 3 (drop the unique index + let N WABAs share the package) and made real for existing customers by Task 4 (backfill). Everything downstream depends on these two. Do them first.
  • qontak-billing Tasks 1, 2, 6 can run fully in parallel with the hub_core work — the deduction engine is already company-scoped, so they are verify/harden (1, 2) plus one net-new event (6), with no dependency on Task 3/4.
  • Task 5 (/billings/info) gates the entire frontend. It must land after Tasks 3/4 (pool exists) and before FE Task 7, which in turn gates Tasks 8–11.
  • Frontend splits into "actionable now" vs "design-blocked." Tasks 7–8 (store plumbing + re-gating an existing column) need no new design and can proceed as soon as Task 5 is up. Tasks 9–11 (net-new components) are blocked on RFC Detail 2.F (frames + component contracts) — push Design + the FE owner to close Detail 2.F so these unblock.
  • Externally, the one thing to chase is Detail 2.F: it is the sole blocker for 6.5 of the 7 FE days.

Skipped stories

Story / taskReason
WABA-S02 — WAB-Additional carry-over on contract renewalExcluded by request. Also RFC §5 #4: the contract-renewal trigger for carryOverAdditionalQuota (qontak-billing/.../monthly_reset_billing_component.go:422-451) wasn't fully traced — verify before scoping.
WABA-S01 — WABI monthly resetNo net-new task: reuses the existing qontak-billing monthly-reset worker unchanged (RFC Decision 4). Covered by Task 1's verification that reset applies to the single pool.
WABA-S03 — Postpaid limit configuration (Modpanel)No net-new task: reuses the existing moderator-be → qontak-billing PUT /iag/v1/quota-managements/components/{code}/update path. Add a thin moderator-be task only if Finance UX changes.
FE Tasks 9, 10, 11Included above but 🚫 blocked on RFC Detail 2.F (design frames + component contracts). Actionable portion is zero until Detail 2.F lands.