Qontak | Billing & Platform | Postpaid Usage Monthly Scheduler — Phase 1: Automation
Product Requirements Document · NEW PRD v1.0
HEADER BLOCK
| Field | Value |
|---|---|
| PM | Qontak PM Group |
| PRD Version | 1.0 |
| Status | DRAFT |
| PRD Type | NEW |
| Epic | BIF-8641 |
| Squad | Bifrost |
| RFC Link | TBD — pending PRD approval |
| Figma Master | TBD — pending design |
| Anchor | No — standalone, single-squad |
| Labels | epic:qontak-platform | module:billing-platform | feature:postpaid-usage-scheduler |
| Last Updated | 2026-06-22 |
✅ Reformat Complete
This PRD was reformatted from [PRD] Postpaid Usage Monthly Scheduler. All gaps resolved as of 2026-06-23. No outstanding flags.
v1.3 update: Technical accuracy pass against
moderator-be,qontak-billing, andreport-workersource repositories. Key additions: service ownership map, data source clarification, billing type enum mapping, dependency corrections, cron timing constraint, and job infrastructure alignment. See CHANGELOG for full diff.
Table of Contents
- HEADER BLOCK
- 3. One-liner + Problem
- 4. Target Users + Persona Context
- 5. Non-Goals
- Scope Changes
- 6. Constraints
- 7. New Features
- 8. API & Webhook Behavior
- 9. System Flow + User Stories + ACs
- 10. Rollout
- 11. Observability
- 12. Success Metrics
- 13. Launch Plan & Stage Gates
- 14. Dependencies
- 15. Key Decisions + Alternatives Rejected
- 16. Open Questions
- PRD CHANGELOG
3. One-liner + Problem
One-liner: Automate monthly postpaid usage snapshot generation and bulk export in Modpanel so the Finance team can download all client reports in one click instead of one by one.
Problem: The Finance team manually downloads postpaid usage reports for each client individually at the start of every month to process billing and reconciliation. With a growing client volume and the introduction of Billing V3 — which adds multiple postpaid components per client (WhatsApp, Voice Calls, whitelisted quotas) — this manual process is highly inefficient and prone to operational bottlenecks. Without automation, Finance hours are consumed by repetitive report downloads each billing cycle, leaving no margin as client count scales.
4. Target Users + Persona Context
| Persona | Role | Goal | Pain | Workaround |
|---|---|---|---|---|
| Primary — Finance Staff | Finance team member responsible for processing monthly postpaid billing and reconciliation across all CID clients | Download all client postpaid usage reports at the start of each month to run billing and reconciliation accurately | Must manually open each client's usage page and download one report at a time — highly time-consuming as client volume grows; Billing V3 multiplies the number of downloads per client | Opens each client page in Modpanel individually, downloads one report at a time, aggregates manually — consumes hours at the start of every billing cycle |
(See Constraints for platform scope and access.)
5. Non-Goals
- This does not provide real-time usage data — reports are monthly snapshots only, frozen on the 1st of each month.
- This does not support postpaid usage downloads for Billing V1 components beyond WA Balance and MUV — no other V1 types are included.
- This does not allow Finance users to edit, correct, or reprocess usage data — read and download only.
- This does not expose the dashboard to external clients or CID admins — internal Finance Modpanel use only.
- This does not send automated email or push notifications to Finance when the monthly snapshot is ready — Finance checks the dashboard manually.
- This does not support on-demand or manual snapshot regeneration — only the automated 1st-of-month cron job generates snapshots.
- This does not integrate with external accounting or ERP systems (e.g. SAP, Oracle) — download to local file only.
Scope Changes
- Backend — new cron job for monthly snapshot generation; snapshot storage table; async ZIP export job queue; bulk download API.
- Frontend — new Postpaid Usage Dashboard in Modpanel (
/modpanel/postpaid-usage) with search, filter, bulk selection, and async download UI.
6. Constraints
| Field | Value |
|---|---|
| Platform | Modpanel web only (internal admin panel). No mobile. |
| Performance — page load | Dashboard table loads ≤ 3s for up to 1,000 records |
| Performance — ZIP job | Async ZIP generation completes ≤ 30 minutes from trigger |
| Performance — cron job | Monthly snapshot generation completes within 2 hours of 00:00 on the 1st |
| Plan scope | Internal Finance Modpanel users only — no plan/tier restriction |
| Feature flag | Flipper flag :postpaid_usage_scheduler_enabled — default: ON; global Modpanel flag, no per-CID toggle. Follows moderator-be Flipper convention (symbol-keyed, managed via Core::Services::Preference). |
| Data limits | Max 50MB per ZIP download request. Pagination: 50 rows per page. |
| Read/write | Finance Modpanel users: read + download only. No write, edit, or delete access on usage data. |
6.2 Service Architecture & Ownership
This feature spans three existing services. Engineering must coordinate across all three.
| Service | Role in this feature | Key existing assets |
|---|---|---|
| qontak-billing (Go) | Source of truth for postpaid usage data. Accumulates daily usage into monthly_usages table via cron at 18:00 Asia/Jakarta. Also holds wa_conversation_logs, muv_credit_logs, and organization_package_component_postpaid_limits for V3 quotas. Exposes /iag/v1/quota-managements/summary-usage API. | monthly_usages, wa_conversation_logs, muv_credit_logs, organization_package_component_postpaid_limits |
| report-worker (Go) | Owns async export job infrastructure. Uses gocraft/work (Redis-backed). Tracks job status in billing_log_exports table. Uploads files to OSS with private ACL. Sends email notification on completion. | billing_log_exports table (pending/completed/failed/expired), ExportBillingLogsJobName worker, BillingLogsExport multiplexer pattern, OSS uploader service |
| moderator-be (Ruby/Rails) | Owns Modpanel UI, Finance user auth (Pundit + role/permission model), API gateway to downstream services, cron scheduling (sidekiq-cron). An existing download_monthly_postpaid_usage endpoint handles per-client manual downloads — this feature replaces that workflow for Finance. | Sidekiq + sidekiq-cron (config/schedule.yml), Pundit RBAC, Flipper flags, existing /api/v1/qontak/chat_panel/reports/download_monthly_postpaid_usage |
Cron timing constraint: qontak-billing's daily usage aggregation runs at 18:00 Asia/Jakarta. The 1st-of-month snapshot job in moderator-be must run after the last daily aggregation for the prior month has completed. Recommended schedule: 0 2 1 * * Asia/Jakarta (02:00 on the 1st) to guarantee the billing data is fully finalized before snapshot generation begins.
6.1 Data Lifecycle
| Artifact Type | Retention Period | Cleanup Trigger | User-Visible Effect |
|---|---|---|---|
| Monthly usage snapshot (DB record) | 24 months | Scheduled annual purge or manual cleanup by engineering | None — historical data available for 2 years |
| In-progress ZIP job | Until job completes or fails | Job completion or failure event | None |
| Generated ZIP file | 24 hours from creation | TTL-based cleanup job | "Download link expired" shown if accessed after 24h |
| Failed ZIP job artifact | 24 hours | Nightly cleanup cron | None — Finance sees "Generation failed. Try again." |
7. New Features
Feature: Postpaid Usage Dashboard
| Field | Detail |
|---|---|
| URL | /modpanel/postpaid-usage |
| Access | Finance Modpanel users only |
Component Tree:
| Component | Parent | Purpose |
|---|---|---|
| PostpaidUsagePage | — | Top-level page container |
| PageHeader | PostpaidUsagePage | Displays page title "Postpaid Usage" |
| FilterBar | PostpaidUsagePage | Contains search and date filter controls |
| SearchInput | FilterBar | Search by WABA ID or Company ID (CID); filters table on input |
| YearMonthPicker | FilterBar | Filter by month; defaults to most recently generated month |
| BulkActionBar | PostpaidUsagePage | Visible when ≥1 row selected; shows count + Download All button |
| SelectionCount | BulkActionBar | Displays "X records selected" |
| DownloadAllButton | BulkActionBar | Triggers async ZIP job for all selected records |
| UsageTable | PostpaidUsagePage | Paginated table of postpaid usage snapshot records |
| SelectAllCheckbox | UsageTable | Header row checkbox; selects all records matching current filter across all pages |
| UsageRow | UsageTable | One row per CID per postpaid type per month |
| RowCheckbox | UsageRow | Selects individual record |
| WABAId | UsageRow | WABA ID for the record |
| CompanyId | UsageRow | Company ID (CID) |
| CompanyName | UsageRow | Company name |
| PostpaidType | UsageRow | Postpaid type label: WA Balance / MUV / Call Balance |
| YearMonth | UsageRow | Reporting period in YYYY-MM format |
| ReportDate | UsageRow | Date the snapshot was generated |
| Pagination | UsageTable | 50 rows per page |
| ZIPStatusToast | PostpaidUsagePage | Appears on download trigger; updates to download link when ZIP is ready |
UI States:
| State | Description |
|---|---|
| Empty | "No usage data available for this period." — no action button |
| Loading | Skeleton rows (5) in UsageTable while fetching |
| Error | "Could not load usage data. Try again." + Retry button |
| Success | Table populated with records; pagination visible if >50 results |
Figma: TBD — pending design
8. API & Webhook Behavior
| # | Behavior | Service Owner | Entity Affected | Triggered By | Expected Behavior | Failure Behavior |
|---|---|---|---|---|---|---|
| 1 | Monthly cron → snapshot generation | moderator-be (Sidekiq worker via sidekiq-cron) | Snapshot records created in postpaid_usage_snapshots table (new) | Scheduled cron at 0 2 1 * * Asia/Jakarta — 02:00 on the 1st of each month (after qontak-billing's 18:00 daily aggregation finishes for month-end) | Reads previous month's finalized usage from qontak-billing (monthly_usages table, keyed by organization_id + timestamp). For V3 whitelisted quota components, additionally reads organization_package_component_postpaid_limits. Creates frozen snapshot records per CID per billing type (V1: WA_BALANCE_V1, MUV_V1; V3: WA_BALANCE_V3, MUV_V3, CALL_BALANCE_V3, plus whitelisted quota component codes). Completes within 2 hours. | If a single CID fails: log snapshot_failed with cid, year_month, reason and continue to next CID — job does not abort. If failure rate exceeds 5% of total CIDs: alert Bifrost engineering. |
| 2 | Read paginated dashboard records | moderator-be | Snapshot records read from postpaid_usage_snapshots; returned to Finance dashboard | Finance user loads /modpanel/postpaid-usage | Returns paginated snapshot records (50 per page) for the selected year_month filter, defaulting to most recently generated month. Columns: WABA ID, Company ID, Company Name, Postpaid Type, Year-Month, Report Date. | If DB query fails: return error state — "Could not load usage data. Try again." Log usage_dashboard_load_failed. |
| 3 | Search and filter records | moderator-be | Snapshot records read (filtered subset) | Finance user submits search query or changes YearMonthPicker | Accepts optional search_query (exact match on WABA ID or Company ID) and required year_month filter. Returns matching records paginated. Both filters combinable. | If no records match: return empty state — "No records found for this filter." |
| 4 | Initiate async ZIP job | moderator-be (triggers job); report-worker (executes job via gocraft/work Redis queue) | New billing_log_exports record created (status: pending); job_id returned | Finance user clicks "Download All" with ≥1 records selected | Validates total estimated file size ≤ 50MB. If within limit: enqueues job in report-worker (follows ExportBillingLogsJobName pattern with new quota_type routing), inserts billing_log_exports row, returns job_id, logs bulk_download_triggered. Finance sees "File is generating..." toast. If exceeds 50MB: reject immediately — "Selection exceeds 50MB limit. Reduce your selection and try again." Log zip_size_limit_exceeded. | If job queue (Redis) is unavailable: return error — "Could not start download. Please try again." No job created. |
| 5 | ZIP job completion → deliver download link | report-worker | ZIP file generated and uploaded to OSS; billing_log_exports.export_status → completed; download URL returned to Finance | Async job in report-worker completes processing | Packages selected records as individual files per CID per postpaid type into a single ZIP. Uploads ZIP to OSS with private ACL. Updates billing_log_exports.export_status = 'completed' and file_storage with OSS path. Returns presigned download URL to Finance (expires 24 hours after creation — matches OSS presigned URL TTL). Logs zip_job_completed with job_id, user_id, file_size_mb, duration_seconds. | If job fails: updates billing_log_exports.export_status = 'failed' with error_details; show "Generation failed. Try again." Log zip_job_failed. Uses exponential backoff retry (report-worker standard: powers of 2 in seconds, configurable MaxFails). If Finance accesses expired link: show "Download link expired. Generate again." |
HTTP methods, endpoint paths, request/response JSON schemas, and error codes to be resolved by Engineering in the RFC. File format (CSV) and individual-file naming within the ZIP are confirmed — see §9.3.
9. System Flow + User Stories + ACs
9.1 System Flow
Flow: Postpaid Usage Monthly Scheduler — Snapshot Generation + Dashboard + Bulk Download Type: User Journey + System Event
- On the 1st of each month at 00:00, the cron job triggers monthly snapshot generation
- Backend calculates previous month's postpaid usage for all eligible CIDs (Billing V1: WA Balance, MUV; Billing V3: WA Balance, MUV, Call Balance, whitelisted quotas)
- Snapshots saved as static records — data is frozen at this point and will not change
- If a CID snapshot fails: failure is logged with
snapshot_failedevent; job continues to next CID without aborting - Finance user navigates to
/modpanel/postpaid-usage - Dashboard loads with YearMonthPicker defaulting to the most recently generated month; UsageTable fetches and displays paginated records (50 per page)
- Finance user searches by WABA ID or CID and/or selects a Year-Month filter; table updates to filtered results
- Finance user selects records using row checkboxes or SelectAll (selects all records matching current filter across all pages)
- Finance user clicks "Download All" — system validates total selection does not exceed 50MB
- If selection exceeds 50MB: request rejected immediately; Finance sees "Selection exceeds 50MB limit. Reduce your selection and try again."
- If within limit: async ZIP job initiated; Finance sees "File is generating..." toast;
bulk_download_triggeredevent logged - Backend processes ZIP in background — packages individual
.csvfiles per CID per postpaid type, named{CID} {Company Name} {Month YYYY} {Type}.csv(e.g.12345 Citra Angkasa April 2026 WA Balance.csv) - ZIP job completes: download link provided to Finance;
zip_job_completedevent logged - If ZIP job fails: Finance sees "Generation failed. Try again.";
zip_job_failedevent logged - Download link expires 24 hours after creation; Finance sees "Download link expired" if accessed after expiry
📊 System Flow — Postpaid Usage Monthly Scheduler
sequenceDiagram
autonumber
participant CronJob as Sidekiq Cron (moderator-be)
participant QontakBilling as qontak-billing
participant ModeratorBe as moderator-be (API)
participant ReportWorker as report-worker (async)
participant OSS as OSS Storage
participant Modpanel as Modpanel (UI)
actor Finance as Finance User
Note over CronJob: 1st of every month — 02:00 Asia/Jakarta
CronJob->>QontakBilling: Fetch finalized monthly_usages for prior month (all org_ids)
QontakBilling-->>CronJob: Usage data per organization
CronJob->>ModeratorBe: Save frozen snapshot records (postpaid_usage_snapshots)
ModeratorBe-->>CronJob: Snapshots saved for all eligible CIDs
alt CID snapshot fails
CronJob-->>ModeratorBe: Log snapshot_failed; continue to next CID
end
Finance->>Modpanel: Navigate to /modpanel/postpaid-usage
Modpanel->>ModeratorBe: Fetch usage records (default: most recent month)
ModeratorBe-->>Modpanel: Return paginated snapshot records
Finance->>Modpanel: Search / filter by CID, WABA ID, or Year-Month
Modpanel->>ModeratorBe: Fetch filtered records
ModeratorBe-->>Modpanel: Return filtered list
Finance->>Modpanel: Select records (checkbox / Select All across all pages)
Finance->>Modpanel: Click "Download All"
Modpanel->>ModeratorBe: Request async ZIP job
alt Selection exceeds 50MB
ModeratorBe-->>Modpanel: Reject — "Selection exceeds 50MB limit"
Modpanel-->>Finance: Show error toast
else Within limit
ModeratorBe->>ReportWorker: Enqueue export job (Redis/gocraft-work); insert billing_log_exports (status: pending)
ModeratorBe-->>Modpanel: Job accepted (job_id returned)
Modpanel-->>Finance: Show "File is generating..." toast
Note over ReportWorker: Processing ZIP in background (exponential backoff retry on failure)
alt ZIP job succeeds
ReportWorker->>OSS: Upload ZIP (private ACL)
ReportWorker->>ModeratorBe: Update billing_log_exports → completed; file_storage set
ModeratorBe-->>Modpanel: ZIP ready — presigned download URL (24h TTL)
Modpanel-->>Finance: Provide download link
else ZIP job fails
ReportWorker->>ModeratorBe: Update billing_log_exports → failed; error_details set
ModeratorBe-->>Modpanel: Job failed
Modpanel-->>Finance: Show "Generation failed. Try again."
end
end
9.3 Export Format Reference
Each file inside the ZIP follows the naming convention and column schema below, based on confirmed sample exports. File format is CSV for all types.
File Naming Convention: {CID} {Company Name} {Month YYYY} {Type}.csv
| Type | Example filename |
|---|---|
| WA Balance | 12345 Citra Angkasa April 2026 WA Balance.csv |
| MUV | 12345 Citra Angkasa April 2026 MUV.csv |
| Call Balance | 12345 Citra Angkasa April 2026 Call Balance.csv |
Notes on naming:
{Month}is the full English month name (e.g.April,January) — notYYYY-MM{YYYY}is the 4-digit reporting year{Type}exactly matches the postpaid type label:WA Balance,MUV, orCall Balance- No "Postpaid Usage" text in the filename
WA Balance CSV — Column Schema
| Column | Type | Example |
|---|---|---|
created_at (GMT+7) | date (YYYY-MM-DD) | 2026-04-20 |
recipient | string (phone with + prefix) | +62881010422155 |
conversation_type | string | BI |
conversation_category | string | utility |
count_messages | integer | 17 |
sum_credit | decimal | 6233.05 |
country | string (ISO 3166-1 alpha-2) | ID |
credited_to | string | wa_balance |
MUV CSV — Column Schema
| Column | Type | Example |
|---|---|---|
Created at | datetime string (Mon DD YYYY, hh:mm:ss AM/PM +07:00) | Apr 01 2026, 02:04:18 PM +07:00 |
Channel | string | wa_cloud |
Customer name | string | Dwi Riptono |
Account unique id | string | 6281215727642 |
Recipient | string | PT Citra Angkasa Lintas Media |
Credited To | string | muv_credit |
Call Balance CSV — Column Schema
| Column | Type | Example |
|---|---|---|
created_at (GMT+7) | date (YYYY-MM-DD) | 2026-04-20 |
recipient | string (phone, no + prefix) | 62881010422155 |
call_direction | string (inbound / outbound) | inbound |
count_call_id | integer | 17 |
sum_credit | decimal | 6233.05 |
country | string (ISO 3166-1 alpha-2) | ID |
9.2 User Stories
| User Story | Importance | Mockup / Technical Notes | Acceptance Criteria |
|---|---|---|---|
| [PUMS-S01] — Monthly Snapshot Generation As a Backend System, I want to automatically generate and freeze postpaid usage data for all eligible CIDs on the 1st day of each month, so that the Finance team has accurate, static data ready for bulk download without real-time querying delays. | Must Have | Figma: N/A — backend only, no new UI Data Fields (new postpaid_usage_snapshots table in moderator-be):• cid (string, required) — maps to organization_packages.company_id in qontak-billing• organization_id (uuid, required) — internal org identifier; join key for qontak-billing queries• waba_id (string) — to be confirmed: sourcing from qontak-billing or moderator-be org data• year_month (string, required) — format YYYY-MM; represents the previous calendar month• billing_type (enum, required) — WA_BALANCE_V1, MUV_V1, WA_BALANCE_V3, MUV_V3, CALL_BALANCE_V3, plus whitelisted quota component codes (e.g. CP-QONTAKCHAT-2025-0005) for V3• usage_value (decimal, required) — frozen usage amount; sourced from monthly_usages.mcc_total (WA Balance), monthly_usages.muv_total (MUV), organization_package_component_postpaid_limits.usage_quota (V3 quotas)• report_date (date, required) — date snapshot was generatedImplementation Notes (for Engineering/RFC): • Cron schedule: 0 2 1 * * Asia/Jakarta in moderator-be/config/schedule.yml (sidekiq-cron format). Class: Core::Workers::Sidekiq::PostpaidUsageSnapshotWorker. Queue: postpaid_usage_snapshot.• Data source: qontak-billing's monthly_usages table (keyed by organization_id + timestamp = 1st of month, 00:00 Jakarta). For V3 quotas: organization_package_component_postpaid_limits (keyed by organization_package_id). Note: organization_name in qontak-billing is stored encrypted (organization_name_ciphertext) — decryption must be handled in the snapshot pipeline.• Billing version differentiation: organization_packages.billing_version — "1.0.0" = V1, "3.0.0" = V3. "2.0.0" behavior to be confirmed by Engineering.• Existing cron reference: chatbot_ai_reset_monthly_task (same 1st-of-month pattern) in config/schedule.yml.Before-After Behavior: Before: no automated snapshot exists; Finance downloads live usage data manually per client via the existing /api/v1/qontak/chat_panel/reports/download_monthly_postpaid_usage endpoint (one client at a time). After: cron job runs at 02:00 on the 1st and saves a frozen snapshot per CID per billing type for the previous month. | — Happy Path — • AC-1: Given it is 00:00 on the 1st day of a new month, when the cron job runs, then the system generates and saves postpaid usage snapshots for all eligible CIDs covering the previous calendar month. • AC-2: Given a snapshot has been generated for a CID, when a Finance user views or downloads the report at any point during the month, then the data reflects usage exactly as of the last day of the reporting month and does not change. • AC-3: Given the cron job runs, when processing Billing V1 clients, then snapshots are generated for WA Balance and MUV only — no other V1 components. • AC-4: Given the cron job runs, when processing Billing V3 clients, then snapshots are generated for WA Balance, MUV, Call Balance, and all whitelisted quotas. — Error / Unhappy Path — • ERR-1: Given the cron job is processing CIDs, when snapshot generation fails for a specific CID, then the failure is logged with snapshot_failed event (properties: cid, year_month, reason) and the job continues to the next CID without aborting.• ERR-2: Given the entire cron run completes, when the snapshot_failed rate exceeds 5% of total CIDs, then Bifrost engineering is notified for investigation.— Permission Model — • CAN: System (cron job) — no user can trigger this manually • CANNOT: Finance users, Modpanel admins, or any user role • Unauthorized: N/A — system-triggered only; no manual trigger endpoint exposed — UI States — • N/A — backend only |
| [PUMS-S02] — View Postpaid Usage Table As a Finance user, I want to view a table listing postpaid usage for all clients with columns for WABA ID, Company ID, Company Name, Postpaid Type, Year-Month, and Report Date, so that I have a clear overview of all usage data ready for billing. | Must Have | Figma: TBD — pending design Data Fields: • waba_id (string) — displayed in WABA ID column• company_id (string) — displayed in Company ID column• company_name (string) — displayed in Company Name column• postpaid_type (enum) — displayed in Postpaid Type column• year_month (string, YYYY-MM) — displayed in Year-Month column• report_date (date) — displayed in Report Date columnBefore-After Behavior: Before: no dedicated dashboard exists; Finance navigates to individual client pages to find usage data. After: /modpanel/postpaid-usage shows a paginated table of all clients' postpaid usage snapshots, defaulting to the most recently generated month. | — Happy Path — • AC-1: Given a Finance user navigates to /modpanel/postpaid-usage, when the page loads (per S6 §Constraints: postpaid_usage_scheduler_enabled | default: ON), then the UsageTable displays rows with WABA ID, Company ID, Company Name, Postpaid Type, Year-Month, and Report Date columns.• AC-2: Given the dashboard loads, when no filter is manually selected, then the YearMonthPicker defaults to the most recently generated month and the table shows only records for that month. • AC-3: Given the table has more than 50 records for the selected filter, when the page loads, then pagination is visible and a maximum of 50 rows are displayed per page. — Error / Unhappy Path — • ERR-1: Given a Finance user loads the dashboard, when the backend returns an error, then the table shows "Could not load usage data. Try again." with a Retry button, and the usage_dashboard_loaded event is not fired.• ERR-2: Given the YearMonthPicker is set to a month with no generated snapshots, when the table loads, then it shows "No usage data available for this period." with no action button. — Permission Model — • CAN: Finance Modpanel users • CANNOT: Non-Finance Modpanel users; external CID admins • Unauthorized: Redirect to Modpanel homepage or show 403 — UI States — • Loading: Skeleton rows (5) in UsageTable while fetching • Empty: "No usage data available for this period." • Error: "Could not load usage data. Try again." + Retry button • Success: Table populated; pagination visible if >50 results |
| [PUMS-S03] — Identify Postpaid Types As a Finance user, I want to see exactly what kind of postpaid usage each record represents (e.g. WA Balance, Call Balance), so that I can apply the correct billing and reconciliation procedures for each service. | Must Have | Figma: TBD — pending design Data Fields: • postpaid_type (enum, required) — mapped from Billing V1/V3 component data; values: WA Balance, MUV, Call BalanceTechnical Note: Map from Billing V3 components — WA Balance (V1 & V3), MUV (V1 & V3), Call Balance (V3 only). Before-After Behavior: Before: no postpaid type label visible in any single view; Finance infers type from client billing tier. After: each row in the UsageTable shows a PostpaidType column with a clear label. | — Happy Path — • AC-1: Given a Finance user views the UsageTable, when a record belongs to a Billing V1 or V3 client with WA Balance, then the Postpaid Type column displays "WA Balance". • AC-2: Given a Finance user views the UsageTable, when a record belongs to a Billing V1 or V3 client with MUV, then the Postpaid Type column displays "MUV". • AC-3: Given a Finance user views the UsageTable, when a record belongs to a Billing V3 client with Call Balance, then the Postpaid Type column displays "Call Balance". — Error / Unhappy Path — • ERR-1: Given a snapshot record exists with an unmapped or unknown billing component, when the table renders that row, then the Postpaid Type column displays "Unknown" rather than a blank cell. — Permission Model — • CAN: Finance Modpanel users (same as PUMS-S02) • CANNOT: Non-Finance users • Unauthorized: Column not rendered — UI States — • (Inherits from PUMS-S02 — PostpaidType is a column within the UsageTable) |
| [PUMS-S04] — Search and Filter Records As a Finance user, I want to search by WABA ID or Company ID and filter by Year-Month, so that I can quickly locate specific clients or navigate historical data. | Must Have | Figma: TBD — pending design Data Fields: • search_query (string, optional) — exact match on WABA ID or Company ID• year_month_filter (string, YYYY-MM, required) — defaults to most recent generated monthBefore-After Behavior: Before: no search or filter capability exists for this data. After: SearchInput filters the table by exact CID or WABA ID match; YearMonthPicker filters by month; selections are combinable. | — Happy Path — • AC-1: Given a Finance user enters a specific CID into the SearchInput, when the input is submitted, then the UsageTable updates to display only records where Company ID exactly matches the entered value. • AC-2: Given a Finance user enters a specific WABA ID into the SearchInput, when the input is submitted, then the UsageTable updates to display only records where WABA ID exactly matches the entered value. • AC-3: Given a Finance user selects "March 2026" in the YearMonthPicker, when the filter is applied, then the system fetches and displays only snapshot records generated for March 2026. • AC-4: Given both a search query and a Year-Month filter are applied, when the table loads, then only records matching both conditions are displayed. — Error / Unhappy Path — • ERR-1: Given a Finance user enters a CID or WABA ID that has no matching records for the selected month, when the table renders, then it shows "No records found for this filter." with no action button. • ERR-2: Given a Finance user clears the SearchInput, when the input is cleared, then the table returns to the full list for the currently selected Year-Month filter. — Permission Model — • CAN: Finance Modpanel users • CANNOT: Non-Finance users • Unauthorized: Filter controls not rendered — UI States — • Loading: Skeleton rows (5) while filtered results fetch • Empty: "No records found for this filter." • Error: "Could not load results. Try again." + Retry button • Success: Table updates with filtered records |
| [PUMS-S05] — Bulk Selection As a Finance user, I want to select individual rows or use a Select All checkbox to select all filtered records across all pages, so that I can choose exactly which clients to include in the bulk download. | Must Have | Figma: TBD — pending design Data Fields: • selected_record_ids (array of strings, required) — IDs of all selected snapshot records; stored client-side until Download All is triggeredBefore-After Behavior: Before: no selection UI exists. After: each UsageRow has a RowCheckbox; UsageTable header has a SelectAllCheckbox that selects all records matching the current filter across all pages; BulkActionBar appears when ≥1 row is selected showing selection count and Download All button. | — Happy Path — • AC-1: Given the UsageTable is populated, when a Finance user clicks a RowCheckbox, then that record is added to the selection queue and the BulkActionBar appears showing "1 record selected". • AC-2: Given a Finance user clicks the SelectAllCheckbox, when the action fires, then all records matching the current filter across all pages are selected and the BulkActionBar shows the total count (e.g. "243 records selected"). • AC-3: Given records are selected across pages, when a Finance user navigates to a different page, then the selection persists and previously selected records remain checked. • AC-4: Given a Finance user clicks the SelectAllCheckbox again after all records are selected, when the action fires, then all selections are cleared and the BulkActionBar is hidden. • AC-5: Given records are selected, when a Finance user changes the YearMonthPicker or SearchInput filter, then all existing selections are cleared automatically. — Error / Unhappy Path — • ERR-1: Given a Finance user has selected records, when the page is refreshed, then selections are cleared (no persistence across sessions). — Permission Model — • CAN: Finance Modpanel users • CANNOT: Non-Finance users • Unauthorized: Checkboxes not rendered — UI States — • Loading: Checkboxes disabled while table is fetching • Empty: SelectAllCheckbox hidden when table has no records • Error: Checkboxes disabled during error state • Success: Checkboxes active; BulkActionBar visible when ≥1 selected |
| [PUMS-S06] — Bulk Download (Async ZIP) As a Finance user, I want to click "Download All" to export selected records as a single ZIP file containing individual spreadsheets per client per postpaid type, so that I can export all usage data in one action without browser timeouts. | Must Have | Figma: TBD — pending design Data Fields: • job_id (string) — returned when ZIP job is accepted• selected_record_ids (array, required) — passed to backend on trigger• file_size_mb (decimal) — validated against 50MB limit before job creation• download_url (string) — returned when ZIP job completes; expires 24h after creationTechnical Note: Async export is handled by report-worker (Go service, gocraft/work + Redis). moderator-be enqueues the job; report-worker executes it. Job status is tracked in the billing_log_exports table (status values: pending → completed / failed / expired; job_id from gocraft/work stored in billing_log_exports.job_id). Generated ZIP uploaded to OSS with private ACL; presigned URL served as the download link (24h TTL). Follows the existing ExportBillingLogsJobName pattern — a new quota_type value routes to the postpaid usage ZIP handler.File format: CSV (confirmed from sample exports — see §9.3). File naming convention: {CID} {Company Name} {Month YYYY} {Type}.csv — where {Month} is the full English month name (e.g. April), {YYYY} is the 4-digit year, and {Type} is one of WA Balance, MUV, Call Balance (e.g. 12345 Citra Angkasa April 2026 WA Balance.csv). Column schemas per type are defined in §9.3. Retry on failure: exponential backoff (powers of 2 in seconds), configurable MaxFails.Before-After Behavior: Before: Finance downloads reports one by one from individual client pages via the existing download_monthly_postpaid_usage endpoint. After: Finance selects multiple records and clicks "Download All"; report-worker packages all selected reports into a single ZIP; Finance receives a presigned download link when ready. | — Happy Path — • AC-1: Given a Finance user has selected ≥1 records and the total estimated file size is ≤ 50MB (per S6 §Data limits), when they click "Download All", then the system initiates an async background job, returns a job_id, and the ZIPStatusToast shows "File is generating...".• AC-2: Given the async ZIP job completes successfully, when the job status updates, then the ZIPStatusToast updates to show a download link and zip_job_completed is logged with job_id, user_id, file_size_mb, duration_seconds.• AC-3: Given the Finance user downloads and extracts the ZIP, when inspecting the contents, then each individual file is a CSV named using the exact format {CID} {Company Name} {Month YYYY} {Type}.csv (e.g. 12345 Citra Angkasa April 2026 WA Balance.csv), and the columns match the schema defined in §9.3 for the respective postpaid type.• AC-4: Given a Finance user tries to access the download link more than 24 hours after the ZIP was generated, when they click the link, then the system shows "Download link expired. Generate again." (per S6.1 §Data Lifecycle: Generated ZIP file retention = 24 hours). — Error / Unhappy Path — • ERR-1: Given a Finance user has selected records with an estimated total size exceeding 50MB (per S6 §Data limits), when they click "Download All", then the request is rejected immediately and the ZIPStatusToast shows "Selection exceeds 50MB limit. Reduce your selection and try again." and zip_size_limit_exceeded is logged.• ERR-2: Given the async ZIP job fails mid-processing, when the job status updates, then the ZIPStatusToast shows "Generation failed. Try again." and zip_job_failed is logged with job_id, user_id, reason.• ERR-3: Given a Finance user has 0 records selected, when they attempt to click "Download All", then the DownloadAllButton is disabled and cannot be triggered. — Permission Model — • CAN: Finance Modpanel users • CANNOT: Non-Finance users • Unauthorized: DownloadAllButton not rendered; BulkActionBar not visible — UI States — • Loading: ZIPStatusToast shows "File is generating..." with spinner; DownloadAllButton disabled during job • Empty: N/A — button only visible when ≥1 record selected • Error: ZIPStatusToast shows error message with retry instruction • Success: ZIPStatusToast shows download link |
10. Rollout
| Field | Value |
|---|---|
| Feature flag | postpaid_usage_scheduler_enabled — default: ON (see Constraints) |
| Stage 1 | Internal QA — Bifrost engineers + 1–2 Finance representatives on staging |
| Stage 2 (GA) | All Finance Modpanel users on production |
| Backward compat | No existing behavior affected — new dashboard and new snapshot table only |
| Migration | None required — new schema, no existing data transformation |
| First snapshot | Triggered automatically by cron on the 1st of the next calendar month after deployment — no manual backfill |
11. Observability
Key Events:
| Event Name | Trigger | Properties |
|---|---|---|
snapshot_generated | Cron job successfully generates snapshot for a CID | cid, year_month, billing_type, record_count |
snapshot_failed | Cron job fails to generate snapshot for a CID | cid, year_month, reason |
usage_dashboard_loaded | Finance user loads the Postpaid Usage Dashboard | user_id, year_month_filter, record_count_returned |
bulk_download_triggered | Finance user clicks "Download All" | user_id, selected_count, year_month_filter |
zip_job_completed | Async ZIP job finishes successfully | job_id, user_id, file_size_mb, duration_seconds |
zip_job_failed | Async ZIP job fails | job_id, user_id, reason |
zip_size_limit_exceeded | Selection exceeds 50MB — job rejected before creation | user_id, estimated_size_mb, selected_count |
| Field | Detail |
|---|---|
| Dashboard owner | Bifrost PM + Engineering lead |
| Alerts | None defined for initial launch — Bifrost team monitors via event data during post-launch review cadence |
11.1 Post-Launch Monitoring Cadence
| Field | Detail |
|---|---|
| Review cadence | Weekly for first 4 weeks post-GA, then monthly |
| Owner | Bifrost PM + Engineering lead |
| Review scope | snapshot_failed rate, zip_job_failed rate, zip_size_limit_exceeded frequency |
| Trigger threshold 1 | snapshot_failed rate > 5% in any monthly run → immediate investigation by Bifrost engineering |
| Trigger threshold 2 | zip_job_failed rate > 10% in any 7-day window → engineering escalation within 24 hours |
| Rollback consideration | If snapshot generation consistently fails for > 10% of CIDs after 2 consecutive monthly runs, PM disables postpaid_usage_scheduler_enabled globally pending root cause fix |
12. Success Metrics
Efficiency & Impact:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| ⭐ Time Finance spends on monthly postpaid usage download | Total hours Finance team spends downloading and aggregating postpaid usage reports each billing cycle | 3 hours per monthly billing cycle (confirmed by Finance team, 2026-06-22) | ≤ 30 minutes per monthly cycle within 60 days of GA (~83% reduction) |
Quality & Reliability:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| Snapshot generation success rate | % of eligible CIDs with successful monthly snapshots per run | N/A — new feature | ≥ 99% per monthly run |
| ZIP job completion success rate | % of bulk download requests that complete successfully | N/A — new feature | ≥ 95% within 60 days of GA |
Adoption:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| Finance adoption rate | % of monthly billing cycles where Finance uses bulk download instead of manual per-client download | N/A — new feature | 100% within 30 days of GA (internal tool — full adoption expected) |
13. Launch Plan & Stage Gates
| Stage | Audience | Duration | Success Gate to Advance | Owner |
|---|---|---|---|---|
| Internal QA | Bifrost engineers + 1–2 Finance representatives on staging | 1 week | Cron job generates snapshots for all eligible CIDs with 0 failures; dashboard loads correctly at /modpanel/postpaid-usage; bulk download produces valid ZIP with correct file naming ({CID} {Company Name} {Month YYYY} {Type}.csv) and column schemas matching §9.3; 0 P0/P1 bugs | PM + QA |
| GA | All Finance Modpanel users on production | Ongoing | Snapshot success rate ≥ 99% on first production run; ZIP job success rate ≥ 95% in first 2 weeks; Finance confirms workflow replaces manual per-client download process | PM |
14. Dependencies
| Dependency | Type | What we need | Owner | Risk |
|---|---|---|---|---|
| qontak-billing | Data source (internal service) | Read access to monthly_usages (WA Balance, MUV aggregations per org per month) and organization_package_component_postpaid_limits (V3 quota usage). API endpoint: /iag/v1/quota-managements/summary-usage. | qontak-billing team | Medium — if monthly aggregation data schema changes, snapshot generation breaks. Pin API contract in RFC. |
| report-worker | Infrastructure (async job execution) | New job type registered under ExportBillingLogsJobName pattern for ZIP generation; new quota_type routing in BillingLogsExport multiplexer; OSS upload for ZIP output; billing_log_exports table used for status tracking. | report-worker team (Bifrost) | Low — same team; pattern already established. New handler needs to be added. |
| OSS (Alibaba Cloud Object Storage) | Infrastructure (file storage) | Storage for generated ZIP files; private ACL + presigned URL (24h TTL). Already integrated in report-worker. | Infra | Low — already operational for existing exports. |
| Redis | Infrastructure (job queue) | gocraft/work job queue for async export. Already operational in report-worker. | Infra | Low — already operational. |
| Modpanel Finance role/permission | Auth | New permission billing_postpaid_usage_download (or equivalent) scoped to Finance Modpanel users. Requires Permission + Role migration in moderator-be. | Bifrost | Low — established pattern; one migration. |
Note on WABA ID sourcing: WABA ID is required as a dashboard column but its relationship to organization_id/company_id in qontak-billing is not confirmed from code inspection. Engineering must identify the join path before the RFC is written — this is an open technical question, not a PM-level open question.
15. Key Decisions + Alternatives Rejected
15a — Decisions Made
| Date | Decision | Rationale |
|---|---|---|
| 2026-05-25 | Snapshots generated automatically on the 1st of each month via cron, not on-demand | Static snapshots ensure Finance always has consistent, frozen data for billing — real-time queries risk data inconsistency mid-billing cycle |
| 2026-06-23 | Cron time set to 02:00 Asia/Jakarta (not 00:00) | qontak-billing's daily usage aggregation runs at 18:00 Asia/Jakarta. Running the snapshot cron at 02:00 on the 1st guarantees qontak-billing's month-end aggregation (running 18:00 on the last day of the prior month) is complete before snapshot generation begins. 00:00 would risk reading incomplete data. |
| 2026-05-25 | Bulk download uses async ZIP job, not synchronous download | Synchronous download times out on large client volumes; async avoids browser timeout and gives Finance a single ZIP instead of multiple files |
| 2026-05-25 | No per-CID feature flag — global Modpanel flag only | Internal tool; no need for staged per-CID rollout |
| 2026-05-25 | File naming convention: {CID} {Company Name} {Month YYYY} {Type}.csv | Standardized naming allows Finance to identify files without opening them; format confirmed as CSV from sample exports (see §9.3) |
15b — Alternatives Rejected
| Alternative | Why Rejected | Date |
|---|---|---|
| Real-time usage query instead of monthly snapshot | Data inconsistency risk — querying live data mid-month produces different results than querying on billing date; Finance needs frozen, auditable records | 2026-05-25 |
| Synchronous bulk download | Browser timeout on large client volumes; poor UX for Finance when selecting hundreds of records | 2026-05-25 |
| One ZIP file per postpaid type (not per CID) | Finance reconciles per client, not per type — per-CID files match their workflow and make it easier to locate a specific client's report | 2026-05-25 |
16. Open Questions
| # | Question | Owner | Impact if unresolved |
|---|---|---|---|
| OQ-1 | WABA ID sourcing: The postpaid_usage_snapshots table requires a waba_id column, but the join path from organization_id/company_id (used in qontak-billing) to WABA ID is not confirmed from code. Engineering must identify where WABA ID is stored (moderator-be org model? WhatsApp packages table?) and whether it's 1:1 or 1:many per CID. | Bifrost Engineering | Blocks RFC writing. Dashboard column may need to be dropped or sourced differently if WABA ID is not reliably queryable at snapshot time. |
| ✅ Resolved 2026-06-30 — File format confirmed as CSV based on sample exports provided by Finance (see §9.3). No new XLSX dependency needed in report-worker. | |||
| OQ-3 | Billing version "2.0.0" scope: The billing version enum in qontak-billing includes "2.0.0". It is unclear whether V2 clients exist in production and which postpaid types apply to them. Snapshot generation logic must handle V2 explicitly (include or exclude). | Bifrost Engineering + qontak-billing | Incorrect handling silently omits or double-counts V2 CIDs in the snapshot. |
PRD CHANGELOG
| Version | Date | By | Section | Type | Summary |
|---|---|---|---|---|---|
| 1.0 | 2026-06-22 | Claude | All | REFORMATTED | Reformatted from [PRD] Postpaid Usage Monthly Scheduler to Qontak PRD template v1.1. 3 sections extracted from source (S1, S8 flow, S8 stories), 13 sections filled via coaching interview, 2 flags remaining (S7 API deferred to RFC, S12 success metrics baseline TBD). |
| 1.1 | 2026-06-22 | Claude | S12, S16, Flag Summary | MODIFIED | Confirmed Finance baseline at 3 hours/month; updated S12 primary KPI baseline and target; cleared S12 flag; resolved S16 open question. |
| 1.2 | 2026-06-23 | Claude | S8, Flag Summary | FILLED | Added PM-level API & Webhook Behavior table (5 behaviors: monthly cron snapshot, paginated dashboard read, search/filter, async ZIP job initiation, ZIP delivery + expiry). HTTP contracts deferred to RFC. Flag summary updated to ✅ Reformat Complete. |
| 1.3 | 2026-06-23 | Claude | S6, S8, S9.1, S9.2 (PUMS-S01, S06), S14, S15, S16 | ENRICHED | Technical accuracy pass against moderator-be, qontak-billing, report-worker source code. Added: §6.2 Service Architecture (3-service map with table/API references); corrected cron time 00:00→02:00 Jakarta (billing aggregation dependency); Flipper flag format corrected; S8 table expanded with service owners + data sources + job infrastructure (billing_log_exports, gocraft/work, OSS); System Flow diagram updated with 6 participants (Sidekiq Cron, qontak-billing, moderator-be, report-worker, OSS, Modpanel); PUMS-S01 Technical Notes updated with actual table names, encryption note, billing version enum, cron schedule format; PUMS-S06 Technical Notes updated with report-worker pattern, billing_log_exports, retry strategy; S14 Dependencies completely rewritten (5 real dependencies identified — qontak-billing, report-worker, OSS, Redis, Modpanel permission); S15 added cron timing decision; S16 reopened with 3 engineering open questions (WABA ID sourcing, XLSX vs CSV, V2 billing version). |
| 1.4 | 2026-06-30 | Claude | S8, S9.1, S9.2 (PUMS-S06), S9.3 (new), S13, S15a, S16 | ENRICHED | Added §9.3 Export Format Reference with confirmed CSV column schemas for all 3 postpaid types (WA Balance, MUV, Call Balance) from Finance-provided sample exports. File format confirmed as CSV (resolves OQ-2). File naming convention updated throughout: {CID} {Company Name} {Month YYYY} {Type}.csv — month spelled out (e.g. April), no "Postpaid Usage" text, type is full label. PUMS-S06 AC-3 updated to reference §9.3 column schemas. S8 footnote updated to reflect confirmed format. S13 stage gate updated to reference §9.3. S15a naming decision updated with CSV confirmation. OQ-2 closed. |