Skip to main content

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 IDTitleAcceptance CriteriaPoints
US-BILL-001Consume billing.events from NATSGiven a valid billing.message.charged.v1 message, when the consumer processes it, then a billing_events row is persisted and NATS message ACKed within 5s3
US-BILL-002Idempotent ingestion on duplicate messageIdGiven 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 ACKed2
US-BILL-003Resolve pricing from pricing_tablesGiven an event with (accountTier, operatorId, direction), when ingesting, then the correct active PricingTable row is resolved using effectiveFrom/effectiveTo3
US-BILL-004Redis 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 log2
US-BILL-005Compute customerPrice (PER_SEGMENT model)Given pricingModel=PER_SEGMENT, unitPrice=0.02, segmentCount=3, then customerPrice=0.062
US-BILL-006Compute customerPrice (FLAT_PER_MESSAGE model)Given pricingModel=FLAT_PER_MESSAGE, unitPrice=0.05, segmentCount=3, then customerPrice=0.051
US-BILL-007Compute operatorCost and marginGiven resolved OperatorCost, then operatorCost and margin are persisted; negative margin is allowed and triggers billing_negative_margin_total metric increment2
US-BILL-008Update usage_summaries on ingestGiven a successful billing_events insert, then the corresponding usage_summaries hourly bucket is upserted with incremented counts and amounts3
US-BILL-009Handle pricing rule not foundGiven 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 written2
US-BILL-010Pricing cache invalidation on admin updateGiven a PricingTable create/update/delete, then the corresponding Redis cache key is DELeted immediately1

Epic EP-BILL-02: Invoice Generation

Goal: Generate monthly invoices automatically, render PDFs, store to S3, and notify downstream services.

Story IDTitleAcceptance CriteriaPoints
US-BILL-011Monthly 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 min3
US-BILL-012Invoice aggregation from usage_summariesGiven usage_summaries rows for a period, when aggregating, then totalMessages, totalSegments, subtotalAmount match the SUM of the summary rows3
US-BILL-013PDF render and S3 uploadGiven aggregated usage, when invoice is generated, then a PDF is rendered from the Handlebars template and stored at invoices/{accountId}/{year}/{month}/{invoiceId}.pdf5
US-BILL-014Invoice persisted as FINALIZEDGiven successful PDF upload, then invoices row transitions from DRAFT to FINALIZED with generated_at and s3_key set2
US-BILL-015Publish billing.invoice.generated eventGiven invoice FINALIZED, then billing.invoice.generated.v1 is published to NATS with correct invoiceId, accountId, subtotalAmount, s3Key2
US-BILL-016Per-account failure isolationGiven one account's PDF render fails, then other accounts' invoices continue generating; failed account logged and alerted2
US-BILL-017CronJob concurrency preventionGiven a K8s CronJob with concurrencyPolicy: Forbid, then if a previous run is still active on 1st of month, the new run is skipped1
US-BILL-018Invoice download via presigned URLGiven a GET /v1/billing/invoices/{id}/download request from an authenticated billing:read user, then a presigned S3 URL valid for 15 min is returned2
US-BILL-019Invoice void by platform.financeGiven 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 persisted3

Epic EP-BILL-03: Pricing & Cost Administration

Goal: Provide admin APIs for managing versioned pricing rules and operator costs.

Story IDTitleAcceptance CriteriaPoints
US-BILL-020Create pricing table entryGiven a POST /v1/admin/pricing with valid fields, then a new pricing_tables row is created and effectiveTo=null (active)2
US-BILL-021Prevent overlapping active pricing rulesGiven an existing active row for (accountTier, operatorId, direction, currency), when creating another active row for the same key, then a 409 conflict is returned2
US-BILL-022Versioned pricing updateGiven 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 specified3
US-BILL-023Delete (soft) pricing ruleGiven a DELETE /v1/admin/pricing/{id}, then effectiveTo=today is set; no row is physically deleted; historical billing_events retain pricingTableId reference1
US-BILL-024Create operator cost entryGiven a POST /v1/admin/operator-costs with valid fields, then a new operator_costs row is created2
US-BILL-025Versioned operator cost updateGiven a PATCH /v1/admin/operator-costs/{id}, then the same versioning pattern as pricing applies2
US-BILL-026List active pricing rulesGiven a GET /v1/admin/pricing?activeOnly=true, then only rows with effectiveTo IS NULL are returned1

Epic EP-BILL-04: Usage Query API

Goal: Expose accurate, fast usage data for customer self-service and admin reporting.

