feat: moved update product to new router
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { Context, Env, Handler, MiddlewareHandler } from "hono";
|
||||
import type { ZodType, z } from "zod/v4";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import { validator } from "./validatorMiddleware.js";
|
||||
|
||||
@@ -59,6 +60,7 @@ export function createRoute<
|
||||
>(opts: {
|
||||
body?: Body;
|
||||
query?: Query;
|
||||
withTx?: boolean;
|
||||
handler: (
|
||||
c: ValidatedContext<HonoEnv, Body, Query>,
|
||||
) => Response | Promise<Response>;
|
||||
@@ -73,7 +75,19 @@ export function createRoute<
|
||||
}
|
||||
|
||||
const wrappedHandler = async (c: ValidatedContext<HonoEnv, Body, Query>) => {
|
||||
return await opts.handler(c);
|
||||
if (opts.withTx) {
|
||||
const db = c.get("ctx").db;
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
c.set("ctx", {
|
||||
...c.get("ctx"),
|
||||
db: tx as unknown as DrizzleCli,
|
||||
});
|
||||
return await opts.handler(c);
|
||||
});
|
||||
} else {
|
||||
return await opts.handler(c);
|
||||
}
|
||||
};
|
||||
|
||||
return [...middlewares, wrappedHandler as Handler] as const;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { orgConfigMiddleware } from "./honoMiddlewares/orgConfigMiddleware.js";
|
||||
import { secretKeyMiddleware } from "./honoMiddlewares/secretKeyMiddleware.js";
|
||||
import { traceMiddleware } from "./honoMiddlewares/traceMiddleware.js";
|
||||
import type { HonoEnv } from "./honoUtils/HonoEnv.js";
|
||||
import { honoProductRouter } from "./internal/products/honoProductRouter.js";
|
||||
import { honoProductRouter } from "./internal/products/productRouter.js";
|
||||
import { auth } from "./utils/auth.js";
|
||||
|
||||
const ALLOWED_ORIGINS = [
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { constructProduct } from "@/internal/products/productUtils.js";
|
||||
import {
|
||||
AppEnv,
|
||||
CreateProductSchema,
|
||||
EntInsertSchema,
|
||||
Entitlement,
|
||||
Feature,
|
||||
Price,
|
||||
Product,
|
||||
ProductV2,
|
||||
type Entitlement,
|
||||
type Feature,
|
||||
type Price,
|
||||
type Product,
|
||||
type ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { constructProduct } from "@/internal/products/productUtils.js";
|
||||
|
||||
export const parseChatProducts = async ({
|
||||
db,
|
||||
@@ -25,13 +25,13 @@ export const parseChatProducts = async ({
|
||||
orgId: string;
|
||||
chatProducts: ProductV2[];
|
||||
}) => {
|
||||
let products: Product[] = [];
|
||||
const products: Product[] = [];
|
||||
|
||||
let allPrices: Price[] = [];
|
||||
let allEnts: Entitlement[] = [];
|
||||
const allPrices: Price[] = [];
|
||||
const allEnts: Entitlement[] = [];
|
||||
|
||||
for (const product of chatProducts) {
|
||||
let backendProduct: Product = constructProduct({
|
||||
const backendProduct: Product = constructProduct({
|
||||
productData: CreateProductSchema.parse({
|
||||
...product,
|
||||
}),
|
||||
@@ -39,7 +39,7 @@ export const parseChatProducts = async ({
|
||||
env: AppEnv.Sandbox,
|
||||
});
|
||||
|
||||
let { prices, entitlements } = await handleNewProductItems({
|
||||
const { prices, entitlements } = await handleNewProductItems({
|
||||
db,
|
||||
curPrices: [],
|
||||
curEnts: [],
|
||||
|
||||
@@ -19,7 +19,7 @@ export const validateOneOffTrial = async ({
|
||||
freeTrial,
|
||||
}: {
|
||||
prices: Price[];
|
||||
freeTrial: FreeTrial | null;
|
||||
freeTrial: FreeTrial | CreateFreeTrial | null;
|
||||
}) => {
|
||||
if (isOneOff(prices) && freeTrial) {
|
||||
throw new RecaseError({
|
||||
@@ -194,5 +194,5 @@ export const handleNewFreeTrial = async ({
|
||||
});
|
||||
}
|
||||
|
||||
return createdFreeTrial;
|
||||
return createdFreeTrial as FreeTrial;
|
||||
};
|
||||
|
||||
@@ -1,95 +1,45 @@
|
||||
import {
|
||||
type CreateProductParams,
|
||||
CreateProductParamsSchema,
|
||||
type CreateProductV2Params,
|
||||
CreateProductV2ParamsSchema,
|
||||
type Entitlement,
|
||||
type FreeTrial,
|
||||
type Product,
|
||||
type FullProduct,
|
||||
type Price,
|
||||
ProductAlreadyExistsError,
|
||||
type ProductItem,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import { getEntsWithFeature } from "../entitlements/entitlementUtils.js";
|
||||
import {
|
||||
handleNewFreeTrial,
|
||||
validateOneOffTrial,
|
||||
} from "../free-trials/freeTrialUtils.js";
|
||||
import { ProductService } from "../ProductService.js";
|
||||
import { handleNewProductItems } from "../product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { isDefaultTrial } from "../productUtils/classifyProduct.js";
|
||||
import { getGroupToDefaults } from "../productUtils.js";
|
||||
|
||||
const validateCreateProduct = async ({
|
||||
ctx,
|
||||
body,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
body: CreateProductParams;
|
||||
}) => {
|
||||
// const { free_trial, items } = body;
|
||||
const { org, env, db, features } = ctx;
|
||||
|
||||
// const productData = CreateProductSchema.parse(req.body);
|
||||
|
||||
// validateId("Product", productData.id);
|
||||
|
||||
// if (nullish(req.body.name)) {
|
||||
// productData.name = keyToTitle(productData.id);
|
||||
// }
|
||||
|
||||
const existing = await ProductService.get({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
id: body.id,
|
||||
});
|
||||
|
||||
// 1. If existing product, throw error
|
||||
if (existing) {
|
||||
throw new ProductAlreadyExistsError({ productId: body.id });
|
||||
}
|
||||
|
||||
// 2. Validate items if exist
|
||||
|
||||
// if (items && !Array.isArray(items)) {
|
||||
// throw new RecaseError({
|
||||
// message: "Items must be an array",
|
||||
// code: ErrCode.InvalidRequest,
|
||||
// });
|
||||
// } else if (items) {
|
||||
// validateProductItems({
|
||||
// newItems: items,
|
||||
// features,
|
||||
// orgId: req.orgId,
|
||||
// env: req.env,
|
||||
// });
|
||||
// }
|
||||
|
||||
// 3. Validate free trial if exist
|
||||
// let freeTrial: FreeTrial | null = null;
|
||||
// if (notNullish(free_trial)) {
|
||||
// // console.log("Free trial before:", free_trial);
|
||||
// freeTrial = validateAndInitFreeTrial({
|
||||
// freeTrial: free_trial,
|
||||
// internalProductId: productData.id,
|
||||
// isCustom: false,
|
||||
// });
|
||||
// // console.log("Free trial after:", freeTrial);
|
||||
// }
|
||||
|
||||
return {
|
||||
features,
|
||||
// freeTrial,
|
||||
// productData,
|
||||
};
|
||||
};
|
||||
import { getProductResponse } from "../productUtils/productResponseUtils/getProductResponse.js";
|
||||
import {
|
||||
constructProduct,
|
||||
getGroupToDefaults,
|
||||
initProductInStripe,
|
||||
} from "../productUtils.js";
|
||||
|
||||
export const disableCurrentDefault = async ({
|
||||
req,
|
||||
newProduct,
|
||||
items,
|
||||
freeTrial,
|
||||
// items,
|
||||
// freeTrial,
|
||||
}: {
|
||||
req: AutumnContext;
|
||||
newProduct: Product;
|
||||
items: ProductItem[];
|
||||
freeTrial: FreeTrial;
|
||||
newProduct: CreateProductV2Params;
|
||||
// items: ProductItem[];
|
||||
// freeTrial: FreeTrial;
|
||||
}) => {
|
||||
const { db, org, env, logger } = req;
|
||||
|
||||
let defaultProds = await ProductService.listDefault({
|
||||
db,
|
||||
orgId: org.id,
|
||||
@@ -104,13 +54,7 @@ export const disableCurrentDefault = async ({
|
||||
defaultProds,
|
||||
})?.[newProduct.group];
|
||||
|
||||
const willBeDefaultTrial = isDefaultTrial({
|
||||
product: {
|
||||
...newProduct,
|
||||
free_trial: freeTrial,
|
||||
items: items || [],
|
||||
},
|
||||
});
|
||||
const willBeDefaultTrial = isDefaultTrial({ product: newProduct });
|
||||
|
||||
if (willBeDefaultTrial) {
|
||||
// Disable current default trial
|
||||
@@ -142,100 +86,102 @@ export const disableCurrentDefault = async ({
|
||||
* Route: POST /products - Create a product
|
||||
*/
|
||||
export const createProduct = createRoute({
|
||||
body: CreateProductParamsSchema,
|
||||
body: CreateProductV2ParamsSchema,
|
||||
handler: async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
const query = c.req.valid("query");
|
||||
const ctx = c.get("ctx");
|
||||
// const query = c.req.valid("query");
|
||||
|
||||
const { logger, org, features, env, db } = ctx;
|
||||
const { items } = body;
|
||||
|
||||
// const { features, freeTrial, productData } = await validateCreateProduct({
|
||||
// ctx,
|
||||
// body,
|
||||
// });
|
||||
const existing = await ProductService.get({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
id: body.id,
|
||||
});
|
||||
|
||||
// const newProduct = constructProduct({
|
||||
// productData,
|
||||
// orgId: org.id,
|
||||
// env,
|
||||
// });
|
||||
// 1. If existing product, throw error
|
||||
if (existing) throw new ProductAlreadyExistsError({ productId: body.id });
|
||||
|
||||
return c.json({});
|
||||
await disableCurrentDefault({
|
||||
req: ctx,
|
||||
newProduct: body,
|
||||
});
|
||||
|
||||
// await disableCurrentDefault({
|
||||
// req,
|
||||
// newProduct,
|
||||
// items,
|
||||
// freeTrial: freeTrial || null,
|
||||
// });
|
||||
const product = await ProductService.insert({
|
||||
db,
|
||||
product: constructProduct({
|
||||
productData: body,
|
||||
orgId: org.id,
|
||||
env,
|
||||
}),
|
||||
});
|
||||
|
||||
// const product = await ProductService.insert({ db, product: newProduct });
|
||||
const { items, free_trial } = body;
|
||||
|
||||
// let prices: Price[] = [];
|
||||
// let entitlements: Entitlement[] = [];
|
||||
// if (notNullish(items)) {
|
||||
// const res = await handleNewProductItems({
|
||||
// db,
|
||||
// product,
|
||||
// features,
|
||||
// curPrices: [],
|
||||
// curEnts: [],
|
||||
// newItems: items,
|
||||
// logger,
|
||||
// isCustom: false,
|
||||
// newVersion: false,
|
||||
// });
|
||||
// prices = res.prices;
|
||||
// entitlements = res.entitlements;
|
||||
// }
|
||||
let prices: Price[] = [];
|
||||
let entitlements: Entitlement[] = [];
|
||||
if (items) {
|
||||
const res = await handleNewProductItems({
|
||||
db,
|
||||
product,
|
||||
features,
|
||||
curPrices: [],
|
||||
curEnts: [],
|
||||
newItems: items,
|
||||
logger,
|
||||
isCustom: false,
|
||||
newVersion: false,
|
||||
});
|
||||
prices = res.prices;
|
||||
entitlements = res.entitlements;
|
||||
}
|
||||
|
||||
// await validateOneOffTrial({
|
||||
// prices,
|
||||
// freeTrial: freeTrial || null,
|
||||
// });
|
||||
await validateOneOffTrial({
|
||||
prices,
|
||||
freeTrial: free_trial || null,
|
||||
});
|
||||
|
||||
// await initProductInStripe({
|
||||
// db,
|
||||
// product: {
|
||||
// ...product,
|
||||
// prices,
|
||||
// entitlements,
|
||||
// } as FullProduct,
|
||||
// org,
|
||||
// env,
|
||||
// logger,
|
||||
// });
|
||||
let newFreeTrial: FreeTrial | null = null;
|
||||
if (free_trial) {
|
||||
newFreeTrial =
|
||||
(await handleNewFreeTrial({
|
||||
db,
|
||||
newFreeTrial: free_trial,
|
||||
curFreeTrial: null,
|
||||
internalProductId: product.internal_id,
|
||||
isCustom: false,
|
||||
})) || null;
|
||||
}
|
||||
|
||||
// if (notNullish(freeTrial)) {
|
||||
// await handleNewFreeTrial({
|
||||
// db,
|
||||
// newFreeTrial: freeTrial,
|
||||
// curFreeTrial: null,
|
||||
// internalProductId: product.internal_id,
|
||||
// isCustom: false,
|
||||
// });
|
||||
// }
|
||||
const newFullProduct: FullProduct = {
|
||||
...product,
|
||||
prices,
|
||||
entitlements: getEntsWithFeature({ ents: entitlements, features }),
|
||||
free_trial: newFreeTrial,
|
||||
};
|
||||
|
||||
// await addTaskToQueue({
|
||||
// jobName: JobName.DetectBaseVariant,
|
||||
// payload: {
|
||||
// curProduct: {
|
||||
// ...product,
|
||||
// prices,
|
||||
// entitlements: [],
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
await initProductInStripe({
|
||||
db,
|
||||
product: newFullProduct,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
// res.status(200).json(
|
||||
// APIProductSchema.parse({
|
||||
// ...product,
|
||||
// autumn_id: product.internal_id,
|
||||
// items: items || [],
|
||||
// free_trial: freeTrial,
|
||||
// }),
|
||||
// );
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.DetectBaseVariant,
|
||||
payload: {
|
||||
curProduct: newFullProduct,
|
||||
},
|
||||
});
|
||||
|
||||
const productResponse = await getProductResponse({
|
||||
product: newFullProduct,
|
||||
features,
|
||||
});
|
||||
|
||||
return c.json(productResponse);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,214 +1,214 @@
|
||||
import {
|
||||
ErrCode,
|
||||
CreateProductV2ParamsSchema,
|
||||
mapToProductV2,
|
||||
notNullish,
|
||||
ProductNotFoundError,
|
||||
ProductV2Schema,
|
||||
RecaseError,
|
||||
UpdateProductQuerySchema,
|
||||
UpdateProductSchema,
|
||||
UpdateProductV2ParamsSchema,
|
||||
} from "@autumn/shared";
|
||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { handleNewFreeTrial } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { validateOneOffTrial } from "../../free-trials/freeTrialUtils.js";
|
||||
import { ProductService } from "../../ProductService.js";
|
||||
import { productsAreSame } from "../../productUtils/compareProductUtils.js";
|
||||
import { initProductInStripe } from "../../productUtils.js";
|
||||
import { mapToProductItems } from "../../productV2Utils.js";
|
||||
import {
|
||||
disableCurrentDefault,
|
||||
handleCreateProduct,
|
||||
} from "../handleCreateProduct_old.js";
|
||||
handleNewFreeTrial,
|
||||
validateOneOffTrial,
|
||||
} from "../../free-trials/freeTrialUtils.js";
|
||||
import { ProductService } from "../../ProductService.js";
|
||||
import { handleNewProductItems } from "../../product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { productsAreSame } from "../../productUtils/compareProductUtils.js";
|
||||
import { getProductResponse } from "../../productUtils/productResponseUtils/getProductResponse.js";
|
||||
import { initProductInStripe } from "../../productUtils.js";
|
||||
import { disableCurrentDefault } from "../handleCreateProduct.js";
|
||||
import { handleVersionProductV2 } from "../handleVersionProduct.js";
|
||||
import { handleUpdateProductDetails } from "./updateProductDetails.js";
|
||||
|
||||
export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
routeHandler({
|
||||
req,
|
||||
res,
|
||||
action: "Update product",
|
||||
handler: async () => {
|
||||
const { productId } = req.params;
|
||||
const { version, upsert, disable_version } = req.query;
|
||||
const { orgId, env, logger, db } = req;
|
||||
export const handleUpdateProductV2 = createRoute({
|
||||
body: UpdateProductV2ParamsSchema,
|
||||
query: UpdateProductQuerySchema,
|
||||
handler: async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
const ctx = c.get("ctx");
|
||||
const productId = c.req.param("productId");
|
||||
|
||||
const [features, org, fullProduct, rewardPrograms, _defaultProds] =
|
||||
await Promise.all([
|
||||
FeatureService.getFromReq(req),
|
||||
OrgService.getFromReq(req),
|
||||
ProductService.getFull({
|
||||
db,
|
||||
idOrInternalId: productId,
|
||||
orgId,
|
||||
env,
|
||||
version: version ? parseInt(version) : undefined,
|
||||
allowNotFound: upsert === "true",
|
||||
}),
|
||||
RewardProgramService.getByProductId({
|
||||
db,
|
||||
productIds: [productId],
|
||||
orgId,
|
||||
env,
|
||||
}),
|
||||
ProductService.listDefault({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
}),
|
||||
]);
|
||||
const { db, org, env, features, logger } = ctx;
|
||||
const { version, upsert, disable_version } = c.req.valid("query");
|
||||
// const { productId } = req.params;
|
||||
// const { orgId, env, logger, db } = req;
|
||||
|
||||
if (!fullProduct) {
|
||||
if (upsert === "true") {
|
||||
await handleCreateProduct(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ProductNotFoundError({ productId: productId });
|
||||
}
|
||||
|
||||
const cusProductsCurVersion =
|
||||
await CusProductService.getByInternalProductId({
|
||||
db,
|
||||
internalProductId: fullProduct.internal_id,
|
||||
});
|
||||
|
||||
const cusProductExists = cusProductsCurVersion.length > 0;
|
||||
|
||||
await disableCurrentDefault({
|
||||
req,
|
||||
newProduct: {
|
||||
...fullProduct,
|
||||
...req.body,
|
||||
},
|
||||
items:
|
||||
req.body.items ||
|
||||
mapToProductItems({
|
||||
prices: fullProduct.prices,
|
||||
entitlements: fullProduct.entitlements,
|
||||
features,
|
||||
}),
|
||||
freeTrial: req.body.free_trial || fullProduct.free_trial || null,
|
||||
});
|
||||
|
||||
await handleUpdateProductDetails({
|
||||
const [fullProduct, rewardPrograms, _defaultProds] = await Promise.all([
|
||||
ProductService.getFull({
|
||||
db,
|
||||
curProduct: fullProduct,
|
||||
newProduct: UpdateProductSchema.parse(req.body),
|
||||
newFreeTrial: req.body.free_trial,
|
||||
items: req.body.items,
|
||||
org,
|
||||
rewardPrograms,
|
||||
logger,
|
||||
idOrInternalId: productId,
|
||||
orgId: org.id,
|
||||
env,
|
||||
version: version ? parseInt(version) : undefined,
|
||||
allowNotFound: upsert === "true",
|
||||
}),
|
||||
RewardProgramService.getByProductId({
|
||||
db,
|
||||
productIds: [productId],
|
||||
orgId: org.id,
|
||||
env,
|
||||
}),
|
||||
ProductService.listDefault({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!fullProduct) throw new ProductNotFoundError({ productId: productId });
|
||||
|
||||
// // How to go into another route handler?
|
||||
// if (upsert === "true") await handleCreateProduct(c);
|
||||
|
||||
// Start a transaction?
|
||||
|
||||
const cusProductsCurVersion =
|
||||
await CusProductService.getByInternalProductId({
|
||||
db,
|
||||
internalProductId: fullProduct.internal_id,
|
||||
});
|
||||
|
||||
const itemsExist = notNullish(req.body.items);
|
||||
if (cusProductExists && itemsExist) {
|
||||
if (disable_version === "true") {
|
||||
throw new RecaseError({
|
||||
message: "Cannot auto save product as there are existing customers",
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
const curProductV2 = mapToProductV2({
|
||||
product: fullProduct,
|
||||
features,
|
||||
});
|
||||
|
||||
const { itemsSame, freeTrialsSame } = productsAreSame({
|
||||
newProductV2: req.body,
|
||||
curProductV1: fullProduct,
|
||||
features,
|
||||
});
|
||||
const productSame = itemsSame && freeTrialsSame;
|
||||
const cusProductExists = cusProductsCurVersion.length > 0;
|
||||
|
||||
if (!productSame) {
|
||||
await handleVersionProductV2({
|
||||
req,
|
||||
res,
|
||||
latestProduct: fullProduct,
|
||||
org,
|
||||
env,
|
||||
items: req.body.items,
|
||||
freeTrial: req.body.free_trial,
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.status(200).send(fullProduct);
|
||||
return;
|
||||
}
|
||||
await disableCurrentDefault({
|
||||
req: ctx,
|
||||
newProduct: CreateProductV2ParamsSchema.parse({
|
||||
...fullProduct,
|
||||
...body,
|
||||
}),
|
||||
});
|
||||
|
||||
const { items, free_trial } = req.body;
|
||||
await handleUpdateProductDetails({
|
||||
db,
|
||||
curProduct: fullProduct,
|
||||
newProduct: UpdateProductSchema.parse(body),
|
||||
newFreeTrial: body.free_trial || curProductV2.free_trial || undefined,
|
||||
items: body.items || curProductV2.items,
|
||||
org,
|
||||
rewardPrograms,
|
||||
logger: ctx.logger,
|
||||
});
|
||||
|
||||
if (free_trial !== undefined) {
|
||||
await validateOneOffTrial({
|
||||
prices: fullProduct.prices,
|
||||
freeTrial: free_trial,
|
||||
const itemsExist = notNullish(body.items);
|
||||
|
||||
if (cusProductExists && itemsExist) {
|
||||
const newProductV2 = ProductV2Schema.parse({
|
||||
...body,
|
||||
items: body.items || [],
|
||||
});
|
||||
|
||||
if (disable_version === "true") {
|
||||
throw new RecaseError({
|
||||
message: "Cannot auto save product as there are existing customers",
|
||||
});
|
||||
}
|
||||
|
||||
const { itemsSame, freeTrialsSame } = productsAreSame({
|
||||
newProductV2,
|
||||
curProductV1: fullProduct,
|
||||
features,
|
||||
});
|
||||
|
||||
const productSame = itemsSame && freeTrialsSame;
|
||||
|
||||
if (!productSame) {
|
||||
const newProduct = await handleVersionProductV2({
|
||||
ctx,
|
||||
newProductV2,
|
||||
latestProduct: fullProduct,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
return c.json(newProduct);
|
||||
}
|
||||
|
||||
return c.json(fullProduct);
|
||||
}
|
||||
|
||||
const { free_trial } = body;
|
||||
|
||||
if (body.items) {
|
||||
await handleNewProductItems({
|
||||
db,
|
||||
curPrices: fullProduct.prices,
|
||||
curEnts: fullProduct.entitlements,
|
||||
newItems: items,
|
||||
newItems: body.items,
|
||||
features,
|
||||
product: fullProduct,
|
||||
logger,
|
||||
logger: ctx.logger,
|
||||
isCustom: false,
|
||||
});
|
||||
}
|
||||
|
||||
// New full product
|
||||
const newFullProduct = await ProductService.getFull({
|
||||
db,
|
||||
idOrInternalId: fullProduct.id,
|
||||
orgId,
|
||||
env,
|
||||
// New full product
|
||||
const newFullProduct = await ProductService.getFull({
|
||||
db,
|
||||
idOrInternalId: fullProduct.id,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
if (free_trial !== undefined) {
|
||||
await validateOneOffTrial({
|
||||
prices: newFullProduct.prices,
|
||||
freeTrial: free_trial,
|
||||
});
|
||||
|
||||
if (free_trial !== undefined) {
|
||||
await validateOneOffTrial({
|
||||
prices: newFullProduct.prices,
|
||||
freeTrial: free_trial,
|
||||
});
|
||||
|
||||
await handleNewFreeTrial({
|
||||
db,
|
||||
curFreeTrial: fullProduct.free_trial,
|
||||
newFreeTrial: free_trial,
|
||||
internalProductId: fullProduct.internal_id,
|
||||
isCustom: false,
|
||||
product: fullProduct,
|
||||
});
|
||||
}
|
||||
|
||||
// New full product
|
||||
await initProductInStripe({
|
||||
await handleNewFreeTrial({
|
||||
db,
|
||||
curFreeTrial: fullProduct.free_trial,
|
||||
newFreeTrial: free_trial,
|
||||
internalProductId: fullProduct.internal_id,
|
||||
isCustom: false,
|
||||
product: fullProduct,
|
||||
});
|
||||
}
|
||||
|
||||
// New full product
|
||||
await initProductInStripe({
|
||||
db,
|
||||
product: newFullProduct,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info("Adding task to queue to detect base variant");
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.DetectBaseVariant,
|
||||
payload: {
|
||||
curProduct: newFullProduct,
|
||||
},
|
||||
});
|
||||
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.RewardMigration,
|
||||
payload: {
|
||||
oldPrices: fullProduct.prices,
|
||||
productId: fullProduct.id,
|
||||
orgId: org.id,
|
||||
env,
|
||||
},
|
||||
});
|
||||
|
||||
return c.json(
|
||||
getProductResponse({
|
||||
product: newFullProduct,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info("Adding task to queue to detect base variant");
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.DetectBaseVariant,
|
||||
payload: {
|
||||
curProduct: newFullProduct,
|
||||
},
|
||||
});
|
||||
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.RewardMigration,
|
||||
payload: {
|
||||
oldPrices: fullProduct.prices,
|
||||
productId: fullProduct.id,
|
||||
orgId: org.id,
|
||||
env,
|
||||
},
|
||||
});
|
||||
res.status(200).send({ message: "Product updated" });
|
||||
return;
|
||||
},
|
||||
});
|
||||
features,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
ErrCode,
|
||||
ProductNotFoundError,
|
||||
UpdateProductSchema,
|
||||
} from "@autumn/shared";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { handleNewFreeTrial } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { validateOneOffTrial } from "../../free-trials/freeTrialUtils.js";
|
||||
import { ProductService } from "../../ProductService.js";
|
||||
import { productsAreSame } from "../../productUtils/compareProductUtils.js";
|
||||
import { initProductInStripe } from "../../productUtils.js";
|
||||
import { mapToProductItems } from "../../productV2Utils.js";
|
||||
import {
|
||||
disableCurrentDefault,
|
||||
handleCreateProduct,
|
||||
} from "../handleCreateProduct_old.js";
|
||||
import { handleVersionProductV2 } from "../handleVersionProduct.js";
|
||||
import { handleUpdateProductDetails } from "./updateProductDetails.js";
|
||||
|
||||
export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
routeHandler({
|
||||
req,
|
||||
res,
|
||||
action: "Update product",
|
||||
handler: async () => {
|
||||
const { productId } = req.params;
|
||||
const { version, upsert, disable_version } = req.query;
|
||||
const { orgId, env, logger, db } = req;
|
||||
|
||||
const [features, org, fullProduct, rewardPrograms, _defaultProds] =
|
||||
await Promise.all([
|
||||
FeatureService.getFromReq(req),
|
||||
OrgService.getFromReq(req),
|
||||
ProductService.getFull({
|
||||
db,
|
||||
idOrInternalId: productId,
|
||||
orgId,
|
||||
env,
|
||||
version: version ? parseInt(version) : undefined,
|
||||
allowNotFound: upsert === "true",
|
||||
}),
|
||||
RewardProgramService.getByProductId({
|
||||
db,
|
||||
productIds: [productId],
|
||||
orgId,
|
||||
env,
|
||||
}),
|
||||
ProductService.listDefault({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!fullProduct) {
|
||||
if (upsert === "true") {
|
||||
await handleCreateProduct(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ProductNotFoundError({ productId: productId });
|
||||
}
|
||||
|
||||
const cusProductsCurVersion =
|
||||
await CusProductService.getByInternalProductId({
|
||||
db,
|
||||
internalProductId: fullProduct.internal_id,
|
||||
});
|
||||
|
||||
const cusProductExists = cusProductsCurVersion.length > 0;
|
||||
|
||||
await disableCurrentDefault({
|
||||
req,
|
||||
newProduct: {
|
||||
...fullProduct,
|
||||
...req.body,
|
||||
},
|
||||
items:
|
||||
req.body.items ||
|
||||
mapToProductItems({
|
||||
prices: fullProduct.prices,
|
||||
entitlements: fullProduct.entitlements,
|
||||
features,
|
||||
}),
|
||||
freeTrial: req.body.free_trial || fullProduct.free_trial || null,
|
||||
});
|
||||
|
||||
await handleUpdateProductDetails({
|
||||
db,
|
||||
curProduct: fullProduct,
|
||||
newProduct: UpdateProductSchema.parse(req.body),
|
||||
newFreeTrial: req.body.free_trial,
|
||||
items: req.body.items,
|
||||
org,
|
||||
rewardPrograms,
|
||||
logger,
|
||||
});
|
||||
|
||||
const itemsExist = notNullish(req.body.items);
|
||||
if (cusProductExists && itemsExist) {
|
||||
if (disable_version === "true") {
|
||||
throw new RecaseError({
|
||||
message: "Cannot auto save product as there are existing customers",
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const { itemsSame, freeTrialsSame } = productsAreSame({
|
||||
newProductV2: req.body,
|
||||
curProductV1: fullProduct,
|
||||
features,
|
||||
});
|
||||
const productSame = itemsSame && freeTrialsSame;
|
||||
|
||||
if (!productSame) {
|
||||
await handleVersionProductV2({
|
||||
req,
|
||||
res,
|
||||
latestProduct: fullProduct,
|
||||
org,
|
||||
env,
|
||||
items: req.body.items,
|
||||
freeTrial: req.body.free_trial,
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.status(200).send(fullProduct);
|
||||
return;
|
||||
}
|
||||
|
||||
const { items, free_trial } = req.body;
|
||||
|
||||
if (free_trial !== undefined) {
|
||||
await validateOneOffTrial({
|
||||
prices: fullProduct.prices,
|
||||
freeTrial: free_trial,
|
||||
});
|
||||
}
|
||||
|
||||
await handleNewProductItems({
|
||||
db,
|
||||
curPrices: fullProduct.prices,
|
||||
curEnts: fullProduct.entitlements,
|
||||
newItems: items,
|
||||
features,
|
||||
product: fullProduct,
|
||||
logger,
|
||||
isCustom: false,
|
||||
});
|
||||
|
||||
// New full product
|
||||
const newFullProduct = await ProductService.getFull({
|
||||
db,
|
||||
idOrInternalId: fullProduct.id,
|
||||
orgId,
|
||||
env,
|
||||
});
|
||||
|
||||
if (free_trial !== undefined) {
|
||||
await validateOneOffTrial({
|
||||
prices: newFullProduct.prices,
|
||||
freeTrial: free_trial,
|
||||
});
|
||||
|
||||
await handleNewFreeTrial({
|
||||
db,
|
||||
curFreeTrial: fullProduct.free_trial,
|
||||
newFreeTrial: free_trial,
|
||||
internalProductId: fullProduct.internal_id,
|
||||
isCustom: false,
|
||||
product: fullProduct,
|
||||
});
|
||||
}
|
||||
|
||||
// New full product
|
||||
await initProductInStripe({
|
||||
db,
|
||||
product: newFullProduct,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info("Adding task to queue to detect base variant");
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.DetectBaseVariant,
|
||||
payload: {
|
||||
curProduct: newFullProduct,
|
||||
},
|
||||
});
|
||||
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.RewardMigration,
|
||||
payload: {
|
||||
oldPrices: fullProduct.prices,
|
||||
productId: fullProduct.id,
|
||||
orgId: org.id,
|
||||
env,
|
||||
},
|
||||
});
|
||||
res.status(200).send({ message: "Product updated" });
|
||||
return;
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
type AppEnv,
|
||||
type CreateFreeTrial,
|
||||
type FreeTrial,
|
||||
type FullProduct,
|
||||
isFreeProductV2,
|
||||
@@ -15,7 +16,6 @@ import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { isStripeConnected } from "@/internal/orgs/orgUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { FreeTrialService } from "../../free-trials/FreeTrialService.js";
|
||||
import { ProductService } from "../../ProductService.js";
|
||||
import { usagePriceToProductName } from "../../prices/priceUtils/usagePriceUtils/convertUsagePrice.js";
|
||||
import {
|
||||
@@ -125,7 +125,7 @@ const willBeDefaultTrial = ({
|
||||
}: {
|
||||
newProduct: UpdateProduct;
|
||||
curProduct: FullProduct;
|
||||
newFreeTrial: FreeTrial;
|
||||
newFreeTrial?: FreeTrial | CreateFreeTrial;
|
||||
newItems: ProductItem[];
|
||||
}) => {
|
||||
// 1. Get final default
|
||||
@@ -157,7 +157,7 @@ export const handleUpdateProductDetails = async ({
|
||||
db: DrizzleCli;
|
||||
curProduct: FullProduct;
|
||||
newProduct: UpdateProduct;
|
||||
newFreeTrial: FreeTrial;
|
||||
newFreeTrial?: FreeTrial | CreateFreeTrial;
|
||||
items: ProductItem[];
|
||||
org: Organization;
|
||||
rewardPrograms: RewardProgram[];
|
||||
@@ -170,11 +170,6 @@ export const handleUpdateProductDetails = async ({
|
||||
env: curProduct.env as AppEnv,
|
||||
});
|
||||
|
||||
const trialConfig = await FreeTrialService.getByProductId({
|
||||
db,
|
||||
productId: curProduct.internal_id,
|
||||
});
|
||||
|
||||
// Should error if:
|
||||
// - New product is a default product
|
||||
// - Org is not allowed to have paid default products
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import {
|
||||
type AppEnv,
|
||||
CreateProductSchema,
|
||||
type FreeTrial,
|
||||
CreateProductV2ParamsSchema,
|
||||
type FullProduct,
|
||||
type Organization,
|
||||
type ProductItem,
|
||||
type ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js";
|
||||
import { handleNewFreeTrial } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
@@ -22,48 +21,45 @@ import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import { getEntsWithFeature } from "../entitlements/entitlementUtils.js";
|
||||
|
||||
export const handleVersionProductV2 = async ({
|
||||
req,
|
||||
res,
|
||||
ctx,
|
||||
newProductV2,
|
||||
latestProduct,
|
||||
org,
|
||||
env,
|
||||
items,
|
||||
freeTrial,
|
||||
// items,
|
||||
// freeTrial,
|
||||
}: {
|
||||
req: any;
|
||||
res: any;
|
||||
ctx: AutumnContext;
|
||||
newProductV2: ProductV2;
|
||||
latestProduct: FullProduct;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
items: ProductItem[];
|
||||
freeTrial: FreeTrial;
|
||||
// items: ProductItem[];
|
||||
// freeTrial: FreeTrial;
|
||||
}) => {
|
||||
const { db } = req;
|
||||
const { db, features } = ctx;
|
||||
|
||||
const curVersion = latestProduct.version;
|
||||
const newVersion = curVersion + 1;
|
||||
|
||||
const features = await FeatureService.getFromReq(req);
|
||||
|
||||
console.log(
|
||||
`Updating product ${latestProduct.id} version from ${curVersion} to ${newVersion}`,
|
||||
);
|
||||
|
||||
const newProduct = constructProduct({
|
||||
productData: CreateProductSchema.parse({
|
||||
productData: CreateProductV2ParamsSchema.parse({
|
||||
...latestProduct,
|
||||
...req.body,
|
||||
...newProductV2,
|
||||
version: newVersion,
|
||||
}),
|
||||
orgId: org.id,
|
||||
env: latestProduct.env as AppEnv,
|
||||
processor: latestProduct.processor,
|
||||
baseVariantId: latestProduct.base_variant_id,
|
||||
processor: latestProduct.processor || undefined,
|
||||
});
|
||||
|
||||
// Validate product items...
|
||||
validateProductItems({
|
||||
newItems: items,
|
||||
newItems: newProductV2.items,
|
||||
features,
|
||||
orgId: org.id,
|
||||
env,
|
||||
@@ -85,7 +81,7 @@ export const handleVersionProductV2 = async ({
|
||||
db,
|
||||
curPrices: latestProduct.prices,
|
||||
curEnts: latestProduct.entitlements,
|
||||
newItems: items,
|
||||
newItems: newProductV2.items,
|
||||
features,
|
||||
product: newProduct,
|
||||
logger: console,
|
||||
@@ -104,27 +100,16 @@ export const handleVersionProductV2 = async ({
|
||||
});
|
||||
|
||||
// Handle new free trial
|
||||
if (freeTrial) {
|
||||
if (newProductV2.free_trial) {
|
||||
await handleNewFreeTrial({
|
||||
db,
|
||||
newFreeTrial: freeTrial,
|
||||
newFreeTrial: newProductV2.free_trial,
|
||||
curFreeTrial: null,
|
||||
internalProductId: newProduct.internal_id,
|
||||
isCustom: false,
|
||||
});
|
||||
}
|
||||
|
||||
// await addTaskToQueue({
|
||||
// jobName: JobName.DetectBaseVariant,
|
||||
// payload: {
|
||||
// curProduct: {
|
||||
// ...newProduct,
|
||||
// // prices: customPrices,
|
||||
// // entitlements: getEntsWithFeature({ ents: customEnts, features }),
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
await initProductInStripe({
|
||||
db,
|
||||
product: {
|
||||
@@ -142,16 +127,10 @@ export const handleVersionProductV2 = async ({
|
||||
payload: {
|
||||
oldPrices: latestProduct.prices,
|
||||
productId: latestProduct.id,
|
||||
// newPrices: customPrices,
|
||||
// product: {
|
||||
// ...newProduct,
|
||||
// prices: customPrices,
|
||||
// entitlements: getEntsWithFeature({ ents: customEnts, features }),
|
||||
// },
|
||||
orgId: org.id,
|
||||
env,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).send(newProduct);
|
||||
return newProduct;
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Hono } from "hono";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import { createProduct } from "./handlers/handleCreateProduct.js";
|
||||
|
||||
// Create a Hono app for products
|
||||
export const honoProductRouter = new Hono<HonoEnv>();
|
||||
|
||||
// POST /products - Create a product
|
||||
honoProductRouter.post("", ...createProduct);
|
||||
@@ -14,7 +14,6 @@ import { handleDeleteProduct } from "./handlers/handleDeleteProduct.js";
|
||||
import { handleGetProduct } from "./handlers/handleGetProduct.js";
|
||||
import { handleGetProductDeleteInfo } from "./handlers/handleGetProductDeleteInfo.js";
|
||||
import { handleListProductsBeta } from "./handlers/handleListProductsBeta.js";
|
||||
import { handleUpdateProductV2 } from "./handlers/handleUpdateProduct/handleUpdateProduct.js";
|
||||
import { productsAreSame } from "./productUtils/compareProductUtils.js";
|
||||
|
||||
export const productBetaRouter: Router = Router();
|
||||
@@ -24,11 +23,9 @@ export const productRouter: Router = Router();
|
||||
|
||||
productRouter.get("", handleListProductsBeta);
|
||||
|
||||
// productRouter.post("", handleCreateProduct);
|
||||
|
||||
productRouter.get("/:productId", handleGetProduct);
|
||||
|
||||
productRouter.post("/:productId", handleUpdateProductV2);
|
||||
// productRouter.post("/:productId", handleUpdateProductV2);
|
||||
|
||||
productRouter.delete("/:productId", handleDeleteProduct);
|
||||
|
||||
@@ -148,3 +145,15 @@ productRouter.get("/:productId/has_customers", async (req: any, res: any) =>
|
||||
);
|
||||
|
||||
productRouter.get("/:productId/deletion_info", handleGetProductDeleteInfo);
|
||||
|
||||
import { Hono } from "hono";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import { createProduct } from "./handlers/handleCreateProduct.js";
|
||||
import { handleUpdateProductV2 } from "./handlers/handleUpdateProduct/handleUpdateProduct.js";
|
||||
|
||||
// Create a Hono app for products
|
||||
export const honoProductRouter = new Hono<HonoEnv>();
|
||||
|
||||
// POST /products - Create a product
|
||||
honoProductRouter.post("", ...createProduct);
|
||||
honoProductRouter.post("/:productId", ...handleUpdateProductV2);
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
type AppEnv,
|
||||
BillingInterval,
|
||||
BillingType,
|
||||
type CreateProduct,
|
||||
type CreateProductV2Params,
|
||||
EntInterval,
|
||||
type Entitlement,
|
||||
EntitlementSchema,
|
||||
@@ -75,21 +75,28 @@ export const constructProduct = ({
|
||||
orgId,
|
||||
env,
|
||||
processor,
|
||||
baseVariantId,
|
||||
}: {
|
||||
productData: CreateProduct;
|
||||
productData: CreateProductV2Params;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
processor?: any;
|
||||
baseVariantId?: string | null;
|
||||
processor?: {
|
||||
id: string;
|
||||
type: string;
|
||||
};
|
||||
}) => {
|
||||
const newProduct: Product = {
|
||||
...productData,
|
||||
org_id: orgId,
|
||||
id: productData.id,
|
||||
name: productData.name,
|
||||
is_add_on: productData.is_add_on,
|
||||
is_default: productData.is_default,
|
||||
version: productData.version || 1,
|
||||
group: productData.group,
|
||||
|
||||
env,
|
||||
processor,
|
||||
internal_id: generateId("prod"),
|
||||
org_id: orgId,
|
||||
created_at: Date.now(),
|
||||
processor,
|
||||
base_variant_id: null,
|
||||
archived: false,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { FullProduct, Price, ProductV2 } from "@autumn/shared";
|
||||
import type {
|
||||
CreateProductParams,
|
||||
FullProduct,
|
||||
Price,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { pricesOnlyOneOff } from "../prices/priceUtils.js";
|
||||
import { isFeatureItem } from "../product-items/productItemUtils/getItemType.js";
|
||||
import { isFreeProduct } from "../productUtils.js";
|
||||
@@ -31,15 +36,19 @@ export const isMainProduct = ({
|
||||
return !prodIsAddOn({ product }) && !oneOffOrAddOn({ product, prices });
|
||||
};
|
||||
|
||||
export const isFreeProductV2 = ({ product }: { product: ProductV2 }) => {
|
||||
return product.items.every((item) => isFeatureItem(item));
|
||||
export const isFreeProductV2 = ({
|
||||
product,
|
||||
}: {
|
||||
product: ProductV2 | CreateProductParams;
|
||||
}) => {
|
||||
return (product.items || []).every((item) => isFeatureItem(item));
|
||||
};
|
||||
|
||||
export const isDefaultTrial = ({
|
||||
product,
|
||||
skipDefault = false,
|
||||
}: {
|
||||
product: ProductV2;
|
||||
product: ProductV2 | CreateProductParams;
|
||||
skipDefault?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import {
|
||||
constructBooleanFeature,
|
||||
constructMeteredFeature,
|
||||
} from "@/internal/features/utils/constructFeatureUtils.js";
|
||||
import {
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
CouponDurationType,
|
||||
CreateFreeTrial,
|
||||
type CreateFreeTrial,
|
||||
CreateFreeTrialSchema,
|
||||
CreateReward,
|
||||
type CreateReward,
|
||||
FeatureUsageType,
|
||||
FreeTrial,
|
||||
FreeTrialDuration,
|
||||
ProductItem,
|
||||
ProductV2,
|
||||
type ProductItem,
|
||||
type ProductV2,
|
||||
RewardType,
|
||||
} from "@autumn/shared";
|
||||
import { keyToTitle } from "../genUtils.js";
|
||||
import {
|
||||
constructBooleanFeature,
|
||||
constructMeteredFeature,
|
||||
} from "@/internal/features/utils/constructFeatureUtils.js";
|
||||
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
|
||||
import { keyToTitle } from "../genUtils.js";
|
||||
|
||||
export enum TestFeatureType {
|
||||
Boolean = "boolean",
|
||||
@@ -139,11 +138,11 @@ export const constructProduct = ({
|
||||
);
|
||||
}
|
||||
|
||||
let id_ =
|
||||
const id_ =
|
||||
id ||
|
||||
(isAnnual ? `${type}-annual` : interval ? `${type}-${interval}` : type);
|
||||
|
||||
let product: ProductV2 = {
|
||||
const product: ProductV2 = {
|
||||
id: id_,
|
||||
name: id
|
||||
? keyToTitle(id)
|
||||
|
||||
@@ -5,7 +5,9 @@ import { z } from "zod/v4";
|
||||
|
||||
export const CreateProductItemParamsSchema = ProductItemSchema;
|
||||
|
||||
export const CreateProductParamsSchema = z.object({
|
||||
// Base product params
|
||||
|
||||
export const CreateProductV2ParamsSchema = z.object({
|
||||
id: z.string().nonempty().regex(idRegex),
|
||||
|
||||
name: z.string().refine((val) => val.length > 0, {
|
||||
@@ -18,7 +20,32 @@ export const CreateProductParamsSchema = z.object({
|
||||
group: z.string().default(""),
|
||||
|
||||
items: z.array(CreateProductItemParamsSchema).optional(),
|
||||
free_trial: CreateFreeTrialSchema.optional(),
|
||||
free_trial: CreateFreeTrialSchema.nullish().default(null),
|
||||
});
|
||||
|
||||
export type CreateProductParams = z.infer<typeof CreateProductParamsSchema>;
|
||||
export const UpdateProductV2ParamsSchema = CreateProductV2ParamsSchema.extend({
|
||||
id: z.string().nonempty().regex(idRegex).optional(),
|
||||
name: z
|
||||
.string()
|
||||
.refine((val) => val.length > 0, {
|
||||
message: "name must be a non-empty string",
|
||||
})
|
||||
.optional(),
|
||||
|
||||
is_add_on: z.boolean().optional(),
|
||||
is_default: z.boolean().optional(),
|
||||
version: z.number().optional(),
|
||||
group: z.string().optional(),
|
||||
|
||||
// items: z.array(CreateProductItemParamsSchema).optional(),
|
||||
free_trial: CreateFreeTrialSchema.nullish(),
|
||||
});
|
||||
|
||||
export const UpdateProductQuerySchema = z.object({
|
||||
version: z.string().optional(),
|
||||
upsert: z.string().optional(),
|
||||
disable_version: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CreateProductV2Params = z.infer<typeof CreateProductV2ParamsSchema>;
|
||||
export type UpdateProductV2Params = z.infer<typeof UpdateProductV2ParamsSchema>;
|
||||
|
||||
@@ -13,7 +13,7 @@ export const ProductSchema = z.object({
|
||||
version: z.number(),
|
||||
group: z.string(),
|
||||
|
||||
env: z.nativeEnum(AppEnv),
|
||||
env: z.enum(AppEnv),
|
||||
internal_id: z.string(),
|
||||
org_id: z.string(),
|
||||
created_at: z.number(),
|
||||
|
||||
@@ -36,6 +36,174 @@ paths:
|
||||
group:
|
||||
default: ""
|
||||
type: string
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
feature_id:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
feature_type:
|
||||
anyOf:
|
||||
- type: string
|
||||
enum:
|
||||
- single_use
|
||||
- continuous_use
|
||||
- boolean
|
||||
- static
|
||||
- type: "null"
|
||||
included_usage:
|
||||
anyOf:
|
||||
- anyOf:
|
||||
- type: number
|
||||
- type: string
|
||||
const: inf
|
||||
- type: "null"
|
||||
interval:
|
||||
anyOf:
|
||||
- type: string
|
||||
enum:
|
||||
- minute
|
||||
- hour
|
||||
- day
|
||||
- week
|
||||
- month
|
||||
- quarter
|
||||
- semi_annual
|
||||
- year
|
||||
- type: "null"
|
||||
interval_count:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
entity_feature_id:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
usage_model:
|
||||
anyOf:
|
||||
- type: string
|
||||
enum:
|
||||
- prepaid
|
||||
- pay_per_use
|
||||
- type: "null"
|
||||
price:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
tiers:
|
||||
anyOf:
|
||||
- type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
to:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: string
|
||||
const: inf
|
||||
amount:
|
||||
type: number
|
||||
required:
|
||||
- to
|
||||
- amount
|
||||
- type: "null"
|
||||
billing_units:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
usage_limit:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
reset_usage_when_enabled:
|
||||
anyOf:
|
||||
- type: boolean
|
||||
- type: "null"
|
||||
config:
|
||||
anyOf:
|
||||
- type: object
|
||||
properties:
|
||||
on_increase:
|
||||
anyOf:
|
||||
- type: string
|
||||
enum:
|
||||
- bill_immediately
|
||||
- prorate_immediately
|
||||
- prorate_next_cycle
|
||||
- bill_next_cycle
|
||||
- type: "null"
|
||||
on_decrease:
|
||||
anyOf:
|
||||
- type: string
|
||||
enum:
|
||||
- prorate
|
||||
- prorate_immediately
|
||||
- prorate_next_cycle
|
||||
- none
|
||||
- no_prorations
|
||||
- type: "null"
|
||||
rollover:
|
||||
anyOf:
|
||||
- type: object
|
||||
properties:
|
||||
max:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
duration:
|
||||
default: month
|
||||
type: string
|
||||
enum:
|
||||
- month
|
||||
- forever
|
||||
length:
|
||||
type: number
|
||||
required:
|
||||
- max
|
||||
- length
|
||||
- type: "null"
|
||||
- type: "null"
|
||||
created_at:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
entitlement_id:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
price_id:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
price_config:
|
||||
anyOf:
|
||||
- {}
|
||||
- type: "null"
|
||||
free_trial:
|
||||
type: object
|
||||
properties:
|
||||
length:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: number
|
||||
unique_fingerprint:
|
||||
default: false
|
||||
type: boolean
|
||||
duration:
|
||||
default: day
|
||||
type: string
|
||||
enum:
|
||||
- day
|
||||
- month
|
||||
- year
|
||||
card_required:
|
||||
default: true
|
||||
type: boolean
|
||||
required:
|
||||
- length
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import type { ProductV2 } from "@autumn/shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { WarningBox } from "@/components/general/modal-components/WarningBox";
|
||||
import { ToggleButton } from "@/components/general/ToggleButton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -6,19 +12,11 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
|
||||
import { ProductService } from "@/services/products/ProductService";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { getBackendErr, navigateTo } from "@/utils/genUtils";
|
||||
import { ProductConfig } from "../../ProductConfig";
|
||||
import { ProductV2 } from "@autumn/shared";
|
||||
import { ToggleButton } from "@/components/general/ToggleButton";
|
||||
import { WarningBox } from "@/components/general/modal-components/WarningBox";
|
||||
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
|
||||
|
||||
const defaultProduct = {
|
||||
name: "",
|
||||
|
||||
Reference in New Issue
Block a user