Skip to content

Billing Domain Implementation

Version: 1.0.0 Last Updated: 2026-02-11 Status: Stable

Overview

The Billing domain manages team balance accounting, transaction history, auto-refill configuration, and invoice generation. It maintains the source-of-truth for team spending and provides audit trails for all financial operations.

Key Responsibilities: - Track team balance across payment methods - Record all financial transactions (deposits, charges, refunds) - Manage auto-refill thresholds and triggers - Generate and serve invoices as PDF receipts - Support multiple payment gateways (Stripe, PayPal)


Architecture

graph TB
    BA["BillingAccount<br/>(Team Balance)<br/>- balance<br/>- auto_refill_config"]
    BT["BillingTransaction<br/>(Audit Trail)<br/>- type<br/>- amount<br/>- gateway<br/>- status"]
    BPM["BillingPaymentMethod<br/>(Saved Cards)<br/>- external_id<br/>- card_last_four<br/>- is_default"]
    INV["BillingInvoice<br/>(Receipts)<br/>- transaction_id<br/>- pdf_url<br/>- issued_at"]

    BA -->|has many| BT
    BA -->|has many| BPM
    BT -->|has one| INV
    BA -->|linked to| TEAM["Team"]
    BT -->|linked to| USER["User<br/>created_by"]
    BPM -->|linked to| USER

    style BA fill:#3b82f6,color:#fff
    style BT fill:#10b981,color:#fff
    style BPM fill:#f59e0b,color:#fff
    style INV fill:#8b5cf6,color:#fff

Data Model

Entities

BillingAccount

Team-scoped billing account holding balance and payment setup.

class BillingAccount(Base, TimestampMixin):
    id: int                                    # Primary key
    team_id: int                               # FK → Team (unique)
    balance: Decimal                           # Current team balance (always >= 0)
    currency: str = "USD"                      # ISO 4217 code
    stripe_customer_id: str | None             # Stripe customer ID
    auto_refill_config: dict | None            # Config below
    created_at: datetime
    updated_at: datetime

auto_refill_config JSON Structure:

{
  "enabled": bool,
  "threshold": 50.0,                    // Refill when balance <= $50
  "amount": 100.0,                      // Add $100 on trigger
  "payment_method_id": "pm_1234abcd",  // Stripe saved card
  "last_triggered_at": "2026-01-15T10:30:00Z"
}

BillingTransaction

Immutable audit record of every financial change.

class BillingTransaction(Base, TimestampMixin):
    id: int                          # Primary key
    billing_account_id: int          # FK → BillingAccount
    created_by_user_id: int | None   # User who initiated (nullable for system)

    # Transaction details
    transaction_type: str            # "deposit" | "charge" | "refund"
    amount: Decimal                  # Always > 0 (direction from type)
    currency: str = "USD"            # ISO 4217 code
    status: str                      # "pending" | "completed" | "failed" | "refunded"

    # Payment gateway metadata
    payment_gateway: str             # "stripe" | "paypal"
    stripe_payment_intent_id: str | None

    # Balance snapshot
    balance_before: Decimal | None
    balance_after: Decimal | None

    # Details
    description: str | None          # Human-readable (e.g., "Stripe one-time payment, card ****1234")
    metadata: dict | None            # Gateway-specific data

    created_at: datetime
    updated_at: datetime

BillingPaymentMethod

Tokenized payment method (never stores raw card data).

class BillingPaymentMethod(Base, TimestampMixin):
    id: int                               # Primary key
    billing_account_id: int               # FK → BillingAccount
    user_id: int | None                   # FK → User (who added it)

    # Stripe metadata
    provider: str = "stripe"              # Future: "paypal"
    external_method_id: str               # e.g., "pm_1234abcd"

    # Safe display info
    card_brand: str | None                # "visa" | "mastercard" | "amex"
    card_last_four: str | None            # Last 4 digits (e.g., "4242")
    card_exp_month: int | None
    card_exp_year: int | None

    # Settings
    is_default: bool = False
    payment_metadata: dict | None         # Additional Stripe data

    created_at: datetime
    updated_at: datetime

BillingInvoice (NEW)

Generated PDF receipt for transactions.

