feat: 🎸 wip
This commit is contained in:
@@ -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({
|
||||
// const productResponse = await getProductResponse({
|
||||
// product: newFullProduct,
|
||||
// features,
|
||||
// });
|
||||
|
||||
try {
|
||||
const planResponse = await getPlanResponse({
|
||||
product: newFullProduct,
|
||||
features,
|
||||
});
|
||||
|
||||
return c.json(productResponse);
|
||||
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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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,34 +35,34 @@ 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,
|
||||
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,
|
||||
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,
|
||||
interval_count: item.interval_count ?? undefined,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
: {}),
|
||||
|
||||
// Convert rollover config
|
||||
...(item.config?.rollover
|
||||
? [
|
||||
{
|
||||
? {
|
||||
rollover: {
|
||||
max: item.config.rollover.max ?? null,
|
||||
expiry_duration_type: rolloverToResetIntv(
|
||||
@@ -65,25 +70,22 @@ export const itemsToPlanFeatures = ({
|
||||
),
|
||||
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,
|
||||
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),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
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: [
|
||||
...planFeaturesToItems({
|
||||
features: plan.features,
|
||||
}),
|
||||
plan.price
|
||||
? constructPriceItem({
|
||||
price: plan.price.amount,
|
||||
interval: plan.price.interval,
|
||||
})
|
||||
: undefined,
|
||||
],
|
||||
archived: plan.archived,
|
||||
});
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
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,
|
||||
|
||||
@@ -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<ApiFreeTrialV2 | null> => {
|
||||
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<ApiPlan> => {
|
||||
// 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,
|
||||
// },
|
||||
});
|
||||
};
|
||||
@@ -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<typeof ApiFreeTrialV2Schema>;
|
||||
|
||||
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(),
|
||||
|
||||
15
shared/api/products/planEnums.ts
Normal file
15
shared/api/products/planEnums.ts
Normal file
@@ -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",
|
||||
}
|
||||
@@ -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,11 +14,12 @@ 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({
|
||||
price: z
|
||||
.object({
|
||||
amount: z.number().optional(),
|
||||
tiers: z.array(UsageTierSchema).optional(),
|
||||
|
||||
@@ -29,34 +30,54 @@ export const ApiPlanFeatureSchema = z
|
||||
usage_model: z.enum(UsageModel),
|
||||
|
||||
// Use max_purchase for pay per use features
|
||||
max_purchase: z.number(),
|
||||
}),
|
||||
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(),
|
||||
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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user