Files
cfw-autumn/server/tests/integration/balances/usage-windows/usage-window-own-feature.test.ts
2026-06-11 15:28:59 +01:00

311 lines
8.7 KiB
TypeScript

import { test } from "bun:test";
import type { ApiCustomerV5 } from "@autumn/shared";
import { ApiVersion, ResetInterval } from "@autumn/shared";
import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect.js";
import { expectUsageLimitCorrect } from "@tests/integration/utils/expectUsageLimitCorrect.js";
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";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import {
expectCustomerBalance,
expectCustomerUsageLimit,
setCustomerUsageLimit,
} from "../utils/usage-limit-utils/customerUsageLimitUtils.js";
// Usage windows where the cap is set on the TRACKED feature's own id (no
// credit-system indirection): the customer holds cusEnts of the capped
// feature directly, including MULTIPLE cusEnts whose drains must aggregate
// onto one customer-scoped counter, and interval mismatches between the
// cusEnt's reset and the cap's window.
// initScenario only exposes clients up to V2_2; build the latest-version client
// directly (same pattern as the v2.2-vs-v2.3 parity tests).
const autumnV2_3 = new AutumnInt({ version: ApiVersion.V2_3 });
// Same feature, same interval (credits/mo cusEnt + credits 5/mo cap), tracked
// directly: the cap binds independently of the 100-credit balance.
test.concurrent(
`${chalk.yellowBright("usage-window-own-feature1: cap on the tracked feature itself clamps at the cap")}`,
async () => {
const customerProduct = products.base({
id: "uw-own-same-interval",
items: [items.monthlyCredits({ includedUsage: 100 })],
});
const customerId = "uw-own-same-interval-1";
await initScenario({
customerId,
setup: [
s.customer({ testClock: false }),
s.products({ list: [customerProduct] }),
],
actions: [s.billing.attach({ productId: customerProduct.id })],
});
await setCustomerUsageLimit({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Credits,
limit: 5,
});
// Consume exactly to the cap, tracking the capped feature directly.
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Credits,
value: 5,
});
await expectCustomerBalance({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Credits,
granted: 100,
remaining: 95,
usage: 5,
});
// Over the cap: clamps to 0 with 95 credits still in the balance.
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Credits,
value: 1,
});
await expectCustomerBalance({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Credits,
granted: 100,
remaining: 95,
usage: 5,
});
await expectCustomerUsageLimit({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Credits,
usage: 5,
limit: 5,
});
},
);
// Sub-interval cap: the cusEnt resets monthly but the cap windows daily.
test.concurrent(
`${chalk.yellowBright("usage-window-own-feature2: daily cap on a monthly cusEnt clamps while balance remains")}`,
async () => {
const customerProduct = products.base({
id: "uw-own-day-cap",
items: [items.monthlyCredits({ includedUsage: 100 })],
});
const customerId = "uw-own-day-cap-1";
await initScenario({
customerId,
setup: [
s.customer({ testClock: false }),
s.products({ list: [customerProduct] }),
],
actions: [s.billing.attach({ productId: customerProduct.id })],
});
await setCustomerUsageLimit({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Credits,
limit: 2,
interval: ResetInterval.Day,
});
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Credits,
value: 2,
});
// The day window is exhausted; the next track clamps to 0 against the 98
// remaining monthly credits.
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Credits,
value: 1,
});
await expectCustomerBalance({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Credits,
granted: 100,
remaining: 98,
usage: 2,
});
await expectCustomerUsageLimit({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Credits,
usage: 2,
limit: 2,
});
},
);
// MULTIPLE cusEnts of the capped feature (monthly 1 + lifetime 10): one
// direct track spans both, and both drains must land on the same counter.
test.concurrent(
`${chalk.yellowBright("usage-window-own-feature3: direct track across two cusEnts aggregates onto one counter")}`,
async () => {
const monthlyCreditsItem = items.monthlyCredits({ includedUsage: 1 });
const lifetimeCreditsItem = constructFeatureItem({
featureId: TestFeature.Credits,
includedUsage: 10,
interval: null,
});
const freePlan = products.base({
id: "uw-own-multi-credit",
items: [monthlyCreditsItem, lifetimeCreditsItem],
});
const customerId = "uw-own-multi-credit-1";
await initScenario({
customerId,
setup: [
s.customer({ testClock: false }),
s.products({ list: [freePlan] }),
],
actions: [s.billing.attach({ productId: freePlan.id })],
});
await setCustomerUsageLimit({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Credits,
limit: 2,
interval: ResetInterval.Day,
});
// Track 2 credits directly: 1 drains the monthly cusEnt, 1 spills into
// lifetime. The counter must see the SUM (2), not one cusEnt's drain.
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Credits,
value: 2,
});
const atCap = await autumnV2_3.customers.get<ApiCustomerV5>(customerId);
expectBalanceCorrect({
customer: atCap,
featureId: TestFeature.Credits,
granted: 11,
remaining: 9,
usage: 2,
breakdown: {
[ResetInterval.Month]: { remaining: 0, usage: 1 },
[ResetInterval.OneOff]: { remaining: 9, usage: 1 },
},
});
// Cap exhausted: clamps to 0 instead of draining the 9 lifetime credits.
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Credits,
value: 1,
});
const afterClamp =
await autumnV2_3.customers.get<ApiCustomerV5>(customerId);
expectBalanceCorrect({
customer: afterClamp,
featureId: TestFeature.Credits,
granted: 11,
remaining: 9,
usage: 2,
});
expectUsageLimitCorrect({
customer: afterClamp,
featureId: TestFeature.Credits,
usage: 2,
limit: 2,
});
},
);
// Metered (non-credit) own-feature cap across two cusEnts: messages monthly
// 100 + lifetime 100, cap 150/mo. The second track must clamp to the exact
// remaining headroom after the first track spanned both cusEnts.
test.concurrent(
`${chalk.yellowBright("usage-window-own-feature4: metered cap aggregates across two cusEnts and clamps to headroom")}`,
async () => {
const monthlyMessagesItem = items.monthlyMessages({ includedUsage: 100 });
const lifetimeMessagesItem = items.lifetimeMessages({
includedUsage: 100,
});
const freePlan = products.base({
id: "uw-own-multi-messages",
items: [monthlyMessagesItem, lifetimeMessagesItem],
});
const customerId = "uw-own-multi-messages-1";
await initScenario({
customerId,
setup: [
s.customer({ testClock: false }),
s.products({ list: [freePlan] }),
],
actions: [s.billing.attach({ productId: freePlan.id })],
});
await setCustomerUsageLimit({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Messages,
limit: 150,
});
// Track 120: drains the monthly cusEnt (100) then 20 from lifetime.
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 120,
});
const afterSpan = await autumnV2_3.customers.get<ApiCustomerV5>(customerId);
expectBalanceCorrect({
customer: afterSpan,
featureId: TestFeature.Messages,
granted: 200,
remaining: 80,
usage: 120,
breakdown: {
[ResetInterval.Month]: { remaining: 0, usage: 100 },
[ResetInterval.OneOff]: { remaining: 80, usage: 20 },
},
});
// Counter = 120 across both cusEnts, so headroom is 30: track 50 applies
// exactly 30 from lifetime.
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 50,
});
const atCap = await autumnV2_3.customers.get<ApiCustomerV5>(customerId);
expectBalanceCorrect({
customer: atCap,
featureId: TestFeature.Messages,
granted: 200,
remaining: 50,
usage: 150,
breakdown: {
[ResetInterval.Month]: { remaining: 0, usage: 100 },
[ResetInterval.OneOff]: { remaining: 50, usage: 50 },
},
});
expectUsageLimitCorrect({
customer: atCap,
featureId: TestFeature.Messages,
usage: 150,
limit: 150,
});
},
);