Story IDTitleAcceptance CriteriaPoints
US-BILL-027Usage query — account scopeGiven a GET /v1/billing/usage?from=&to= by an authenticated account.admin, then usage for their account is returned aggregated by granularity=day3
US-BILL-028Usage query — cross-account (platform.admin)Given a platform.admin querying with ?accountId=, then usage for any account is returned2
US-BILL-029Usage query — granularity optionsGiven `?granularity=hourday
US-BILL-030Usage query P95 ≤ 300 msGiven a 13-month date range, then the usage query returns within 300 ms P95 (uses pre-aggregated usage_summaries)3
US-BILL-031Tenant isolation on usage queryGiven 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 IDTitleAcceptance CriteriaPoints
US-BILL-032Prometheus metrics endpointGiven /metrics, then all billing metric families are exposed in Prometheus format2
US-BILL-033BillingPricingNotFound alert firesGiven an event with no matching pricing rule, then BillingPricingNotFound alert fires within 2 min2
US-BILL-034BillingNatsLag alertGiven consumer lag > 10,000 events, then BillingNatsLag alert fires1
US-BILL-035BillingNegativeMargin alertGiven a negative margin event persisted, then BillingNegativeMargin counter increments and alert fires1
US-BILL-036Readiness probe checks PG + Redis + NATSGiven /health/ready, then 200 only when PG, Redis, and NATS are all reachable1

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 IDTitleAcceptance CriteriaPoints
US-BILL-037UCS-2 segment-count parity with smpp-connectorGiven 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 pinning5
US-BILL-038Multi-currency pricing_tables with currency codepricing_tables adds currency CHAR(3) NOT NULL; lookups partition by currency; existing rows backfilled to AFN3
US-BILL-039Daily FX-rate ingestion and pinningCron 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 audit5
US-BILL-040Tenant tax (VAT) configurationbill.tenant_tax_config (tenantId, taxRate decimal(5,4), taxId VARCHAR, taxRegion); applied at invoice generation; line-items vs. invoice-level configurable3
US-BILL-041Tax-inclusive vs. tax-exclusive pricing modesTenant config flag priceModelInclusive: bool; invoice rendering shows subtotal, tax, total accordingly; PDF template updated3
US-BILL-042Compliance-blocked refund/credit reversalWhen compliance.message.blocked.v1 arrives for a pre-credited tenant, billing reverses the credit hold within 30 s; emits billing.credit.reversed.v1; audit row3

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 IDTitleAcceptance CriteriaPoints
US-BILL-043Wallet model and balance reservationbill.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 lock5
US-BILL-044Top-up via PSP webhookPOST /v1/wallets/topup/webhook/{provider} accepts PSP completion; HMAC-verified; updates bill.wallet_transactions; emits billing.wallet.topped_up.v15
US-BILL-045Auto-suspend on zero balance (pre-paid)Wallet balance < threshold (default 0) triggers tenant suspension via auth.tenant.suspended.v1 event; tenant-portal alert + email3
US-BILL-046Net-30 invoicing modeTenants in mode=POSTPAID receive monthly invoices with 30-d net terms; overdue alerts after 7/14/30 d3
US-BILL-047Credit-note generationPOST /v1/admin/credit-notes issues credit note tied to original invoice; PDF generated; tenant balance adjusted5
US-BILL-048Dispute workflowTenant raises dispute via POST /v1/billing/disputes with reason; finance triages; status workflow OPEN → IN_REVIEW → RESOLVED/REJECTED; SLA target 7 d5
US-BILL-049Wallet-balance dashboard for tenantCustomer-portal page shows live balance, recent transactions, top-up history; warns when balance projected to deplete in < 7 d3

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 IDTitleAcceptance CriteriaPoints
US-BILL-050Reserved-capacity contracts modelbill.reserved_contracts (tenantId, mno, reservedTps, term, monthlyFee, slaCreditPercent); enforced at routing-engine via EP-RE-06 preferences5
US-BILL-051SLA-credit calculationMonthly cron computes per-tenant SLA achievement vs. contract; if breach → credit accrued at slaCreditPercent × monthlyFee; emits billing.sla_credit.accrued.v15
US-BILL-052Reserved-capacity dashboard for tenantCustomer-portal shows: contracted TPS, actual TPS, headroom, SLA status; downloadable PDF per period3
US-BILL-053Reserved-capacity overage handlingTPS above reserved billed at on-demand rate; documented in invoice line-items separately3
US-BILL-054Contract management API for financeAdmin REST CRUD on contracts; immutable after effectiveFrom; supersession via new contract row3

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 IDTitleAcceptance CriteriaPoints
US-BILL-055Daily CDR-vs-billing reconciliationCron 02:00 UTC compares cdr.records vs billing_events for previous day; mismatches written to bill.reconciliation_exceptions; report emailed to finance5
US-BILL-056Negative-margin auto-investigationBillingNegativeMargin events trigger automated investigation: pull last-known operator cost, last pricing rule version, dispatched payload; ticket auto-created in finance queue3
US-BILL-057Revenue-leakage dashboardGrafana panel: leakage $/day per category (unbilled DLR, duplicate bill, mis-priced); alert if > 0.1% of daily revenue5
US-BILL-058Per-tenant exposure capHard limit bill.tenant_exposure_cap (e.g., AFN 10M unbilled); exceeded → tenant auto-suspended pending review; finance approval to release3