6.7 KiB
Subscription Update Billing Guide
Proration & Charges
When updating a subscription via subscriptions.update (custom plan), charges/credits are calculated based on the billing model:
Billing Models
| Model | On Update Behavior |
|---|---|
| Base Price | Prorated charge/credit for price difference |
| Consumable | No immediate overage charge (billed in arrears at cycle end) |
| Allocated | Prorated charge for current overage above new included amount |
| Prepaid | Full refund of previous prepaid, full charge for new prepaid |
Detailed Behavior
1. Base Price Changes
- Increase: Charge prorated difference for remaining cycle
- Decrease: Credit prorated difference for remaining cycle
- Remove: Credit full remaining prorated amount
// $20/mo -> $30/mo at start of cycle = charge $10
expect(preview.total).toBe(10);
// $30/mo -> $20/mo at start of cycle = credit $10
expect(preview.total).toBe(-10);
// Mid-cycle (15 days): $20/mo -> $30/mo = charge ~$5 (prorated)
expect(preview.total).toBe(5);
2. Consumable Features
- Never charge overage on update
- Overage is billed at end of billing cycle
- Even if usage exceeds new included amount, preview.total = 0 for the consumable portion
// 80 used, 50 included = 30 overage, but...
expect(preview.total).toBe(0); // Consumable overage NOT charged on update
3. Allocated Features (Seat-Based)
- Charge prorated amount for overage seats above new included amount
- Based on current usage vs new included allowance
// Using 5 seats, decrease included from 5 to 3
// Overage = 5 - 3 = 2 seats @ $10/seat = $20
expect(preview.total).toBe(20);
// Using 2 seats, increase included from 2 to 5
// No overage, no charge
expect(preview.total).toBe(0);
⚠️ Important: Allocated Features Create Invoices on Track
For allocated features (seat-based / prorated billing), tracking usage past the included boundary immediately creates a prorated invoice. This is handled in adjustAllowance.ts.
This means:
- When
track()causes usage to exceed included seats, an invoice is created immediately - This is different from consumable features, which only bill at cycle end
// Example: Product with 3 included seats @ $10/seat overage
// Customer tracks 5 seats (2 over included)
await autumnV1.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: 5, // 2 over the 3 included
});
// This immediately creates an invoice for the 2 extra seats (prorated)
// Invoice count is now: 1 (initial) + 1 (track overage) = 2
// Later, when updating subscription:
await autumnV1.subscriptions.update(updateParams);
// Invoice count becomes: 1 (initial) + 1 (track overage) + 1 (update) = 3
This affects invoice count expectations in tests:
- Usage within included: No extra invoice from track
- Usage exceeds included: +1 invoice from track
4. Prepaid Features
Prepaid features require options with quantity when attaching or updating. The quantity is:
- The total units you want (NOT multiplied by billing_units)
- NOT inclusive of
included_usage(included_usage is separate free balance)
Billing logic on update:
- Refund previous prepaid amount:
old_packs * old_price - Charge new prepaid amount:
new_packs * new_price - preview.total = new charge - old refund
// Setup: $10 per 100 units (1 pack = 100 units at $10)
const prepaidItem = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
// Attach with 2 packs (200 units)
await initScenario({
actions: [
s.attach({
productId: "pro",
options: [{ feature_id: TestFeature.Messages, quantity: 200 }], // 2 packs
}),
],
});
// Upgrade to 5 packs (500 units)
const preview = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 500 }], // 5 packs
});
// preview.total = (5 - 2) * $10 = $30
expect(preview.total).toBe(30);
// Downgrade to 3 packs (300 units)
const preview2 = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 300 }], // 3 packs
});
// preview.total = (3 - 5) * $10 = -$20 (credit)
expect(preview2.total).toBe(-20);
Prepaid with Price/Billing Unit Changes
When changing price or billing units via items, the calculation uses old and new pack costs:
// Old: 3 packs of 100 @ $10 = $30
// New: 3 packs of 100 @ $15 = $45
// preview.total = $45 - $30 = $15
expect(preview.total).toBe(15);
// Old: 300 units / 100 = 3 packs @ $10 = $30
// New: 300 units / 50 = 6 packs @ $10 = $60
// preview.total = $60 - $30 = $30
expect(preview.total).toBe(30);
Preview vs Invoice Matching
Always verify that preview.total matches the actual invoice:
const updateParams = {
customer_id: customerId,
product_id: pro.id,
items: [newItem, priceItem],
};
const preview = await autumnV1.subscriptions.previewUpdate(updateParams);
expect(preview.total).toBe(expectedAmount);
await autumnV1.subscriptions.update(updateParams);
const customer = await autumnV1.customers.get(customerId);
await expectCustomerInvoiceCorrect({
customer,
count: expectedInvoiceCount,
latestTotal: preview.total,
});
Invoice Count Guidelines
| Transition | Expected Count |
|---|---|
| Free-to-Free | 0 |
| Free-to-Paid | 1 |
| Paid-to-Paid (upgrade/downgrade) | Initial (1) + Update (1) = 2 |
| Paid-to-Paid (allocated to prepaid) | Initial (1) + Arrear Settlement (1) + Prepaid (1) = 3 |
Allocated Feature Invoice Counts
For allocated features, invoice count depends on whether usage exceeded included at any point:
| Scenario | Invoice Count |
|---|---|
| Usage stays within included, then update | Initial (1) + Update (1) = 2 |
| Usage exceeds included via track, then update | Initial (1) + Track Overage (1) + Update (1) = 3 |
| Usage exceeds included via track, update increases included to cover usage | Initial (1) + Track Overage (1) + Update Credit (1) = 3 |
// Example: 3 included seats, track 5 seats (2 over), then increase to 10 included
await expectCustomerInvoiceCorrect({
customer,
count: 3, // 1 (attach) + 1 (track overage) + 1 (update credit)
latestTotal: preview.total,
});
No-Charge Updates
These updates should have preview.total = 0:
- Adding/removing boolean features (no price impact)
- Changing included usage (no billing attached)
- Changing feature intervals (month → week)
- Updating consumable features (overage not charged on update)
- Increasing allocated seats when within included amount