Files
cfw-autumn/server/tests/integration/billing/update-subscription/version-update/version-multi-version.test.ts
2026-01-15 15:04:37 +00:00

378 lines
11 KiB
TypeScript

import { expect, test } from "bun:test";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect";
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";
// ═══════════════════════════════════════════════════════════════════════════════
// MULTI-VERSION: Scenarios with multiple product versions (v1, v2, v3)
// ═══════════════════════════════════════════════════════════════════════════════
// 4.1 Sequential upgrades: v1 → v2 → v3
test.concurrent(`${chalk.yellowBright("version-multi: sequential v1 to v2 to v3")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const priceItemV1 = items.monthlyPrice({ price: 20 });
const pro = products.base({ id: "pro", items: [messagesItem, priceItemV1] });
const { customerId, autumnV1, ctx } = await initScenario({
customerId: "version-multi-seq",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
],
actions: [s.attach({ productId: "pro" })],
});
// Create v2 with $30 price
const priceItemV2 = items.monthlyPrice({ price: 30 });
await autumnV1.products.update(pro.id, {
items: [messagesItem, priceItemV2],
});
// Update to v2
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 2,
});
// Verify on v2
const customerV2 = await autumnV1.customers.get(customerId);
const productV2 = customerV2.products.find((p) => p.id === pro.id);
expect(productV2?.version).toBe(2);
// Create v3 with $40 price (need to update customer to v2 first for versioning to trigger)
const priceItemV3 = items.monthlyPrice({ price: 40 });
await autumnV1.products.update(pro.id, {
items: [messagesItem, priceItemV3],
});
// Update to v3
const previewV3 = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: pro.id,
version: 3,
});
// Should charge $10 difference ($40 - $30)
expect(previewV3.total).toBe(10);
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 3,
});
// Verify on v3
const customer = await autumnV1.customers.get(customerId);
const productV3 = customer.products.find((p) => p.id === pro.id);
expect(productV3?.version).toBe(3);
// Should have 3 invoices (initial + v2 upgrade + v3 upgrade)
await expectCustomerInvoiceCorrect({
customerId,
count: 3,
latestTotal: previewV3.total,
});
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
});
});
// 4.2 Skip v2, go directly to v3
test.concurrent(`${chalk.yellowBright("version-multi: skip v2 go to v3")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const priceItemV1 = items.monthlyPrice({ price: 20 });
const pro = products.base({ id: "pro", items: [messagesItem, priceItemV1] });
const { customerId, autumnV1, ctx } = await initScenario({
customerId: "version-multi-skip",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
],
actions: [s.attach({ productId: "pro" })],
});
// Create v2 with $30 price
const priceItemV2 = items.monthlyPrice({ price: 30 });
await autumnV1.products.update(pro.id, {
items: [messagesItem, priceItemV2],
});
// Update to v2 first (required to create v3)
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 2,
});
// Create v3 with $40 price
const priceItemV3 = items.monthlyPrice({ price: 40 });
await autumnV1.products.update(pro.id, {
items: [messagesItem, priceItemV3],
});
// Now downgrade to v1 first (so we can test skipping v2)
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 1,
});
// Now skip v2 and go directly to v3
const previewV3 = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: pro.id,
version: 3,
});
// Should charge $20 difference ($40 - $20)
expect(previewV3.total).toBe(20);
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 3,
});
// Verify on v3
const customer = await autumnV1.customers.get(customerId);
const product = customer.products.find((p) => p.id === pro.id);
expect(product?.version).toBe(3);
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
});
});
// 4.3 Downgrade from v3 to v1
test.concurrent(`${chalk.yellowBright("version-multi: downgrade v3 to v1")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const priceItemV1 = items.monthlyPrice({ price: 20 });
const pro = products.base({ id: "pro", items: [messagesItem, priceItemV1] });
const { customerId, autumnV1, ctx } = await initScenario({
customerId: "version-multi-down",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
],
actions: [s.attach({ productId: "pro" })],
});
// Create v2 with $30 price
const priceItemV2 = items.monthlyPrice({ price: 30 });
await autumnV1.products.update(pro.id, {
items: [messagesItem, priceItemV2],
});
// Upgrade to v2
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 2,
});
// Create v3 with $40 price
const priceItemV3 = items.monthlyPrice({ price: 40 });
await autumnV1.products.update(pro.id, {
items: [messagesItem, priceItemV3],
});
// Upgrade to v3
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 3,
});
// Verify on v3
const customerV3 = await autumnV1.customers.get(customerId);
const productV3 = customerV3.products.find((p) => p.id === pro.id);
expect(productV3?.version).toBe(3);
// Now downgrade all the way to v1
const previewV1 = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: pro.id,
version: 1,
});
// Should credit $20 difference ($20 - $40)
expect(previewV1.total).toBe(-20);
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 1,
});
// Verify on v1
const customer = await autumnV1.customers.get(customerId);
const product = customer.products.find((p) => p.id === pro.id);
expect(product?.version).toBe(1);
await expectCustomerInvoiceCorrect({
customerId,
count: 4, // Initial + v2 + v3 + downgrade to v1
latestTotal: previewV1.total,
});
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
});
});
// 4.4 Bounce between versions: v1 → v2 → v1 → v2
test.concurrent(`${chalk.yellowBright("version-multi: bounce v1 v2 v1 v2")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const priceItemV1 = items.monthlyPrice({ price: 20 });
const pro = products.base({ id: "pro", items: [messagesItem, priceItemV1] });
const { customerId, autumnV1, ctx } = await initScenario({
customerId: "version-multi-bounce",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
],
actions: [s.attach({ productId: "pro" })],
});
// Create v2 with $30 price
const priceItemV2 = items.monthlyPrice({ price: 30 });
await autumnV1.products.update(pro.id, {
items: [messagesItem, priceItemV2],
});
// v1 → v2
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 2,
});
const customerAfterFirst = await autumnV1.customers.get(customerId);
expect(
customerAfterFirst.products.find((p) => p.id === pro.id)?.version,
).toBe(2);
// v2 → v1
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 1,
});
const customerAfterSecond = await autumnV1.customers.get(customerId);
expect(
customerAfterSecond.products.find((p) => p.id === pro.id)?.version,
).toBe(1);
// v1 → v2 again
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: pro.id,
version: 2,
});
const customer = await autumnV1.customers.get(customerId);
expect(customer.products.find((p) => p.id === pro.id)?.version).toBe(2);
// Should have 4 invoices (initial + 3 version changes)
await expectCustomerInvoiceCorrect({
customerId,
count: 4,
latestTotal: 10, // $30 - $20
});
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
});
});
// 4.5 Multiple customers on different versions
test.concurrent(`${chalk.yellowBright("version-multi: customers on different versions")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const priceItemV1 = items.monthlyPrice({ price: 20 });
const pro = products.base({ id: "pro", items: [messagesItem, priceItemV1] });
const customer1Id = "version-multi-cus1";
const customer2Id = "version-multi-cus2";
// First customer on v1
const { autumnV1, ctx } = await initScenario({
customerId: customer1Id,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({
list: [pro],
customerIdsToDelete: [customer1Id, customer2Id],
}),
],
actions: [s.attach({ productId: "pro" })],
});
// Create v2
const priceItemV2 = items.monthlyPrice({ price: 30 });
await autumnV1.products.update(pro.id, {
items: [messagesItem, priceItemV2],
});
// Second customer - use initScenario with empty products list to reuse customer1's product
await initScenario({
customerId: customer2Id,
setup: [s.customer({ paymentMethod: "success" }), s.products({ list: [] })],
actions: [], // No attachments yet - we'll attach manually to customer1's product
});
// Attach customer2 to customer1's product (gets latest version v2 by default)
// Note: pro.id was mutated by initScenario to include customer1's prefix
await autumnV1.attach({
customer_id: customer2Id,
product_id: pro.id,
});
// Customer 1 stays on v1
const customer1 = await autumnV1.customers.get(customer1Id);
expect(customer1.products.find((p) => p.id === pro.id)?.version).toBe(1);
// // Customer 2 on v2
const customer2 = await autumnV1.customers.get(customer2Id);
expect(customer2.products.find((p) => p.id === pro.id)?.version).toBe(2);
// Now upgrade customer 1 to v2
await autumnV1.subscriptions.update({
customer_id: customer1Id,
product_id: pro.id,
version: 2,
});
const customer1Updated = await autumnV1.customers.get(customer1Id);
expect(customer1Updated.products.find((p) => p.id === pro.id)?.version).toBe(
2,
);
await expectSubToBeCorrect({
db: ctx.db,
customerId: customer1Id,
org: ctx.org,
env: ctx.env,
});
});