Skip to main content

Qontak | Billing & Platform | Postpaid Usage Monthly Scheduler — Phase 1: Automation

Product Requirements Document · NEW PRD v1.0


HEADER BLOCK

FieldValue
PMQontak PM Group
PRD Version1.0
StatusDRAFT
PRD TypeNEW
EpicBIF-8641
SquadBifrost
RFC LinkTBD — pending PRD approval
Figma MasterTBD — pending design
AnchorNo — standalone, single-squad
Labelsepic:qontak-platform | module:billing-platform | feature:postpaid-usage-scheduler
Last Updated2026-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, and report-worker source 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


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

PersonaRoleGoalPainWorkaround
Primary — Finance StaffFinance team member responsible for processing monthly postpaid billing and reconciliation across all CID clientsDownload all client postpaid usage reports at the start of each month to run billing and reconciliation accuratelyMust 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 clientOpens 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

  1. This does not provide real-time usage data — reports are monthly snapshots only, frozen on the 1st of each month.
  2. This does not support postpaid usage downloads for Billing V1 components beyond WA Balance and MUV — no other V1 types are included.
  3. This does not allow Finance users to edit, correct, or reprocess usage data — read and download only.
  4. This does not expose the dashboard to external clients or CID admins — internal Finance Modpanel use only.
  5. This does not send automated email or push notifications to Finance when the monthly snapshot is ready — Finance checks the dashboard manually.
  6. This does not support on-demand or manual snapshot regeneration — only the automated 1st-of-month cron job generates snapshots.
  7. 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

FieldValue
PlatformModpanel web only (internal admin panel). No mobile.
Performance — page loadDashboard table loads ≤ 3s for up to 1,000 records
Performance — ZIP jobAsync ZIP generation completes ≤ 30 minutes from trigger
Performance — cron jobMonthly snapshot generation completes within 2 hours of 00:00 on the 1st
Plan scopeInternal Finance Modpanel users only — no plan/tier restriction
Feature flagFlipper 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 limitsMax 50MB per ZIP download request. Pagination: 50 rows per page.
Read/writeFinance 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.

ServiceRole in this featureKey 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 TypeRetention PeriodCleanup TriggerUser-Visible Effect
Monthly usage snapshot (DB record)24 monthsScheduled annual purge or manual cleanup by engineeringNone — historical data available for 2 years
In-progress ZIP jobUntil job completes or failsJob completion or failure eventNone
Generated ZIP file24 hours from creationTTL-based cleanup job"Download link expired" shown if accessed after 24h
Failed ZIP job artifact24 hoursNightly cleanup cronNone — Finance sees "Generation failed. Try again."

7. New Features

Feature: Postpaid Usage Dashboard

FieldDetail
URL/modpanel/postpaid-usage
AccessFinance Modpanel users only

Component Tree:

ComponentParentPurpose
PostpaidUsagePageTop-level page container
PageHeaderPostpaidUsagePageDisplays page title "Postpaid Usage"
FilterBarPostpaidUsagePageContains search and date filter controls
SearchInputFilterBarSearch by WABA ID or Company ID (CID); filters table on input
YearMonthPickerFilterBarFilter by month; defaults to most recently generated month
BulkActionBarPostpaidUsagePageVisible when ≥1 row selected; shows count + Download All button
SelectionCountBulkActionBarDisplays "X records selected"
DownloadAllButtonBulkActionBarTriggers async ZIP job for all selected records
UsageTablePostpaidUsagePagePaginated table of postpaid usage snapshot records
SelectAllCheckboxUsageTableHeader row checkbox; selects all records matching current filter across all pages
UsageRowUsageTableOne row per CID per postpaid type per month
RowCheckboxUsageRowSelects individual record
WABAIdUsageRowWABA ID for the record
CompanyIdUsageRowCompany ID (CID)
CompanyNameUsageRowCompany name
PostpaidTypeUsageRowPostpaid type label: WA Balance / MUV / Call Balance
YearMonthUsageRowReporting period in YYYY-MM format
ReportDateUsageRowDate the snapshot was generated
PaginationUsageTable50 rows per page
ZIPStatusToastPostpaidUsagePageAppears on download trigger; updates to download link when ZIP is ready

UI States:

StateDescription
Empty"No usage data available for this period." — no action button
LoadingSkeleton rows (5) in UsageTable while fetching
Error"Could not load usage data. Try again." + Retry button
SuccessTable populated with records; pagination visible if >50 results

Figma: TBD — pending design


8. API & Webhook Behavior

