Files
cfw-autumn/server/tests/integration/billing/attach/custom-plan-patch/attach-patch-customize.test.ts
2026-06-14 14:59:36 +01:00

104 lines
3.5 KiB
TypeScript

/**
* Regression: attach with PATCH-style customize (add_items / remove_items).
*
* Red-failure mode (pre-fix):
* - setupAttachBillingContext called setupAttachProductContext WITHOUT fullCustomer,
* so the patch pipeline (setupAttachPatchProductContext -> setupPatchContext) was
* never reached. Patch-style customize fell back to customizePlanV1ToV0, which only
* understands PUT-style {price, items} — it dropped EVERY feature item, leaving the
* customer on the plan's base price with no entitlements at all.
*
* Green-success criteria (post-fix):
* - Base items are retained, remove_items are dropped, add_items are present, and
* feature_quantities apply — matching multiAttach / createSchedule patch behavior.
*/
import { expect, test } from "bun:test";
import {
type ApiCustomerV5,
type AttachParamsV1Input,
} from "@autumn/shared";
import {
expectCustomerProducts,
expectProductActive,
} from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect";
import { expectFlagCorrect } from "@tests/integration/utils/expectFlagCorrect";
import { TestFeature } from "@tests/setup/v2Features";
import { items } from "@tests/utils/fixtures/items";
import { itemsV2 } from "@tests/utils/fixtures/itemsV2";
import { products } from "@tests/utils/fixtures/products";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
import chalk from "chalk";
test.concurrent(`${chalk.yellowBright("attach patch customize: add_items/remove_items keep base items")}`, async () => {
const customerId = "attach-patch-customize-basic";
// Mirrors the reported payload: base price + several feature items, a boolean
// flag to remove, and a prepaid item whose quantity comes from feature_quantities.
const scale = products.base({
id: "scale",
items: [
items.monthlyPrice({ price: 500 }),
items.monthlyMessages({ includedUsage: 100 }), // retained canary
items.adminRights(), // boolean flag to remove
items.prepaidUsers({ billingUnits: 1 }), // quantity via feature_quantities
],
});
const { autumnV2_2 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [scale] }),
],
actions: [],
});
await autumnV2_2.billing.attach<AttachParamsV1Input>({
customer_id: customerId,
plan_id: scale.id,
customize: {
add_items: [itemsV2.dashboard()],
remove_items: [{ feature_id: TestFeature.AdminRights }],
},
invoice_mode: { enabled: true, finalize: false },
feature_quantities: [{ feature_id: TestFeature.Users, quantity: 3 }],
enable_plan_immediately: true,
});
const customer = await autumnV2_2.customers.get<ApiCustomerV5>(customerId);
// Plan active immediately (enable_plan_immediately overrides invoice-mode deferral).
await expectProductActive({ customer, productId: scale.id });
await expectCustomerProducts({ customer, active: [scale.id] });
// Base feature item retained (the bug dropped this entirely).
expectBalanceCorrect({
customer,
featureId: TestFeature.Messages,
remaining: 100,
usage: 0,
planId: scale.id,
});
// add_items applied.
expectFlagCorrect({
customer,
featureId: TestFeature.Dashboard,
planId: scale.id,
});
// remove_items applied.
expect(customer.flags?.[TestFeature.AdminRights]).toBeUndefined();
// feature_quantities applied to the prepaid item.
expectBalanceCorrect({
customer,
featureId: TestFeature.Users,
remaining: 3,
usage: 0,
planId: scale.id,
});
});