Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/atmn/src/compose/models/planModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ type UsageLimit = {
enabled?: boolean;
limit: number;
interval: "day" | "week" | "month" | "year";
anchor?: "billing_cycle" | "utc";
filter?: UsageLimitFilter;
};

type UsageLimitFilter = {
properties: Record<string, string>;
};

type UsageAlert = {
Expand All @@ -90,6 +96,8 @@ type UsageAlert = {
| "usage_percentage"
| "remaining"
| "remaining_percentage";
basis?: "balance" | "included" | "recurring" | "usage_limit";
filter?: UsageLimitFilter;
name?: string;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*/

import { afterAll, beforeAll, expect, test } from "bun:test";
import type { CustomerBillingControls } from "@autumn/shared";
import {
getPlayHistory,
getTestSvixAppId,
Expand All @@ -29,7 +30,6 @@ import {
waitForWebhook,
} from "@tests/integration/utils/svixWebhookTestUtils.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import type { CustomerBillingControls } from "@autumn/shared";
import { items } from "@tests/utils/fixtures/items.js";
import { products } from "@tests/utils/fixtures/products.js";
import { timeout } from "@tests/utils/genUtils.js";
Expand Down Expand Up @@ -88,6 +88,7 @@ const fortyPercentAlert = {
feature_id: TestFeature.Messages,
threshold: 40,
threshold_type: "remaining_percentage" as const,
basis: "balance" as const,
enabled: true,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ test(`${chalk.yellowBright("org-alert-env1: sandbox_usage_alerts triggers webhoo
feature_id: TestFeature.Messages,
threshold: 500,
threshold_type: "usage",
basis: "balance",
enabled: true,
name: "sandbox-only-alert",
},
Expand Down Expand Up @@ -157,6 +158,7 @@ test(`${chalk.yellowBright("org-alert-env2: usage_alerts (live field) is ignored
feature_id: TestFeature.Messages,
threshold: 400,
threshold_type: "usage",
basis: "balance",
enabled: true,
name: "live-only-alert-should-not-fire",
},
Expand Down Expand Up @@ -202,6 +204,7 @@ test(`${chalk.yellowBright("org-alert-env3: with both fields set in sandbox, onl
feature_id: TestFeature.Messages,
threshold: 300,
threshold_type: "usage",
basis: "balance",
enabled: true,
name: "sandbox-side",
},
Expand All @@ -211,6 +214,7 @@ test(`${chalk.yellowBright("org-alert-env3: with both fields set in sandbox, onl
feature_id: TestFeature.Messages,
threshold: 300,
threshold_type: "usage",
basis: "balance",
enabled: true,
name: "live-side",
},
Expand All @@ -229,14 +233,15 @@ test(`${chalk.yellowBright("org-alert-env3: with both fields set in sandbox, onl
value: 500,
});

const sandboxResult = await waitForWebhook<BalancesUsageAlertTriggeredPayload>({
token: playToken,
predicate: (payload) =>
payload.type === "balances.usage_alert_triggered" &&
payload.data?.customer_id === customerId &&
payload.data?.usage_alert?.name === "sandbox-side",
timeoutMs: 15000,
});
const sandboxResult =
await waitForWebhook<BalancesUsageAlertTriggeredPayload>({
token: playToken,
predicate: (payload) =>
payload.type === "balances.usage_alert_triggered" &&
payload.data?.customer_id === customerId &&
payload.data?.usage_alert?.name === "sandbox-side",
timeoutMs: 15000,
});
expect(sandboxResult).not.toBeNull();

const liveResult = await waitForWebhook<BalancesUsageAlertTriggeredPayload>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@

import { afterAll, afterEach, beforeAll, expect, test } from "bun:test";
import type { DbUsageAlert } from "@autumn/shared";
import { setCustomerOverageAllowed } from "@tests/integration/balances/utils/overage-allowed-utils/customerOverageAllowedUtils.js";
import {
getTestSvixAppId,
setupWebhookTest,
type WebhookTestSetup,
waitForWebhook,
} from "@tests/integration/utils/svixWebhookTestUtils.js";
import { setCustomerOverageAllowed } from "@tests/integration/balances/utils/overage-allowed-utils/customerOverageAllowedUtils.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { items } from "@tests/utils/fixtures/items.js";
import { products } from "@tests/utils/fixtures/products.js";
Expand Down Expand Up @@ -93,6 +93,7 @@ test(`${chalk.yellowBright("org-pct-200: usage_percentage threshold of 200% fire
feature_id: TestFeature.Messages,
threshold: 200,
threshold_type: "usage_percentage",
basis: "balance",
enabled: true,
name: "org-200pct",
},
Expand Down Expand Up @@ -148,6 +149,7 @@ test(`${chalk.yellowBright("org-pct-300: usage_percentage threshold of 300% fire
feature_id: TestFeature.Messages,
threshold: 300,
threshold_type: "usage_percentage",
basis: "balance",
enabled: true,
name: "org-300pct",
},
Expand Down Expand Up @@ -203,6 +205,7 @@ test(`${chalk.yellowBright("org-pct-200-not-yet: usage_percentage threshold of 2
feature_id: TestFeature.Messages,
threshold: 200,
threshold_type: "usage_percentage",
basis: "balance",
enabled: true,
name: "org-200pct-not-yet",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ test(`${chalk.yellowBright("org-alert1: org-level alert fires when customer cros
feature_id: TestFeature.Messages,
threshold: 750,
threshold_type: "usage",
basis: "balance",
enabled: true,
name: "org-threshold-750",
},
Expand Down Expand Up @@ -175,6 +176,7 @@ test(`${chalk.yellowBright("org-alert1b: org usage_percentage alert fires for en
feature_id: TestFeature.Messages,
threshold: 100,
threshold_type: "usage_percentage",
basis: "balance",
enabled: true,
name: "org-entity-100pct",
},
Expand Down Expand Up @@ -230,6 +232,7 @@ test(`${chalk.yellowBright("org-alert2: org-level alert fires for multiple custo
feature_id: TestFeature.Messages,
threshold: 600,
threshold_type: "usage",
basis: "balance",
enabled: true,
name: "org-multi-cust",
},
Expand Down Expand Up @@ -299,6 +302,7 @@ test(`${chalk.yellowBright("org-alert3: org and customer alerts at same threshol
feature_id: TestFeature.Messages,
threshold: 400,
threshold_type: "usage",
basis: "balance",
enabled: true,
name: "org-coexist",
},
Expand All @@ -318,6 +322,7 @@ test(`${chalk.yellowBright("org-alert3: org and customer alerts at same threshol
feature_id: TestFeature.Messages,
threshold: 400,
threshold_type: "usage",
basis: "balance",
enabled: true,
name: "customer-coexist",
},
Expand Down Expand Up @@ -370,6 +375,7 @@ test(`${chalk.yellowBright("org-alert4: org-level global alert (no feature_id) f
{
threshold: 80,
threshold_type: "usage_percentage",
basis: "balance",
enabled: true,
name: "org-global",
},
Expand Down Expand Up @@ -420,6 +426,7 @@ test(`${chalk.yellowBright("org-alert5: disabled org-level alert does not fire")
feature_id: TestFeature.Messages,
threshold: 300,
threshold_type: "usage",
basis: "balance",
enabled: false,
name: "org-disabled",
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { CustomerBillingControls, DbUsageAlert } from "@autumn/shared";
import type {
CustomerBillingControlsParams,
DbUsageAlertParams,
} from "@autumn/shared";
import type { AutumnV2_1Client } from "../spend-limit-utils/entitySpendLimitUtils.js";

export const setCustomerUsageAlerts = async ({
Expand All @@ -8,9 +11,9 @@ export const setCustomerUsageAlerts = async ({
}: {
autumn: AutumnV2_1Client;
customerId: string;
usageAlerts: DbUsageAlert[];
usageAlerts: DbUsageAlertParams[];
}) => {
const billingControls: CustomerBillingControls = {
const billingControls: CustomerBillingControlsParams = {
usage_alerts: usageAlerts,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ import {
import { expectVariantPlanCorrect } from "../utils/expectVariantPointer.js";
import { seedBaseWithVariant } from "../utils/seedVariantPlans.js";

const overageAllowed = [
{ feature_id: TestFeature.Messages, enabled: true },
];
const overageAllowed = [{ feature_id: TestFeature.Messages, enabled: true }];
const autoTopups = [
{
feature_id: TestFeature.Messages,
Expand All @@ -45,6 +43,7 @@ const usageAlerts = [
enabled: true,
threshold: 80,
threshold_type: "usage_percentage" as const,
basis: "balance" as const,
},
];

Expand Down Expand Up @@ -207,7 +206,8 @@ test.concurrent(
plans: [
{
plan_id: variantId,
versioning: "new_version", active: true,
versioning: "new_version",
active: true,
metadata: { stamp: "v2" },
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const usageAlertControls: CustomerBillingControls = {
feature_id: TestFeature.Messages,
threshold: 90,
threshold_type: "usage_percentage",
basis: "balance",
enabled: true,
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const usageAlertControls: EntityBillingControls = {
feature_id: TestFeature.Messages,
threshold: 80,
threshold_type: "usage_percentage",
basis: "balance",
enabled: true,
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ test(`${chalk.yellowBright("org config handler: array field (usage_alerts) merge
enabled: true,
threshold: 80,
threshold_type: "usage_percentage",
basis: "balance",
};

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const alert = ({
}: Pick<DbUsageAlert, "threshold" | "threshold_type">): DbUsageAlert => ({
threshold,
threshold_type,
basis: "balance",
enabled: true,
feature_id: "messages",
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, test } from "bun:test";
import {
filterUnresolvableUsageLimitAlerts,
usageAlertIdentity,
} from "@autumn/shared";

const alert = ({
basis,
filter,
threshold = 80,
}: {
basis?: "balance" | "included" | "recurring" | "usage_limit";
filter?: { properties: Record<string, string> };
threshold?: number;
}) => ({
feature_id: "messages",
enabled: true,
threshold,
threshold_type: "usage_percentage" as const,
...(basis && { basis }),
...(filter && { filter }),
});

describe("usageAlertIdentity", () => {
test("a missing basis reads as balance", () => {
expect(usageAlertIdentity(alert({}))).toBe(
usageAlertIdentity(alert({ basis: "balance" })),
);
});

test("basis is part of the identity", () => {
expect(usageAlertIdentity(alert({ basis: "included" }))).not.toBe(
usageAlertIdentity(alert({ basis: "balance" })),
);
});

test("filters canonicalise by sorted key=value pairs", () => {
const left = usageAlertIdentity(
alert({
basis: "usage_limit",
filter: { properties: { b: "2", a: "1" } },
}),
);
const right = usageAlertIdentity(
alert({
basis: "usage_limit",
filter: { properties: { a: "1", b: "2" } },
}),
);
expect(left).toBe(right);
});
});

describe("filterUnresolvableUsageLimitAlerts", () => {
const dailyLimit = (filter?: { properties: Record<string, string> }) => ({
feature_id: "messages",
...(filter && { filter }),
});

test("alerts on balance-backed bases never need a limit", () => {
expect(
filterUnresolvableUsageLimitAlerts({
usageAlerts: [
alert({ basis: "balance" }),
alert({ basis: "included" }),
alert({ basis: "recurring" }),
],
usageLimitLists: [[]],
}),
).toEqual([]);
});

test("a usage_limit alert resolves against any list, matching filter identity", () => {
const usageAlerts = [
alert({ basis: "usage_limit" }),
alert({ basis: "usage_limit", filter: { properties: { key: "a" } } }),
alert({ basis: "usage_limit", filter: { properties: { key: "b" } } }),
];
const unresolvable = filterUnresolvableUsageLimitAlerts({
usageAlerts,
usageLimitLists: [
[dailyLimit()],
null,
[dailyLimit({ properties: { key: "a" } })],
],
});
expect(unresolvable.map(({ index }) => index)).toEqual([2]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const ALLOWED_DEFAULTS: Record<string, string> = {
"plans.billing_controls.spend_limits.enabled": "gated by `billing_controls`",
"plans.billing_controls.usage_limits.enabled": "gated by `billing_controls`",
"plans.billing_controls.usage_alerts.enabled": "gated by `billing_controls`",
"plans.billing_controls.usage_alerts.basis": "gated by `billing_controls`",
"plans.billing_controls.overage_allowed.enabled":
"gated by `billing_controls`",
};
Expand Down
Loading
Loading