Files
cfw-autumn/server/tests/integration/billing/attach/errors/custom-line-items-errors.test.ts
2026-03-02 10:58:11 +00:00

180 lines
6.3 KiB
TypeScript

/**
* Custom Line Items Error Tests (Attach V2)
*
* Tests that custom_line_items is rejected in invalid scenarios:
* 1. Creating a new subscription (free → paid)
* 2. Scheduled switch / downgrade
* 3. Removing a trial (Stripe-managed invoice)
*/
import { test } from "bun:test";
import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils";
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: custom_line_items on subscription create should fail
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Customer has free product
* - Attach pro ($20/mo) with custom_line_items
*
* Expected Result:
* - Error: custom_line_items only valid for subscription updates
*
* Why:
* - Creating a new subscription generates its own Stripe invoice.
* Custom line items can't override that.
*/
test.concurrent(`${chalk.yellowBright("error: custom_line_items on subscription create")}`, async () => {
const customerId = "cli-err-create-sub";
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const free = products.base({
id: "free",
items: [messagesItem],
});
const proMessagesItem = items.monthlyMessages({ includedUsage: 500 });
const pro = products.pro({
id: "pro",
items: [proMessagesItem],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [free, pro] }),
],
actions: [s.billing.attach({ productId: free.id })],
});
await expectAutumnError({
func: async () => {
await autumnV1.billing.attach({
customer_id: customerId,
product_id: pro.id,
redirect_mode: "if_required",
custom_line_items: [{ amount: 10, description: "Should not work" }],
});
},
errMessage:
"custom_line_items can only be used when updating an existing subscription",
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 2: custom_line_items on downgrade (scheduled switch) should fail
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Customer has premium ($50/mo)
* - Downgrade to pro ($20/mo) with custom_line_items
*
* Expected Result:
* - Error: custom_line_items only valid for subscription updates
*
* Why:
* - Downgrades are scheduled for end of cycle. There is no immediate invoice
* to override with custom line items.
*/
test.concurrent(`${chalk.yellowBright("error: custom_line_items on downgrade (scheduled switch)")}`, async () => {
const customerId = "cli-err-downgrade";
const proMessagesItem = items.monthlyMessages({ includedUsage: 500 });
const pro = products.pro({
id: "pro",
items: [proMessagesItem],
});
const premiumMessagesItem = items.monthlyMessages({ includedUsage: 1000 });
const premium = products.premium({
id: "premium",
items: [premiumMessagesItem],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [s.billing.attach({ productId: premium.id })],
});
await expectAutumnError({
func: async () => {
await autumnV1.billing.attach({
customer_id: customerId,
product_id: pro.id,
redirect_mode: "if_required",
custom_line_items: [{ amount: -10, description: "Should not work" }],
});
},
errMessage:
"custom_line_items can only be used when updating an existing subscription",
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 3: custom_line_items on trial removal should fail
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Customer has proWithTrial (7-day trial, currently trialing)
* - Upgrade to premium (no trial) with custom_line_items
*
* Expected Result:
* - Error: custom_line_items cannot be used when creating Stripe-managed invoice
*
* Why:
* - When a trial is removed (trialing → non-trialing), Stripe automatically
* generates an invoice. Custom line items can't override Stripe's invoice.
*/
test.concurrent(`${chalk.yellowBright("error: custom_line_items on trial removal")}`, async () => {
const customerId = "cli-err-trial-removal";
const proMessagesItem = items.monthlyMessages({ includedUsage: 500 });
const proTrial = products.proWithTrial({
id: "pro-trial",
items: [proMessagesItem],
trialDays: 7,
cardRequired: true,
});
const premiumMessagesItem = items.monthlyMessages({ includedUsage: 1000 });
const premium = products.premium({
id: "premium",
items: [premiumMessagesItem],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [proTrial, premium] }),
],
actions: [s.billing.attach({ productId: proTrial.id })],
});
await expectAutumnError({
func: async () => {
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
custom_line_items: [{ amount: 50, description: "Should not work" }],
});
},
errMessage:
"custom_line_items cannot be used when the subscription update creates a Stripe-managed invoice",
});
});