chore: cleaned up openapi for products endpoint group

This commit is contained in:
John Yeo
2025-10-24 16:16:09 +01:00
parent 55ac51197a
commit 8183ad1b5f
8 changed files with 421 additions and 354 deletions

View File

@@ -90,7 +90,7 @@ export const constructProduct = ({
is_add_on: productData.is_add_on, is_add_on: productData.is_add_on,
is_default: productData.is_default, is_default: productData.is_default,
version: productData.version || 1, version: productData.version || 1,
group: productData.group, group: productData.group || "",
env, env,
internal_id: generateId("prod"), internal_id: generateId("prod"),

View File

@@ -1,99 +1,103 @@
import "dotenv/config"; import "dotenv/config";
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { existsSync, writeFileSync } from "node:fs"; import { existsSync, writeFileSync } from "node:fs";
import { AppEnv } from "@models/genModels/genEnums.js";
import yaml from "yaml"; import yaml from "yaml";
import { z } from "zod/v4";
import { createDocument } from "zod-openapi"; import { createDocument } from "zod-openapi";
import { CustomerDataSchema } from "./common/customerData.js"; import { CustomerDataSchema } from "./common/customerData.js";
import { EntityDataSchema } from "./common/entityData.js"; import { EntityDataSchema } from "./common/entityData.js";
import { coreOps } from "./core/coreOpenApi.js";
import { ApiCusFeatureSchema } from "./customers/cusFeatures/apiCusFeature.js"; import { ApiCusFeatureSchema } from "./customers/cusFeatures/apiCusFeature.js";
import { ApiCusProductSchema } from "./customers/cusProducts/apiCusProduct.js"; import { ApiCusProductSchema } from "./customers/cusProducts/apiCusProduct.js";
import { import { ApiCustomerWithMeta } from "./customers/customersOpenApi.js";
ApiCustomerWithMeta, import { ApiEntityWithMeta } from "./entities/entitiesOpenApi.js";
customerOps, import { ApiFeatureWithMeta } from "./features/featuresOpenApi.js";
} from "./customers/customersOpenApi.js";
import { ApiEntityWithMeta, entityOps } from "./entities/entitiesOpenApi.js";
import { ApiFeatureWithMeta, featureOps } from "./features/featuresOpenApi.js";
import { ApiProductItemSchema } from "./products/apiProductItem.js"; import { ApiProductItemSchema } from "./products/apiProductItem.js";
import { ApiProductWithMeta, productOps } from "./products/productsOpenApi.js"; import { ApiProductWithMeta, productOps } from "./products/productsOpenApi.js";
import { referralOps } from "./referrals/referralsOpenApi.js";
const API_VERSION = "1.2.0"; const API_VERSION = "1.2.0";
const document = createDocument({ const document = createDocument(
openapi: "3.1.0", {
info: { openapi: "3.1.0",
title: "Autumn API", info: {
version: API_VERSION, title: "Autumn API",
}, version: API_VERSION,
servers: [
{
url: "https://api.useautumn.com",
description: "Production server",
}, },
],
security: [ servers: [
{ {
secretKey: [], url: "https://api.useautumn.com",
}, description: "Production server",
], },
components: { ],
schemas: {
AutumnError: z security: [
.object({ {
message: z.string(), secretKey: [],
code: z.string(), },
env: z.enum(AppEnv), ],
}) components: {
.meta({ schemas: {
id: "AutumnError", CustomerData: CustomerDataSchema.meta({
description: "An error that occurred in the API", id: "CustomerData",
description: "Customer data for creating or updating a customer",
}), }),
CustomerData: CustomerDataSchema.meta({ EntityData: EntityDataSchema.meta({
id: "CustomerData", id: "EntityData",
description: "Customer data for creating or updating a customer", description: "Entity data for creating an entity",
}), }),
EntityData: EntityDataSchema.meta({ Customer: ApiCustomerWithMeta,
id: "EntityData", CustomerProduct: ApiCusProductSchema,
description: "Entity data for creating an entity", CustomerFeature: ApiCusFeatureSchema.meta({
}), id: "CustomerFeature",
Customer: ApiCustomerWithMeta, description: "Customer feature object returned by the API",
CustomerProduct: ApiCusProductSchema, }),
CustomerFeature: ApiCusFeatureSchema.meta({ Product: ApiProductWithMeta,
id: "CustomerFeature", ProductItem: ApiProductItemSchema,
description: "Customer feature object returned by the API", Feature: ApiFeatureWithMeta,
}), Entity: ApiEntityWithMeta,
Product: ApiProductWithMeta, },
ProductItem: ApiProductItemSchema, securitySchemes: {
Feature: ApiFeatureWithMeta, secretKey: {
Entity: ApiEntityWithMeta, type: "http",
}, scheme: "bearer",
securitySchemes: { bearerFormat: "JWT",
secretKey: { },
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
}, },
}, },
},
paths: { paths: {
...productOps, ...productOps,
...coreOps, // ...coreOps,
...featureOps, // ...featureOps,
...customerOps, // ...customerOps,
...entityOps, // ...entityOps,
...referralOps, // ...referralOps,
},
}, },
}); {
// Disable the "Output" suffix that zod-openapi adds to response schemas
outputIdSuffix: "",
},
);
// Export to YAML file during build // Export to YAML file during build
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
try { try {
// If --no-build flag is present, return after writing openapi.yml
if (process.argv.includes("--no-build")) {
const yamlContent = yaml.stringify(
JSON.parse(JSON.stringify(document, null, 2)),
);
writeFileSync(
`${process.env.STAINLESS_PATH?.replace("\\ ", " ")}/openapi.yml`,
yamlContent,
"utf8",
);
console.log(
`OpenAPI document exported to ${process.env.STAINLESS_PATH}/openapi.yml`,
);
process.exit(0);
}
// Convert to JSON first to strip out Zod schemas and function references // Convert to JSON first to strip out Zod schemas and function references
const jsonStr = JSON.stringify(document, null, 2); const jsonStr = JSON.stringify(document, null, 2);

View File

@@ -4,26 +4,24 @@ import { z } from "zod/v4";
export const APIFreeTrialSchema = z.object({ export const APIFreeTrialSchema = z.object({
duration: z.enum(FreeTrialDuration).meta({ duration: z.enum(FreeTrialDuration).meta({
description: "The duration type of the free trial", description: "The duration type of the free trial",
example: "<string>",
}), }),
length: z.number().meta({ length: z.number().meta({
description: "The length of the free trial", description: "The length of the duration type specified",
example: 123,
}), }),
unique_fingerprint: z.boolean().meta({ unique_fingerprint: z.boolean().meta({
description: description:
"Whether the free trial is limited to one per customer fingerprint", "Whether the free trial is limited to one per customer fingerprint",
example: true,
}), }),
card_required: z.boolean().meta({ card_required: z.boolean().meta({
description: "Whether the free trial requires a card", description:
example: true, "Whether the free trial requires a card. If false, the customer can attach the product without going through a checkout flow or having a card on file.",
}), }),
// For Cus Product // For Cus Product
trial_available: z.boolean().nullish().default(true).meta({ trial_available: z.boolean().nullish().default(true).meta({
description: "Whether the free trial is available", description:
example: true, "Used in customer context. Whether the free trial is available for the customer if they were to attach the product.",
}), }),
}); });

View File

@@ -4,6 +4,105 @@ import { z } from "zod/v4";
import { APIFreeTrialSchema } from "./apiFreeTrial.js"; import { APIFreeTrialSchema } from "./apiFreeTrial.js";
import { ApiProductItemSchema } from "./apiProductItem.js"; import { ApiProductItemSchema } from "./apiProductItem.js";
export const PRODUCT_EXAMPLE = {
id: "Pro Product",
name: "Pro Plan",
group: null,
env: "sandbox",
is_add_on: false,
is_default: false,
archived: false,
version: 1,
created_at: 1761296829908,
items: [
{
type: "price",
feature_id: null,
interval: "month",
interval_count: 1,
price: 20,
display: {
primary_text: "$20",
secondary_text: "per month",
},
},
{
type: "priced_feature",
feature_id: "words",
included_usage: 1000,
interval: "month",
interval_count: 1,
price: 0.5,
usage_model: "pay_per_use",
billing_units: 1000,
reset_usage_when_enabled: true,
entity_feature_id: null,
display: {
primary_text: "1,000 Words",
secondary_text: "then $0.5 per 1,000 Words",
},
},
{
type: "feature",
feature_id: "dashboard",
// feature_type: "static",
// feature: {
// id: "dashboard",
// name: "Dashboard",
// type: "boolean",
// display: {
// singular: "Dashboard",
// plural: "Dashboard",
// },
// archived: false,
// },
entity_feature_id: null,
display: {
primary_text: "Dashboard",
},
},
{
type: "feature",
feature_id: "messages",
// feature_type: "single_use",
// feature: {
// id: "messages",
// name: "Messages",
// type: "single_use",
// display: {
// singular: "Messages",
// plural: "Messages",
// },
// archived: false,
// },
included_usage: 10,
interval: "month",
interval_count: 1,
reset_usage_when_enabled: true,
entity_feature_id: null,
display: {
primary_text: "10 Messages",
},
},
],
free_trial: {
duration: "day",
length: 7,
unique_fingerprint: false,
card_required: true,
},
base_variant_id: null,
scenario: "new",
// properties: {
// is_free: false,
// is_one_off: false,
// interval_group: "month",
// has_trial: false,
// updateable: false,
// },
};
export const ApiProductPropertiesSchema = z.object({ export const ApiProductPropertiesSchema = z.object({
is_free: z.boolean().meta({ is_free: z.boolean().meta({
description: "True if the product has no base price or usage prices", description: "True if the product has no base price or usage prices",
@@ -30,107 +129,64 @@ export const ApiProductPropertiesSchema = z.object({
}); });
export const ApiProductSchema = z.object({ export const ApiProductSchema = z.object({
id: z.string().meta({ id: z
description: "The ID of the product you set when creating the product", .string()
example: "pro_plan", .describe("The ID of the product you set when creating the product"),
}),
name: z.string().meta({ name: z.string().describe("The name of the product"),
description: "The name of the product",
example: "Pro Plan",
}),
group: z.string().nullable().meta({ group: z
description: "The group of the product", .string()
example: "product_set_1", .nullable()
}), .describe("Product group which this product belongs to"),
env: z.enum(AppEnv).meta({ env: z.enum(AppEnv).describe("The environment of the product"),
description: "The environment of the product",
example: "production",
}),
is_add_on: z.boolean().meta({ is_add_on: z
description: .boolean()
.describe(
"Whether the product is an add-on and can be purchased alongside other products", "Whether the product is an add-on and can be purchased alongside other products",
example: true, ),
}),
is_default: z.boolean().meta({ is_default: z
description: "Whether the product is the default product", .boolean()
example: true, .describe("Whether the product is the default product"),
}),
archived: z.boolean({ message: "archived should be a boolean" }).meta({ archived: z
description: .boolean({ message: "archived should be a boolean" })
.describe(
"Whether this product has been archived and is no longer available", "Whether this product has been archived and is no longer available",
example: false, ),
}),
version: z.number().meta({ version: z.number().describe("The current version of the product"),
description: "The version of the product",
example: 1,
}),
created_at: z.number().meta({ created_at: z
description: .number()
.describe(
"The timestamp of when the product was created in milliseconds since epoch", "The timestamp of when the product was created in milliseconds since epoch",
example: 1759247877000, ),
}),
items: z.array(ApiProductItemSchema).meta({ items: z
description: "Array of product items that define the features and pricing", .array(ApiProductItemSchema)
example: [ .describe(
{ "Array of product items that define the product's features and pricing",
feature_id: "<string>", ),
feature_type: "single_use",
included_usage: 123,
interval: "<string>",
usage_model: "prepaid",
price: 123,
billing_units: 123,
entity_feature_id: "<string>",
reset_usage_when_enabled: true,
tiers: [
{
to: 123,
amount: 123,
},
],
},
],
}),
free_trial: APIFreeTrialSchema.nullable().meta({ free_trial: APIFreeTrialSchema.nullable().describe(
description: "Free trial configuration for this product, if available", "Free trial configuration for this product, if available",
example: { ),
duration: "<string>",
length: 123,
unique_fingerprint: true,
},
}),
base_variant_id: z.string().nullable().meta({ base_variant_id: z
description: "ID of the base variant this product is derived from", .string()
example: "pro_monthly", .nullable()
}), .describe("ID of the base variant this product is derived from"),
scenario: z.enum(AttachScenario).optional().meta({ scenario: z
description: .enum(AttachScenario)
"Scenario context for when this product is used in attach flows", .optional()
example: "upgrade", .describe("Scenario for when this product is used in attach flows"),
}),
properties: ApiProductPropertiesSchema.optional().meta({ properties: ApiProductPropertiesSchema.optional(),
description: "Additional properties and metadata for the product",
example: {
is_free: false,
is_one_off: false,
interval_group: "monthly",
has_trial: true,
updateable: true,
},
}),
}); });
export type ApiProduct = z.infer<typeof ApiProductSchema>; export type ApiProduct = z.infer<typeof ApiProductSchema>;

View File

@@ -12,64 +12,61 @@ import { z } from "zod/v4";
export const ApiProductItemSchema = z export const ApiProductItemSchema = z
.object({ .object({
// Feature stuff // Feature stuff
type: z.enum(ProductItemType).nullish().meta({ type: z
description: "The type of the product item", .enum(ProductItemType)
example: "<string>", .nullish()
}), .describe("The type of the product item"),
feature_id: z.string().nullish().meta({
description: feature_id: z
"The feature ID of the product item. Should be `null` for prices.", .string()
example: "<string>", .nullish()
}), .describe(
"The feature ID of the product item. If the item is a fixed price, should be `null`",
),
feature_type: z.enum(ProductItemFeatureType).nullish().meta({ feature_type: z.enum(ProductItemFeatureType).nullish().meta({
description: description:
"Single use features are used once and then depleted, like API calls or credits. Continuous use features are those being used on an ongoing-basis, like storage or seats.", "Single use features are used once and then depleted, like API calls or credits. Continuous use features are those being used on an ongoing-basis, like storage or seats.",
example: "<string>",
}), }),
// Feature response // Feature response
feature: ApiFeatureSchema.nullish().meta({ feature: ApiFeatureSchema.nullish().meta({
description: "The feature itself", internal: true,
}), }),
included_usage: z.number().or(z.literal(Infinite)).nullish().meta({ included_usage: z.number().or(z.literal(Infinite)).nullish().meta({
description: "The amount of usage included for this feature.", description: "The amount of usage included for this feature.",
example: 123,
}), }),
interval: z.enum(ProductItemInterval).nullish().meta({ interval: z.enum(ProductItemInterval).nullish().meta({
description: description:
"The reset or billing interval of the product item. If null, feature will have no reset date, and if there's a price, it will be billed one-off.", "The reset or billing interval of the product item. If null, feature will have no reset date, and if there's a price, it will be billed one-off.",
}), }),
interval_count: z.number().nullish().meta({ interval_count: z.number().nullish().meta({
description: "The number of intervals between resets", description: "The interval count of the product item.",
}), }),
// Price config // Price config
price: z.number().nullish().meta({ price: z.number().nullish().meta({
description: description:
"The price of the product item. Should be `null` if tiered pricing is set.", "The price of the product item. Should be `null` if tiered pricing is set.",
example: 123,
}), }),
tiers: z
.array(PriceTierSchema) tiers: z.array(PriceTierSchema).nullish().meta({
.nullish() description:
.meta({ "Tiered pricing for the product item. Not applicable for fixed price items.",
description: "Tiered pricing for the product item.", }),
example: [
{ to: 100, amount: 10 },
{ to: 200, amount: 20 },
],
}),
usage_model: z.enum(UsageModel).nullish().meta({ usage_model: z.enum(UsageModel).nullish().meta({
description: description:
"Whether the feature should be prepaid upfront or billed for how much they use end of billing period.", "Whether the feature should be prepaid upfront or billed for how much they use end of billing period.",
example: "<string>",
}), }),
billing_units: z.number().nullish().meta({ billing_units: z.number().nullish().meta({
description: "The amount per billing unit (eg. $9 / 250 units)", description: "The amount per billing unit (eg. $9 / 250 units)",
example: 250,
}), }),
reset_usage_when_enabled: z.boolean().nullish().meta({ reset_usage_when_enabled: z.boolean().nullish().meta({
description: description:
"Whether the usage should be reset when the product is enabled.", "Whether the usage should be reset when the product is enabled.",
@@ -77,7 +74,6 @@ export const ApiProductItemSchema = z
entity_feature_id: z.string().nullish().meta({ entity_feature_id: z.string().nullish().meta({
description: "The entity feature ID of the product item if applicable.", description: "The entity feature ID of the product item if applicable.",
example: "<string>",
}), }),
display: z display: z
@@ -88,16 +84,16 @@ export const ApiProductItemSchema = z
.nullish() .nullish()
.meta({ .meta({
description: "The display of the product item.", description: "The display of the product item.",
example: { primary_text: "<string>", secondary_text: "<string>" },
}), }),
quantity: z.number().nullish().meta({ quantity: z.number().nullish().meta({
description: "The quantity of the product item.", description:
example: 123, "Used in customer context. Quantity of the feature the customer has prepaid for.",
}), }),
next_cycle_quantity: z.number().nullish().meta({ next_cycle_quantity: z.number().nullish().meta({
description: "The quantity of the product item in the next cycle.", description:
example: 123, "Used in customer context. Quantity of the feature the customer will prepay for in the next cycle.",
}), }),
}) })
.meta({ .meta({

View File

@@ -3,7 +3,11 @@ import { ProductItemSchema } from "@models/productV2Models/productItemModels/pro
import { idRegex } from "@utils/utils.js"; import { idRegex } from "@utils/utils.js";
import { z } from "zod/v4"; import { z } from "zod/v4";
export const CreateProductItemParamsSchema = ProductItemSchema; // Use the full ProductItemSchema but mark backend fields as internal
export const CreateProductItemParamsSchema = ProductItemSchema.meta({
id: "CreateProductItemParams",
description: "Product item defining features and pricing within a product",
});
// Base product params // Base product params
@@ -35,43 +39,95 @@ const CREATE_PRODUCT_EXAMPLE = {
}, },
}; };
const descriptions = {
id: "The ID of the product. Used to identify the product in other API calls like checkout or update product.",
name: "The name of the product",
is_add_on:
"Whether the product is an add-on. Add-on products can be attached multiple times and don't to through upgrade / downgrade flows.",
is_default:
"Whether the product is the default product. Default products are enabled by default for new customers.",
group:
"Product group which this product belongs to. Products within a group have upgrade / downgrade logic when the customer moves between them.",
items:
"Array of product items that define the product's features and pricing",
free_trial: "Free trial configuration for this product, if available",
// Update only
archived:
"Archive this product using this flag. Archived products are hidden on the dashboard.",
};
export const CreateProductV2ParamsSchema = z export const CreateProductV2ParamsSchema = z
.object({ .object({
id: z.string().nonempty().regex(idRegex), id: z.string().nonempty().regex(idRegex).meta({
description: descriptions.id,
name: z.string().refine((val) => val.length > 0, {
message: "name must be a non-empty string",
}), }),
is_add_on: z.boolean().default(false), name: z
is_default: z.boolean().default(false), .string()
version: z.number().optional(), .refine((val) => val.length > 0, {
group: z.string().nullable().default(""), message: "name must be a non-empty string",
})
.meta({
description: descriptions.name,
}),
items: z.array(CreateProductItemParamsSchema).optional(), is_add_on: z.boolean().default(false).meta({
free_trial: CreateFreeTrialSchema.nullish().default(null), description: descriptions.is_add_on,
}),
is_default: z.boolean().default(false).meta({
description: descriptions.is_default,
}),
group: z.string().nullable().default("").meta({
description: descriptions.group,
}),
items: z.array(CreateProductItemParamsSchema).optional().meta({
description: descriptions.items,
}),
free_trial: CreateFreeTrialSchema.nullish().default(null).meta({
description: descriptions.free_trial,
}),
}) })
.meta({ .meta({
examples: [CREATE_PRODUCT_EXAMPLE], examples: [CREATE_PRODUCT_EXAMPLE],
}); });
export const UpdateProductV2ParamsSchema = z.object({ export const UpdateProductV2ParamsSchema = z.object({
id: z.string().nonempty().regex(idRegex).optional(), id: z.string().nonempty().regex(idRegex).optional().meta({
description: descriptions.id,
}),
name: z name: z
.string() .string()
.refine((val) => val.length > 0, { .refine((val) => val.length > 0, {
message: "name must be a non-empty string", message: "name must be a non-empty string",
}) })
.optional(), .optional()
.meta({
description: descriptions.name,
}),
is_add_on: z.boolean().optional(), is_add_on: z.boolean().optional().meta({
is_default: z.boolean().optional(), description: descriptions.is_add_on,
}),
is_default: z.boolean().optional().meta({
description: descriptions.is_default,
}),
// version: z.number().optional(), // version: z.number().optional(),
group: z.string().nonempty().nullable().optional(), group: z.string().nonempty().nullable().optional().meta({
archived: z.boolean().optional(), description: descriptions.group,
}),
archived: z.boolean().optional().meta({
description: descriptions.archived,
}),
items: z.array(CreateProductItemParamsSchema).optional(), items: z.array(CreateProductItemParamsSchema).optional(),
free_trial: CreateFreeTrialSchema.nullish(), free_trial: CreateFreeTrialSchema.nullish().meta({
description: descriptions.free_trial,
}),
}); });
export const UpdateProductQuerySchema = z.object({ export const UpdateProductQuerySchema = z.object({

View File

@@ -1,120 +1,14 @@
import { SuccessResponseSchema } from "@api/common/commonResponses.js"; import { SuccessResponseSchema } from "@api/common/commonResponses.js";
import { z } from "zod/v4";
import { ApiProductSchema, PRODUCT_EXAMPLE } from "./apiProduct.js";
import { import {
CreateProductV2ParamsSchema, CreateProductV2ParamsSchema,
UpdateProductV2ParamsSchema, UpdateProductV2ParamsSchema,
} from "@api/models.js"; } from "./productOpModels.js";
import { z } from "zod/v4";
import { ApiProductSchema } from "./apiProduct.js";
export const PRODUCT_EXAMPLE = {
id: "Pro Product",
name: "Pro Plan",
group: null,
env: "sandbox",
is_add_on: false,
is_default: false,
archived: false,
version: 1,
created_at: 1761296829908,
items: [
{
type: "price",
feature_id: null,
feature: null,
interval: "month",
interval_count: 1,
price: 20,
display: {
primary_text: "$20",
secondary_text: "per month",
},
},
{
type: "priced_feature",
feature_id: "words",
feature_type: "single_use",
// feature: {
// id: "words",
// name: "Words",
// type: "single_use",
// display: {
// singular: "Words",
// plural: "Words",
// },
// archived: false,
// },
included_usage: 1000,
interval: "month",
interval_count: 1,
price: 0.5,
usage_model: "pay_per_use",
billing_units: 1000,
reset_usage_when_enabled: true,
entity_feature_id: null,
display: {
primary_text: "1,000 Words",
secondary_text: "then $0.5 per 1,000 Words",
},
},
{
type: "feature",
feature_id: "dashboard",
feature_type: "static",
// feature: {
// id: "dashboard",
// name: "Dashboard",
// type: "boolean",
// display: {
// singular: "Dashboard",
// plural: "Dashboard",
// },
// archived: false,
// },
entity_feature_id: null,
display: {
primary_text: "Dashboard",
},
},
{
type: "feature",
feature_id: "messages",
feature_type: "single_use",
// feature: {
// id: "messages",
// name: "Messages",
// type: "single_use",
// display: {
// singular: "Messages",
// plural: "Messages",
// },
// archived: false,
// },
included_usage: 10,
interval: "month",
interval_count: 1,
reset_usage_when_enabled: true,
entity_feature_id: null,
display: {
primary_text: "10 Messages",
},
},
],
free_trial: null,
base_variant_id: null,
scenario: "new",
// properties: {
// is_free: false,
// is_one_off: false,
// interval_group: "month",
// has_trial: false,
// updateable: false,
// },
};
// Register schema with .meta() for OpenAPI spec generation // Register schema with .meta() for OpenAPI spec generation
export const ApiProductWithMeta = ApiProductSchema.meta({ export const ApiProductWithMeta = ApiProductSchema.meta({
id: "Product", id: "Product",
description: "A product",
examples: [PRODUCT_EXAMPLE], examples: [PRODUCT_EXAMPLE],
}); });
@@ -130,7 +24,7 @@ export const productOps = {
}, },
responses: { responses: {
"200": { "200": {
description: "200 OK", description: "",
content: { content: {
"application/json": { "application/json": {
schema: z.object({ schema: z.object({
@@ -151,7 +45,7 @@ export const productOps = {
}, },
responses: { responses: {
"200": { "200": {
description: "200 OK", description: "",
content: { "application/json": { schema: ApiProductWithMeta } }, content: { "application/json": { schema: ApiProductWithMeta } },
}, },
}, },
@@ -168,7 +62,7 @@ export const productOps = {
}, },
responses: { responses: {
"200": { "200": {
description: "Product retrieved successfully", description: "",
content: { "application/json": { schema: ApiProductWithMeta } }, content: { "application/json": { schema: ApiProductWithMeta } },
}, },
}, },
@@ -188,7 +82,7 @@ export const productOps = {
}, },
responses: { responses: {
"200": { "200": {
description: "200 OK", description: "",
content: { "application/json": { schema: ApiProductWithMeta } }, content: { "application/json": { schema: ApiProductWithMeta } },
}, },
}, },
@@ -206,7 +100,7 @@ export const productOps = {
}, },
responses: { responses: {
"200": { "200": {
description: "Product deleted successfully", description: "",
content: { content: {
"application/json": { "application/json": {
schema: SuccessResponseSchema, schema: SuccessResponseSchema,

View File

@@ -61,48 +61,111 @@ export const RolloverConfigSchema = z.object({
}); });
const ProductItemConfigSchema = z.object({ const ProductItemConfigSchema = z.object({
on_increase: z.nativeEnum(OnIncrease).nullish(), on_increase: z.enum(OnIncrease).nullish(),
on_decrease: z.nativeEnum(OnDecrease).nullish(), on_decrease: z.enum(OnDecrease).nullish(),
rollover: RolloverConfigSchema.nullish(), rollover: RolloverConfigSchema.nullish(),
}); });
export const ProductItemSchema = z.object({ export const ProductItemSchema = z.object({
// Feature stuff // Feature stuff
feature_id: z.string().nullish(), feature_id: z.string().nullish().meta({
feature_type: z.nativeEnum(ProductItemFeatureType).nullish(), description:
included_usage: z.union([z.number(), z.literal(Infinite)]).nullish(), "The feature ID of the product item. Should be null for fixed price items.",
interval: z.preprocess((val) => { }),
if (val === "") {
throw new Error("Interval cannot be empty."); feature_type: z.enum(ProductItemFeatureType).nullish().meta({
} internal: true,
return val; }),
}, z.enum(ProductItemInterval).nullish()),
interval_count: z.number().nullish(), included_usage: z
entity_feature_id: z.string().nullish(), .union([z.number(), z.literal(Infinite)])
.nullish()
.meta({
description:
"The amount of usage included for this feature (per interval).",
}),
interval: z
.preprocess((val) => {
if (val === "") {
throw new Error("Interval cannot be empty.");
}
return val;
}, z.enum(ProductItemInterval).nullish())
.meta({
description:
"The reset or billing interval of the product item. If null, feature will have no reset date, and if there's a price, it will be billed one-off.",
}),
interval_count: z.number().nullish().meta({
description: "Interval count of the feature.",
}),
entity_feature_id: z.string().nullish().meta({
description:
"The feature ID of the entity (like seats) to track sub-balances for.",
}),
// Price config // Price config
usage_model: z.nativeEnum(UsageModel).nullish(), usage_model: z.enum(UsageModel).nullish().meta({
price: z.number().nullish(), description:
tiers: z.array(PriceTierSchema).nullish(), "Whether the feature should be prepaid upfront or billed for how much they use end of billing period.",
billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units) }),
usage_limit: z.number().nullish(),
price: z.number().nullish().meta({
description:
"The price of the product item. Should be null if tiered pricing is set.",
}),
tiers: z.array(PriceTierSchema).nullish().meta({
description:
"Tiered pricing for the product item. Not applicable for fixed price items.",
}),
billing_units: z.number().nullish().meta({
description:
"The billing units of the product item (eg $1 for 30 credits).",
}),
// Others // Others
// carry_over_usage: z.boolean().nullish(), // carry_over_usage: z.boolean().nullish(),
reset_usage_when_enabled: z.boolean().nullish(), reset_usage_when_enabled: z.boolean().nullish().meta({
config: ProductItemConfigSchema.nullish(), description:
"Whether the usage should be reset when the product is enabled.",
}),
display: z display: z
.object({ .object({
primary_text: z.string(), primary_text: z.string(),
secondary_text: z.string().nullish(), secondary_text: z.string().nullish(),
}) })
.nullish(), .nullish()
.meta({
internal: true,
}),
// Hidden from users for now.
usage_limit: z.number().nullish().meta({
internal: true,
}),
config: ProductItemConfigSchema.nullish().meta({
internal: true,
}),
// Stored in backend // Stored in backend
created_at: z.number().nullish(), created_at: z.number().nullish().meta({
entitlement_id: z.string().nullish(), internal: true,
price_id: z.string().nullish(), }),
price_config: z.any().nullish(), entitlement_id: z.string().nullish().meta({
internal: true,
}),
price_id: z.string().nullish().meta({
internal: true,
}),
price_config: z.any().nullish().meta({
internal: true,
}),
}); });
export const LimitedItemSchema = ProductItemSchema.extend({ export const LimitedItemSchema = ProductItemSchema.extend({