Skip to content

Credit Pack Purchasing

Credit packs let organizations buy extra credits on top of their subscription allowance. Purchased credits persist across billing cycles and sit in the third position of the credit waterfall — after op-type and included credits, before overdraft.

The purchasing flow was added in PR #1111. The webhook fulfillment side (crediting org_budget.purchased_credits on checkout.session.completed) was already landed in PR #127.

GET /api/billing/credit-packs

Auth: None — the catalog is public, matching the subscription tier listing.

Returns all active packs sorted by display order. Each pack includes an is_checkout_ready flag that’s true only when the admin has populated a Stripe Price ID.

Response type — CreditPackInfo:

pub struct CreditPackInfo {
pub id: Uuid,
pub name: String,
pub credits: i32,
pub price_cents: i32,
pub is_checkout_ready: bool,
}
POST /api/billing/credit-packs/checkout

Auth: Owner-only. Non-owners get a 403.

Parameter: credit_pack_id: Uuid

Creates a Stripe Checkout Session in payment mode (not subscription) and returns a StripeRedirect { url }. The session includes two metadata keys that the webhook dispatcher uses to route the completion event:

Metadata keyValuePurpose
credit_pack_idUUID of the packIdentifies which pack was purchased
type"credit_pack"Disambiguates from future one-time payment flows

The service calls ensure_org_stripe_customer to guarantee a Stripe customer exists before creating the session. Success and cancel URLs redirect back to /settings/tab/billing?checkout=success|canceled.

GET /api/billing/credit-packs/purchases

Auth: Any org member (session-scoped to the caller’s org).

Returns the org’s purchase history, most recent first, joined with the pack name.

Response type — CreditPackPurchaseInfo:

pub struct CreditPackPurchaseInfo {
pub id: Uuid,
pub pack_name: String,
pub credits: i32,
pub amount_cents: i32,
pub purchased_at: String, // RFC 3339
}

CreditPacksSection renders on the billing settings tab when the org has an active subscription. It sits between the usage view and the subscription tier cards.

Layout:

  • Pack cards — a 3-column grid showing name, price (via format_price_cents), and credit count. Owners see a “Buy credits” button; non-owners see the catalog without actions. Packs without a Stripe Price ID show a disabled “Not available” button.
  • Purchase history — a responsive table (header row on sm: and up) with columns: Date, Pack, Credits, Amount.

Purchase flow:

  1. Owner clicks “Buy credits” on a pack card
  2. Button shows “Redirecting…” and calls create_credit_pack_checkout_session_api
  3. On success, navigate_to_external redirects to Stripe’s hosted checkout page
  4. On error, a toast notification surfaces the failure message

This PR extracted two helpers from the billing section into src/shared/utils/:

FunctionSignaturePurpose
format_price_centsfn(i32) -> StringFormats cents as $X.XX with .abs() guard
navigate_to_externalfn(&str)Full-page JS redirect via document::eval — used for Stripe Checkout, Billing Portal, and OAuth handoffs
src/mods/billing/
├── api/
│ ├── list_credit_packs_api.rs # GET catalog
│ ├── create_credit_pack_checkout_session_api.rs # POST checkout
│ └── list_credit_pack_purchases_api.rs # GET history
├── components/
│ └── credit_packs_section_component.rs # UI surface
├── services/
│ └── create_credit_pack_checkout_session_service.rs # Stripe session creation
└── types/
├── credit_pack_info_type.rs # CreditPackInfo
└── credit_pack_purchase_info_type.rs # CreditPackPurchaseInfo
src/shared/utils/
├── format_price_cents.rs
└── navigate_to_external.rs