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

178 lines
5.1 KiB
TypeScript

import { expect, test } from "bun:test";
import { ApiVersion, ResetInterval } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { items } from "@tests/utils/fixtures/items.js";
import { products } from "@tests/utils/fixtures/products.js";
import { timeout } from "@tests/utils/genUtils.js";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
expectCustomerBalance,
expectCustomerUsageLimit,
} from "../utils/usage-limit-utils/customerUsageLimitUtils.js";
/**
* TDD test for INDIVIDUAL usage limits on two member features (action1 +
* action2) of one credit system: each cap gates only its own feature while
* both features drain the shared credits pool.
* Credit costs: 1 action1 = 0.2 credits, 1 action2 = 0.6 credits.
*
* Contract under test:
* - caps armed together: action1 -> 5 units, action2 -> 4 units
* - each feature's checks/tracks are gated by ITS cap only; exhausting
* action1's cap leaves action2 open
* - both drain shared credits: final usage = 5*0.2 + 4*0.6 = 3.4 credits
* - each usage_limits entry reports its own window usage
* - a direct credits check is not gated by either member cap
*/
const autumnV2_3 = new AutumnInt({ version: ApiVersion.V2_3 });
test.concurrent(
`${chalk.yellowBright("uw-multi-cap1: individual caps on action1 and action2 over shared credits")}`,
async () => {
const freePlan = products.base({
id: "uw-multi-cap-credits",
items: [items.monthlyCredits({ includedUsage: 100 })],
});
const customerId = "uw-multi-cap-1";
await initScenario({
customerId,
setup: [
s.customer({ testClock: false }),
s.products({ list: [freePlan] }),
],
actions: [s.billing.attach({ productId: freePlan.id })],
});
// Arm BOTH caps in one update (billing_controls replaces the array).
await timeout(2000);
await autumnV2_3.customers.update(customerId, {
billing_controls: {
usage_limits: [
{
feature_id: TestFeature.Action1,
limit: 5,
interval: ResetInterval.Month,
},
{
feature_id: TestFeature.Action2,
limit: 4,
interval: ResetInterval.Month,
},
],
},
});
await timeout(3000);
// ── action1: 3 of 5 used (0.6 credits) ──
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Action1,
value: 3,
});
const action1Within = await autumnV2_3.check({
customer_id: customerId,
feature_id: TestFeature.Action1,
required_balance: 2,
});
expect(action1Within.allowed).toBe(true);
const action1Beyond = await autumnV2_3.check({
customer_id: customerId,
feature_id: TestFeature.Action1,
required_balance: 3,
});
expect(action1Beyond.allowed).toBe(false);
// ── action2 is untouched by action1's usage: its own cap (4) gates it ──
const action2Fresh = await autumnV2_3.check({
customer_id: customerId,
feature_id: TestFeature.Action2,
required_balance: 4,
});
expect(action2Fresh.allowed).toBe(true);
const action2FreshBeyond = await autumnV2_3.check({
customer_id: customerId,
feature_id: TestFeature.Action2,
required_balance: 5,
});
expect(action2FreshBeyond.allowed).toBe(false);
// ── action2: 2 of 4 used (1.2 credits) ──
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Action2,
value: 2,
});
// ── Exhaust action1 (track 5, clamps to 2 -> 5 total = 1 credit) ──
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Action1,
value: 5,
});
const action1Exhausted = await autumnV2_3.check({
customer_id: customerId,
feature_id: TestFeature.Action1,
required_balance: 1,
});
expect(action1Exhausted.allowed).toBe(false);
// action2 still open for its remaining 2 units.
const action2StillOpen = await autumnV2_3.check({
customer_id: customerId,
feature_id: TestFeature.Action2,
required_balance: 2,
});
expect(action2StillOpen.allowed).toBe(true);
// ── Exhaust action2 (track 5, clamps to 2 -> 4 total = 2.4 credits) ──
await autumnV2_3.track({
customer_id: customerId,
feature_id: TestFeature.Action2,
value: 5,
});
// ── Shared pool drained by both: 5*0.2 + 4*0.6 = 3.4 credits ──
await timeout(3000);
await expectCustomerBalance({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Credits,
granted: 100,
remaining: 96.6,
usage: 3.4,
});
// ── Each entry reports its own window usage ──
await expectCustomerUsageLimit({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Action1,
usage: 5,
limit: 5,
});
await expectCustomerUsageLimit({
autumn: autumnV2_3,
customerId,
featureId: TestFeature.Action2,
usage: 4,
limit: 4,
});
// ── Neither member cap gates a direct credits check ──
const creditsCheck = await autumnV2_3.check({
customer_id: customerId,
feature_id: TestFeature.Credits,
required_balance: 50,
});
expect(creditsCheck.allowed).toBe(true);
},
);