| name | billing |
| description | Debug, edit, and fix billing operations. Covers the V2 action-based architecture (attach, multiAttach, updateSubscription, allocatedInvoice, createWithDefaults, setupPayment). Use when working on billing, subscription, invoicing, or Stripe integration code. |
Billing Operations Guide
When to Use This Skill
- Debugging billing issues (double charges, missing invoices, wrong subscription items)
- Adding new billing actions
- Understanding how Autumn state maps to Stripe
- Fixing subscription update/cancel/attach flows
- Working with subscription schedules (future changes)
- Understanding allocated invoice (mid-cycle usage-based invoicing)
V2 Billing Actions
All billing logic is orchestrated through billingActions (billing/v2/actions/index.ts). Handlers are thin — they call an action, then format the response.
export const billingActions = {
attach,
multiAttach,
setupPayment,
updateSubscription,
migrate,
legacy: {
attach: legacyAttach,
updateQuantity,
renew,
},
} as const;
Two additional billing operations live outside billingActions but use the same evaluate+execute pipeline:
createAllocatedInvoice — mid-cycle invoicing triggered by balance deduction
createCustomerWithDefaults — customer creation with default products
Action Quick Reference
| Action | Trigger | What It Does |
|---|
attach | HTTP billing.attach | Add/upgrade/downgrade a single product. Handles transitions, prorations, trials, checkout mode |
multiAttach | HTTP billing.multi_attach | Attach multiple products atomically. At most one transition allowed |
updateSubscription | HTTP billing.update | Change quantity, cancel (immediate/end-of-cycle), uncancel, update custom plan items |
setupPayment | HTTP billing.setup_payment | Create Stripe setup checkout. Optionally validates a plan via preview first |
createAllocatedInvoice | Programmatic (balance deduction) | Invoice for allocated usage changes (prepaid overages, usage upgrades/downgrades) |
createCustomerWithDefaults | Programmatic (customer creation) | Two-phase: create customer + products in DB, then create Stripe subscription for paid defaults |
Each HTTP action also has a preview variant (billing.preview_attach, billing.preview_multi_attach, billing.preview_update) that runs setup+compute+evaluate but skips execution.
The legacy V1 attach (POST /attach) still exists and delegates to billingActions.legacy.attach, which converts old AttachParams format into V2 billing context overrides. Similarly legacyUpdateQuantity and legacyRenew bridge old flows to V2.
Handler Pattern
Handlers are thin wrappers — they parse params, call the action, format response:
export const handleAttachV2 = createRoute({
versionedBody: { latest: AttachParamsV1Schema, [ApiVersion.V1_Beta]: AttachParamsV0Schema },
resource: AffectedResource.Attach,
lock: { },
handler: async (c) => {
const ctx = c.get("ctx");
const body = c.req.valid("json");
const { billingContext, billingResult } = await billingActions.attach({
ctx,
params: body,
preview: false,
});
return c.json(billingResultToResponse({ billingContext, billingResult }), 200);
},
});
The 4-Layer Pattern (Inside Each Action)
Every action follows: Setup → Compute → Evaluate → Execute
export async function attach({ ctx, params, preview }) {
const billingContext = await setupAttachBillingContext({ ctx, params });
const autumnBillingPlan = computeAttachPlan({ ctx, attachBillingContext: billingContext, params });
const stripeBillingPlan = await evaluateStripeBillingPlan({ ctx, billingContext, autumnBillingPlan });
handleAttachV2Errors({ ctx, billingContext, billingPlan, params });
if (preview) return { billingContext, billingPlan };
const billingResult = await executeBillingPlan({ ctx, billingContext, billingPlan });
return { billingContext, billingPlan, billingResult };
}
Key principle: evaluateStripeBillingPlan and executeBillingPlan are UNIFIED across all actions. Only modify them when adding new Stripe action types.
See V2 Four-Layer Pattern Deep Dive for detailed explanation.
Allocated Invoice
Not an HTTP endpoint — triggered during executePostgresDeduction when allocated (prepaid) usage changes.
File: server/src/internal/balances/utils/allocatedInvoice/createAllocatedInvoice.ts
When it fires: A customer with usage-based allocated pricing (e.g., prepaid seats) has their usage change. The system needs to invoice for the delta.
Flow:
- Setup (
setupAllocatedInvoiceContext) — re-fetches full customer, computes previous/new usage and overage from entitlement snapshots
- Compute (
computeAllocatedInvoicePlan) — builds refund line item for previous usage + charge line item for new usage. Handles upgrade (delete replaceables) and downgrade (create replaceables) scenarios
- Evaluate + Execute — standard unified pipeline (
evaluateStripeBillingPlan → executeBillingPlan)
- Post-execute — if Stripe invoice payment fails, voids invoice and throws
PayInvoiceFailed
- Mutation — calls
refreshDeductionUpdate to mutate the deduction update with replaceable and balance changes
Key difference from other actions: Produces only updateCustomerEntitlements + lineItems (no insertCustomerProducts). The AutumnBillingPlan is minimal since the customer product already exists.
Two Critical Stripe Mappings
Getting billing right means getting these two mappings right:
1. Subscription Items (Immediate Changes)
When: Updating a subscription right now (add/remove/change items immediately)
Key function: buildStripeSubscriptionItemsUpdate
Flow:
FullCusProduct[]
→ filter by subscription ID
→ filter by active statuses
→ customerProductToStripeItemSpecs()
→ diff against current subscription
→ Stripe.SubscriptionUpdateParams.Item[]
See Stripe Subscription Items Reference for details.
2. Schedule Phases (Future Changes)
When: Scheduling changes for the future (downgrades at cycle end, scheduled cancellations)
Key function: buildStripePhasesUpdate
Flow:
FullCusProduct[]
→ normalize timestamps to seconds
→ buildTransitionPoints() (find all start/end times)
→ for each period: filter active products
→ customerProductsToPhaseItems()
→ Stripe.SubscriptionScheduleUpdateParams.Phase[]
See Stripe Schedule Phases Reference for details.
Stripe Invoice Decision Tree
Critical: Stripe sometimes forces invoice creation. If you also create a manual invoice, customer gets double-charged.
Does Stripe force-create an invoice?
├── Creating a new subscription?
│ └── YES → Stripe creates invoice. DO NOT create manual invoice.
│
├── Removing trial from subscription? (isTrialing && !willBeTrialing)
│ └── YES → Stripe creates invoice. DO NOT create manual invoice.
│
└── Otherwise
└── NO → We create manual invoice using buildStripeInvoiceAction()
Key functions:
shouldCreateManualStripeInvoice() - Returns true if WE should create invoice
willStripeSubscriptionUpdateCreateInvoice() - Returns true if STRIPE will create invoice
See Stripe Invoice Rules Reference for full decision tree.
Common Issues & Fixes
| Symptom | Likely Cause | Quick Fix |
|---|
| Double invoice charge | Created manual invoice when Stripe already did | Check shouldCreateManualStripeInvoice() |
| Subscription items wrong | customerProductToStripeItemSpecs output incorrect | Debug spec generation, check quantity rules |
| Schedule phases wrong | Transition points incorrect | Check buildTransitionPoints, run schedule phases tests |
| Trial not ending | trialContext not set up correctly | Check setupTrialContext |
| Quantities wrong | Metered vs licensed confusion | undefined = metered, 0 = entity placeholder, N = licensed |
| Allocated invoice fails | Stripe payment failed for usage delta | Invoice is voided, PayInvoiceFailed thrown |
See Common Bugs Reference for detailed debugging steps.
Adding a New Billing Action
-
Create action function: billing/v2/actions/myAction/myAction.ts
- Follow the attach.ts pattern: setup → compute → evaluate → errors → execute
- Return
{ billingContext, billingPlan, billingResult }
-
Create setup function: billing/v2/actions/myAction/setup/setupMyActionBillingContext.ts
- Extend
BillingContext interface if needed
- Use shared setup functions (
setupFullCustomerContext, setupStripeBillingContext, etc.)
-
Create compute function: billing/v2/actions/myAction/compute/computeMyActionPlan.ts
- Return
AutumnBillingPlan with insertCustomerProducts, lineItems, etc.
-
Create error handler: billing/v2/actions/myAction/errors/handleMyActionErrors.ts
-
Register in billingActions: billing/v2/actions/index.ts
-
Create handler (if HTTP-exposed): billing/v2/handlers/handleMyAction.ts
- Thin wrapper calling
billingActions.myAction()
-
DO NOT modify evaluateStripeBillingPlan or executeBillingPlan unless absolutely necessary
Invoicing Utilities (Pure Calculations)
The shared/utils/billingUtils/ folder contains pure calculation functions that determine what customers are charged.
Key utilities:
| Function | Location | Purpose |
|---|
priceToLineAmount | invoicingUtils/lineItemUtils/ | Calculate charge amount for a price |
tiersToLineAmount | invoicingUtils/lineItemUtils/ | Calculate tiered/usage-based amounts |
applyProration | invoicingUtils/prorationUtils/ | Calculate partial period charges |
buildLineItem | invoicingUtils/lineItemBuilders/ | Core line item builder |
fixedPriceToLineItem | invoicingUtils/lineItemBuilders/ | Build line item for fixed prices |
usagePriceToLineItem | invoicingUtils/lineItemBuilders/ | Build line item for usage prices |
Key concepts:
LineItem.amount is positive for charges, negative for refunds
context.direction controls the sign ("charge" vs "refund")
- Proration is applied automatically when
billingPeriod is provided
- Consumable prices don't prorate (usage is charged as-is)
See Invoicing Utilities Reference for detailed documentation.
Key File Locations
V2 Actions (server/src/internal/billing/v2/actions/)
| Action | Key Files |
|---|
| attach | attach/attach.ts, attach/setup/setupAttachBillingContext.ts, attach/compute/computeAttachPlan.ts |
| multiAttach | multiAttach/multiAttach.ts, multiAttach/setup/, multiAttach/compute/ |
| updateSubscription | updateSubscription/updateSubscription.ts, updateSubscription/compute/ (cancel/, customPlan/, updateQuantity/) |
| setupPayment | setupPayment/setupPayment.ts |
Shared V2 Infrastructure (server/src/internal/billing/v2/)
| Layer | Key Files |
|---|
| Evaluate | providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts |
| Execute | execute/executeBillingPlan.ts, execute/executeAutumnBillingPlan.ts |
| Shared Setup | setup/setupFullCustomerContext.ts, setup/setupBillingCycleAnchor.ts, providers/stripe/setup/setupStripeBillingContext.ts |
| Shared Compute | compute/computeAutumnUtils/buildAutumnLineItems.ts, compute/finalize/finalizeLineItems.ts |
Non-billingActions Operations
| Operation | Key Files |
|---|
| allocatedInvoice | server/src/internal/balances/utils/allocatedInvoice/createAllocatedInvoice.ts, compute/computeAllocatedInvoicePlan.ts |
| createWithDefaults | server/src/internal/customers/actions/createWithDefaults/createCustomerWithDefaults.ts |
Types
| Type | Location | Purpose |
|---|
BillingContext | shared/models/billingModels/context/billingContext.ts | Customer, products, Stripe state, timestamps |
AutumnBillingPlan | shared/models/billingModels/plan/autumnBillingPlan.ts | Autumn state changes (inserts, deletes, line items) |
StripeBillingPlan | Types in billing/v2/providers/stripe/ | Stripe actions (subscription, invoice, schedule) |
Reference Files
Load these on-demand for detailed information: