Files
cfw-autumn/server/tests/_temp/sync-v2-entity-binding.test.ts
2026-05-18 20:14:59 +01:00

99 lines
3.1 KiB
TypeScript

/**
* TDD repro for syncV2 entity binding bug.
*
* Bug: When the caller passes `phases[].plans[].internal_entity_id` to
* `billing.sync_v2`, the inserted customer product is NOT bound to that
* entity. `initCustomerProduct` sources `internal_entity_id` only from
* `fullCustomer.entity` (the customer's currently-set entity context),
* ignoring the plan's intent. The data DOES flow through SyncPlanInstance →
* SyncProductContext.plan (it's even logged in logSyncContext), but
* `initImmediateSyncCustomerProduct` never threads it down to
* `initFullCustomerProduct`, and `InitFullCustomerProductOptions` has no
* field to receive it.
*
* Red (current): cusProduct.internal_entity_id is null after sync.
* Green (after fix): cusProduct.internal_entity_id === plan.internal_entity_id.
*/
import { expect, test } from "bun:test";
import chalk from "chalk";
import { createStripeSubscriptionFromProduct } from "@tests/integration/billing/sync/utils/syncTestUtils";
import { TestFeature } from "@tests/setup/v2Features";
import { items } from "@tests/utils/fixtures/items";
import { products } from "@tests/utils/fixtures/products";
import ctx from "@tests/utils/testInitUtils/createTestContext";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
import { CusService } from "@/internal/customers/CusService";
import { EntityService } from "@/internal/api/entities/EntityService";
test(
chalk.yellowBright(
"sync-v2 entity binding: plan.internal_entity_id must propagate to inserted customer product",
),
async () => {
const customerId = "sync-v2-entity-bind";
const pro = products.pro({
id: "sync-v2-entity-pro",
items: [items.monthlyMessages({ includedUsage: 100 })],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
s.entities({ count: 1, featureId: TestFeature.Users }),
],
actions: [],
});
const fullCustomerBefore = await CusService.getFull({
ctx,
idOrInternalId: customerId,
});
const entityList = await EntityService.list({
db: ctx.db,
internalCustomerId: fullCustomerBefore.internal_id,
});
expect(entityList.length).toBeGreaterThan(0);
const targetEntity = entityList[0];
const targetEntityInternalId = targetEntity.internal_id;
const stripeSubscription = await createStripeSubscriptionFromProduct({
ctx,
customerId,
productId: pro.id,
});
expect(stripeSubscription.status).toBe("active");
await autumnV1.post("/billing.sync_v2", {
customer_id: customerId,
stripe_subscription_id: stripeSubscription.id,
phases: [
{
starts_at: "now",
plans: [
{
plan_id: pro.id,
internal_entity_id: targetEntityInternalId,
},
],
},
],
});
const fullCustomerAfter = await CusService.getFull({
ctx,
idOrInternalId: customerId,
});
const cusProduct = fullCustomerAfter.customer_products.find(
(cp) => cp.product_id === pro.id,
);
expect(cusProduct).toBeDefined();
expect(cusProduct!.internal_entity_id).toBe(targetEntityInternalId);
},
);