Billing Service — Jira Epics & User Stories
Status: populated Owner: Product + Platform Engineering Last updated: 2026-04-18
Epic EP-BILL-01: Chargeable Event Ingestion
Goal: Reliably consume billing.events from NATS, apply versioned pricing, compute margin, and persist billing records without double-billing.
| Story ID | Title | Acceptance Criteria | Points |
|---|---|---|---|
| US-BILL-001 | Consume billing.events from NATS | Given a valid billing.message.charged.v1 message, when the consumer processes it, then a billing_events row is persisted and NATS message ACKed within 5s | 3 |
| US-BILL-002 | Idempotent ingestion on duplicate messageId | Given a billing.events message with a messageId already in billing_events, when delivered again, then the second insert is silently ignored and the message is ACKed | 2 |
| US-BILL-003 | Resolve pricing from pricing_tables | Given an event with (accountTier, operatorId, direction), when ingesting, then the correct active PricingTable row is resolved using effectiveFrom/effectiveTo | 3 |
| US-BILL-004 | Redis pricing cache (TTL 60s) | Given a pricing lookup, when the Redis key exists and is fresh, then PG is not queried; when Redis is unavailable, then PG is queried with a warning log | 2 |
| US-BILL-005 | Compute customerPrice (PER_SEGMENT model) | Given pricingModel=PER_SEGMENT, unitPrice=0.02, segmentCount=3, then customerPrice=0.06 | 2 |
| US-BILL-006 | Compute customerPrice (FLAT_PER_MESSAGE model) | Given pricingModel=FLAT_PER_MESSAGE, unitPrice=0.05, segmentCount=3, then customerPrice=0.05 | 1 |
| US-BILL-007 | Compute operatorCost and margin | Given resolved OperatorCost, then operatorCost and margin are persisted; negative margin is allowed and triggers billing_negative_margin_total metric increment | 2 |
| US-BILL-008 | Update usage_summaries on ingest | Given a successful billing_events insert, then the corresponding usage_summaries hourly bucket is upserted with incremented counts and amounts | 3 |
| US-BILL-009 | Handle pricing rule not found | Given no matching pricing_tables row for an event, then the NATS message is NAKed with a delay, BillingPricingNotFound alert fires, and no partial record is written | 2 |
| US-BILL-010 | Pricing cache invalidation on admin update | Given a PricingTable create/update/delete, then the corresponding Redis cache key is DELeted immediately | 1 |
Epic EP-BILL-02: Invoice Generation
Goal: Generate monthly invoices automatically, render PDFs, store to S3, and notify downstream services.
| Story ID | Title | Acceptance Criteria | Points |
|---|---|---|---|
| US-BILL-011 | Monthly invoice cron (1st of month 00:05 UTC) | Given the CronJob fires on 1st of month, then invoices are generated for all accounts with usage in the previous month within 60 min | 3 |
| US-BILL-012 | Invoice aggregation from usage_summaries | Given usage_summaries rows for a period, when aggregating, then totalMessages, totalSegments, subtotalAmount match the SUM of the summary rows | 3 |
| US-BILL-013 | PDF render and S3 upload | Given aggregated usage, when invoice is generated, then a PDF is rendered from the Handlebars template and stored at invoices/{accountId}/{year}/{month}/{invoiceId}.pdf | 5 |
| US-BILL-014 | Invoice persisted as FINALIZED | Given successful PDF upload, then invoices row transitions from DRAFT to FINALIZED with generated_at and s3_key set | 2 |
| US-BILL-015 | Publish billing.invoice.generated event | Given invoice FINALIZED, then billing.invoice.generated.v1 is published to NATS with correct invoiceId, accountId, subtotalAmount, s3Key | 2 |
| US-BILL-016 | Per-account failure isolation | Given one account's PDF render fails, then other accounts' invoices continue generating; failed account logged and alerted | 2 |
| US-BILL-017 | CronJob concurrency prevention | Given a K8s CronJob with concurrencyPolicy: Forbid, then if a previous run is still active on 1st of month, the new run is skipped | 1 |
| US-BILL-018 | Invoice download via presigned URL | Given a GET /v1/billing/invoices/{id}/download request from an authenticated billing:read user, then a presigned S3 URL valid for 15 min is returned | 2 |
| US-BILL-019 | Invoice void by platform.finance | Given a POST /v1/admin/invoices/{id}/void with a mandatory reason, then invoice status transitions to VOID and voided_by/voided_at/void_reason are persisted | 3 |
Epic EP-BILL-03: Pricing & Cost Administration
Goal: Provide admin APIs for managing versioned pricing rules and operator costs.
| Story ID | Title | Acceptance Criteria | Points |
|---|---|---|---|
| US-BILL-020 | Create pricing table entry | Given a POST /v1/admin/pricing with valid fields, then a new pricing_tables row is created and effectiveTo=null (active) | 2 |
| US-BILL-021 | Prevent overlapping active pricing rules | Given an existing active row for (accountTier, operatorId, direction, currency), when creating another active row for the same key, then a 409 conflict is returned | 2 |
| US-BILL-022 | Versioned pricing update | Given a PATCH /v1/admin/pricing/{id}, then the existing row gets effectiveTo=today and a new row is created with the updated values and effectiveFrom as specified | 3 |
| US-BILL-023 | Delete (soft) pricing rule | Given a DELETE /v1/admin/pricing/{id}, then effectiveTo=today is set; no row is physically deleted; historical billing_events retain pricingTableId reference | 1 |
| US-BILL-024 | Create operator cost entry | Given a POST /v1/admin/operator-costs with valid fields, then a new operator_costs row is created | 2 |
| US-BILL-025 | Versioned operator cost update | Given a PATCH /v1/admin/operator-costs/{id}, then the same versioning pattern as pricing applies | 2 |
| US-BILL-026 | List active pricing rules | Given a GET /v1/admin/pricing?activeOnly=true, then only rows with effectiveTo IS NULL are returned | 1 |
Epic EP-BILL-04: Usage Query API
Goal: Expose accurate, fast usage data for customer self-service and admin reporting.
| Story ID | Title | Acceptance Criteria | Points |
|---|---|---|---|
| US-BILL-027 | Usage query — account scope | Given a GET /v1/billing/usage?from=&to= by an authenticated account.admin, then usage for their account is returned aggregated by granularity=day | 3 |
| US-BILL-028 | Usage query — cross-account (platform.admin) | Given a platform.admin querying with ?accountId=, then usage for any account is returned | 2 |
| US-BILL-029 | Usage query — granularity options | Given `?granularity=hour | day |
| US-BILL-030 | Usage query P95 ≤ 300 ms | Given a 13-month date range, then the usage query returns within 300 ms P95 (uses pre-aggregated usage_summaries) | 3 |
| US-BILL-031 | Tenant isolation on usage query | Given account A's JWT, when querying usage, then account B's data is never returned (RLS enforced) | 2 |
Epic EP-BILL-05: Observability & Reliability
| Story ID | Title | Acceptance Criteria | Points |
|---|---|---|---|
| US-BILL-032 | Prometheus metrics endpoint | Given /metrics, then all billing metric families are exposed in Prometheus format | 2 |
| US-BILL-033 | BillingPricingNotFound alert fires | Given an event with no matching pricing rule, then BillingPricingNotFound alert fires within 2 min | 2 |
| US-BILL-034 | BillingNatsLag alert | Given consumer lag > 10,000 events, then BillingNatsLag alert fires | 1 |
| US-BILL-035 | BillingNegativeMargin alert | Given a negative margin event persisted, then BillingNegativeMargin counter increments and alert fires | 1 |
| US-BILL-036 | Readiness probe checks PG + Redis + NATS | Given /health/ready, then 200 only when PG, Redis, and NATS are all reachable | 1 |
Epic EP-BILL-06: Multi-Currency (AFN, USD), Tax Engine, FX Rate Pinning per Invoice
Goal: Support AFN and USD invoicing with daily-pinned FX rates, configurable per-tenant tax (VAT) engine, and segment-pricing parity for UCS-2 (Pashto/Dari) so customers are not over-billed on multi-segment messages in non-Latin scripts.
| Story ID | Title | Acceptance Criteria | Points |
|---|---|---|---|
| US-BILL-037 | UCS-2 segment-count parity with smpp-connector | Given a Pashto/Dari message billed using @ghasi/sms-segment-counter, then the segmentCount matches what smpp-connector actually sent (per US-SC-036 contract test); contract CI gate enforces version pinning | 5 |
| US-BILL-038 | Multi-currency pricing_tables with currency code | pricing_tables adds currency CHAR(3) NOT NULL; lookups partition by currency; existing rows backfilled to AFN | 3 |
| US-BILL-039 | Daily FX-rate ingestion and pinning | Cron 00:30 UTC fetches AFN↔USD from configurable provider (DAB primary, fallback to OANDA); rate stored in bill.fx_rates per day; invoices pin the rate for their billing period; manual override endpoint with audit | 5 |
| US-BILL-040 | Tenant tax (VAT) configuration | bill.tenant_tax_config (tenantId, taxRate decimal(5,4), taxId VARCHAR, taxRegion); applied at invoice generation; line-items vs. invoice-level configurable | 3 |
| US-BILL-041 | Tax-inclusive vs. tax-exclusive pricing modes | Tenant config flag priceModelInclusive: bool; invoice rendering shows subtotal, tax, total accordingly; PDF template updated | 3 |
| US-BILL-042 | Compliance-blocked refund/credit reversal | When compliance.message.blocked.v1 arrives for a pre-credited tenant, billing reverses the credit hold within 30 s; emits billing.credit.reversed.v1; audit row | 3 |
Epic EP-BILL-07: Pre-Paid Wallet + Post-Paid Invoice Dual Model + Credit Notes
Goal: Support both pre-paid (wallet top-up; debit on send) and post-paid (monthly invoice; net-30) tenants on the same platform, plus issue credit notes for refunds and disputes.
| Story ID | Title | Acceptance Criteria | Points |
|---|---|---|---|
| US-BILL-043 | Wallet model and balance reservation | bill.wallets (tenantId, balance NUMERIC(18,4), currency); POST /v1/wallets/reserve reserves on submit; release on terminal status; balance per-tenant atomic via row-level lock | 5 |
| US-BILL-044 | Top-up via PSP webhook | POST /v1/wallets/topup/webhook/{provider} accepts PSP completion; HMAC-verified; updates bill.wallet_transactions; emits billing.wallet.topped_up.v1 | 5 |
| US-BILL-045 | Auto-suspend on zero balance (pre-paid) | Wallet balance < threshold (default 0) triggers tenant suspension via auth.tenant.suspended.v1 event; tenant-portal alert + email | 3 |
| US-BILL-046 | Net-30 invoicing mode | Tenants in mode=POSTPAID receive monthly invoices with 30-d net terms; overdue alerts after 7/14/30 d | 3 |
| US-BILL-047 | Credit-note generation | POST /v1/admin/credit-notes issues credit note tied to original invoice; PDF generated; tenant balance adjusted | 5 |
| US-BILL-048 | Dispute workflow | Tenant raises dispute via POST /v1/billing/disputes with reason; finance triages; status workflow OPEN → IN_REVIEW → RESOLVED/REJECTED; SLA target 7 d | 5 |
| US-BILL-049 | Wallet-balance dashboard for tenant | Customer-portal page shows live balance, recent transactions, top-up history; warns when balance projected to deplete in < 7 d | 3 |
Epic EP-BILL-08: Reserved-Capacity / Committed-Throughput SLA-Backed Pricing Tiers
Goal: Enterprise tenants can purchase reserved TPS capacity per MNO with SLA-backed credits when capacity not delivered.
| Story ID | Title | Acceptance Criteria | Points |
|---|---|---|---|
| US-BILL-050 | Reserved-capacity contracts model | bill.reserved_contracts (tenantId, mno, reservedTps, term, monthlyFee, slaCreditPercent); enforced at routing-engine via EP-RE-06 preferences | 5 |
| US-BILL-051 | SLA-credit calculation | Monthly cron computes per-tenant SLA achievement vs. contract; if breach → credit accrued at slaCreditPercent × monthlyFee; emits billing.sla_credit.accrued.v1 | 5 |
| US-BILL-052 | Reserved-capacity dashboard for tenant | Customer-portal shows: contracted TPS, actual TPS, headroom, SLA status; downloadable PDF per period | 3 |
| US-BILL-053 | Reserved-capacity overage handling | TPS above reserved billed at on-demand rate; documented in invoice line-items separately | 3 |
| US-BILL-054 | Contract management API for finance | Admin REST CRUD on contracts; immutable after effectiveFrom; supersession via new contract row | 3 |
Epic EP-BILL-09: Revenue Assurance / Leakage Detection (vs. CDR)
Goal: Cross-check billing.events against the canonical CDR pipeline (EP-CDR-*) to detect revenue leakage (DLRs not billed, double billing, mis-priced messages).
| Story ID | Title | Acceptance Criteria | Points |
|---|---|---|---|
| US-BILL-055 | Daily CDR-vs-billing reconciliation | Cron 02:00 UTC compares cdr.records vs billing_events for previous day; mismatches written to bill.reconciliation_exceptions; report emailed to finance | 5 |
| US-BILL-056 | Negative-margin auto-investigation | BillingNegativeMargin events trigger automated investigation: pull last-known operator cost, last pricing rule version, dispatched payload; ticket auto-created in finance queue | 3 |
| US-BILL-057 | Revenue-leakage dashboard | Grafana panel: leakage $/day per category (unbilled DLR, duplicate bill, mis-priced); alert if > 0.1% of daily revenue | 5 |
| US-BILL-058 | Per-tenant exposure cap | Hard limit bill.tenant_exposure_cap (e.g., AFN 10M unbilled); exceeded → tenant auto-suspended pending review; finance approval to release | 3 |