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.mdSlicing: 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
| # | Task | Layer | FE | BE | QA | Total |
|---|---|---|---|---|---|---|
| 1 | Verify/harden company-scoped deduction for multi-WABA pool (qontak-billing) | BE | — | 2.0 | 0.5 | 2.5 |
| 2 | Bounded retry + billing_deduction_failed on exhaustion (qontak-billing) | BE | — | 1.5 | 0.5 | 2.0 |
| 3 | Drop unique FK index + many-WABA package creation (hub_core) | BE | — | 2.5 | 0.5 | 3.0 |
| 4 | Backfill organization_package_id for V3 + verify (hub_core) | BE | — | 1.5 | 0.5 | 2.0 |
| 5 | /billings/info aggregated balance + billing_version + cache (hub_core/hub_service) | BE | — | 3.0 | 1.0 | 4.0 |
| 6 | Net-new balance_below_zero event + dedup (qontak-billing) | BE | — | 2.0 | 0.5 | 2.5 |
| 7 | billing_version in BillingStore + show_waba_id in AppConfig (hub-chat) | FE | 1.0 | — | 0.5 | 1.5 |
| 8 | Re-gate waba_id column to preference on both tables (hub-chat) | FE | 1.0 | — | 0.5 | 1.5 |
| 9 | waba_id filter dropdown on MCC view — net-new (hub-chat) | FE | 1.5 | — | 0.5 | 2.0 |
| 10 | SharedBalanceTooltip + aggregated balance display (hub-chat) | FE | 2.0 | — | 0.5 | 2.5 |
| 11 | LowBalanceBanner (warning + below-zero) (hub-chat) | FE | 1.5 | — | 0.5 | 2.0 |
| Grand total | 7.0 | 12.5 | 6.0 | 25.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
| Action | File | What changes |
|---|---|---|
| verify | internal/app/usecase/quota_management/deduction.go | confirm hierarchy L169-241 + requeue L71-90 unchanged; ensure req.ExtraAttrs.waba_id flows to the log write L264-284 |
| verify | db/queries/organization_package_components.sql | GetQuotaForUpdate L87-137 returns the single pool for company_id (no per-WABA branch) |
| extend | internal/app/usecase/quota_management/deduction_test.go | add 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
- Explore — open
internal/app/usecase/quota_management/deduction.go; re-read the bucket branch (L169-241) and theCreateBillingLogcall (L264-284); confirm whetherwaba_idis written (viaExtraAttrsor a column). If not persisted for reporting, add it to the log write. - Red — add table-driven tests in
deduction_test.gofor: multi-WABA same-company pool draw, cross-bucket split, non-billable skip, and a concurrent (-race) two-WABA deduction assertingremainingnever goes below what a serial run would give. - Confirm query — verify
GetQuotaForUpdateindb/queries/organization_package_components.sqlhas nowaba_idpredicate and keys oncompany_id; regenerate sqlc only if a query is touched (sqlc generate). - Green —
make testuntil the new cases pass under-race. - Quality gate —
make testclean;staticcheckperstaticcheck.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 nobilling_logrow /total_price=0(WABA-S04/AC-2). - The originating
waba_idis 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
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 2.0 |
| QA | 0.5 |
| Total | 2.5 |
Assumptions: no deduction-logic rewrite needed under confirmed cardinality; grows to ~3 BE if
waba_idpropagation 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
| Action | File | What changes |
|---|---|---|
| extend | internal/app/usecase/quota_management/deduction.go | bound the requeue (L71-90) to N attempts via job max_fails; on exhaustion slog + metric billing_deduction_failed |
| verify | job/worker config (deduction worker registration) | confirm/adjust max_fails for the deduction job |
| extend | internal/app/usecase/quota_management/deduction_test.go | assert requeue on conflict, and billing_deduction_failed after N failures |
Implementation steps
- Explore — open
deduction.goL71-90; find whereEnqueueQuotaManagementDeductionis called and where the deduction worker's retry policy (max_fails) is configured. - Red — add a test forcing repeated
rowAffected == 0and assert the job stops after N attempts and logsbilling_deduction_failed. - Implement — thread an attempt count (or rely on
max_fails) and add the exhaustionslog.ErrorContext+ metric. - Green —
make test. - Quality gate —
make test+staticcheck.
Acceptance criteria
- Lock conflict re-enqueues the deduction (existing behavior preserved).
- After the bounded attempts,
billing_deduction_failedis logged withorganization_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
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 1.5 |
| QA | 0.5 |
| Total | 2.0 |
Assumptions: the job queue (
gocraft/work) already supportsmax_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
| Action | File | What changes |
|---|---|---|
| create | database/billing/db/migrate/2026XXXXXXXXXX_drop_unique_org_package_id_index_on_whatsapp_packages.rb | drop index_whatsapp_packages_on_organization_package_id (unique), re-add non-unique |
| extend | app/core/domains/services/billing/wa_package.rb | in 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? |
| verify | app/core/domains/repositories/billings/helpers.rb | find_whatsapp_package L217-232 still resolves by organization_package_id |
| create/extend | app/core/domains/services/billing/wa_package_spec.rb | 2nd WABA on same org → new row, same organization_package_id; unique-violation no longer raised |
Implementation steps
- Explore — open
app/core/domains/services/billing/wa_package.rb; readcreate(L38-76) andcreate_whatsapp_package(L213-240) and thewaba_save_organization_idgate inhelpers.rb. - 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 secondWhatsappPackagerow with the sameorganization_package_idand does not raise. - Migration — write the drop+recreate-non-unique migration under
database/billing/db/migrate/. - Logic — adjust the early-return in
createso additional WABAs proceed tocreate_whatsapp_packagewhenuse_org_package_idis true. - Green —
RAILS_ENV=test bundle exec rails app:db:migratethenbundle exec rspec app/core/domains/services/billing/wa_package_spec.rb. - Quality gate — full
rspec app --tag ~@is_skip_pipelinefor 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_idON, a company's 2nd WABA creates a newWhatsappPackagerow sharing the existingorganization_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
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 2.5 |
| QA | 0.5 |
| Total | 3.0 |
Assumptions: the
waba_save_organization_idpreference 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
| Action | File | What changes |
|---|---|---|
| create | lib/tasks/billing/backfill_organization_package_id.rake | batched, idempotent backfill for V3 rows; logs progress + final coverage count |
| create | app/core/.../backfill_organization_package_id_spec.rb (or rake spec) | asserts rows get the FK; re-run is a no-op |
Implementation steps
- Explore — confirm how V3 orgs are identified (
billing_v3?/is_billing_v3) and howOrganizationPackage↔WhatsappPackagerelate (organization_package.rb). - Red — spec: seed V3
WhatsappPackagerows withoutorganization_package_id; run task; assert all populated; second run changes nothing. - Implement — batched update (respect production traffic); emit a verification count (
WhatsappPackageV3 rows with NULLorganization_package_id== 0). - Green — run the spec; dry-run on staging.
- Quality gate —
rspecclean; document the verification query in the task output.
Acceptance criteria
- All V3
WhatsappPackagerows have a non-NULLorganization_package_idafter 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
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 1.5 |
| QA | 0.5 |
| Total | 2.0 |
Assumptions: the target
organization_package_idper 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
| Action | File | What changes |
|---|---|---|
| extend | app/core/domains/interactors/billings/billing_info.rb [unverified — confirm exact path; class Interactors::Billings::BillingInfo] | add billing_version + aggregated buckets to the result |
| extend | app/core/domains/builders/billings/billing_info.rb [unverified — class Builders::Billings::BillingInfo] | shape aggregated WABI/Additional/Postpaid; read via qontak-billing quota info client |
| extend | app/apps/billings/services/quota_management.rb | add a read of /iag/v1/quota-managements/info (company-scoped) if not already present |
| extend | app/services/api/core/v1/billings/resources/billings.rb (hub_service) | /info L607-629 — expose new fields (Grape entity/presenter) |
| extend | billing info rspec (spec/.../billings_spec.rb in hub_service) | assert billing_version:"3.0.0" + aggregated fields for V3; timeout → cached |
Implementation steps
- Explore — trace
GET /info(billings.rb:607-629) →Interactors::Billings::BillingInfo→Builders::Billings::BillingInfoin hub_core; confirm exact file paths (the grounding referenced them by class name — verify before editing). - Red — hub_service rspec: V3 org
/billings/inforeturnsbilling_version+ aggregated WABI/Additional/Postpaid; V1/V2 unchanged; simulated qontak-billing timeout returns last-known cache + logsbilling_aggregate_balance_timeout. - Implement read — add/confirm a company-scoped read of qontak-billing
/iag/v1/quota-managements/infoinquota_management.rb; aggregate in the builder. - Cache — Redis key
billing_info:{company}short TTL; invalidate via qontak-billingPUT .../{company_id}/invalidate-cache(or key delete) on deduction/top-up. - Green —
RAILS_ENV=test bundle exec rspecfor the billing resources spec. - Quality gate — full billing-area rspec.
Acceptance criteria
- For a V3 org,
/billings/infoincludesbilling_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_timeoutlogged.
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
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 3.0 |
| QA | 1.0 |
| Total | 4.0 |
Assumptions: qontak-billing
/iag/v1/quota-managements/inforeturns 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
| Action | File | What changes |
|---|---|---|
| extend | internal/app/usecase/worker/quota_management_alert.go | add balance_below_zero branch (aggregated remaining < 0) distinct from the ≤threshold warning (L97-98) |
| extend | internal/app/usecase/quota_management/deduction.go | dedup key check before enqueuing the alert (L99-103) |
| extend | alert worker test | assert below-zero emits the distinct event; duplicate crossing within a cycle is suppressed |
Implementation steps
- Explore — open
quota_management_alert.go(threshold calc L90-102) anddeduction.goL99-103 / L293-306 (whereshouldTriggerAlertis computed). - Red — tests: aggregated remaining < 0 emits
balance_below_zero(not the plain warning); second crossing within the same cycle is deduped. - Implement — add the below-zero branch + a Redis dedup key with a per-cycle TTL.
- Green —
make test. - Quality gate —
make test+staticcheck.
Acceptance criteria
- Crossing the threshold (40% default) emits
low_balance_warningonce per crossing (existing, verified). - Aggregated remaining < 0 emits a distinct
balance_below_zeroevent. - 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
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 2.0 |
| QA | 0.5 |
| Total | 2.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_idreporting 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
| Action | File | What changes |
|---|---|---|
| extend | common/store/BillingStore.ts | add billing_version to the BillingInfo interface (L4-19) + populate from /billings/info (L137) |
| extend | common/store/AppConfigStore.ts | add show_waba_id?: boolean under billing_reports (L38-43) |
| extend | common/store/__tests__/BillingStore.spec.ts [unverified — confirm test path] | assert billing_version parsed; billingM1Version-style gate derivable |
Implementation steps
- Explore — open
common/store/BillingStore.ts(L4-19, L122-158) andAppConfigStore.ts(L38-43); mirror the existingbilling_reportsflag pattern. - Red — store spec: given a
/billings/infopayload withbilling_version:"3.0.0",billingInfo.billing_versionis set; givenappConfig.billing_reports.show_waba_id=true, the flag reads true. - Implement — extend the interfaces + parsing.
- Green —
pnpm test. - Quality gate —
pnpm lint && pnpm build.
Acceptance criteria
-
BillingStore.billingInfo.billing_versionis populated from/billings/info. -
AppConfig.billing_reports.show_waba_idis 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
| Discipline | Days |
|---|---|
| Frontend | 1.0 |
| Backend | — |
| QA | 0.5 |
| Total | 1.5 |
Assumptions:
/billings/inforeturnsbilling_version(delivered by Task 5);client_configs/configcan carry the newbilling_reports.show_waba_idflag.
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 ofbilling_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_idcolumn shows only for V3 orgs with the reporting preference ON — not whenever data happens to contain awaba_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
| Action | File | What changes |
|---|---|---|
| extend | features/subscriptions/usages/TableComponentCampaignBroadcast.vue | replace hasWabaId (L308-310) in the column defs (L311-320) with shouldShowWabaId |
| extend | features/subscriptions/usages/TableComponentWhatsappBalance.vue | replace hasWabaId (L395-397) in column defs (L370-392) + cell v-if (L74) |
| extend | features/subscriptions/usages/__tests__/TableComponentCampaignBroadcast.spec.ts | column shown iff pref ON AND V3; hidden for V1/V2 or pref OFF |
| extend | features/subscriptions/usages/__tests__/TableComponentWhatsappBalance.spec.ts [unverified — confirm test path] | same assertions |
Implementation steps
- Explore — open both table components; find
hasWabaIdcomputed and its use in the header/cell templates. - Red — update specs: assert column visibility keyed to
shouldShowWabaId(pref ON + V3), not to data presence. - Implement — import the gate from Task 7; swap the computed.
- Green —
pnpm testfor both specs. - Quality gate —
pnpm lint && pnpm build.
Acceptance criteria
- Column visible iff
billing_report_show_waba_idON andbilling_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
| Discipline | Days |
|---|---|
| Frontend | 1.0 |
| Backend | — |
| QA | 0.5 |
| Total | 1.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
| Action | File | What changes |
|---|---|---|
| extend | pages/subscriptions/usages/index.vue | add the filter control to the MCC view's filter section |
| extend | features/subscriptions/usages/TableComponentWhatsappBalance.vue | pass selected waba_id into the /mcc_logs request (L523); no-match empty state |
| extend | features/subscriptions/usages/__tests__/TableComponentWhatsappBalance.spec.ts | filter sets waba_id param; empty state on no match |
Implementation steps
- Unblock — obtain the filter frame + confirm the dropdown contract (options source = distinct
waba_ids; selected value;@change) per Detail 2.F. - Explore — open
pages/subscriptions/usages/index.vuefor the existing filter pattern andTableComponentWhatsappBalance.vuefor the/mcc_logscall (L523). - Red — spec: selecting a
waba_idadds?waba_id=to the request; no-match shows "No records found for WABA ID [id]". - Implement — add the dropdown (pixel3) + wire the query param.
- Green —
pnpm test. - Quality gate —
pnpm 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
shouldShowWabaIdis true.
Test strategy
Vitest asserting the request param on selection and the empty-state render; mock $customFetch.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | — |
| QA | 0.5 |
| Total | 2.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
| Action | File | What changes |
|---|---|---|
| create | features/subscriptions/packages/SharedBalanceTooltip.vue | info icon + v-mp-tooltip/<mp-tooltip> popover; rendered only when billingM1Version |
| extend | features/subscriptions/packages/UsageDetail.vue | show aggregated WABI/Additional/Postpaid single figures for V3 (L43-47 area) |
| extend | features/subscriptions/packages/PackageDetails.vue | mount the tooltip in the balance section |
| create | features/subscriptions/packages/__tests__/SharedBalanceTooltip.spec.ts | renders for V3 only; hidden when billing info missing (fail-safe) |
Implementation steps
- Unblock — obtain the balance + tooltip frames and confirm the tooltip's props/copy per Detail 2.F.
- Explore — open
UsageDetail.vue(balance rows) andInvoicesDetailPage.vue:159-167(existing<mp-tooltip>pattern). - Red — spec: tooltip renders for
billing_version==="3.0.0"only; hidden when billing info unresolved (WABA-S07/ERR-1). - Implement — build the tooltip; switch
UsageDetailto the aggregated figure for V3. - Green —
pnpm test. - Quality gate —
pnpm 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
| Discipline | Days |
|---|---|
| Frontend | 2.0 |
| Backend | — |
| QA | 0.5 |
| Total | 2.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
| Action | File | What changes |
|---|---|---|
| create | features/subscriptions/packages/LowBalanceBanner.vue | <MpBanner> warning + below-zero variants; state prop; dismiss event |
| extend | features/subscriptions/packages/PackageDetails.vue | mount banner above the quota section; bind to aggregated balance/threshold |
| create | features/subscriptions/packages/__tests__/LowBalanceBanner.spec.ts | warning below threshold; below-zero variant; cleared when restored |
Implementation steps
- Unblock — obtain both banner variants' frames + confirm the
state/dismisscontract per Detail 2.F. - Explore — open
BannerRingGroup.vuefor the existing<MpBanner>pattern. - Red — spec: warning when balance below threshold; distinct below-zero when < 0; hidden when restored.
- Implement — build the banner; wire to aggregated balance from
/billings/info. - Green —
pnpm test. - Quality gate —
pnpm 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
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | — |
| QA | 0.5 |
| Total | 2.0 |
Assumptions: banner reacts to
/billings/infoaggregated 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 / task | Reason |
|---|---|
| WABA-S02 — WAB-Additional carry-over on contract renewal | Excluded 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 reset | No 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, 11 | Included above but 🚫 blocked on RFC Detail 2.F (design frames + component contracts). Actionable portion is zero until Detail 2.F lands. |