chore: cleaned up openapi for products endpoint group
This commit is contained in:
@@ -90,7 +90,7 @@ export const constructProduct = ({
|
||||
is_add_on: productData.is_add_on,
|
||||
is_default: productData.is_default,
|
||||
version: productData.version || 1,
|
||||
group: productData.group,
|
||||
group: productData.group || "",
|
||||
|
||||
env,
|
||||
internal_id: generateId("prod"),
|
||||
|
||||
@@ -1,99 +1,103 @@
|
||||
import "dotenv/config";
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, writeFileSync } from "node:fs";
|
||||
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||
import yaml from "yaml";
|
||||
import { z } from "zod/v4";
|
||||
import { createDocument } from "zod-openapi";
|
||||
import { CustomerDataSchema } from "./common/customerData.js";
|
||||
import { EntityDataSchema } from "./common/entityData.js";
|
||||
import { coreOps } from "./core/coreOpenApi.js";
|
||||
import { ApiCusFeatureSchema } from "./customers/cusFeatures/apiCusFeature.js";
|
||||
import { ApiCusProductSchema } from "./customers/cusProducts/apiCusProduct.js";
|
||||
import {
|
||||
ApiCustomerWithMeta,
|
||||
customerOps,
|
||||
} from "./customers/customersOpenApi.js";
|
||||
import { ApiEntityWithMeta, entityOps } from "./entities/entitiesOpenApi.js";
|
||||
import { ApiFeatureWithMeta, featureOps } from "./features/featuresOpenApi.js";
|
||||
import { ApiCustomerWithMeta } from "./customers/customersOpenApi.js";
|
||||
import { ApiEntityWithMeta } from "./entities/entitiesOpenApi.js";
|
||||
import { ApiFeatureWithMeta } from "./features/featuresOpenApi.js";
|
||||
import { ApiProductItemSchema } from "./products/apiProductItem.js";
|
||||
import { ApiProductWithMeta, productOps } from "./products/productsOpenApi.js";
|
||||
import { referralOps } from "./referrals/referralsOpenApi.js";
|
||||
|
||||
const API_VERSION = "1.2.0";
|
||||
|
||||
const document = createDocument({
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "Autumn API",
|
||||
version: API_VERSION,
|
||||
},
|
||||
|
||||
servers: [
|
||||
{
|
||||
url: "https://api.useautumn.com",
|
||||
description: "Production server",
|
||||
const document = createDocument(
|
||||
{
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "Autumn API",
|
||||
version: API_VERSION,
|
||||
},
|
||||
],
|
||||
|
||||
security: [
|
||||
{
|
||||
secretKey: [],
|
||||
},
|
||||
],
|
||||
components: {
|
||||
schemas: {
|
||||
AutumnError: z
|
||||
.object({
|
||||
message: z.string(),
|
||||
code: z.string(),
|
||||
env: z.enum(AppEnv),
|
||||
})
|
||||
.meta({
|
||||
id: "AutumnError",
|
||||
description: "An error that occurred in the API",
|
||||
servers: [
|
||||
{
|
||||
url: "https://api.useautumn.com",
|
||||
description: "Production server",
|
||||
},
|
||||
],
|
||||
|
||||
security: [
|
||||
{
|
||||
secretKey: [],
|
||||
},
|
||||
],
|
||||
components: {
|
||||
schemas: {
|
||||
CustomerData: CustomerDataSchema.meta({
|
||||
id: "CustomerData",
|
||||
description: "Customer data for creating or updating a customer",
|
||||
}),
|
||||
CustomerData: CustomerDataSchema.meta({
|
||||
id: "CustomerData",
|
||||
description: "Customer data for creating or updating a customer",
|
||||
}),
|
||||
EntityData: EntityDataSchema.meta({
|
||||
id: "EntityData",
|
||||
description: "Entity data for creating an entity",
|
||||
}),
|
||||
Customer: ApiCustomerWithMeta,
|
||||
CustomerProduct: ApiCusProductSchema,
|
||||
CustomerFeature: ApiCusFeatureSchema.meta({
|
||||
id: "CustomerFeature",
|
||||
description: "Customer feature object returned by the API",
|
||||
}),
|
||||
Product: ApiProductWithMeta,
|
||||
ProductItem: ApiProductItemSchema,
|
||||
Feature: ApiFeatureWithMeta,
|
||||
Entity: ApiEntityWithMeta,
|
||||
},
|
||||
securitySchemes: {
|
||||
secretKey: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
EntityData: EntityDataSchema.meta({
|
||||
id: "EntityData",
|
||||
description: "Entity data for creating an entity",
|
||||
}),
|
||||
Customer: ApiCustomerWithMeta,
|
||||
CustomerProduct: ApiCusProductSchema,
|
||||
CustomerFeature: ApiCusFeatureSchema.meta({
|
||||
id: "CustomerFeature",
|
||||
description: "Customer feature object returned by the API",
|
||||
}),
|
||||
Product: ApiProductWithMeta,
|
||||
ProductItem: ApiProductItemSchema,
|
||||
Feature: ApiFeatureWithMeta,
|
||||
Entity: ApiEntityWithMeta,
|
||||
},
|
||||
securitySchemes: {
|
||||
secretKey: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
paths: {
|
||||
...productOps,
|
||||
...coreOps,
|
||||
...featureOps,
|
||||
...customerOps,
|
||||
...entityOps,
|
||||
...referralOps,
|
||||
paths: {
|
||||
...productOps,
|
||||
// ...coreOps,
|
||||
// ...featureOps,
|
||||
// ...customerOps,
|
||||
// ...entityOps,
|
||||
// ...referralOps,
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
// Disable the "Output" suffix that zod-openapi adds to response schemas
|
||||
outputIdSuffix: "",
|
||||
},
|
||||
);
|
||||
|
||||
// Export to YAML file during build
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
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
|
||||
const jsonStr = JSON.stringify(document, null, 2);
|
||||
|
||||
|
||||
@@ -4,26 +4,24 @@ import { z } from "zod/v4";
|
||||
export const APIFreeTrialSchema = z.object({
|
||||
duration: z.enum(FreeTrialDuration).meta({
|
||||
description: "The duration type of the free trial",
|
||||
example: "<string>",
|
||||
}),
|
||||
length: z.number().meta({
|
||||
description: "The length of the free trial",
|
||||
example: 123,
|
||||
description: "The length of the duration type specified",
|
||||
}),
|
||||
unique_fingerprint: z.boolean().meta({
|
||||
description:
|
||||
"Whether the free trial is limited to one per customer fingerprint",
|
||||
example: true,
|
||||
}),
|
||||
|
||||
card_required: z.boolean().meta({
|
||||
description: "Whether the free trial requires a card",
|
||||
example: true,
|
||||
description:
|
||||
"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
|
||||
trial_available: z.boolean().nullish().default(true).meta({
|
||||
description: "Whether the free trial is available",
|
||||
example: true,
|
||||
description:
|
||||
"Used in customer context. Whether the free trial is available for the customer if they were to attach the product.",
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,105 @@ import { z } from "zod/v4";
|
||||
import { APIFreeTrialSchema } from "./apiFreeTrial.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({
|
||||
is_free: z.boolean().meta({
|
||||
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({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the product you set when creating the product",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
id: z
|
||||
.string()
|
||||
.describe("The ID of the product you set when creating the product"),
|
||||
|
||||
name: z.string().meta({
|
||||
description: "The name of the product",
|
||||
example: "Pro Plan",
|
||||
}),
|
||||
name: z.string().describe("The name of the product"),
|
||||
|
||||
group: z.string().nullable().meta({
|
||||
description: "The group of the product",
|
||||
example: "product_set_1",
|
||||
}),
|
||||
group: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe("Product group which this product belongs to"),
|
||||
|
||||
env: z.enum(AppEnv).meta({
|
||||
description: "The environment of the product",
|
||||
example: "production",
|
||||
}),
|
||||
env: z.enum(AppEnv).describe("The environment of the product"),
|
||||
|
||||
is_add_on: z.boolean().meta({
|
||||
description:
|
||||
is_add_on: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"Whether the product is an add-on and can be purchased alongside other products",
|
||||
example: true,
|
||||
}),
|
||||
),
|
||||
|
||||
is_default: z.boolean().meta({
|
||||
description: "Whether the product is the default product",
|
||||
example: true,
|
||||
}),
|
||||
is_default: z
|
||||
.boolean()
|
||||
.describe("Whether the product is the default product"),
|
||||
|
||||
archived: z.boolean({ message: "archived should be a boolean" }).meta({
|
||||
description:
|
||||
archived: z
|
||||
.boolean({ message: "archived should be a boolean" })
|
||||
.describe(
|
||||
"Whether this product has been archived and is no longer available",
|
||||
example: false,
|
||||
}),
|
||||
),
|
||||
|
||||
version: z.number().meta({
|
||||
description: "The version of the product",
|
||||
example: 1,
|
||||
}),
|
||||
version: z.number().describe("The current version of the product"),
|
||||
|
||||
created_at: z.number().meta({
|
||||
description:
|
||||
created_at: z
|
||||
.number()
|
||||
.describe(
|
||||
"The timestamp of when the product was created in milliseconds since epoch",
|
||||
example: 1759247877000,
|
||||
}),
|
||||
),
|
||||
|
||||
items: z.array(ApiProductItemSchema).meta({
|
||||
description: "Array of product items that define the features and pricing",
|
||||
example: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
items: z
|
||||
.array(ApiProductItemSchema)
|
||||
.describe(
|
||||
"Array of product items that define the product's features and pricing",
|
||||
),
|
||||
|
||||
free_trial: APIFreeTrialSchema.nullable().meta({
|
||||
description: "Free trial configuration for this product, if available",
|
||||
example: {
|
||||
duration: "<string>",
|
||||
length: 123,
|
||||
unique_fingerprint: true,
|
||||
},
|
||||
}),
|
||||
free_trial: APIFreeTrialSchema.nullable().describe(
|
||||
"Free trial configuration for this product, if available",
|
||||
),
|
||||
|
||||
base_variant_id: z.string().nullable().meta({
|
||||
description: "ID of the base variant this product is derived from",
|
||||
example: "pro_monthly",
|
||||
}),
|
||||
base_variant_id: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe("ID of the base variant this product is derived from"),
|
||||
|
||||
scenario: z.enum(AttachScenario).optional().meta({
|
||||
description:
|
||||
"Scenario context for when this product is used in attach flows",
|
||||
example: "upgrade",
|
||||
}),
|
||||
scenario: z
|
||||
.enum(AttachScenario)
|
||||
.optional()
|
||||
.describe("Scenario for when this product is used in attach flows"),
|
||||
|
||||
properties: ApiProductPropertiesSchema.optional().meta({
|
||||
description: "Additional properties and metadata for the product",
|
||||
example: {
|
||||
is_free: false,
|
||||
is_one_off: false,
|
||||
interval_group: "monthly",
|
||||
has_trial: true,
|
||||
updateable: true,
|
||||
},
|
||||
}),
|
||||
properties: ApiProductPropertiesSchema.optional(),
|
||||
});
|
||||
|
||||
export type ApiProduct = z.infer<typeof ApiProductSchema>;
|
||||
|
||||
@@ -12,64 +12,61 @@ import { z } from "zod/v4";
|
||||
export const ApiProductItemSchema = z
|
||||
.object({
|
||||
// Feature stuff
|
||||
type: z.enum(ProductItemType).nullish().meta({
|
||||
description: "The type of the product item",
|
||||
example: "<string>",
|
||||
}),
|
||||
feature_id: z.string().nullish().meta({
|
||||
description:
|
||||
"The feature ID of the product item. Should be `null` for prices.",
|
||||
example: "<string>",
|
||||
}),
|
||||
type: z
|
||||
.enum(ProductItemType)
|
||||
.nullish()
|
||||
.describe("The type of the product item"),
|
||||
|
||||
feature_id: z
|
||||
.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({
|
||||
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.",
|
||||
example: "<string>",
|
||||
}),
|
||||
|
||||
// Feature response
|
||||
feature: ApiFeatureSchema.nullish().meta({
|
||||
description: "The feature itself",
|
||||
internal: true,
|
||||
}),
|
||||
|
||||
included_usage: z.number().or(z.literal(Infinite)).nullish().meta({
|
||||
description: "The amount of usage included for this feature.",
|
||||
example: 123,
|
||||
}),
|
||||
|
||||
interval: 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: "The number of intervals between resets",
|
||||
description: "The interval count of the product item.",
|
||||
}),
|
||||
|
||||
// Price config
|
||||
price: z.number().nullish().meta({
|
||||
description:
|
||||
"The price of the product item. Should be `null` if tiered pricing is set.",
|
||||
example: 123,
|
||||
}),
|
||||
tiers: z
|
||||
.array(PriceTierSchema)
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Tiered pricing for the product item.",
|
||||
example: [
|
||||
{ to: 100, amount: 10 },
|
||||
{ to: 200, amount: 20 },
|
||||
],
|
||||
}),
|
||||
|
||||
tiers: z.array(PriceTierSchema).nullish().meta({
|
||||
description:
|
||||
"Tiered pricing for the product item. Not applicable for fixed price items.",
|
||||
}),
|
||||
|
||||
usage_model: z.enum(UsageModel).nullish().meta({
|
||||
description:
|
||||
"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({
|
||||
description: "The amount per billing unit (eg. $9 / 250 units)",
|
||||
example: 250,
|
||||
}),
|
||||
|
||||
reset_usage_when_enabled: z.boolean().nullish().meta({
|
||||
description:
|
||||
"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({
|
||||
description: "The entity feature ID of the product item if applicable.",
|
||||
example: "<string>",
|
||||
}),
|
||||
|
||||
display: z
|
||||
@@ -88,16 +84,16 @@ export const ApiProductItemSchema = z
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "The display of the product item.",
|
||||
example: { primary_text: "<string>", secondary_text: "<string>" },
|
||||
}),
|
||||
|
||||
quantity: z.number().nullish().meta({
|
||||
description: "The quantity of the product item.",
|
||||
example: 123,
|
||||
description:
|
||||
"Used in customer context. Quantity of the feature the customer has prepaid for.",
|
||||
}),
|
||||
|
||||
next_cycle_quantity: z.number().nullish().meta({
|
||||
description: "The quantity of the product item in the next cycle.",
|
||||
example: 123,
|
||||
description:
|
||||
"Used in customer context. Quantity of the feature the customer will prepay for in the next cycle.",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
|
||||
@@ -3,7 +3,11 @@ import { ProductItemSchema } from "@models/productV2Models/productItemModels/pro
|
||||
import { idRegex } from "@utils/utils.js";
|
||||
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
|
||||
|
||||
@@ -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
|
||||
.object({
|
||||
id: z.string().nonempty().regex(idRegex),
|
||||
|
||||
name: z.string().refine((val) => val.length > 0, {
|
||||
message: "name must be a non-empty string",
|
||||
id: z.string().nonempty().regex(idRegex).meta({
|
||||
description: descriptions.id,
|
||||
}),
|
||||
|
||||
is_add_on: z.boolean().default(false),
|
||||
is_default: z.boolean().default(false),
|
||||
version: z.number().optional(),
|
||||
group: z.string().nullable().default(""),
|
||||
name: z
|
||||
.string()
|
||||
.refine((val) => val.length > 0, {
|
||||
message: "name must be a non-empty string",
|
||||
})
|
||||
.meta({
|
||||
description: descriptions.name,
|
||||
}),
|
||||
|
||||
items: z.array(CreateProductItemParamsSchema).optional(),
|
||||
free_trial: CreateFreeTrialSchema.nullish().default(null),
|
||||
is_add_on: z.boolean().default(false).meta({
|
||||
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({
|
||||
examples: [CREATE_PRODUCT_EXAMPLE],
|
||||
});
|
||||
|
||||
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
|
||||
.string()
|
||||
.refine((val) => val.length > 0, {
|
||||
message: "name must be a non-empty string",
|
||||
})
|
||||
.optional(),
|
||||
.optional()
|
||||
.meta({
|
||||
description: descriptions.name,
|
||||
}),
|
||||
|
||||
is_add_on: z.boolean().optional(),
|
||||
is_default: z.boolean().optional(),
|
||||
is_add_on: z.boolean().optional().meta({
|
||||
description: descriptions.is_add_on,
|
||||
}),
|
||||
is_default: z.boolean().optional().meta({
|
||||
description: descriptions.is_default,
|
||||
}),
|
||||
// version: z.number().optional(),
|
||||
group: z.string().nonempty().nullable().optional(),
|
||||
archived: z.boolean().optional(),
|
||||
group: z.string().nonempty().nullable().optional().meta({
|
||||
description: descriptions.group,
|
||||
}),
|
||||
archived: z.boolean().optional().meta({
|
||||
description: descriptions.archived,
|
||||
}),
|
||||
|
||||
items: z.array(CreateProductItemParamsSchema).optional(),
|
||||
free_trial: CreateFreeTrialSchema.nullish(),
|
||||
free_trial: CreateFreeTrialSchema.nullish().meta({
|
||||
description: descriptions.free_trial,
|
||||
}),
|
||||
});
|
||||
|
||||
export const UpdateProductQuerySchema = z.object({
|
||||
|
||||
@@ -1,120 +1,14 @@
|
||||
import { SuccessResponseSchema } from "@api/common/commonResponses.js";
|
||||
import { z } from "zod/v4";
|
||||
import { ApiProductSchema, PRODUCT_EXAMPLE } from "./apiProduct.js";
|
||||
import {
|
||||
CreateProductV2ParamsSchema,
|
||||
UpdateProductV2ParamsSchema,
|
||||
} from "@api/models.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,
|
||||
// },
|
||||
};
|
||||
} from "./productOpModels.js";
|
||||
|
||||
// Register schema with .meta() for OpenAPI spec generation
|
||||
export const ApiProductWithMeta = ApiProductSchema.meta({
|
||||
id: "Product",
|
||||
description: "A product",
|
||||
examples: [PRODUCT_EXAMPLE],
|
||||
});
|
||||
|
||||
@@ -130,7 +24,7 @@ export const productOps = {
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
description: "",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
@@ -151,7 +45,7 @@ export const productOps = {
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
description: "",
|
||||
content: { "application/json": { schema: ApiProductWithMeta } },
|
||||
},
|
||||
},
|
||||
@@ -168,7 +62,7 @@ export const productOps = {
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Product retrieved successfully",
|
||||
description: "",
|
||||
content: { "application/json": { schema: ApiProductWithMeta } },
|
||||
},
|
||||
},
|
||||
@@ -188,7 +82,7 @@ export const productOps = {
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
description: "",
|
||||
content: { "application/json": { schema: ApiProductWithMeta } },
|
||||
},
|
||||
},
|
||||
@@ -206,7 +100,7 @@ export const productOps = {
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Product deleted successfully",
|
||||
description: "",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: SuccessResponseSchema,
|
||||
|
||||
@@ -61,48 +61,111 @@ export const RolloverConfigSchema = z.object({
|
||||
});
|
||||
|
||||
const ProductItemConfigSchema = z.object({
|
||||
on_increase: z.nativeEnum(OnIncrease).nullish(),
|
||||
on_decrease: z.nativeEnum(OnDecrease).nullish(),
|
||||
on_increase: z.enum(OnIncrease).nullish(),
|
||||
on_decrease: z.enum(OnDecrease).nullish(),
|
||||
rollover: RolloverConfigSchema.nullish(),
|
||||
});
|
||||
|
||||
export const ProductItemSchema = z.object({
|
||||
// Feature stuff
|
||||
feature_id: z.string().nullish(),
|
||||
feature_type: z.nativeEnum(ProductItemFeatureType).nullish(),
|
||||
included_usage: z.union([z.number(), z.literal(Infinite)]).nullish(),
|
||||
interval: z.preprocess((val) => {
|
||||
if (val === "") {
|
||||
throw new Error("Interval cannot be empty.");
|
||||
}
|
||||
return val;
|
||||
}, z.enum(ProductItemInterval).nullish()),
|
||||
interval_count: z.number().nullish(),
|
||||
entity_feature_id: z.string().nullish(),
|
||||
feature_id: z.string().nullish().meta({
|
||||
description:
|
||||
"The feature ID of the product item. Should be null for fixed price items.",
|
||||
}),
|
||||
|
||||
feature_type: z.enum(ProductItemFeatureType).nullish().meta({
|
||||
internal: true,
|
||||
}),
|
||||
|
||||
included_usage: z
|
||||
.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
|
||||
usage_model: z.nativeEnum(UsageModel).nullish(),
|
||||
price: z.number().nullish(),
|
||||
tiers: z.array(PriceTierSchema).nullish(),
|
||||
billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units)
|
||||
usage_limit: z.number().nullish(),
|
||||
usage_model: z.enum(UsageModel).nullish().meta({
|
||||
description:
|
||||
"Whether the feature should be prepaid upfront or billed for how much they use end of billing period.",
|
||||
}),
|
||||
|
||||
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
|
||||
// carry_over_usage: z.boolean().nullish(),
|
||||
reset_usage_when_enabled: z.boolean().nullish(),
|
||||
config: ProductItemConfigSchema.nullish(),
|
||||
reset_usage_when_enabled: z.boolean().nullish().meta({
|
||||
description:
|
||||
"Whether the usage should be reset when the product is enabled.",
|
||||
}),
|
||||
|
||||
display: z
|
||||
.object({
|
||||
primary_text: z.string(),
|
||||
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
|
||||
created_at: z.number().nullish(),
|
||||
entitlement_id: z.string().nullish(),
|
||||
price_id: z.string().nullish(),
|
||||
price_config: z.any().nullish(),
|
||||
created_at: z.number().nullish().meta({
|
||||
internal: true,
|
||||
}),
|
||||
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({
|
||||
|
||||
Reference in New Issue
Block a user