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

249 lines
8.0 KiB
TypeScript

import { test } from "bun:test";
import type { ApiCustomerV3, ApiEntityV0 } from "@autumn/shared";
import {
expectCustomerFeatureCorrect,
expectCustomerFeatureExists,
} from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import {
expectCustomerProducts,
expectProductActive,
expectProductNotPresent,
} from "@tests/integration/billing/utils/expectCustomerProductCorrect";
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: Multi-attach at entity level when customer has product
// ═══════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("multi-attach entities: entity-level multi-attach when customer has product")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const usersItem = items.monthlyUsers({ includedUsage: 5 });
// Customer-level product
const customerPlan = products.pro({
id: "cus-plan",
items: [messagesItem],
});
// Entity-level products
const entityPlanA = products.base({
id: "ent-plan-a",
items: [usersItem, items.monthlyPrice({ price: 10 })],
});
const entityPlanB = products.base({
id: "ent-plan-b",
items: [items.dashboard(), items.monthlyPrice({ price: 5 })],
group: `${customerPlan.id}-group-b`,
});
const { customerId, autumnV1, entities } = await initScenario({
customerId: "ma-ent-cus-has-product",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({
list: [customerPlan, entityPlanA, entityPlanB],
}),
s.entities({ count: 2, featureId: TestFeature.Users }),
],
actions: [
// Customer already has a product
s.billing.attach({ productId: customerPlan.id }),
],
});
// Multi-attach to entity 0
await autumnV1.billing.multiAttach({
customer_id: customerId,
entity_id: entities[0].id,
plans: [{ plan_id: entityPlanA.id }, { plan_id: entityPlanB.id }],
});
// Verify entity 0 has both entity plans
const entity0 = await autumnV1.entities.get<ApiEntityV0>(
customerId,
entities[0].id,
);
await expectCustomerProducts({
customer: entity0,
active: [entityPlanA.id, entityPlanB.id],
});
// Entity 0 should have dashboard from entityPlanB
await expectCustomerFeatureExists({
customer: entity0,
featureId: TestFeature.Dashboard,
});
// Verify entity 1 has no entity-level products
const entity1 = await autumnV1.entities.get<ApiEntityV0>(
customerId,
entities[1].id,
);
await expectProductNotPresent({
customer: entity1,
productId: entityPlanA.id,
});
await expectProductNotPresent({
customer: entity1,
productId: entityPlanB.id,
});
// Customer still has its product
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectProductActive({
customer,
productId: customerPlan.id,
});
// Invoice: $20 (customer plan) + $10 (entity plan A) + $5 (entity plan B) = $35
await expectCustomerInvoiceCorrect({
customer,
count: 2, // 1 for customer plan attach + 1 for entity multi-attach
latestTotal: 15,
});
});
// ═══════════════════════════════════════════════════════════════════
// Test 2: Multi-attach at entity level when sibling entity has product
// ═══════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("multi-attach entities: entity-level when sibling entity has product")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 200 });
const usersItem = items.monthlyUsers({ includedUsage: 5 });
const planA = products.pro({
id: "plan-a",
items: [messagesItem],
});
const planB = products.base({
id: "plan-b",
items: [usersItem, items.monthlyPrice({ price: 25 })],
group: "group-b",
});
const { customerId, autumnV1, entities } = await initScenario({
customerId: "ma-ent-sibling",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [planA, planB] }),
s.entities({ count: 2, featureId: TestFeature.Users }),
],
actions: [
// Entity 0 already has planA
s.billing.attach({ productId: planA.id, entityIndex: 0 }),
],
});
// Multi-attach planB to entity 1 (sibling)
await autumnV1.billing.multiAttach({
customer_id: customerId,
entity_id: entities[1].id,
plans: [{ plan_id: planB.id }],
});
// Entity 0 has planA, not planB
const entity0 = await autumnV1.entities.get<ApiEntityV0>(
customerId,
entities[0].id,
);
await expectProductActive({
customer: entity0,
productId: planA.id,
});
await expectProductNotPresent({
customer: entity0,
productId: planB.id,
});
// Entity 1 has planB, not planA
const entity1 = await autumnV1.entities.get<ApiEntityV0>(
customerId,
entities[1].id,
);
await expectProductActive({
customer: entity1,
productId: planB.id,
});
await expectProductNotPresent({
customer: entity1,
productId: planA.id,
});
});
// ═══════════════════════════════════════════════════════════════════
// Test 3: Multi-attach at customer level when entity has product
// ═══════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("multi-attach entities: customer-level multi-attach when entity has product")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const wordsItem = items.monthlyWords({ includedUsage: 50 });
// Entity will have this product
const entityPlan = products.pro({
id: "ent-plan",
items: [messagesItem],
});
// Customer-level plans
const cusPlanA = products.base({
id: "cus-plan-a",
items: [wordsItem, items.monthlyPrice({ price: 15 })],
});
const cusPlanB = products.base({
id: "cus-plan-b",
items: [items.dashboard(), items.monthlyPrice({ price: 10 })],
group: "group-b",
});
const { customerId, autumnV1, entities } = await initScenario({
customerId: "ma-ent-cus-level",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [entityPlan, cusPlanA, cusPlanB] }),
s.entities({ count: 1, featureId: TestFeature.Users }),
],
actions: [
// Entity 0 already has entityPlan
s.billing.attach({ productId: entityPlan.id, entityIndex: 0 }),
],
});
// Multi-attach at customer level
await autumnV1.billing.multiAttach({
customer_id: customerId,
plans: [{ plan_id: cusPlanA.id }, { plan_id: cusPlanB.id }],
});
// Customer has both customer-level plans
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [cusPlanA.id, cusPlanB.id],
});
// Entity 0 still has its own plan
const entity0 = await autumnV1.entities.get<ApiEntityV0>(
customerId,
entities[0].id,
);
await expectProductActive({
customer: entity0,
productId: entityPlan.id,
});
// Customer features: words from cusPlanA + messages from entity's plan (inherited)
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Words,
balance: 50,
});
// Dashboard from cusPlanB
await expectCustomerFeatureExists({
customer,
featureId: TestFeature.Dashboard,
});
});