Files
cfw-autumn/server/tests/integration/billing/multi-attach/basic/multi-attach-addons.test.ts
2026-02-24 20:25:30 +00:00

255 lines
7.9 KiB
TypeScript

import { expect, test } from "bun:test";
import type { ApiCustomerV3 } from "@autumn/shared";
import {
expectCustomerFeatureCorrect,
expectCustomerFeatureExists,
} from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect";
import { TestFeature } from "@tests/setup/v2Features.js";
import { items } from "@tests/utils/fixtures/items.js";
import { products } from "@tests/utils/fixtures/products.js";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
import chalk from "chalk";
// ═══════════════════════════════════════════════════════════════════
// Test 1: Two add-ons when customer has main recurring
// Checks: preview total, post-attach all three products active,
// features, invoice (2 invoices total), sub
// ═══════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("multi-attach addons: two add-ons when customer has main recurring")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
const wordsItem = items.monthlyWords({ includedUsage: 200 });
const dashboardItem = items.dashboard();
const mainPlan = products.pro({
id: "main",
items: [messagesItem],
});
const addonA = products.recurringAddOn({
id: "addon-a",
items: [wordsItem],
});
const addonB = products.oneOffAddOn({
id: "addon-b",
items: [dashboardItem],
});
const { customerId, autumnV1, ctx } = await initScenario({
customerId: "ma-addon-two-addons",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [mainPlan, addonA, addonB] }),
],
actions: [s.billing.attach({ productId: mainPlan.id })],
});
const multiAttachParams = {
customer_id: customerId,
plans: [{ plan_id: addonA.id }, { plan_id: addonB.id }],
};
// 1. Preview — $20 (recurring addon) + $10 (one-off addon) = $30
const preview = await autumnV1.billing.previewMultiAttach(multiAttachParams);
expect(preview.total).toBeCloseTo(30, 0);
// 2. Attach
await autumnV1.billing.multiAttach(multiAttachParams);
// 3. Verify
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [mainPlan.id, addonA.id, addonB.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 500,
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Words,
balance: 200,
});
await expectCustomerFeatureExists({
customer,
featureId: TestFeature.Dashboard,
});
// Invoice count: 1 (main attach) + 1 (multi-attach addons) = 2
await expectCustomerInvoiceCorrect({
customer,
count: 2,
latestTotal: 30,
});
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
flags: { checkNotTrialing: true },
});
});
// ═══════════════════════════════════════════════════════════════════
// Test 2: Add-on + main recurring when customer already has add-on
// Checks: preview total, post-attach all three products active,
// features, invoice (2 invoices total), sub
// ═══════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("multi-attach addons: add-on + main recurring when customer has add-on")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 300 });
const wordsItem = items.monthlyWords({ includedUsage: 100 });
const dashboardItem = items.dashboard();
const existingAddon = products.recurringAddOn({
id: "existing-addon",
items: [dashboardItem],
});
const mainPlan = products.pro({
id: "main",
items: [messagesItem],
});
const newAddon = products.oneOffAddOn({
id: "new-addon",
items: [wordsItem],
});
const { customerId, autumnV1, ctx } = await initScenario({
customerId: "ma-addon-plus-main",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [existingAddon, mainPlan, newAddon] }),
],
actions: [s.billing.attach({ productId: existingAddon.id })],
});
const multiAttachParams = {
customer_id: customerId,
plans: [{ plan_id: mainPlan.id }, { plan_id: newAddon.id }],
};
// 1. Preview — $20 (main) + $10 (new addon one-off) = $30
const preview = await autumnV1.billing.previewMultiAttach(multiAttachParams);
expect(preview.total).toBeCloseTo(30, 0);
// 2. Attach
await autumnV1.billing.multiAttach(multiAttachParams);
// 3. Verify
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [existingAddon.id, mainPlan.id, newAddon.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 300,
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Words,
balance: 100,
});
await expectCustomerFeatureExists({
customer,
featureId: TestFeature.Dashboard,
});
// Invoice count: 1 (existing addon) + 1 (multi-attach) = 2
await expectCustomerInvoiceCorrect({
customer,
count: 2,
latestTotal: 30,
});
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
flags: { checkNotTrialing: true },
});
});
// ═══════════════════════════════════════════════════════════════════
// Test 3: Multiple recurring add-ons (no main plan)
// Checks: preview total, post-attach products, features, invoice, sub
// ═══════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("multi-attach addons: multiple recurring add-ons without main plan")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const wordsItem = items.monthlyWords({ includedUsage: 50 });
const addonA = products.recurringAddOn({
id: "addon-a",
items: [messagesItem],
});
const addonB = products.recurringAddOn({
id: "addon-b",
items: [wordsItem],
});
const { customerId, autumnV1, ctx } = await initScenario({
customerId: "ma-addon-multi-recurring",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [addonA, addonB] }),
],
actions: [],
});
const multiAttachParams = {
customer_id: customerId,
plans: [{ plan_id: addonA.id }, { plan_id: addonB.id }],
};
// 1. Preview — $20 (addonA) + $20 (addonB) = $40
const preview = await autumnV1.billing.previewMultiAttach(multiAttachParams);
expect(preview.total).toBeCloseTo(40, 0);
// 2. Attach
await autumnV1.billing.multiAttach(multiAttachParams);
// 3. Verify
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [addonA.id, addonB.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 100,
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Words,
balance: 50,
});
await expectCustomerInvoiceCorrect({
customer,
count: 1,
latestTotal: 40,
});
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
flags: { checkNotTrialing: true },
});
});