Files
cfw-autumn/server/tests/integration/billing/attach/immediate-switch/paid-features/immediate-switch-prepaid-no-options.test.ts

1083 lines
31 KiB
TypeScript

/**
* Immediate Switch Prepaid No-Options Tests (Attach V2)
*
* Tests for upgrades involving prepaid features where options are NOT passed on upgrade.
*
* IMPORTANT: Immediate switch always involves a DIFFERENT product.
* You cannot update quantity on the same product via attach.
*
* Key behaviors:
* - When no options passed on upgrade, quantity carries over from previous product
* - When partial options passed, only specified features change
* - Balance should be recalculated based on new product config + carried-over quantity
*/
import { expect, test } from "bun:test";
import type { ApiCustomerV3 } from "@autumn/shared";
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { TestFeature } from "@tests/setup/v2Features";
import { items } from "@tests/utils/fixtures/items";
import { products } from "@tests/utils/fixtures/products";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
import chalk from "chalk";
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 1: Pro with prepaid (200 units), upgrade to Premium with NO options
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid (200 units purchased)
* - Upgrade to Premium with prepaid, NO options passed
*
* Expected Result:
* - Quantity carries over (200 units)
* - Balance = 200 (same as before)
* - Only base price difference charged (prepaid quantity unchanged)
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 1: quantity carries over")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-carry";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// Verify initial state: 200 units
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
// 1. Preview upgrade - NO options passed
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
// No options - quantity should carry over
});
// Base diff: $50 - $20 = $30 (prepaid quantity same, no prepaid diff)
expect(preview.total).toBe(30);
// 2. Attach premium - NO options
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
// No options
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify balance carried over: 200
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
// Invoices: initial ($20 base + $20 prepaid = $40) + upgrade ($30 base only)
await expectCustomerInvoiceCorrect({
customer,
count: 2,
latestTotal: 30,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 2: Prepaid upgrade no options, billing units change (100 → 50)
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid (200 units @ 100 units/pack = 2 packs)
* - Upgrade to Premium with prepaid (50 units/pack), NO options
*
* Expected Result:
* - Quantity carries over: 200 units
* - New product has 4 packs (200 / 50)
* - Prepaid cost increases (4 packs vs 2 packs)
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 2: billing units 100 to 50")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-units-100-50";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 50,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// Verify initial: 200 units
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
// 1. Preview upgrade - NO options
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
// Base diff: $50 - $20 = $30
// Prepaid diff: (4 packs - 2 packs) * $10 = $20
// Total: $50
expect(preview.total).toBe(50);
// 2. Attach premium - NO options
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify balance carried over: 200 units (same quantity)
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 3: Prepaid upgrade no options, billing units change (50 → 100)
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid (200 units @ 50 units/pack = 4 packs)
* - Upgrade to Premium with prepaid (100 units/pack), NO options
*
* Expected Result:
* - Quantity carries over: 200 units
* - New product has 2 packs (200 / 100)
* - Prepaid cost decreases (2 packs vs 4 packs)
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 3: billing units 50 to 100")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-units-50-100";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 50,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// 1. Preview upgrade - NO options
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
// Base diff: $50 - $20 = $30
// Prepaid diff: (2 packs - 4 packs) * $10 = -$20
// Total: $10
expect(preview.total).toBe(10);
// 2. Attach premium - NO options
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify balance carried over: 200 units
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 4: Prepaid upgrade no options, included usage increases
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid (0 included, 200 purchased = 200 total)
* - Upgrade to Premium with prepaid (100 included), NO options
*
* Expected Result:
* - Quantity carries over: 200 purchased
* - Balance = 100 (included) + 200 (purchased) = 300
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 4: included usage increases")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-incl-inc";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 100,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// Verify initial: 200 units
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
// 1. Preview upgrade - NO options
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
// Base price difference: $50 - $20 = $30
// Prepaid refund: -$10 (prorated unused from old product, new product no paid units)
expect(preview.total).toBe(20);
// 2. Attach premium - NO options
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify balance: 100 included + 200 carried over = 300
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 5: Prepaid upgrade no options, included usage decreases
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid (100 included, 200 purchased = 300 total)
* - Upgrade to Premium with prepaid (0 included), NO options
*
* Expected Result:
* - Quantity carries over: 200 purchased
* - Balance = 0 (included) + 200 (purchased) = 200
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 5: included usage decreases")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-incl-dec";
const proPrepaid = items.prepaidMessages({
includedUsage: 100,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// Verify initial: 100 included + 200 purchased = 300
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
// 1. Preview upgrade - NO options
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
// Base diff: $50 + $20 - $20 - $10
// Prepaid: same 2 packs, no diff
expect(preview.total).toBe(40);
// 2. Attach premium - NO options
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify balance: 0 included + 200 purchased = 200
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 6: Prepaid upgrade no options, price change
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid (200 units @ $10/pack = 2 packs = $20)
* - Upgrade to Premium with prepaid ($15/pack), NO options
*
* Expected Result:
* - Quantity carries over: 200 units (2 packs)
* - Price diff charged: 2 packs * ($15 - $10) = $10
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 6: price increases")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-price-inc";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 15,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// 1. Preview upgrade - NO options
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
// Base diff: $50 - $20 = $30
// Prepaid diff: 2 packs * ($15 - $10) = $10
// Total: $40
expect(preview.total).toBe(40);
// 2. Attach premium - NO options
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify balance carried over: 200
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 7: Prepaid upgrade no options, price decreases
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid (200 units @ $15/pack = 2 packs = $30)
* - Upgrade to Premium with prepaid ($10/pack), NO options
*
* Expected Result:
* - Quantity carries over: 200 units
* - Credit for price diff: 2 packs * ($10 - $15) = -$10
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 7: price decreases")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-price-dec";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 15,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// 1. Preview upgrade - NO options
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
// Base diff: $50 - $20 = $30
// Prepaid diff: 2 packs * ($10 - $15) = -$10
// Total: $20
expect(preview.total).toBe(20);
// 2. Attach premium - NO options
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify balance carried over: 200
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 8: Prepaid with usage, upgrade no options - balance preserved
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid (200 purchased)
* - Track 50 usage (balance = 150)
* - Upgrade to Premium with prepaid, NO options
*
* Expected Result:
* - Quantity carries over: 200 purchased (usage resets for prepaid on upgrade)
* - Balance = 200 (prepaid resets usage)
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 8: with usage")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-with-usage";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// Wait for webhooks to process and cache to reset
await new Promise((r) => setTimeout(r, 4000));
// Track 50 usage
await autumnV1.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 50,
});
await new Promise((r) => setTimeout(r, 2000));
// Verify state before upgrade: balance = 150
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 150,
usage: 50,
});
// 1. Preview upgrade - NO options
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
// Base diff: $50 - $20 = $30
// Prepaid: same quantity carried over, no diff
expect(preview.total).toBe(30);
// 2. Attach premium - NO options
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify balance: 200 (usage resets on upgrade)
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 9: Multiple prepaid features, upgrade with partial options
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with 2 prepaid features:
* - Messages: 200 purchased
* - Words: 500 purchased
* - Upgrade to Premium, only specify messages option (increase to 300)
*
* Expected Result:
* - Messages: changed to 300
* - Words: carries over at 500
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 9: multiple prepaid partial options")}`, async () => {
const customerId = "imm-switch-prepaid-partial-opts";
const proMessagesPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const proWordsPrepaid = items.prepaid({
featureId: TestFeature.Words,
includedUsage: 0,
billingUnits: 100,
price: 5,
});
const pro = products.pro({
id: "pro",
items: [proMessagesPrepaid, proWordsPrepaid],
});
const premiumMessagesPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premiumWordsPrepaid = items.prepaid({
featureId: TestFeature.Words,
includedUsage: 0,
billingUnits: 100,
price: 5,
});
const premium = products.premium({
id: "premium",
items: [premiumMessagesPrepaid, premiumWordsPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [
{ feature_id: TestFeature.Messages, quantity: 200 },
{ feature_id: TestFeature.Words, quantity: 500 },
],
}),
],
});
// Verify initial state
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
});
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Words,
balance: 500,
});
// 1. Preview upgrade - ONLY messages option (words should carry over)
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
options: [{ feature_id: TestFeature.Messages, quantity: 300 }],
});
// Base diff: $50 - $20 = $30
// Messages: (3 packs - 2 packs) * $10 = $10
// Words: carried over (no change)
expect(preview.total).toBe(40);
// 2. Attach premium - only messages option
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
options: [{ feature_id: TestFeature.Messages, quantity: 300 }],
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify messages changed to 300
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 300,
});
// Verify words carried over at 500
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Words,
balance: 500,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 10: Prepaid upgrade no options, all config changes (billing units, price, included)
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid:
* - 0 included, 200 purchased @ 100 units/pack @ $10/pack
* - Total: 200 units, 2 packs, $20 prepaid
* - Upgrade to Premium with prepaid (NO options):
* - 50 included, 50 units/pack @ $15/pack
*
* Expected Result:
* - Quantity carries over: 200 purchased
* - New packs: 200 / 50 = 4 packs
* - Balance = 50 (included) + 200 (purchased) = 250
* - Prepaid diff: (4 packs * $15) - (2 packs * $10) = $60 - $20 = $40
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 10: all config changes")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-all-change";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 50,
billingUnits: 50,
price: 15,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// Verify initial: 200 units
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
// 1. Preview upgrade - NO options
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
// Base diff: $50 - $20 = $30
// Prepaid diff: (3 * $15) - (2 * $10) = $45 - $20 = $25
// Total: $55
expect(preview.total).toBe(55);
// 2. Attach premium - NO options
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 11: Prepaid upgrade with options set to 0 (explicit reset)
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Pro with prepaid (200 purchased)
* - Upgrade to Premium with options explicitly set to 0
*
* Expected Result:
* - Quantity set to 0 (not carried over because explicit)
* - Credit for removed prepaid
*/
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options 11: options explicitly set to 0")}`, async () => {
const customerId = "imm-switch-prepaid-opts-zero";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
// Verify initial: 200 units
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
});
// 1. Preview upgrade - options explicitly 0
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
options: [{ feature_id: TestFeature.Messages, quantity: 0 }],
});
// Base diff: $50 - $20 = $30
// Prepaid diff: (0 packs - 2 packs) * $10 = -$20
// Total: $10
expect(preview.total).toBe(10);
// 2. Attach premium - options explicitly 0
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
options: [{ feature_id: TestFeature.Messages, quantity: 0 }],
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
// Verify balance: 0 (explicitly set)
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 0,
usage: 0,
});
});