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_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"),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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.",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user