class BillingInvoice(Base, TimestampMixin):
    id: int                           # Primary key
    billing_transaction_id: int       # FK → BillingTransaction (unique)

    # Invoice details
    invoice_number: str               # e.g., "INV-2026-00001"
    issued_at: datetime               # Issue timestamp

    # Storage
    pdf_url: str                      # S3/CDN URL to PDF
    pdf_storage_path: str             # Storage bucket path

    # Metadata for display
    amount: Decimal                   # Amount on invoice
    currency: str = "USD"
    payment_method_description: str   # e.g., "Stripe ••••1234"

    created_at: datetime
    updated_at: datetime

Business Logic

Core Operations

1. Create or Get Billing Account

Operation: get_or_create_account(team_id: int)
Precondition: Team exists
Postcondition: BillingAccount with balance >= 0 exists for team
Side Effects: May insert new account with zero balance
Idempotent: Yes

Auto-creates account on first payment with zero balance.

2. Record Transaction

Operation: record_transaction(
    team_id, transaction_type, amount, gateway,
    payment_intent_id, description, metadata
)
Precondition: BillingAccount exists, amount > 0
Postcondition: BillingTransaction inserted with snapshot
Side Effects: Does NOT update balance immediately (async job)
Idempotent: Yes (via stripe_payment_intent_id uniqueness)
Returns: BillingTransaction object

Transaction type determines direction: - deposit: balance += amount - charge: balance -= amount (fails if insufficient) - refund: balance += amount

3. Update Balance

Operation: update_balance(
    team_id, new_balance, reason
)
Precondition: BillingAccount exists
Postcondition: Balance updated, audit entry created
Side Effects: Invalidates Redis cache
Invariant: balance >= 0 (constraint enforced)

Called after payment confirmation or usage charges. Triggers auto-refill check if enabled.

4. Configure Auto-Refill

Operation: set_auto_refill(
    team_id, enabled, threshold, amount,
    payment_method_id
)
Precondition: User has "billing.edit" permission
Postcondition: auto_refill_config updated
Side Effects: Cache invalidation
Idempotent: Yes

Validates: - threshold > 0 - amount > 0 - payment_method_id exists for team

5. Trigger Auto-Refill

Operation: trigger_auto_refill(team_id)
Precondition: Auto-refill enabled, balance < threshold, no recent trigger
Postcondition: Stripe charge attempted, transaction recorded
Side Effects: May create BillingTransaction
Returns: Boolean (success)
Error Handling: Logs failure, sends owner email notification

Called asynchronously after balance updates. Debounced to prevent rapid retries.

6. Generate Invoice

Operation: generate_invoice(transaction_id)
Precondition: BillingTransaction exists, status = COMPLETED
Postcondition: BillingInvoice created, PDF generated, stored
Side Effects: Uploads PDF to S3, sends email to team owner
Returns: BillingInvoice with pdf_url

Generates PDF receipt from: - Transaction amount, date, method - Team name, company info - Invoice number (sequential) - Issuer business details

7. Download Invoice

Operation: download_invoice(invoice_id)
Precondition: Invoice exists, user owns team
Postcondition: PDF downloaded or redirected to S3 URL
Returns: Binary PDF or 302 redirect

Validates ownership, logs download for audit trail.


State Machine

Transaction Lifecycle:

stateDiagram-v2
    [*] --> PENDING: Record transaction
    PENDING --> COMPLETED: Payment confirmed
    PENDING --> FAILED: Payment failed
    FAILED --> [*]
    COMPLETED --> REFUNDED: Refund issued
    REFUNDED --> [*]
    COMPLETED --> [*]: Expired (>90 days)

Auto-Refill Trigger Flow:

stateDiagram-v2
    [*] --> CHECK: Balance updated
    CHECK --> ELIGIBLE: balance < threshold && enabled
    ELIGIBLE --> CHARGING: Acquire lock
    CHARGING --> SUCCESS: Charge succeeded
    SUCCESS --> UPDATE: Update balance & log
    UPDATE --> [*]
    CHARGING --> RETRY: Charge failed
    RETRY --> BACKOFF: Wait (exponential)
    BACKOFF --> CHARGING: Retry
    CHARGING --> FINAL_FAIL: Max retries exhausted
    FINAL_FAIL --> NOTIFY: Send owner email
    NOTIFY --> [*]
    CHECK --> NOT_ELIGIBLE: Condition not met
    NOT_ELIGIBLE --> [*]