#BehaviorService OwnerEntity AffectedTriggered ByExpected BehaviorFailure Behavior
1Monthly cron → snapshot generationmoderator-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.
2Read paginated dashboard recordsmoderator-beSnapshot records read from postpaid_usage_snapshots; returned to Finance dashboardFinance user loads /modpanel/postpaid-usageReturns 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.
3Search and filter recordsmoderator-beSnapshot records read (filtered subset)Finance user submits search query or changes YearMonthPickerAccepts 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."
4Initiate async ZIP jobmoderator-be (triggers job); report-worker (executes job via gocraft/work Redis queue)New billing_log_exports record created (status: pending); job_id returnedFinance user clicks "Download All" with ≥1 records selectedValidates 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.
5ZIP job completion → deliver download linkreport-workerZIP file generated and uploaded to OSS; billing_log_exports.export_statuscompleted; download URL returned to FinanceAsync job in report-worker completes processingPackages 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

  1. On the 1st of each month at 00:00, the cron job triggers monthly snapshot generation
  2. 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)
  3. Snapshots saved as static records — data is frozen at this point and will not change
  4. If a CID snapshot fails: failure is logged with snapshot_failed event; job continues to next CID without aborting
  5. Finance user navigates to /modpanel/postpaid-usage
  6. Dashboard loads with YearMonthPicker defaulting to the most recently generated month; UsageTable fetches and displays paginated records (50 per page)
  7. Finance user searches by WABA ID or CID and/or selects a Year-Month filter; table updates to filtered results
  8. Finance user selects records using row checkboxes or SelectAll (selects all records matching current filter across all pages)
  9. Finance user clicks "Download All" — system validates total selection does not exceed 50MB
  10. If selection exceeds 50MB: request rejected immediately; Finance sees "Selection exceeds 50MB limit. Reduce your selection and try again."
  11. If within limit: async ZIP job initiated; Finance sees "File is generating..." toast; bulk_download_triggered event logged
  12. Backend processes ZIP in background — packages individual .csv files per CID per postpaid type, named {CID} {Company Name} {Month YYYY} {Type}.csv (e.g. 12345 Citra Angkasa April 2026 WA Balance.csv)
  13. ZIP job completes: download link provided to Finance; zip_job_completed event logged
  14. If ZIP job fails: Finance sees "Generation failed. Try again."; zip_job_failed event logged
  15. 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

TypeExample filename
WA Balance12345 Citra Angkasa April 2026 WA Balance.csv
MUV12345 Citra Angkasa April 2026 MUV.csv
Call Balance12345 Citra Angkasa April 2026 Call Balance.csv

Notes on naming:

  • {Month} is the full English month name (e.g. April, January) — not YYYY-MM
  • {YYYY} is the 4-digit reporting year
  • {Type} exactly matches the postpaid type label: WA Balance, MUV, or Call Balance
  • No "Postpaid Usage" text in the filename

WA Balance CSV — Column Schema

ColumnTypeExample
created_at (GMT+7)date (YYYY-MM-DD)2026-04-20
recipientstring (phone with + prefix)+62881010422155
conversation_typestringBI
conversation_categorystringutility
count_messagesinteger17
sum_creditdecimal6233.05
countrystring (ISO 3166-1 alpha-2)ID
credited_tostringwa_balance

MUV CSV — Column Schema

ColumnTypeExample
Created atdatetime string (Mon DD YYYY, hh:mm:ss AM/PM +07:00)Apr 01 2026, 02:04:18 PM +07:00
Channelstringwa_cloud
Customer namestringDwi Riptono
Account unique idstring6281215727642
RecipientstringPT Citra Angkasa Lintas Media
Credited Tostringmuv_credit

Call Balance CSV — Column Schema

ColumnTypeExample
created_at (GMT+7)date (YYYY-MM-DD)2026-04-20
recipientstring (phone, no + prefix)62881010422155
call_directionstring (inbound / outbound)inbound
count_call_idinteger17
sum_creditdecimal6233.05
countrystring (ISO 3166-1 alpha-2)ID

9.2 User Stories

User StoryImportanceMockup / Technical NotesAcceptance 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 HaveFigma: 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 generated

Implementation 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 HaveFigma: 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 column

Before-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 HaveFigma: TBD — pending design

Data Fields:
postpaid_type (enum, required) — mapped from Billing V1/V3 component data; values: WA Balance, MUV, Call Balance

Technical 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 HaveFigma: 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 month

Before-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 HaveFigma: 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 triggered

Before-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 HaveFigma: 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 creation

Technical 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: pendingcompleted / 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

FieldValue
Feature flagpostpaid_usage_scheduler_enabled — default: ON (see Constraints)
Stage 1Internal QA — Bifrost engineers + 1–2 Finance representatives on staging
Stage 2 (GA)All Finance Modpanel users on production
Backward compatNo existing behavior affected — new dashboard and new snapshot table only
MigrationNone required — new schema, no existing data transformation
First snapshotTriggered automatically by cron on the 1st of the next calendar month after deployment — no manual backfill

11. Observability

Key Events:

Event NameTriggerProperties
snapshot_generatedCron job successfully generates snapshot for a CIDcid, year_month, billing_type, record_count
snapshot_failedCron job fails to generate snapshot for a CIDcid, year_month, reason
usage_dashboard_loadedFinance user loads the Postpaid Usage Dashboarduser_id, year_month_filter, record_count_returned
bulk_download_triggeredFinance user clicks "Download All"user_id, selected_count, year_month_filter
zip_job_completedAsync ZIP job finishes successfullyjob_id, user_id, file_size_mb, duration_seconds
zip_job_failedAsync ZIP job failsjob_id, user_id, reason
zip_size_limit_exceededSelection exceeds 50MB — job rejected before creationuser_id, estimated_size_mb, selected_count
FieldDetail
Dashboard ownerBifrost PM + Engineering lead
AlertsNone defined for initial launch — Bifrost team monitors via event data during post-launch review cadence

