diff --git a/shared/api/customers/apiCustomer.ts b/shared/api/customers/apiCustomer.ts index d501865c5..6ef7f6a1c 100644 --- a/shared/api/customers/apiCustomer.ts +++ b/shared/api/customers/apiCustomer.ts @@ -30,23 +30,40 @@ export const ApiCustomerSchema = z.object({ description: "Your internal ID for the customer", example: "cus_123", }), - created_at: z.number().meta({ - description: - "The date and time the customer was created in milliseconds since epoch", + description: "Timestamp of customer creation in milliseconds since epoch", example: 1717000000, }), - - name: z.string().nullable(), - email: z.string().nullable(), - fingerprint: z.string().nullable(), - stripe_id: z.string().nullable().default(null), - env: z.enum(AppEnv), + name: z.string().nullable().meta({ + description: "Customer’s name", + example: "John Doe", + }), + email: z.string().nullable().meta({ + description: "Customer’s email address", + example: "john@doe.com", + }), + fingerprint: z.string().nullable().meta({ + description: + "Unique identifier (eg. serial number) to detect duplicate customers and prevent key leaks", + example: "fp_9184And92839123hda", + }), + stripe_id: z.string().nullable().default(null).meta({ + description: "Stripe customer ID", + example: "cus_J8A5c31A8tlpwN", + }), + env: z.enum(AppEnv).meta({ + description: "Environment the customer is in", + example: "production", + }), metadata: z.record(z.any(), z.any()).default({}), - - products: z.array(ApiCusProductSchema), - features: z.record(z.string(), ApiCusFeatureSchema), - + products: z.array(ApiCusProductSchema).meta({ + description: "List of products the customer has access to", + example: [], + }), + features: z.record(z.string(), ApiCusFeatureSchema).meta({ + description: "List of features the customer has access to", + example: {}, + }), ...ApiCusExpandSchema.shape, }); diff --git a/shared/api/customers/cusFeatures/apiCusFeature.ts b/shared/api/customers/cusFeatures/apiCusFeature.ts index fa8ec63f2..e925d2183 100644 --- a/shared/api/customers/cusFeatures/apiCusFeature.ts +++ b/shared/api/customers/cusFeatures/apiCusFeature.ts @@ -3,51 +3,133 @@ import { EntInterval } from "@models/productModels/entModels/entEnums.js"; import { z } from "zod/v4"; export const ApiCusRolloverSchema = z.object({ - balance: z.number(), - expires_at: z.number(), + balance: z.number().meta({ + description: "The remaining balance amount that has rolled over", + example: 100, + }), + expires_at: z.number().meta({ + description: "Timestamp when the rollover balance expires", + example: 1759247877000, + }), }); // Version 3 of cus feature response export const ApiCusFeatureBreakdownSchema = z.object({ - interval: z.enum(EntInterval), - interval_count: z.number().nullish(), - balance: z.number().nullish(), - usage: z.number().nullish(), - included_usage: z.number().nullish(), - next_reset_at: z.number().nullish(), - usage_limit: z.number().nullish(), - rollovers: z.array(ApiCusRolloverSchema).nullish(), + interval: z.enum(EntInterval).meta({ + description: "The billing interval for this feature breakdown", + example: "month", + }), + interval_count: z.number().nullish().meta({ + description: "The number of intervals between resets", + example: 1, + }), + balance: z.number().nullish().meta({ + description: "The remaining balance for this interval", + example: 500, + }), + usage: z.number().nullish().meta({ + description: "The current usage amount", + example: 250, + }), + included_usage: z.number().nullish().meta({ + description: "The amount of usage included in this interval", + example: 1000, + }), + next_reset_at: z.number().nullish().meta({ + description: "Timestamp when the usage resets", + example: 1759247877000, + }), + usage_limit: z.number().nullish().meta({ + description: "The maximum usage allowed", + example: 1000, + }), + rollovers: z.array(ApiCusRolloverSchema).nullish().meta({ + description: "Array of rollover balances from previous periods", + example: [{ balance: 100, expires_at: 1759247877000 }], + }), }); export const CoreCusFeatureSchema = z.object({ - interval: z.enum(EntInterval).or(z.literal("multiple")).nullish(), - interval_count: z.number().nullish(), - unlimited: z.boolean().nullish(), - balance: z.number().nullish(), - usage: z.number().nullish(), - included_usage: z.number().nullish(), - next_reset_at: z.number().nullish(), - overage_allowed: z.boolean().nullish(), + interval: z.enum(EntInterval).or(z.literal("multiple")).nullish().meta({ + description: "The billing interval or 'multiple' if the feature has multiple intervals", + example: "month", + }), + interval_count: z.number().nullish().meta({ + description: "The number of intervals between resets", + example: 1, + }), + unlimited: z.boolean().nullish().meta({ + description: "Whether the feature has unlimited usage", + example: false, + }), + balance: z.number().nullish().meta({ + description: "The remaining balance for this feature", + example: 500, + }), + usage: z.number().nullish().meta({ + description: "The current usage amount", + example: 250, + }), + included_usage: z.number().nullish().meta({ + description: "The amount of usage included", + example: 1000, + }), + next_reset_at: z.number().nullish().meta({ + description: "Timestamp when the usage resets", + example: 1759247877000, + }), + overage_allowed: z.boolean().nullish().meta({ + description: "Whether overage usage beyond the limit is allowed", + example: true, + }), - breakdown: z.array(ApiCusFeatureBreakdownSchema).nullish(), + breakdown: z.array(ApiCusFeatureBreakdownSchema).nullish().meta({ + description: "Detailed breakdown by interval for features with multiple intervals", + example: [{ interval: "month", interval_count: 1, balance: 500, usage: 250 }], + }), credit_schema: z .array( z.object({ - feature_id: z.string(), - credit_amount: z.number(), + feature_id: z.string().meta({ + description: "The ID of the feature that credits are applied to", + example: "feature_123", + }), + credit_amount: z.number().meta({ + description: "The amount of credits applied per usage", + example: 10, + }), }), ) - .nullish(), + .nullish() + .meta({ + description: "Credit conversion schema for credit system features", + example: [{ feature_id: "feature_123", credit_amount: 10 }], + }), - usage_limit: z.number().nullish(), - rollovers: z.array(ApiCusRolloverSchema).nullish(), + usage_limit: z.number().nullish().meta({ + description: "The maximum usage allowed", + example: 1000, + }), + rollovers: z.array(ApiCusRolloverSchema).nullish().meta({ + description: "Array of rollover balances from previous periods", + example: [{ balance: 100, expires_at: 1759247877000 }], + }), }); export const ApiCusFeatureSchema = z .object({ - id: z.string(), - type: z.enum(ApiFeatureType), - name: z.string().nullish(), + id: z.string().meta({ + description: "The unique identifier of the feature", + example: "feature_123", + }), + type: z.enum(ApiFeatureType).meta({ + description: "The type of the feature", + example: "single_use", + }), + name: z.string().nullish().meta({ + description: "The name of the feature", + example: "API Calls", + }), }) .extend(CoreCusFeatureSchema.shape); diff --git a/shared/api/customers/cusProducts/apiCusProduct.ts b/shared/api/customers/cusProducts/apiCusProduct.ts index f2ed4e415..e038a1ba6 100644 --- a/shared/api/customers/cusProducts/apiCusProduct.ts +++ b/shared/api/customers/cusProducts/apiCusProduct.ts @@ -3,26 +3,95 @@ import { z } from "zod/v4"; export const ApiCusProductSchema = z .object({ - id: z.string(), - name: z.string().nullable(), - group: z.string().nullable(), - status: z.enum(["active", "expired", "scheduled", "trialing", "past_due"]), - - canceled_at: z.number().nullish(), - started_at: z.number(), - is_default: z.boolean(), - is_add_on: z.boolean(), - version: z.number().nullish(), - - stripe_subscription_ids: z.array(z.string()).nullish(), - current_period_start: z.number().nullish(), - current_period_end: z.number().nullish(), - - entity_id: z.string().nullish(), - - items: z.array(ApiProductItemSchema).nullish(), - - quantity: z.number().optional(), + id: z.string().meta({ + description: "The unique identifier for the product", + example: "pro_plan", + }), + name: z.string().nullable().meta({ + description: "The name of the product", + example: "Pro Plan", + }), + group: z.string().nullable().meta({ + description: "The group the product belongs to", + example: "product_set_1", + }), + status: z + .enum(["active", "expired", "scheduled", "trialing", "past_due"]) + .meta({ + description: "Current status of the product for this customer", + example: "active", + }), + canceled_at: z.number().nullish().meta({ + description: "Timestamp when the product was canceled for the customer", + example: 1717000000, + }), + started_at: z.number().meta({ + description: "Timestamp when the customer started this product", + example: 1700000000000, + }), + is_default: z.boolean().meta({ + description: "Whether this product is the default for the customer", + example: true, + }), + is_add_on: z.boolean().meta({ + description: "Whether the product is an add-on", + example: false, + }), + version: z.number().nullish().meta({ + description: "Version of the product", + example: 1, + }), + stripe_subscription_ids: z + .array(z.string()) + .nullish() + .meta({ + description: + "List of Stripe subscription IDs associated with this product, if any", + example: ["sub_1Nc0JzBAbcxyz", "sub_1Nc0xyBAnopq"], + }), + current_period_start: z.number().nullish().meta({ + description: "Start of the current billing period", + example: 1717000000, + }), + current_period_end: z.number().nullish().meta({ + description: "End of the current billing period", + example: 1719600000, + }), + entity_id: z.string().nullish().meta({ + description: + "ID of the entity this customer product is attached to, if applicable", + example: "entity_1234abcd", + }), + items: z + .array(ApiProductItemSchema) + .nullish() + .meta({ + description: "Array of product items defining the features and pricing", + example: [ + { + feature_id: "", + feature_type: "single_use", + included_usage: 123, + interval: "month", + usage_model: "prepaid", + price: 123, + billing_units: 1000, + entity_feature_id: "", + reset_usage_when_enabled: true, + tiers: [ + { + to: 100, + amount: 10, + }, + ], + }, + ], + }), + quantity: z.number().optional().meta({ + description: + "The number of units of this product held by the customer, if applicable", + example: 1, + }), }) .meta({ id: "CustomerProduct", diff --git a/shared/api/entities/apiEntity.ts b/shared/api/entities/apiEntity.ts index d9b61816f..c416c9309 100644 --- a/shared/api/entities/apiEntity.ts +++ b/shared/api/entities/apiEntity.ts @@ -5,20 +5,43 @@ import { AppEnv } from "@models/genModels/genEnums.js"; import { z } from "zod/v4"; export const ApiBaseEntitySchema = z.object({ - id: z.string().nullable(), - name: z.string().nullable(), - - customer_id: z.string().nullish(), + id: z.string().nullable().meta({ + description: "The unique identifier of the entity", + example: "", + }), + name: z.string().nullable().meta({ + description: "The name of the entity", + example: "", + }), + customer_id: z.string().nullish().meta({ + description: "The customer ID this entity belongs to", + example: "", + }), feature_id: z.string().nullish(), - - created_at: z.number(), - env: z.enum(AppEnv), + created_at: z.number().meta({ + description: "Unix timestamp when the entity was created", + example: 1686168121, + }), + env: z.enum(AppEnv).meta({ + description: "The environment (sandbox/live)", + example: "live", + }), }); export const ApiEntitySchema = ApiBaseEntitySchema.extend({ - products: z.array(ApiCusProductSchema).optional(), - features: z.record(z.string(), ApiCusFeatureSchema).optional(), - invoices: z.array(ApiInvoiceSchema).optional(), + products: z.array(ApiCusProductSchema).optional().meta({ + description: "Products associated with this entity", + example: [], + }), + features: z.record(z.string(), ApiCusFeatureSchema).optional().meta({ + description: "Features associated with this entity", + example: {}, + }), + invoices: z.array(ApiInvoiceSchema).optional().meta({ + description: + "Invoices for this entity (only included when expand=invoices)", + example: [], + }), }); export type EntityResponse = z.infer; diff --git a/shared/api/features/apiFeature.ts b/shared/api/features/apiFeature.ts index dd122a739..c630efdd9 100644 --- a/shared/api/features/apiFeature.ts +++ b/shared/api/features/apiFeature.ts @@ -10,15 +10,28 @@ export enum ApiFeatureType { // Base schema without .meta() to avoid side effects during imports export const ApiFeatureSchema = z.object({ - id: z.string(), - name: z.string().nullish(), - type: z.enum(ApiFeatureType), + id: z.string().meta({ + description: "The unique identifier of the feature", + example: "", + }), + name: z.string().nullish().meta({ + description: "The name of the feature", + example: "", + }), + type: z.enum(ApiFeatureType).meta({ + description: "The type of the feature", + example: "", + }), display: z .object({ singular: z.string(), plural: z.string(), }) - .nullish(), + .nullish() + .meta({ + description: "Display names for the feature", + example: { singular: "", plural: "" }, + }), credit_schema: z .array( @@ -27,9 +40,16 @@ export const ApiFeatureSchema = z.object({ credit_cost: z.number(), }), ) - .nullish(), + .nullish() + .meta({ + description: "Credit cost schema for credit system features", + example: [{ metered_feature_id: "", credit_cost: 123 }], + }), - archived: z.boolean().nullish(), + archived: z.boolean().nullish().meta({ + description: "Whether or not the feature is archived", + example: false, + }), }); export type ApiFeature = z.infer; diff --git a/shared/api/others/apiDiscount.ts b/shared/api/others/apiDiscount.ts index 8e31e4f35..698cde892 100644 --- a/shared/api/others/apiDiscount.ts +++ b/shared/api/others/apiDiscount.ts @@ -5,24 +5,61 @@ import { import { z } from "zod/v4"; export const ApiDiscountSchema = z.object({ - id: z.string(), // either from Autumn or Stripe - name: z.string(), + id: z.string().meta({ + description: "The unique identifier for this discount", + example: "disc_123456", + }), + name: z.string().meta({ + description: "The name of the discount or coupon", + example: "SUMMER20", + }), - type: z.enum(RewardType), - discount_value: z.number(), - duration_type: z.enum(CouponDurationType), - duration_value: z.number().nullish(), - currency: z.string().nullish(), + type: z.enum(RewardType).meta({ + description: "The type of reward", + example: "percentage", + }), + discount_value: z.number().meta({ + description: "The discount value (percentage or fixed amount)", + example: 20, + }), + duration_type: z.enum(CouponDurationType).meta({ + description: "How long the discount lasts", + example: "forever", + }), + duration_value: z.number().nullish().meta({ + description: + "Number of billing periods the discount applies for repeating durations", + example: 3, + }), + currency: z.string().nullish().meta({ + description: "The currency code for fixed amount discounts", + example: "usd", + }), - start: z.number().nullish(), - end: z.number().nullish(), + start: z.number().nullish().meta({ + description: "Timestamp when the discount becomes active", + example: 1759247877000, + }), + end: z.number().nullish().meta({ + description: "Timestamp when the discount expires", + example: 1761839877000, + }), - subscription_id: z.string().nullish(), - total_discount_amount: z.number().nullish(), + subscription_id: z.string().nullish().meta({ + description: "The Stripe subscription ID this discount is applied to", + example: "sub_1A2B3C4D5E6F7G8H", + }), + total_discount_amount: z.number().nullish().meta({ + description: "Total amount saved from this discount", + example: 599, + }), }); export const ApiCusRewardsSchema = z.object({ - discounts: z.array(ApiDiscountSchema), + discounts: z.array(ApiDiscountSchema).meta({ + description: "Array of active discounts applied to the customer", + example: [{ id: "disc_123456", name: "SUMMER20", type: "percentage", discount_value: 20 }], + }), }); export type ApiDiscount = z.infer; diff --git a/shared/api/others/apiInvoice.ts b/shared/api/others/apiInvoice.ts index 4f2a8e7dd..612791dba 100644 --- a/shared/api/others/apiInvoice.ts +++ b/shared/api/others/apiInvoice.ts @@ -1,22 +1,58 @@ import { z } from "zod/v4"; export const ApiInvoiceItemSchema = z.object({ - description: z.string(), - period_start: z.number(), - period_end: z.number(), + description: z.string().meta({ + description: "Description of the invoice line item", + example: "Pro Plan - Monthly Subscription", + }), + period_start: z.number().meta({ + description: "Timestamp when the billing period starts", + example: 1759247877000, + }), + period_end: z.number().meta({ + description: "Timestamp when the billing period ends", + example: 1761839877000, + }), - feature_id: z.string().optional(), - feature_name: z.string().optional(), + feature_id: z.string().optional().meta({ + description: "The ID of the feature associated with this line item", + example: "feature_123", + }), + feature_name: z.string().optional().meta({ + description: "The name of the feature associated with this line item", + example: "API Calls", + }), }); export const ApiInvoiceSchema = z.object({ - product_ids: z.array(z.string()), - stripe_id: z.string(), - status: z.string(), - total: z.number(), - currency: z.string(), - created_at: z.number(), - hosted_invoice_url: z.string().nullish(), + product_ids: z.array(z.string()).meta({ + description: "Array of product IDs included in this invoice", + example: ["pro_plan", "addon_feature"], + }), + stripe_id: z.string().meta({ + description: "The Stripe invoice ID", + example: "in_1A2B3C4D5E6F7G8H", + }), + status: z.string().meta({ + description: "The status of the invoice", + example: "paid", + }), + total: z.number().meta({ + description: "The total amount of the invoice", + example: 2999, + }), + currency: z.string().meta({ + description: "The currency code for the invoice", + example: "usd", + }), + created_at: z.number().meta({ + description: "Timestamp when the invoice was created", + example: 1759247877000, + }), + hosted_invoice_url: z.string().nullish().meta({ + description: "URL to the Stripe-hosted invoice page", + example: "https://invoice.stripe.com/i/acct_123/test_456", + }), // period_start: z.number().nullish(), // period_end: z.number().nullish(), }); diff --git a/shared/api/products/apiFreeTrial.ts b/shared/api/products/apiFreeTrial.ts index aebec3169..87b11b01d 100644 --- a/shared/api/products/apiFreeTrial.ts +++ b/shared/api/products/apiFreeTrial.ts @@ -2,13 +2,29 @@ import { FreeTrialDuration } from "@models/productModels/freeTrialModels/freeTri import { z } from "zod/v4"; export const APIFreeTrialSchema = z.object({ - duration: z.enum(FreeTrialDuration), - length: z.number(), - unique_fingerprint: z.boolean(), - card_required: z.boolean(), + duration: z.enum(FreeTrialDuration).meta({ + description: "The duration type of the free trial", + example: "", + }), + length: z.number().meta({ + description: "The length of the free trial", + example: 123, + }), + 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, + }), // For Cus Product - trial_available: z.boolean().nullish().default(true), + trial_available: z.boolean().nullish().default(true).meta({ + description: "Whether the free trial is available", + example: true, + }), }); export type APIFreeTrial = z.infer; diff --git a/shared/api/products/apiProductItem.ts b/shared/api/products/apiProductItem.ts index 4f8d76927..75f781937 100644 --- a/shared/api/products/apiProductItem.ts +++ b/shared/api/products/apiProductItem.ts @@ -11,33 +11,90 @@ import { z } from "zod/v4"; export const ApiProductItemSchema = z.object({ // Feature stuff - type: z.enum(ProductItemType).nullish(), - feature_id: z.string().nullish(), - feature_type: z.enum(ProductItemFeatureType).nullish(), + type: z.enum(ProductItemType).nullish().meta({ + description: "The type of the product item", + example: "", + }), + feature_id: z.string().nullish().meta({ + description: + "The feature ID of the product item. Should be `null` for prices.", + example: "", + }), + 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: "", + }), // Feature response - feature: ApiFeatureSchema.nullish(), + feature: ApiFeatureSchema.nullish().meta({ + description: "The feature itself", + example: { + id: "", + name: "", + }, + }), - included_usage: z.number().or(z.literal(Infinite)).nullish(), - interval: z.enum(ProductItemInterval).nullish(), - interval_count: z.number().nullish(), + 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.", + example: "", + }), + interval_count: z.number().nullish().meta({ + description: "The number of intervals between resets", + example: 123, + }), // Price config - price: z.number().nullish(), - tiers: z.array(PriceTierSchema).nullish(), - usage_model: z.enum(UsageModel).nullish(), - billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units) - reset_usage_when_enabled: z.boolean().nullish(), + 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 }, + ], + }), + 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: "", + }), + 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.", + }), quantity: z.number().nullish(), next_cycle_quantity: z.number().nullish(), - entity_feature_id: z.string().nullish(), + entity_feature_id: z.string().nullish().meta({ + description: "The entity feature ID of the product item if applicable.", + example: "", + }), display: z .object({ primary_text: z.string(), secondary_text: z.string().nullish(), }) - .nullish(), + .nullish() + .meta({ + description: "The display of the product item.", + example: { primary_text: "", secondary_text: "" }, + }), }); export type ApiProductItem = z.infer; diff --git a/shared/models/productV2Models/productItemModels/productItemModels.ts b/shared/models/productV2Models/productItemModels/productItemModels.ts index 20affbc71..5a5c136bd 100644 --- a/shared/models/productV2Models/productItemModels/productItemModels.ts +++ b/shared/models/productV2Models/productItemModels/productItemModels.ts @@ -27,8 +27,14 @@ export enum ProductItemType { } export const PriceTierSchema = z.object({ - to: z.number().or(z.literal(TierInfinite)), - amount: z.number(), + to: z.number().or(z.literal(TierInfinite)).meta({ + description: "The maximum amount of usage for this tier.", + example: 100, + }), + amount: z.number().meta({ + description: "The price of the product item for this tier.", + example: 10, + }), }); export enum UsageModel {