378 lines
11 KiB
TypeScript
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,
|
|
});
|
|
});
|