Invariants

  1. Balance Non-Negative: balance >= 0 (enforced by constraint)
  2. Transaction Immutability: Transactions never updated after creation (only status changes allowed)
  3. Audit Trail: Every balance change has corresponding transaction(s)
  4. Auto-Refill Debounce: Max one trigger per hour per team
  5. Invoice Uniqueness: One invoice per completed deposit transaction
  6. Currency Consistency: All amounts in transaction match billing account currency

API Endpoints

Account Management

Endpoint Method Permission Purpose
/api/v1/billing/account GET billing.view Get current balance & config
/api/v1/billing/account/auto-refill PUT billing.edit Update auto-refill config

Transaction History

Endpoint Method Permission Purpose
/api/v1/billing/transactions GET billing.view List transactions with filters
/api/v1/billing/transactions/{id} GET billing.view Get single transaction

Invoices (NEW)

Endpoint Method Permission Purpose
/api/v1/billing/invoices GET billing.view List invoices
/api/v1/billing/invoices/{id}/download GET billing.view Download receipt PDF
/api/v1/billing/invoices/{id} GET billing.view Get invoice metadata

External Integrations

Stripe

Used For: - One-time payments (Payment Intents) - Auto-refill charges (Setup Intents + off-session charges) - Saving payment methods - Webhooks for payment confirmation

Key Methods:

stripe.PaymentIntent.create(
    amount=amount_cents,
    currency="usd",
    customer=stripe_customer_id,
    metadata={"team_id": team_id}
)

stripe.PaymentIntent.confirm(
    intent_id,
    payment_method=payment_method_id,
    off_session=True
)

Virtuozzo (Cost Deductions)

Integration Point: After resource usage, deduct from balance

# Pseudocode
usage_cost = calculate_usage_cost(resource)
new_balance = current_balance - usage_cost
await update_balance(team_id, new_balance, reason=f"Charge for {resource}")

Email Service (Postmark)

Triggers: - Auto-refill success: "Auto-refill completed, new balance $X" - Auto-refill failure: "Auto-refill failed, action required" - Invoice issued: "Your receipt #INV-2026-00001"

PDF Generation (ReportLab)

Invoice PDF Includes: - Invoice header with number and date - Team and billing info - Transaction details (date, method, amount) - QR code (optional) linking to online receipt - Footer with company/business details


Caching Strategy

Redis Keys

# Account balance (60 seconds)
f"billing:{team_id}:balance"  Decimal

# Auto-refill config (5 minutes)
f"billing:{team_id}:auto_refill"  JSON

# Last trigger timestamp (24 hours)
f"billing:{team_id}:last_refill_trigger"  ISO datetime

# Rate limiting (per minute)
f"billing:{team_id}:transactions:limit"  int (count)

Invalidation Triggers

  • On balance update
  • On auto-refill config change
  • On transaction creation (for transaction list caching)

Error Handling

Error Cause Response Recovery
Insufficient Balance Charge > balance 400 Bad Request Suggest payment/top-up
Stripe Connection Error Network/API down 503 Service Unavailable Retry with exponential backoff
Payment Intent Not Found Corrupted state 404 Not Found Admin manual investigation
Webhook Signature Invalid Man-in-middle/replay 401 Unauthorized Discard, log, alert security
Invoice PDF Generation Failed ReportLab error 500 Internal Error Retry job, notify admin

Performance Considerations

  1. Transaction Listing: Index on (billing_account_id, created_at) for efficient pagination
  2. Balance Lookups: Redis cache with 60-second TTL
  3. Auto-Refill Check: Per-team distributed lock to prevent race conditions
  4. Invoice Generation: Async job (RabbitMQ) to avoid blocking API response
  5. PDF Storage: S3 with CloudFront CDN for fast downloads

Testing

Unit Tests

  • Balance update invariants
  • Transaction type correctness
  • Auto-refill trigger logic
  • Invoice number sequence

Integration Tests

  • Stripe payment flow (with test API keys)
  • Auto-refill with insufficient balance
  • Concurrent balance updates (lock behavior)
  • Invoice generation and retrieval

E2E Tests

  • Full payment flow: intent → confirmation → invoice download
  • Auto-refill with real Stripe test card