From 175cfb006d9be84ce2c99350baaa5118c4b8001c Mon Sep 17 00:00:00 2001 From: Owen Greenhalgh Date: Thu, 19 Mar 2026 10:44:27 +0000 Subject: [PATCH] feat: add usage alerts schema and database support --- .../apiCusUtils/getApiCustomerBase.ts | 1 + .../billingControls/entityBillingControls.ts | 4 +++ shared/api/billingControls/index.ts | 1 + shared/api/billingControls/usageAlert.ts | 6 +++++ .../customerBillingControls.ts | 7 +++++- .../billingControls/entityBillingControls.ts | 4 +++ .../cusModels/billingControls/usageAlert.ts | 25 +++++++++++++++++++ shared/models/cusModels/cusModels.ts | 2 ++ shared/models/cusModels/cusTable.ts | 2 ++ .../cusModels/entityModels/entityModels.ts | 6 ++++- .../cusModels/entityModels/entityTable.ts | 6 ++++- 11 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 shared/api/billingControls/usageAlert.ts create mode 100644 shared/models/cusModels/billingControls/usageAlert.ts diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase.ts index 30c5e8974..fbb187d48 100644 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase.ts +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase.ts @@ -68,6 +68,7 @@ export const getApiCustomerBase = async ({ billing_controls: { auto_topups: fullCus.auto_topups ?? undefined, spend_limits: fullCus.spend_limits ?? undefined, + usage_alerts: fullCus.usage_alerts ?? undefined, }, invoices: diff --git a/shared/api/billingControls/entityBillingControls.ts b/shared/api/billingControls/entityBillingControls.ts index 693f5b15a..179d63638 100644 --- a/shared/api/billingControls/entityBillingControls.ts +++ b/shared/api/billingControls/entityBillingControls.ts @@ -1,10 +1,14 @@ import { z } from "zod/v4"; import { ApiSpendLimitSchema } from "./spendLimit.js"; +import { ApiUsageAlertSchema } from "./usageAlert.js"; export const ApiEntityBillingControlsSchema = z.object({ spend_limits: z.array(ApiSpendLimitSchema).optional().meta({ description: "List of overage spend limits per feature.", }), + usage_alerts: z.array(ApiUsageAlertSchema).optional().meta({ + description: "List of usage alert configurations per feature.", + }), }); export const ApiEntityBillingControlsParamsSchema = diff --git a/shared/api/billingControls/index.ts b/shared/api/billingControls/index.ts index 3e1cd4fa2..9beba0431 100644 --- a/shared/api/billingControls/index.ts +++ b/shared/api/billingControls/index.ts @@ -1,2 +1,3 @@ export * from "./entityBillingControls.js"; export * from "./spendLimit.js"; +export * from "./usageAlert.js"; diff --git a/shared/api/billingControls/usageAlert.ts b/shared/api/billingControls/usageAlert.ts new file mode 100644 index 000000000..3f49d069b --- /dev/null +++ b/shared/api/billingControls/usageAlert.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; +import { DbUsageAlertSchema } from "../../models/cusModels/billingControls/usageAlert.js"; + +export const ApiUsageAlertSchema = DbUsageAlertSchema; + +export type ApiUsageAlert = z.infer; diff --git a/shared/models/cusModels/billingControls/customerBillingControls.ts b/shared/models/cusModels/billingControls/customerBillingControls.ts index ce52b2214..6ef0eb105 100644 --- a/shared/models/cusModels/billingControls/customerBillingControls.ts +++ b/shared/models/cusModels/billingControls/customerBillingControls.ts @@ -6,6 +6,7 @@ import { } from "./entityBillingControls.js"; import { PurchaseLimitIntervalEnum } from "./purchaseLimitInterval.js"; import { type DbSpendLimit, DbSpendLimitSchema } from "./spendLimit.js"; +import { type DbUsageAlert, DbUsageAlertSchema } from "./usageAlert.js"; export const AutoTopupPurchaseLimitSchema = z.object({ interval: PurchaseLimitIntervalEnum.meta({ @@ -45,6 +46,9 @@ export const CustomerBillingControlsSchema = z.object({ spend_limits: z.array(DbSpendLimitSchema).optional().meta({ description: "List of overage spend limits per feature.", }), + usage_alerts: z.array(DbUsageAlertSchema).optional().meta({ + description: "List of usage alert configurations per feature.", + }), }); export const CustomerBillingControlsParamsSchema = @@ -85,9 +89,10 @@ export type CustomerBillingControlsParams = z.input< typeof CustomerBillingControlsParamsSchema >; -export { EntityBillingControlsSchema, DbSpendLimitSchema }; +export { EntityBillingControlsSchema, DbSpendLimitSchema, DbUsageAlertSchema }; export type { EntityBillingControls, EntityBillingControlsParams, DbSpendLimit, + DbUsageAlert, }; diff --git a/shared/models/cusModels/billingControls/entityBillingControls.ts b/shared/models/cusModels/billingControls/entityBillingControls.ts index afde88943..d815fcd2a 100644 --- a/shared/models/cusModels/billingControls/entityBillingControls.ts +++ b/shared/models/cusModels/billingControls/entityBillingControls.ts @@ -1,10 +1,14 @@ import { z } from "zod/v4"; import { DbSpendLimitSchema } from "./spendLimit.js"; +import { DbUsageAlertSchema } from "./usageAlert.js"; export const EntityBillingControlsSchema = z.object({ spend_limits: z.array(DbSpendLimitSchema).optional().meta({ description: "List of overage spend limits per feature.", }), + usage_alerts: z.array(DbUsageAlertSchema).optional().meta({ + description: "List of usage alert configurations per feature.", + }), }); export type EntityBillingControls = z.infer; diff --git a/shared/models/cusModels/billingControls/usageAlert.ts b/shared/models/cusModels/billingControls/usageAlert.ts new file mode 100644 index 000000000..630041adc --- /dev/null +++ b/shared/models/cusModels/billingControls/usageAlert.ts @@ -0,0 +1,25 @@ +import { z } from "zod/v4"; + +export const UsageAlertThresholdType = z.enum(["usage", "balance"]); + +export const DbUsageAlertSchema = z.object({ + feature_id: z.string().optional().meta({ + description: "The feature id this alert applies to. If not included, the alert applies globally.", + }), + enabled: z.boolean().default(true).meta({ + description: "Whether this usage alert is enabled.", + }), + threshold: z.number().min(0).meta({ + description: "The threshold value that triggers the alert.", + }), + threshold_type: UsageAlertThresholdType.meta({ + description: + 'Whether the threshold is based on, can be usage or balance.', + }), + name: z.string().optional().meta({ + description: + "Optional user-defined label to name an alerts.", + }), +}); + +export type DbUsageAlert = z.infer; diff --git a/shared/models/cusModels/cusModels.ts b/shared/models/cusModels/cusModels.ts index f51c0b704..e96d2d356 100644 --- a/shared/models/cusModels/cusModels.ts +++ b/shared/models/cusModels/cusModels.ts @@ -4,6 +4,7 @@ import { ExternalProcessorsSchema } from "../genModels/processorSchemas.js"; import { AutoTopupSchema, DbSpendLimitSchema, + DbUsageAlertSchema, } from "./billingControls/customerBillingControls.js"; export const CustomerSchema = z.object({ @@ -23,6 +24,7 @@ export const CustomerSchema = z.object({ send_email_receipts: z.boolean().default(false), auto_topups: z.array(AutoTopupSchema).nullish(), spend_limits: z.array(DbSpendLimitSchema).nullish(), + usage_alerts: z.array(DbUsageAlertSchema).nullish(), }); export type Customer = z.infer; diff --git a/shared/models/cusModels/cusTable.ts b/shared/models/cusModels/cusTable.ts index da280c3b4..f73d5544c 100644 --- a/shared/models/cusModels/cusTable.ts +++ b/shared/models/cusModels/cusTable.ts @@ -16,6 +16,7 @@ import { organizations } from "../orgModels/orgTable.js"; import type { AutoTopup, DbSpendLimit, + DbUsageAlert, } from "./billingControls/customerBillingControls.js"; export type CustomerProcessor = { @@ -42,6 +43,7 @@ export const customers = pgTable( send_email_receipts: boolean("send_email_receipts").default(false), auto_topups: jsonb().$type(), spend_limits: jsonb().$type(), + usage_alerts: jsonb().$type(), }, (table) => [ unique("cus_id_constraint").on(table.org_id, table.id, table.env), diff --git a/shared/models/cusModels/entityModels/entityModels.ts b/shared/models/cusModels/entityModels/entityModels.ts index f63ffbace..25c8413f0 100644 --- a/shared/models/cusModels/entityModels/entityModels.ts +++ b/shared/models/cusModels/entityModels/entityModels.ts @@ -1,6 +1,9 @@ import { z } from "zod/v4"; import type { Feature } from "../../featureModels/featureModels.js"; -import { DbSpendLimitSchema } from "../billingControls/customerBillingControls.js"; +import { + DbSpendLimitSchema, + DbUsageAlertSchema, +} from "../billingControls/customerBillingControls.js"; export const EntitySchema = z.object({ id: z.string().nullable(), @@ -14,6 +17,7 @@ export const EntitySchema = z.object({ feature_id: z.string(), internal_feature_id: z.string(), spend_limits: z.array(DbSpendLimitSchema).nullish(), + usage_alerts: z.array(DbUsageAlertSchema).nullish(), }); // export const CreateEntitySchema = z.object({ diff --git a/shared/models/cusModels/entityModels/entityTable.ts b/shared/models/cusModels/entityModels/entityTable.ts index 9715bef54..5cfe847b6 100644 --- a/shared/models/cusModels/entityModels/entityTable.ts +++ b/shared/models/cusModels/entityModels/entityTable.ts @@ -11,7 +11,10 @@ import { } from "drizzle-orm/pg-core"; import { features } from "../../featureModels/featureTable.js"; import { organizations } from "../../orgModels/orgTable.js"; -import type { DbSpendLimit } from "../billingControls/customerBillingControls.js"; +import type { + DbSpendLimit, + DbUsageAlert, +} from "../billingControls/customerBillingControls.js"; import { customers } from "../cusTable.js"; export const entities = pgTable( @@ -27,6 +30,7 @@ export const entities = pgTable( deleted: boolean().default(false).notNull(), internal_feature_id: text("internal_feature_id"), spend_limits: jsonb().$type(), + usage_alerts: jsonb().$type(), // Optional... feature_id: text("feature_id"),