Files
cfw-autumn/server/tests/integration/crud/plans/create-plan-basic.test.ts
2026-02-11 07:48:19 -08:00

431 lines
13 KiB
TypeScript

import { expect, test } from "bun:test";
import {
type ApiPlan,
type ApiProduct,
ApiVersion,
BillingInterval,
BillingMethod,
type CreatePlanParamsInput,
FreeTrialDuration,
OnDecrease,
OnIncrease,
ProductItemInterval,
ResetInterval,
RolloverExpiryDurationType,
TierInfinite,
UsageModel,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
const autumnV2 = new AutumnInt({ version: ApiVersion.V2_0 });
const autumnV1_2 = new AutumnInt({ version: ApiVersion.V1_2 });
// ═══════════════════════════════════════════════════════════════════════════════
// BASIC CREATION TESTS
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("create: minimal plan (id + name only)")}`, async () => {
const productId = "min_plan";
try {
await autumnV2.products.delete(productId);
} catch (_error) {
console.error(_error);
}
const created = await autumnV2.products.create<
ApiPlan,
CreatePlanParamsInput
>({
id: productId,
name: "Minimal Plan",
});
expect(created.id).toBe(productId);
expect(created.features).toHaveLength(0);
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
expect(v1_2.items).toHaveLength(0);
expect(v1_2.is_add_on).toBe(false);
});
test.concurrent(`${chalk.yellowBright("create: with description field")}`, async () => {
const productId = "with_desc";
try {
await autumnV2.products.delete(productId);
} catch (_error) {}
const created = await autumnV2.products.create<
ApiPlan,
CreatePlanParamsInput
>({
id: productId,
name: "With Description",
description: "Test description for V2",
});
expect(created.description).toBe("Test description for V2");
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
// @ts-expect-error: Descriptions aren't in the type, but we're just double checking the response.
expect(v1_2.description).toBeUndefined();
});
// ═══════════════════════════════════════════════════════════════════════════════
// BOOLEAN FEATURE TESTS
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("create: boolean feature")}`, async () => {
const productId = "bool_plan";
try {
await autumnV2.products.delete(productId);
} catch (_error) {}
const created = await autumnV2.products.create<
ApiPlan,
CreatePlanParamsInput
>({
id: productId,
name: "Boolean Plan",
items: [{ feature_id: TestFeature.Dashboard }],
});
expect(created.features).toHaveLength(1);
expect(created.features[0].feature_id).toBe(TestFeature.Dashboard);
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
expect(v1_2.items).toHaveLength(1);
expect(v1_2.items[0]).toMatchObject({
feature_id: TestFeature.Dashboard,
included_usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// FLAGS TESTS
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("create: add_on and auto_enable flags")}`, async () => {
const productId = "flags_test";
try {
await autumnV2.products.delete(productId);
} catch (_error) {}
const created = await autumnV2.products.create<
ApiPlan,
CreatePlanParamsInput
>({
id: productId,
name: "Flags Test",
add_on: true,
auto_enable: false,
});
expect(created.add_on).toBe(true);
expect(created.default).toBe(false);
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
expect(v1_2.is_add_on).toBe(true);
expect(v1_2.is_default).toBe(false);
});
// ═══════════════════════════════════════════════════════════════════════════════
// BASE PRICING TESTS
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("create: monthly base price")}`, async () => {
const productId = "monthly_base";
try {
await autumnV2.products.delete(productId);
} catch (_error) {}
const created = await autumnV2.products.create<
ApiPlan,
CreatePlanParamsInput
>({
id: productId,
name: "Monthly Base",
price: { amount: 2900, interval: BillingInterval.Month },
});
expect(created.price!.amount).toBe(2900);
expect(created.price!.interval).toBe(BillingInterval.Month);
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
const basePrice = v1_2.items.find((i) => !i.feature_id);
expect(basePrice!.price).toBe(2900);
expect(basePrice!.interval).toBe(ProductItemInterval.Month);
});
test.concurrent(`${chalk.yellowBright("create: yearly base price")}`, async () => {
const productId = "yearly_base";
try {
await autumnV2.products.delete(productId);
} catch (_error) {}
const created = await autumnV2.products.create<
ApiPlan,
CreatePlanParamsInput
>({
id: productId,
name: "Yearly Base",
price: { amount: 29900, interval: BillingInterval.Year },
});
expect(created.price!.amount).toBe(29900);
expect(created.price!.interval).toBe(BillingInterval.Year);
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
const basePrice = v1_2.items[0];
expect(basePrice.price).toBe(29900);
expect(basePrice.interval).toBe(ProductItemInterval.Year);
});
// ═══════════════════════════════════════════════════════════════════════════════
// COMPLEX REAL-WORLD SCENARIOS
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("create: full SaaS plan with multiple feature types")}`, async () => {
const productId = "saas_complete";
try {
await autumnV2.products.delete(productId);
} catch (_error) {}
await autumnV2.products.create<ApiPlan, CreatePlanParamsInput>({
id: productId,
name: "SaaS Complete",
description: "Full-featured SaaS plan",
price: { amount: 9900, interval: BillingInterval.Month },
items: [
{ feature_id: TestFeature.Dashboard },
{
feature_id: TestFeature.Messages,
included: 10000,
reset: { interval: ResetInterval.Month },
rollover: {
max: 20000,
expiry_duration_type: RolloverExpiryDurationType.Month,
expiry_duration_length: 1,
},
},
{
feature_id: TestFeature.Users,
included: 10,
reset: { interval: ResetInterval.Month },
proration: {
on_increase: OnIncrease.ProrateImmediately,
on_decrease: OnDecrease.Prorate,
},
},
],
free_trial: {
duration_type: FreeTrialDuration.Day,
duration_length: 14,
card_required: false,
},
});
const v2 = await autumnV2.products.get<ApiPlan>(productId);
expect(v2.features).toHaveLength(3);
expect(v2.free_trial).toBeDefined();
expect(v2.description).toBe("Full-featured SaaS plan");
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
expect(v1_2.items).toHaveLength(4); // 1 base price + 3 features
expect(v1_2.free_trial).toBeDefined();
});
test.concurrent(`${chalk.yellowBright("create: usage-only product (no base price)")}`, async () => {
const productId = "usage_only";
try {
await autumnV2.products.delete(productId);
} catch (_error) {}
await autumnV2.products.create<ApiPlan, CreatePlanParamsInput>({
id: productId,
name: "Usage Only",
items: [
{
feature_id: TestFeature.Messages,
price: {
interval: BillingInterval.Month,
billing_method: BillingMethod.UsageBased,
billing_units: 1,
tiers: [
{ to: 1000, amount: 5 },
{ to: TierInfinite, amount: 2 },
],
},
},
],
});
const v2 = await autumnV2.products.get<ApiPlan>(productId);
expect(v2.price).toBeNull();
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
expect(v1_2.items).toHaveLength(1);
});
test.concurrent(`${chalk.yellowBright("create: metered feature with rollover")}`, async () => {
const productId = "with_rollover";
try {
await autumnV2.products.delete(productId);
} catch (_error) {}
await autumnV2.products.create<ApiPlan, CreatePlanParamsInput>({
id: productId,
name: "With Rollover",
items: [
{
feature_id: TestFeature.Messages,
included: 1000,
reset: { interval: ResetInterval.Month },
rollover: {
max: 2000,
expiry_duration_type: RolloverExpiryDurationType.Month,
expiry_duration_length: 1,
},
},
],
});
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
const item = v1_2.items[0];
expect(item.config).toBeDefined();
expect(item.config!.rollover).toBeDefined();
expect(item.config!.rollover!.max).toBe(2000);
expect(item.config!.rollover!.duration).toBe(
RolloverExpiryDurationType.Month,
);
expect(item.config!.rollover!.length).toBe(1);
});
test.concurrent(`${chalk.yellowBright("create: enterprise plan with all features")}`, async () => {
const productId = "enterprise_complete";
try {
await autumnV2.products.delete(productId);
} catch (_error) {}
await autumnV2.products.create<ApiPlan, CreatePlanParamsInput>({
id: productId,
name: "Enterprise Complete",
description: "Full-featured enterprise plan with all capabilities",
group: "",
add_on: false,
auto_enable: false,
price: { amount: 49900, interval: BillingInterval.Month },
items: [
{ feature_id: TestFeature.Dashboard },
{
feature_id: TestFeature.Messages,
included: 100000,
price: {
interval: BillingInterval.Month,
billing_method: BillingMethod.UsageBased,
billing_units: 1000,
tiers: [
{ to: 100000, amount: 10 },
{ to: 500000, amount: 50 },
{ to: 1000000, amount: 40 },
{ to: TierInfinite, amount: 30 },
],
},
},
{
feature_id: TestFeature.Users,
included: 50,
price: {
amount: 20,
interval: BillingInterval.Month,
billing_method: BillingMethod.UsageBased,
billing_units: 1,
},
proration: {
on_increase: OnIncrease.ProrateImmediately,
on_decrease: OnDecrease.Prorate,
},
},
],
free_trial: {
duration_type: FreeTrialDuration.Day,
duration_length: 30,
card_required: true,
},
});
// V2 validation
const v2 = await autumnV2.products.get<ApiPlan>(productId);
expect(v2).toMatchObject({
price: { amount: 49900 },
description: "Full-featured enterprise plan with all capabilities",
free_trial: { duration_length: 30 },
});
expect(v2.features).toHaveLength(3);
// Boolean feature
expect(v2.features).toEqual(
expect.arrayContaining([
expect.objectContaining({ feature_id: TestFeature.Dashboard }),
]),
);
// Metered feature
const meteredFeature = v2.features.find(
(f) => f.feature_id === TestFeature.Messages,
);
expect(meteredFeature).toMatchObject({
granted_balance: 100000,
price: { billing_units: 1000 },
});
expect(meteredFeature!.price!.tiers).toHaveLength(4);
// Seats feature
const seatsFeature = v2.features.find(
(f) => f.feature_id === TestFeature.Users,
);
expect(seatsFeature).toMatchObject({
granted_balance: 50,
price: expect.objectContaining({
amount: 20,
interval: BillingInterval.Month,
usage_model: UsageModel.PayPerUse,
max_purchase: null,
// interval_count: 1,
// billing_method: BillingMethod.UsageBased,
}),
// proration: {
// on_increase: OnIncrease.ProrateImmediately,
// on_decrease: OnDecrease.ProrateImmediately,
// },
});
// V1.2 validation
const v1_2 = await autumnV1_2.products.get<ApiProduct>(productId);
expect(v1_2.items).toHaveLength(4); // 1 base price + 3 features
expect(v1_2.free_trial).toMatchObject({ length: 30 });
const basePriceItem = v1_2.items.find((i) => !i.feature_id);
expect(basePriceItem).toMatchObject({
price: 49900,
interval: ProductItemInterval.Month,
});
const meteredItem = v1_2.items.find(
(i) => i.feature_id === TestFeature.Messages,
);
expect(meteredItem!.tiers).toHaveLength(4);
expect(meteredItem!.tiers![0]).toMatchObject({ to: 100000, amount: 10 });
const seatsItem = v1_2.items.find((i) => i.feature_id === TestFeature.Users);
expect(seatsItem).toMatchObject({
price: 20,
// config: {
// on_increase: OnIncrease.ProrateImmediately,
// on_decrease: OnDecrease.ProrateImmediately,
// },
});
});