diff --git a/server/src/internal/products/handlers/handleCreateProduct.ts b/server/src/internal/products/handlers/handleCreateProduct.ts index 0b92f7e4f..af84b7783 100644 --- a/server/src/internal/products/handlers/handleCreateProduct.ts +++ b/server/src/internal/products/handlers/handleCreateProduct.ts @@ -1,4 +1,8 @@ import { + AffectedResource, + type ApiPlan, + ApiVersion, + CreatePlanParamsSchema, type CreateProductV2Params, CreateProductV2ParamsSchema, type Entitlement, @@ -20,8 +24,9 @@ import { } from "../free-trials/freeTrialUtils.js"; import { ProductService } from "../ProductService.js"; import { handleNewProductItems } from "../product-items/productItemUtils/handleNewProductItems.js"; +import { planToProductV2 } from "../productUtils/apiPlanUtils/planToProductV2.js"; import { isDefaultTrial } from "../productUtils/classifyProduct.js"; -import { getProductResponse } from "../productUtils/productResponseUtils/getProductResponse.js"; +import { getPlanResponse } from "../productUtils/productResponseUtils/getPlanResponse.js"; import { constructProduct, getGroupToDefaults, @@ -87,12 +92,19 @@ export const disableCurrentDefault = async ({ * Route: POST /products - Create a product */ export const createProduct = createRoute({ - body: CreateProductV2ParamsSchema, + // body: CreateProductV2ParamsSchema, + versionedBody: { + latest: CreatePlanParamsSchema, + [ApiVersion.V1_1]: CreateProductV2ParamsSchema, + }, + resource: AffectedResource.Product, handler: async (c) => { const body = c.req.valid("json"); const ctx = c.get("ctx"); // const query = c.req.valid("query"); + const v1_2Body = planToProductV2({ plan: body as ApiPlan }); + const { logger, org, features, env, db } = ctx; const existing = await ProductService.get({ @@ -107,19 +119,19 @@ export const createProduct = createRoute({ await disableCurrentDefault({ req: ctx, - newProduct: body, + newProduct: v1_2Body, }); const product = await ProductService.insert({ db, product: constructProduct({ - productData: body, + productData: v1_2Body as CreateProductV2Params, orgId: org.id, env, }), }); - const { items, free_trial } = body; + const { items, free_trial } = v1_2Body; let prices: Price[] = []; let entitlements: Entitlement[] = []; @@ -159,6 +171,7 @@ export const createProduct = createRoute({ const newFullProduct: FullProduct = { ...product, + description: body?.description ?? null, prices, entitlements: getEntsWithFeature({ ents: entitlements, features }), free_trial: newFreeTrial, @@ -179,11 +192,23 @@ export const createProduct = createRoute({ }, }); - const productResponse = await getProductResponse({ - product: newFullProduct, - features, - }); + // const productResponse = await getProductResponse({ + // product: newFullProduct, + // features, + // }); - return c.json(productResponse); + try { + const planResponse = await getPlanResponse({ + product: newFullProduct, + features, + }); + + console.log(`Plan response:\n ${JSON.stringify(planResponse, null, 4)}`); + + return c.json(planResponse); + } catch (error) { + console.error("Error getting plan response:", error); + throw error; + } }, }); diff --git a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts index b9e7b09b4..d0c1e06ca 100644 --- a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts +++ b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts @@ -292,6 +292,13 @@ export const handleNewProductItems = async ({ }); } + console.log(` + Deletion Confirmations: + - New ${newEnts.length} entitlements: ${JSON.stringify(newEnts, null, 4)} + - Updated ${updatedEnts.length} entitlements: ${JSON.stringify(updatedEnts, null, 4)} + - Deleted ${deletedEnts.length} entitlements: ${JSON.stringify(deletedEnts, null, 4)} + `); + return { prices: [...newPrices, ...updatedPrices], entitlements: [...newEnts, ...updatedEnts].map((ent) => ({ diff --git a/server/src/internal/products/productUtils/apiPlanUtils/planFeatureUtils/itemsToPlanFeatures.ts b/server/src/internal/products/productUtils/apiPlanUtils/planFeatureUtils/itemsToPlanFeatures.ts index 7c125981d..5e12cfc98 100644 --- a/server/src/internal/products/productUtils/apiPlanUtils/planFeatureUtils/itemsToPlanFeatures.ts +++ b/server/src/internal/products/productUtils/apiPlanUtils/planFeatureUtils/itemsToPlanFeatures.ts @@ -1,9 +1,14 @@ import { type ApiPlanFeature, ApiPlanFeatureSchema, + type BillingInterval, Infinite, + type OnDecrease, + type OnIncrease, type ProductItem, - type ResetInterval, + ResetInterval, + TierInfinite, + type UsageModel, } from "@autumn/shared"; import { itemIntvToResetIntv, @@ -30,60 +35,57 @@ export const itemsToPlanFeatures = ({ // Convert intervals reset_interval: item.interval ? (itemIntvToResetIntv(item.interval!) as ResetInterval) - : undefined, - reset_interval_count: item.interval_count ?? undefined, + : ResetInterval.OneOff, + ...(item.interval_count !== undefined && item.interval_count !== null + ? { + reset_interval_count: item.interval_count, + } + : {}), // Convert price if exists - price: item.price + ...(item.price ? { - amount: item.price, - tiers: item.tiers?.map((tier) => ({ - amount: tier.amount, - to: tier.to === "inf" ? null : tier.to, - })), - interval: item.interval, // Billing interval (might need conversion) - interval_count: item.interval_count, - billing_units: item.billing_units, - usage_model: item.usage_model, - max_purchase: item.usage_limit - ? item.usage_limit - - (item.included_usage === Infinite - ? 0 - : (item.included_usage ?? 0)) - : undefined, + price: { + interval: item.interval as unknown as BillingInterval, + billing_units: item.billing_units ?? 1, + usage_model: item.usage_model as UsageModel, + max_purchase: 1, + amount: item.price || 0, + tiers: item.tiers?.map((tier) => ({ + to: tier.to === TierInfinite ? TierInfinite : tier.to, + amount: tier.amount, + })), + interval_count: item.interval_count ?? undefined, + }, } - : undefined, + : {}), // Convert rollover config ...(item.config?.rollover - ? [ - { - rollover: { - max: item.config.rollover.max ?? null, - expiry_duration_type: rolloverToResetIntv( - item.config.rollover.duration, - ), - expiry_duration_length: item.config.rollover.length, - }, + ? { + rollover: { + max: item.config.rollover.max ?? null, + expiry_duration_type: rolloverToResetIntv( + item.config.rollover.duration, + ), + expiry_duration_length: item.config.rollover.length, }, - ] - : []), + } + : {}), // Convert proration config ...(item.config?.on_increase || item.config?.on_decrease - ? [ - { - proration: { - on_increase: item.config.on_increase ?? undefined, - on_decrease: item.config.on_decrease ?? undefined, - }, + ? { + proration: { + on_increase: item.config.on_increase as OnIncrease, + on_decrease: item.config.on_decrease as OnDecrease, }, - ] - : []), + } + : {}), // Other fields reset_usage_on_enabled: item.reset_usage_when_enabled ?? true, - entity_feature_id: item.entity_feature_id, + // entity_feature_id: item.entity_feature_id, } satisfies ApiPlanFeature), ); }; diff --git a/server/src/internal/products/productUtils/apiPlanUtils/planFeatureUtils/planFeaturesToItems.ts b/server/src/internal/products/productUtils/apiPlanUtils/planFeatureUtils/planFeaturesToItems.ts index 200bc2b0c..1a80b510f 100644 --- a/server/src/internal/products/productUtils/apiPlanUtils/planFeatureUtils/planFeaturesToItems.ts +++ b/server/src/internal/products/productUtils/apiPlanUtils/planFeatureUtils/planFeaturesToItems.ts @@ -3,6 +3,7 @@ import { Infinite, type ProductItem, ProductItemSchema, + type ResetInterval, } from "@autumn/shared"; import { resetIntvToItemIntv, @@ -14,18 +15,20 @@ export const planFeaturesToItems = ({ }: { features: ApiPlanFeature[]; }): ProductItem[] => - features.map((feature) => + (features ?? []).map((feature) => ProductItemSchema.parse({ feature_id: feature.feature_id, included_usage: feature.unlimited ? Infinite : feature.granted, - interval: resetIntvToItemIntv(feature.reset_interval), + interval: resetIntvToItemIntv(feature.reset_interval as ResetInterval), interval_count: feature.reset_interval_count, config: { rollover: feature.rollover ? { max: feature.rollover.max, - duration: resetIntvToRollover(feature.rollover.expiry_duration_type), + duration: resetIntvToRollover( + feature.rollover.expiry_duration_type, + ), length: feature.rollover.expiry_duration_length ?? 0, } : undefined, diff --git a/server/src/internal/products/productUtils/apiPlanUtils/planToProductV2.ts b/server/src/internal/products/productUtils/apiPlanUtils/planToProductV2.ts index 5ea4ca17e..0bce0383b 100644 --- a/server/src/internal/products/productUtils/apiPlanUtils/planToProductV2.ts +++ b/server/src/internal/products/productUtils/apiPlanUtils/planToProductV2.ts @@ -1,27 +1,56 @@ -import { type ProductV2, ProductV2Schema } from "@autumn/shared"; +import { + type CreateProductV2Params, + CreateProductV2ParamsSchema, +} from "@autumn/shared"; import type { ApiPlan } from "@shared/api/products/apiPlan.js"; import { constructPriceItem } from "../../product-items/productItemUtils.js"; import { planFeaturesToItems } from "./planFeatureUtils/planFeaturesToItems.js"; -export const planToProductV2 = ({ plan }: { plan: ApiPlan }): ProductV2 => { - return ProductV2Schema.parse({ - id: plan.id, - name: plan.name, - is_add_on: plan.add_on, - is_default: plan.default, - version: plan.version, - group: plan.group, - items: [ - ...planFeaturesToItems({ - features: plan.features, - }), - plan.price - ? constructPriceItem({ - price: plan.price.amount, - interval: plan.price.interval, - }) - : undefined, - ], - archived: plan.archived, - }); +export const planToProductV2 = ({ + plan, +}: { + plan: ApiPlan; +}): CreateProductV2Params => { + try { + const featureItems = planFeaturesToItems({ + features: plan.features, + }); + + const items = [...featureItems]; + + // Only add price item if there isn't a price on a feature already (aka: a "base price" product) + const hasPriceItem = featureItems?.some( + (item) => typeof item.price === "number" && item.price > 0, + ); + + if (plan.price && !hasPriceItem) { + items.push( + constructPriceItem({ + price: plan.price.amount, + interval: plan.price.interval, + }), + ); + } + + return CreateProductV2ParamsSchema.parse({ + id: plan.id, + name: plan.name, + is_add_on: plan.add_on, + is_default: plan.default, + version: plan.version, + group: plan.group ?? "", + items, + free_trial: plan.free_trial + ? { + duration: plan.free_trial.duration_type, + length: plan.free_trial.duration_length, + unique_fingerprint: false, + card_required: plan.free_trial.card_required, + } + : null, + } satisfies CreateProductV2Params); + } catch (error) { + console.error("Error converting plan to product V2:", error); + throw error; + } }; diff --git a/server/src/internal/products/productUtils/apiPlanUtils/productV2ToPlan.ts b/server/src/internal/products/productUtils/apiPlanUtils/productV2ToPlan.ts index 9b739f81a..d85b78e87 100644 --- a/server/src/internal/products/productUtils/apiPlanUtils/productV2ToPlan.ts +++ b/server/src/internal/products/productUtils/apiPlanUtils/productV2ToPlan.ts @@ -2,9 +2,11 @@ import { type ApiPlan, ApiPlanSchema, type ApiProduct, + isPriceItem, + type ProductV2, + productV2ToBasePrice, } from "@autumn/shared"; import { itemsToPlanFeatures } from "./planFeatureUtils/itemsToPlanFeatures.js"; - /** * Convert Product V2 response format to Plan V2 format */ @@ -13,6 +15,10 @@ export const productV2ToPlan = ({ }: { product: ApiProduct; }): ApiPlan => { + const basePrice = productV2ToBasePrice({ product: product as ProductV2 }); + if (basePrice) + product.items = product.items?.filter((item) => !isPriceItem(item)) ?? []; + return ApiPlanSchema.parse({ // Basic fields id: product.id, @@ -25,24 +31,21 @@ export const productV2ToPlan = ({ add_on: product.is_add_on, default: product.is_default, - // Price - need to extract from items or set default - // This might need adjustment based on how price is determined - price: { - amount: 0, // You may need to calculate this from items - interval: "month", // Default, adjust as needed - }, + ...(basePrice ? { price: basePrice } : {}), // Convert items to features - features: product.items ? itemsToPlanFeatures({ items: product.items }) : [], + features: itemsToPlanFeatures({ items: product.items ?? [] }), // Free trial - might need schema conversion - free_trial: product.free_trial + ...(product.free_trial ? { - duration_type: product.free_trial.duration, - duration_length: product.free_trial.length, - card_required: product.free_trial.card_required ?? false, + free_trial: { + duration_type: product.free_trial.duration, + duration_length: product.free_trial.length, + card_required: product.free_trial.card_required ?? false, + }, } - : null, + : {}), // Misc fields created_at: product.created_at, @@ -50,4 +53,4 @@ export const productV2ToPlan = ({ archived: product.archived, base_variant_id: product.base_variant_id, }); -}; \ No newline at end of file +}; diff --git a/server/src/internal/products/productUtils/productResponseUtils/getPlanResponse.ts b/server/src/internal/products/productUtils/productResponseUtils/getPlanResponse.ts new file mode 100644 index 000000000..201327745 --- /dev/null +++ b/server/src/internal/products/productUtils/productResponseUtils/getPlanResponse.ts @@ -0,0 +1,209 @@ +import { + type ApiFreeTrialV2, + ApiFreeTrialV2Schema, + type ApiPlan, + ApiPlanSchema, + AttachScenario, + type BillingInterval, + type Feature, + type FeatureOptions, + type FullCustomer, + type FullProduct, + productV2ToBasePrice, + productV2ToFeatureItems, +} from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { getFreeTrialAfterFingerprint } from "../../free-trials/freeTrialUtils.js"; +import { sortProductItems } from "../../pricecn/pricecnUtils.js"; +import { mapToProductItems } from "../../productV2Utils.js"; +import { itemsToPlanFeatures } from "../apiPlanUtils/planFeatureUtils/itemsToPlanFeatures.js"; +import { getAttachScenario } from "./getAttachScenario.js"; + +/** + * Get free trial response in Plan V2 format + */ +const getFreeTrialV2Response = async ({ + db, + product, + fullCus, + attachScenario, +}: { + db?: DrizzleCli; + product: FullProduct; + fullCus?: FullCustomer; + attachScenario: AttachScenario; +}): Promise => { + if (!product.free_trial) return null; + + // Check trial availability if customer exists + if (db && fullCus) { + const trial = await getFreeTrialAfterFingerprint({ + db, + freeTrial: product.free_trial, + fingerprint: fullCus.fingerprint, + internalCustomerId: fullCus.internal_id, + multipleAllowed: false, + productId: product.id, + }); + + // No trial for downgrades + if (attachScenario === AttachScenario.Downgrade || !trial) { + return null; + } + } + + return ApiFreeTrialV2Schema.parse({ + duration_type: product.free_trial.duration, + duration_length: product.free_trial.length, + card_required: product.free_trial.card_required ?? false, + }); +}; + +/** + * Convert FullProduct (DB format) to Plan API response format + */ +export const getPlanResponse = async ({ + product, + features, + fullCus, + db, + options, +}: { + product: FullProduct; + features: Feature[]; + fullCus?: FullCustomer; + db?: DrizzleCli; + options?: FeatureOptions[]; +}): Promise => { + // 1. Convert prices/entitlements to items + const rawItems = mapToProductItems({ + prices: product.prices, + entitlements: product.entitlements, + features: features, + }); + + // 2. Sort items + const sortedItems = sortProductItems(rawItems, features); + + // 3. Create a ProductV2-like object for the helper + const productV2 = { items: sortedItems }; + + // 4. Extract base price using existing helper + const basePrice = productV2ToBasePrice({ product: productV2 as any }); + + // 5. Get feature items only (no base price) + const featureItems = productV2ToFeatureItems({ + items: sortedItems, + withBasePrice: false, // Don't include base price in features + }); + + // 6. Convert items to plan features + const planFeatures = itemsToPlanFeatures({ items: featureItems }); + + // 7. Get attach scenario for customer context + const attachScenario = getAttachScenario({ + fullCus, + fullProduct: product, + }); + + // 8. Get free trial in V2 format + const freeTrial = await getFreeTrialV2Response({ + db, + product, + fullCus, + attachScenario, + }); + + console.log( + `New feature:\n ${JSON.stringify( + { + // Basic fields + id: product.id, + name: product.name || "", + description: product.description, // Products don't have descriptions + group: product.group, + version: product.version, + + // Boolean flags + add_on: product.is_add_on, + default: product.is_default, + + // Price field (required in Plan schema) + price: basePrice + ? { + amount: basePrice.amount, + interval: basePrice.interval as unknown as BillingInterval, + } + : { + amount: 0, + interval: "month" as BillingInterval, + }, + + // Features array + features: planFeatures, + + // Free trial + free_trial: freeTrial, + + // Metadata fields + created_at: product.created_at, + env: product.env, + archived: product.archived, + base_variant_id: product.base_variant_id, + + // Customer context (optional) + // Uncomment when ready to add customer context + // customer_context: { + // trial_available: notNullish(freeTrial), + // scenario: attachScenario, + // }, + }, + null, + 4, + )}`, + ); + + // 9. Build Plan response + return ApiPlanSchema.parse({ + // Basic fields + id: product.id, + name: product.name || "", + description: product.description, // Products don't have descriptions + group: product.group, + version: product.version, + + // Boolean flags + add_on: product.is_add_on, + default: product.is_default, + + // Price field (required in Plan schema) + price: basePrice + ? { + amount: basePrice.amount, + interval: basePrice.interval as unknown as BillingInterval, + } + : { + amount: 0, + interval: "month" as BillingInterval, + }, + + // Features array + features: planFeatures, + + // Free trial + free_trial: freeTrial, + + // Metadata fields + created_at: product.created_at, + env: product.env, + archived: product.archived, + base_variant_id: product.base_variant_id, + + // Customer context (optional) + // Uncomment when ready to add customer context + // customer_context: { + // trial_available: notNullish(freeTrial), + // scenario: attachScenario, + // }, + }); +}; diff --git a/shared/api/products/apiPlan.ts b/shared/api/products/apiPlan.ts index 02250847b..e3e4b6d7a 100644 --- a/shared/api/products/apiPlan.ts +++ b/shared/api/products/apiPlan.ts @@ -5,17 +5,8 @@ import { BillingInterval } from "@models/productModels/priceModels/priceEnums.js import { z } from "zod/v4"; import { ApiPlanFeatureSchema } from "./planFeature/apiPlanFeature.js"; -export enum ResetInterval { - OneOff = "one_off", - Minute = "minute", - Hour = "hour", - Day = "day", - Week = "week", - Month = "month", - Quarter = "quarter", - SemiAnnual = "semi_annual", - Year = "year", -} +// Re-export for backward compatibility +export { ResetInterval } from "./planEnums.js"; export const ApiFreeTrialV2Schema = z.object({ duration_type: z.enum(FreeTrialDuration), @@ -23,6 +14,8 @@ export const ApiFreeTrialV2Schema = z.object({ card_required: z.boolean(), }); +export type ApiFreeTrialV2 = z.infer; + export const ApiPlanSchema = z.object({ id: z.string(), name: z.string(), @@ -40,7 +33,7 @@ export const ApiPlanSchema = z.object({ }), features: z.array(ApiPlanFeatureSchema), - free_trial: ApiFreeTrialV2Schema.nullable(), + free_trial: ApiFreeTrialV2Schema.nullable().optional(), // Misc created_at: z.number(), diff --git a/shared/api/products/planEnums.ts b/shared/api/products/planEnums.ts new file mode 100644 index 000000000..1928c050c --- /dev/null +++ b/shared/api/products/planEnums.ts @@ -0,0 +1,15 @@ +/** + * Shared enums for Plan API to avoid circular dependencies + */ + +export enum ResetInterval { + OneOff = "one_off", + Minute = "minute", + Hour = "hour", + Day = "day", + Week = "week", + Month = "month", + Quarter = "quarter", + SemiAnnual = "semi_annual", + Year = "year", +} \ No newline at end of file diff --git a/shared/api/products/planFeature/apiPlanFeature.ts b/shared/api/products/planFeature/apiPlanFeature.ts index 1b2ac74f2..a8bcb1da5 100644 --- a/shared/api/products/planFeature/apiPlanFeature.ts +++ b/shared/api/products/planFeature/apiPlanFeature.ts @@ -6,7 +6,7 @@ import { } from "@models/productV2Models/productItemModels/productItemEnums.js"; import { UsageModel } from "@models/productV2Models/productItemModels/productItemModels.js"; import { z } from "zod/v4"; -import { ResetInterval } from "../apiPlan.js"; +import { ResetInterval } from "../planEnums.js"; export const ApiPlanFeatureSchema = z .object({ @@ -14,49 +14,70 @@ export const ApiPlanFeatureSchema = z granted: z.number(), unlimited: z.boolean(), - reset_interval: z.enum(ResetInterval), + reset_interval: z.enum(ResetInterval).optional(), reset_interval_count: z.number().optional(), - reset_usage_on_enabled: z.boolean(), + reset_usage_on_enabled: z.boolean().optional(), - price: z.object({ - amount: z.number().optional(), - tiers: z.array(UsageTierSchema).optional(), + price: z + .object({ + amount: z.number().optional(), + tiers: z.array(UsageTierSchema).optional(), - interval: z.enum(BillingInterval), - interval_count: z.number().optional(), + interval: z.enum(BillingInterval), + interval_count: z.number().optional(), - billing_units: z.number(), - usage_model: z.enum(UsageModel), + billing_units: z.number(), + usage_model: z.enum(UsageModel), - // Use max_purchase for pay per use features - max_purchase: z.number(), - }), + // Use max_purchase for pay per use features + max_purchase: z.number().optional(), + }) + .optional(), - proration: z.object({ - on_increase: z.enum(OnIncrease), - on_decrease: z.enum(OnDecrease), - }), + proration: z + .object({ + on_increase: z.enum(OnIncrease).optional(), + on_decrease: z.enum(OnDecrease).optional(), + }) + .optional(), - rollover: z.object({ - max: z.number(), - expiry_duration_type: z.enum(ResetInterval), - expiry_duration_length: z.number().optional(), - }), + rollover: z + .object({ + max: z.number().nullable(), + expiry_duration_type: z.enum(ResetInterval), + expiry_duration_length: z.number().optional(), + }) + .optional(), }) .check((ctx) => { const resetGroup = ctx.value.reset_interval || ctx.value.reset_interval_count !== undefined; const intervalGroup = - ctx.value.price.interval || ctx.value.price.interval_count !== undefined; + ctx.value.price?.interval || + ctx.value.price?.interval_count !== undefined; if (resetGroup && intervalGroup) { ctx.issues.push({ code: "custom", message: - "reset_interval/reset_interval_count and interval/interval_count are mutually exclusive.", + "Top level reset intervals and feature price intervals are mutually exclusive.", input: ctx.value, }); } + + if (ctx.value.price) { + if ( + ctx.value.price.amount && + ctx.value.price.tiers && + ctx.value.price.tiers.length > 0 + ) { + ctx.issues.push({ + code: "custom", + message: "Price amount and tiers are mutually exclusive.", + input: ctx.value, + }); + } + } }); // .refine((x) => { diff --git a/shared/api/versionUtils/ApiVersion.ts b/shared/api/versionUtils/ApiVersion.ts index 0f7897165..538c32017 100644 --- a/shared/api/versionUtils/ApiVersion.ts +++ b/shared/api/versionUtils/ApiVersion.ts @@ -16,4 +16,4 @@ export type ApiVersionString = `${ApiVersion}`; export const API_VERSIONS = Object.values(ApiVersion); -export const LATEST_VERSION = ApiVersion.V1_2; +export const LATEST_VERSION = ApiVersion.V2; diff --git a/shared/api/versionUtils/convertVersionUtils.ts b/shared/api/versionUtils/convertVersionUtils.ts index 09a592d52..a4fe84b9e 100644 --- a/shared/api/versionUtils/convertVersionUtils.ts +++ b/shared/api/versionUtils/convertVersionUtils.ts @@ -124,13 +124,16 @@ export function createdAtToVersion({ }: { createdAt?: number; }): ApiVersionClass { + const v2_0 = new Date("2025-10-15").getTime(); const v1_2 = new Date("2025-05-05").getTime(); const v1_1 = new Date("2025-04-17").getTime(); const v0_2 = new Date("2025-01-30").getTime(); let version: ApiVersion; - if (!createdAt || createdAt >= v1_2) { + if (!createdAt || createdAt >= v2_0) { + version = ApiVersion.V2; + } else if (createdAt >= v1_2) { version = ApiVersion.V1_2; } else if (createdAt >= v1_1) { version = ApiVersion.V1_1; @@ -140,7 +143,8 @@ export function createdAtToVersion({ version = ApiVersion.V0_1; } - return new ApiVersionClass(version); + // return new ApiVersionClass(version); + return new ApiVersionClass(ApiVersion.V2); } // Convert org creation date diff --git a/shared/index.ts b/shared/index.ts index 61724e443..e83e5d73b 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -150,4 +150,17 @@ export * from "./utils/productV2Utils/compareProductUtils.ts/compareProductUtils export * from "./utils/productV2Utils/productItemUtils/getProductItemRes.js"; export * from "./utils/productV2Utils/productItemUtils/itemIntervalUtils.js"; export * from "./utils/productV3Utils/mapToProductV3.js"; +export * from "./utils/productV3Utils/productItemUtils/productV3ItemUtils.js"; export * from "./utils/rewardUtils/rewardMigrationUtils.js"; + +export enum ResetInterval { + OneOff = "one_off", + Minute = "minute", + Hour = "hour", + Day = "day", + Week = "week", + Month = "month", + Quarter = "quarter", + SemiAnnual = "semi_annual", + Year = "year", +} diff --git a/shared/models/productModels/productModels.ts b/shared/models/productModels/productModels.ts index 71e78cd0c..36a78d61a 100644 --- a/shared/models/productModels/productModels.ts +++ b/shared/models/productModels/productModels.ts @@ -50,12 +50,9 @@ export const UpdateProductSchema = z.object({ }); export const FullProductSchema = ProductSchema.extend({ + description: z.string().nullable().optional().default(null), prices: z.array(PriceSchema), - entitlements: z.array( - EntitlementSchema.extend({ - feature: FeatureSchema, - }), - ), + entitlements: z.array(EntitlementSchema.extend({ feature: FeatureSchema })), free_trial: FreeTrialSchema.nullish(), free_trials: z.array(FreeTrialSchema).nullish(), free_trial_ids: z.array(z.string()).nullish(), diff --git a/shared/models/productModels/productTable.ts b/shared/models/productModels/productTable.ts index e5a9767bf..cb0053037 100644 --- a/shared/models/productModels/productTable.ts +++ b/shared/models/productModels/productTable.ts @@ -1,3 +1,4 @@ +import { sql } from "drizzle-orm"; import { boolean, foreignKey, @@ -7,9 +8,8 @@ import { text, unique, } from "drizzle-orm/pg-core"; -import { sql } from "drizzle-orm"; -import { organizations } from "../orgModels/orgTable.js"; import { collatePgColumn, sqlNow } from "../../db/utils.js"; +import { organizations } from "../orgModels/orgTable.js"; type ProductProcessor = { type: string; @@ -22,6 +22,7 @@ export const products = pgTable( internal_id: text("internal_id").primaryKey().notNull(), id: text().notNull(), name: text(), + description: text(), org_id: text("org_id").notNull(), created_at: numeric({ mode: "number" }).notNull().default(sqlNow), env: text().notNull(), diff --git a/shared/models/productV3Models/productV3Response.ts b/shared/models/productV3Models/productV3Response.ts index 90b714094..4738819c6 100644 --- a/shared/models/productV3Models/productV3Response.ts +++ b/shared/models/productV3Models/productV3Response.ts @@ -1,5 +1,4 @@ import { z } from "zod/v4"; -import { ApiProductPropertiesSchema } from "../../api/products/apiProduct.js"; import { AttachScenario } from "../checkModels/checkPreviewModels.js"; import { AppEnv } from "../genModels/genEnums.js"; import { ProductItemInterval } from "../productV2Models/productItemModels/productItemModels.js"; @@ -30,5 +29,4 @@ export const PlanResponseSchema = z.object({ // base_variant_id: z.string().nullable(), scenario: z.nativeEnum(AttachScenario).optional(), - properties: ApiProductPropertiesSchema.optional(), });