11.1 Post-Launch Monitoring Cadence

FieldDetail
Review cadenceWeekly for first 4 weeks post-GA, then monthly
OwnerBifrost PM + Engineering lead
Review scopesnapshot_failed rate, zip_job_failed rate, zip_size_limit_exceeded frequency
Trigger threshold 1snapshot_failed rate > 5% in any monthly run → immediate investigation by Bifrost engineering
Trigger threshold 2zip_job_failed rate > 10% in any 7-day window → engineering escalation within 24 hours
Rollback considerationIf 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:

MetricDefinitionBaselineTarget
Time Finance spends on monthly postpaid usage downloadTotal hours Finance team spends downloading and aggregating postpaid usage reports each billing cycle3 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:

MetricDefinitionBaselineTarget
Snapshot generation success rate% of eligible CIDs with successful monthly snapshots per runN/A — new feature≥ 99% per monthly run
ZIP job completion success rate% of bulk download requests that complete successfullyN/A — new feature≥ 95% within 60 days of GA

Adoption:

MetricDefinitionBaselineTarget
Finance adoption rate% of monthly billing cycles where Finance uses bulk download instead of manual per-client downloadN/A — new feature100% within 30 days of GA (internal tool — full adoption expected)

13. Launch Plan & Stage Gates

StageAudienceDurationSuccess Gate to AdvanceOwner
Internal QABifrost engineers + 1–2 Finance representatives on staging1 weekCron 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 bugsPM + QA
GAAll Finance Modpanel users on productionOngoingSnapshot success rate ≥ 99% on first production run; ZIP job success rate ≥ 95% in first 2 weeks; Finance confirms workflow replaces manual per-client download processPM

14. Dependencies

DependencyTypeWhat we needOwnerRisk
qontak-billingData 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 teamMedium — if monthly aggregation data schema changes, snapshot generation breaks. Pin API contract in RFC.
report-workerInfrastructure (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.InfraLow — already operational for existing exports.
RedisInfrastructure (job queue)gocraft/work job queue for async export. Already operational in report-worker.InfraLow — already operational.
Modpanel Finance role/permissionAuthNew permission billing_postpaid_usage_download (or equivalent) scoped to Finance Modpanel users. Requires Permission + Role migration in moderator-be.BifrostLow — 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

DateDecisionRationale
2026-05-25Snapshots generated automatically on the 1st of each month via cron, not on-demandStatic snapshots ensure Finance always has consistent, frozen data for billing — real-time queries risk data inconsistency mid-billing cycle
2026-06-23Cron 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-25Bulk download uses async ZIP job, not synchronous downloadSynchronous download times out on large client volumes; async avoids browser timeout and gives Finance a single ZIP instead of multiple files
2026-05-25No per-CID feature flag — global Modpanel flag onlyInternal tool; no need for staged per-CID rollout
2026-05-25File naming convention: {CID} {Company Name} {Month YYYY} {Type}.csvStandardized naming allows Finance to identify files without opening them; format confirmed as CSV from sample exports (see §9.3)

15b — Alternatives Rejected

AlternativeWhy RejectedDate
Real-time usage query instead of monthly snapshotData inconsistency risk — querying live data mid-month produces different results than querying on billing date; Finance needs frozen, auditable records2026-05-25
Synchronous bulk downloadBrowser timeout on large client volumes; poor UX for Finance when selecting hundreds of records2026-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 report2026-05-25

16. Open Questions

#QuestionOwnerImpact if unresolved
OQ-1WABA 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 EngineeringBlocks RFC writing. Dashboard column may need to be dropped or sourced differently if WABA ID is not reliably queryable at snapshot time.
OQ-2File format — XLSX vs CSVBifrost Engineering + Finance✅ 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-3Billing 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-billingIncorrect handling silently omits or double-counts V2 CIDs in the snapshot.

PRD CHANGELOG

VersionDateBySectionTypeSummary
1.02026-06-22ClaudeAllREFORMATTEDReformatted 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.12026-06-22ClaudeS12, S16, Flag SummaryMODIFIEDConfirmed Finance baseline at 3 hours/month; updated S12 primary KPI baseline and target; cleared S12 flag; resolved S16 open question.
1.22026-06-23ClaudeS8, Flag SummaryFILLEDAdded 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.32026-06-23ClaudeS6, S8, S9.1, S9.2 (PUMS-S01, S06), S14, S15, S16ENRICHEDTechnical 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.42026-06-30ClaudeS8, S9.1, S9.2 (PUMS-S06), S9.3 (new), S13, S15a, S16ENRICHEDAdded §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.