diff --git a/bun.lock b/bun.lock index 6ad887e2b..c5cdb35e4 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,7 @@ "@date-fns/tz": "^1.2.0", "@date-fns/utc": "^2.1.0", "@hono/node-server": "^1.19.5", + "@hono/zod-validator": "^0.7.3", "@hyperbrowser/sdk": "^0.54.0", "@hyperdx/node-opentelemetry": "^0.8.2", "@logtail/node": "^0.5.2", @@ -450,6 +451,8 @@ "@hono/node-server": ["@hono/node-server@1.19.5", "", { "peerDependencies": { "hono": "^4" } }, "sha512-iBuhh+uaaggeAuf+TftcjZyWh2GEgZcVGXkNtskLVoWaXhnJtC5HLHrU8W1KHDoucqO1MswwglmkWLFyiDn4WQ=="], + "@hono/zod-validator": ["@hono/zod-validator@0.7.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], diff --git a/package.json b/package.json index b3a5c3104..55fedbf04 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,9 @@ "dev:wsl": "concurrently \"cd server && npm run dev\" \"cd vite && npm run dev\" \"cd shared && npm run dev\"", "setup": "node setup.js", "setupci": "node setupci", - "db:push": " bun -F @autumn/shared db:push", "db:generate": "bun -F @autumn/shared db:generate", "db:migrate": " bun -F @autumn/shared db:migrate", - "docker:up": "docker compose -f docker-compose.dev.yml up --build", "docker:up:unix": "docker compose -f docker-compose.unix.yml up --build", "docker:up:ci": "docker compose -f docker-compose.ci.yml up --build", diff --git a/server/package.json b/server/package.json index a835e68ef..18b365688 100644 --- a/server/package.json +++ b/server/package.json @@ -33,6 +33,7 @@ "@date-fns/tz": "^1.2.0", "@date-fns/utc": "^2.1.0", "@hono/node-server": "^1.19.5", + "@hono/zod-validator": "^0.7.3", "@hyperbrowser/sdk": "^0.54.0", "@hyperdx/node-opentelemetry": "^0.8.2", "@logtail/node": "^0.5.2", diff --git a/server/src/honoMiddlewares/baseMiddleware.ts b/server/src/honoMiddlewares/baseMiddleware.ts index c499b77f1..4c45ca7d2 100644 --- a/server/src/honoMiddlewares/baseMiddleware.ts +++ b/server/src/honoMiddlewares/baseMiddleware.ts @@ -3,7 +3,7 @@ import type { Context, Next } from "hono"; import { db } from "@/db/initDrizzle.js"; import { ClickHouseManager } from "@/external/clickhouse/ClickHouseManager.js"; import { logger } from "@/external/logtail/logtailUtils.js"; -import type { HonoEnv } from "@/initHono.js"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; import { generateId } from "@/utils/genUtils.js"; /** diff --git a/server/src/honoMiddlewares/errorMiddleware.ts b/server/src/honoMiddlewares/errorMiddleware.ts index c1d076cf9..9743a655b 100644 --- a/server/src/honoMiddlewares/errorMiddleware.ts +++ b/server/src/honoMiddlewares/errorMiddleware.ts @@ -1,4 +1,4 @@ -import { AutumnError, ErrCode } from "@autumn/shared"; +import { ErrCode, RecaseError as SharedRecaseError } from "@autumn/shared"; import type { Context } from "hono"; import type { ContentfulStatusCode } from "hono/utils/http-status"; import Stripe from "stripe"; @@ -127,7 +127,7 @@ export const errorMiddleware = (err: Error, c: Context) => { if (specialCaseResponse) return specialCaseResponse; // 1. Handle RecaseError (our custom errors) - if (err instanceof RecaseError || err instanceof AutumnError) { + if (err instanceof RecaseError || err instanceof SharedRecaseError) { logger.warn( `RECASE WARNING (${ctx.org?.slug || "unknown"}): ${err.message} [${err.code}]`, { diff --git a/server/src/honoMiddlewares/routeHandler.ts b/server/src/honoMiddlewares/routeHandler.ts new file mode 100644 index 000000000..e40138539 --- /dev/null +++ b/server/src/honoMiddlewares/routeHandler.ts @@ -0,0 +1,80 @@ +import type { Context, Env, Handler, MiddlewareHandler } from "hono"; +import type { ZodType, z } from "zod/v4"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { validator } from "./validatorMiddleware.js"; + +/** + * Extended context type that includes validated input + * This tells TypeScript what data is available after validation + */ +type ValidatedContext< + E extends Env, + Body extends ZodType | undefined = undefined, + Query extends ZodType | undefined = undefined, +> = Context< + E, + any, + { + in: { + json: Body extends ZodType ? z.infer : unknown; + query: Query extends ZodType ? z.infer : unknown; + }; + out: { + json: Body extends ZodType ? z.infer : unknown; + query: Query extends ZodType ? z.infer : unknown; + }; + } +>; + +/** + * Create a type-safe route with validation that preserves full type inference! + * By typing the context parameter, TypeScript knows what's available on c.req.valid() + * + * @example + * ```ts + * export const createProduct = createRoute({ + * body: CreateProductParamsSchema, + * handler: async (c) => { + * const body = c.req.valid("json"); // ✅ Fully typed from schema! + * return c.json({ success: true }); + * } + * }); + * + * // With query validation too: + * export const listProducts = createRoute({ + * query: ListProductsQuerySchema, + * handler: async (c) => { + * const query = c.req.valid("query"); // ✅ Fully typed! + * return c.json({ products: [] }); + * } + * }); + * + * // In router: + * honoProductRouter.post("", ...createProduct); + * ``` + */ +export function createRoute< + Body extends ZodType | undefined = undefined, + Query extends ZodType | undefined = undefined, +>(opts: { + body?: Body; + query?: Query; + handler: ( + c: ValidatedContext, + ) => Response | Promise; +}) { + const middlewares: MiddlewareHandler[] = []; + + if (opts.body) { + middlewares.push(validator("json", opts.body)); + } + if (opts.query) { + middlewares.push(validator("query", opts.query)); + } + + const wrappedHandler = async (c: ValidatedContext) => { + return await opts.handler(c); + }; + + return [...middlewares, wrappedHandler as Handler] as const; +} diff --git a/server/src/honoMiddlewares/validatorMiddleware.ts b/server/src/honoMiddlewares/validatorMiddleware.ts new file mode 100644 index 000000000..aa8706341 --- /dev/null +++ b/server/src/honoMiddlewares/validatorMiddleware.ts @@ -0,0 +1,30 @@ +import { zValidator } from "@hono/zod-validator"; +import type { ZodType } from "zod/v4"; + +/** + * Custom validator that wraps @hono/zod-validator with error throwing behavior + * This allows the errorMiddleware to handle validation errors consistently + * Maintains full type inference from zValidator + * + * Usage: + * ```ts + * router.post( + * "/products", + * validator("json", CreateProductSchema), + * async (c) => { + * const body = c.req.valid("json"); // Fully typed! + * // ... handler logic + * } + * ); + * ``` + */ +export const validator = ( + target: "json" | "query" | "param" | "header" | "form", + schema: T, +) => { + return zValidator(target, schema, (result, _c) => { + if (!result.success) { + throw result.error; + } + }); +}; diff --git a/server/src/honoUtils/HonoEnv.ts b/server/src/honoUtils/HonoEnv.ts new file mode 100644 index 000000000..69863c3f1 --- /dev/null +++ b/server/src/honoUtils/HonoEnv.ts @@ -0,0 +1,30 @@ +import type { AppEnv, AuthType, Feature, Organization } from "@autumn/shared"; +import type { ClickHouseClient } from "@clickhouse/client"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { Logger } from "@/external/logtail/logtailUtils.js"; + +export type RequestContext = { + // Variables + org: Organization; + env: AppEnv; + features: Feature[]; + userId?: string; + + // Objects + db: DrizzleCli; + logger: Logger; + clickhouseClient: ClickHouseClient; + + // Info + id: string; + isPublic: boolean; + authType: AuthType; + apiVersion: string; + timestamp: number; +}; + +export type AutumnContext = RequestContext; + +export type HonoEnv = { + Variables: { ctx: AutumnContext }; +}; diff --git a/server/src/initHono.ts b/server/src/initHono.ts index 2f1b06a73..f9f57e497 100644 --- a/server/src/initHono.ts +++ b/server/src/initHono.ts @@ -1,43 +1,16 @@ -import type { AppEnv, AuthType, Feature, Organization } from "@autumn/shared"; -import type { ClickHouseClient } from "@clickhouse/client"; import { getRequestListener } from "@hono/node-server"; import { Hono } from "hono"; import { cors } from "hono/cors"; -import type { DrizzleCli } from "./db/initDrizzle.js"; -import type { Logger } from "./external/logtail/logtailUtils.js"; import { apiVersionMiddleware } from "./honoMiddlewares/apiVersionMiddleware.js"; import { baseMiddleware } from "./honoMiddlewares/baseMiddleware.js"; import { errorMiddleware } from "./honoMiddlewares/errorMiddleware.js"; import { orgConfigMiddleware } from "./honoMiddlewares/orgConfigMiddleware.js"; import { secretKeyMiddleware } from "./honoMiddlewares/secretKeyMiddleware.js"; import { traceMiddleware } from "./honoMiddlewares/traceMiddleware.js"; -import { handleCreateProduct } from "./internal/products/honoProductRouter.js"; +import type { HonoEnv } from "./honoUtils/HonoEnv.js"; +import { honoProductRouter } from "./internal/products/honoProductRouter.js"; import { auth } from "./utils/auth.js"; -type RequestContext = { - // Variables - org: Organization; - env: AppEnv; - features: Feature[]; - userId?: string; - - // Objects - db: DrizzleCli; - logger: Logger; - clickhouseClient: ClickHouseClient; - - // Info - id: string; - isPublic: boolean; - authType: AuthType; - apiVersion: string; - timestamp: number; -}; - -export type HonoEnv = { - Variables: { ctx: RequestContext }; -}; - const ALLOWED_ORIGINS = [ "http://localhost:3000", "http://localhost:5173", @@ -103,8 +76,7 @@ export const createHonoApp = () => { app.use("/v1/*", orgConfigMiddleware); // Step 6: Add pricing middleware, analytics middleware, etc. - - app.post("/v1/products", handleCreateProduct); + app.route("v1/products", honoProductRouter); // Error handler - must be defined after all routes and middleware app.onError(errorMiddleware); diff --git a/server/src/internal/api/entitled/checkRouter.ts b/server/src/internal/api/entitled/checkRouter.ts index 247b4fb3a..3baf40a6e 100644 --- a/server/src/internal/api/entitled/checkRouter.ts +++ b/server/src/internal/api/entitled/checkRouter.ts @@ -70,7 +70,7 @@ checkRouter.post("", async (req: any, res: any) => { if (notNullish(requiredBalance)) { const floatQuantity = parseFloat(requiredBalance); - if (isNaN(floatQuantity)) { + if (Number.isNaN(floatQuantity)) { throw new RecaseError({ message: "Invalid required_balance", code: ErrCode.InvalidRequest, diff --git a/server/src/internal/customers/CusService.ts b/server/src/internal/customers/CusService.ts index d18e37f6a..dad6711b4 100644 --- a/server/src/internal/customers/CusService.ts +++ b/server/src/internal/customers/CusService.ts @@ -5,6 +5,7 @@ import { type Customer, customers, type EntityExpand, + ErrCode, type FullCusProduct, type FullCustomer, type Organization, @@ -13,7 +14,6 @@ import { trace } from "@opentelemetry/api"; import { and, eq, ilike, or, sql } from "drizzle-orm"; import { StatusCodes } from "http-status-codes"; import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { ErrCode } from "@/errors/errCodes.js"; import RecaseError from "@/utils/errorUtils.js"; import { withSpan } from "../analytics/tracer/spanUtils.js"; import { RELEVANT_STATUSES } from "./cusProducts/CusProductService.js"; diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index 3e2a7a19b..bac676590 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -1,7 +1,7 @@ import { APIVersion, type AttachConfig, - AutumnError, + RecaseError, SuccessCode, } from "@autumn/shared"; import type Stripe from "stripe"; @@ -47,9 +47,8 @@ export const handleCreateCheckout = async ({ }); if (itemSets.length === 0) { - throw new AutumnError({ + throw new RecaseError({ message: `Product ${attachParams.products.map((p) => p.name).join(", ")} has no prices, can't create checkout`, - statusCode: 400, }); } diff --git a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts index 0f58d78e8..e637212bc 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts @@ -1,41 +1,46 @@ -import { ExtendedRequest } from "@/utils/models/Request.js"; - -import { AttachBody } from "@autumn/shared"; -import { AttachParams } from "../../cusProducts/AttachParams.js"; -import { notNullish } from "@/utils/genUtils.js"; -import { AttachBranch, AttachErrCode, BillingInterval } from "@autumn/shared"; -import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js"; +import { + type AttachBody, + AttachBranch, + AttachErrCode, + BillingInterval, + cusProductToPrices, + cusProductToProduct, + ErrCode, + type FeatureOptions, + type FullCusProduct, +} from "@autumn/shared"; +import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; +import { hasPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js"; -import { ErrCode } from "@/errors/errCodes.js"; -import RecaseError from "@/utils/errorUtils.js"; +import { productsAreSame } from "@/internal/products/productUtils/compareProductUtils.js"; import { isFreeProduct, isProductUpgrade, } from "@/internal/products/productUtils.js"; -import { cusProductToPrices, cusProductToProduct } from "@autumn/shared"; -import { FeatureOptions, FullCusProduct } from "@autumn/shared"; -import { productsAreSame } from "@/internal/products/productUtils/compareProductUtils.js"; -import { hasPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AttachParams } from "../../cusProducts/AttachParams.js"; +import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js"; +import { isMainTrialBranch } from "./attachUtils.js"; import { attachParamToCusProducts, getCustomerSub, } from "./convertAttachParams.js"; -import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; -import { isMainTrialBranch } from "./attachUtils.js"; const handleMultiProductErrors = async ({ attachParams, }: { attachParams: AttachParams; }) => { - let { products } = attachParams; + const { products } = attachParams; if (pricesOnlyOneOff(attachParams.prices)) { return true; } for (const product of products) { - let { curMainProduct, curSameProduct, curScheduledProduct } = + const { curMainProduct, curSameProduct, curScheduledProduct } = getExistingCusProducts({ product, cusProducts: attachParams.cusProducts!, @@ -55,7 +60,7 @@ const handleMultiProductErrors = async ({ }); } - let curPaidProduct = + const curPaidProduct = curMainProduct && !isFreeProduct(cusProductToPrices({ cusProduct: curMainProduct })); @@ -85,21 +90,21 @@ const getOptionsToUpdate = ({ newOptionsList: FeatureOptions[]; curSameProduct: FullCusProduct; }) => { - let optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[] = []; + const optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[] = []; const prices = cusProductToPrices({ cusProduct: curSameProduct }); for (const newOptions of newOptionsList) { - let internalFeatureId = newOptions.internal_feature_id; - let existingOptions = oldOptionsList.find( + const internalFeatureId = newOptions.internal_feature_id; + const existingOptions = oldOptionsList.find( (o) => o.internal_feature_id === internalFeatureId, ); - let price = findPrepaidPrice({ + const price = findPrepaidPrice({ prices, internalFeatureId: internalFeatureId!, }); - if (price?.config.interval == BillingInterval.OneOff) continue; + if (price?.config.interval === BillingInterval.OneOff) continue; if (existingOptions && existingOptions.quantity !== newOptions.quantity) { optionsToUpdate.push({ @@ -123,9 +128,9 @@ export const checkSameCustom = async ({ fromPreview?: boolean; optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[]; }) => { - let product = attachParams.products[0]; + const product = attachParams.products[0]; - let { itemsSame, freeTrialsSame, onlyEntsChanged } = productsAreSame({ + const { itemsSame, freeTrialsSame, onlyEntsChanged } = productsAreSame({ newProductV1: { ...product, prices: attachParams.prices, @@ -176,7 +181,7 @@ const getSameProductBranch = async ({ attachParams: AttachParams; fromPreview?: boolean; }) => { - let product = attachParams.products[0]; + const product = attachParams.products[0]; let { curSameProduct, curScheduledProduct } = attachParamToCusProducts({ attachParams, @@ -190,7 +195,7 @@ const getSameProductBranch = async ({ return AttachBranch.NewVersion; } - let optionsToUpdate = getOptionsToUpdate({ + const optionsToUpdate = getOptionsToUpdate({ oldOptionsList: curSameProduct.options, newOptionsList: attachParams.optionsList, curSameProduct, @@ -214,7 +219,7 @@ const getSameProductBranch = async ({ // 3. If main product if (curScheduledProduct && !product.is_add_on) { - if (curScheduledProduct.product.id == product.id) { + if (curScheduledProduct.product.id === product.id) { throw new RecaseError({ message: `Product ${product.name} is already scheduled, can't attach again`, code: ErrCode.InvalidRequest, @@ -257,16 +262,19 @@ const getChangeProductBranch = async ({ // 1. If main product is free, it's the same as adding a new product - let mainProduct = cusProductToProduct({ cusProduct: curMainProduct! }); + const mainProduct = cusProductToProduct({ cusProduct: curMainProduct! }); if (isFreeProduct(mainProduct.prices)) { return AttachBranch.MainIsFree; } // 2. If main product is paid, check if upgrade or downgrade - let curPrices = cusProductToPrices({ cusProduct: curMainProduct! }); - let newPrices = attachParams.prices; + const curPrices = cusProductToPrices({ cusProduct: curMainProduct! }); + const newPrices = attachParams.prices; - let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices }); + const isUpgrade = isProductUpgrade({ + prices1: curPrices, + prices2: newPrices, + }); if (isUpgrade) { if (isMainTrialBranch({ attachParams })) { @@ -309,7 +317,7 @@ export const getAttachBranch = async ({ return AttachBranch.MultiProduct; } - let { curSameProduct, curMainProduct } = attachParamToCusProducts({ + const { curSameProduct, curMainProduct } = attachParamToCusProducts({ attachParams, }); @@ -318,7 +326,7 @@ export const getAttachBranch = async ({ return await getSameProductBranch({ attachParams, fromPreview }); } - let product = attachParams.products[0]; + const product = attachParams.products[0]; if (product.is_add_on) { return AttachBranch.AddOn; } diff --git a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts index 49fafc60b..db0c330a0 100644 --- a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts +++ b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts @@ -6,12 +6,12 @@ import { BillingType, cusProductsToCusEnts, cusProductToPrices, + ErrCode, type FullCusProduct, type UsagePriceConfig, } from "@autumn/shared"; import { Decimal } from "decimal.js"; import { StatusCodes } from "http-status-codes"; -import { ErrCode } from "@/errors/errCodes.js"; import { findPriceForFeature } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; import { getBillingType, diff --git a/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts b/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts index 9b30e4344..7388c85db 100644 --- a/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts +++ b/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts @@ -1,12 +1,15 @@ -import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode } from "@/errors/errCodes.js"; -import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; -import { FullCusProduct, Price, UsagePriceConfig } from "@autumn/shared"; -import { Feature } from "@autumn/shared"; -import { FeatureOptions } from "@autumn/shared"; +import { + ErrCode, + type Feature, + type FeatureOptions, + type FullCusProduct, + type Price, + type UsagePriceConfig, +} from "@autumn/shared"; import { Decimal } from "decimal.js"; +import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; -import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; export const mapOptionsList = ({ optionsInput, @@ -19,7 +22,7 @@ export const mapOptionsList = ({ prices: Price[]; curCusProduct?: FullCusProduct; }) => { - let newOptionsList: FeatureOptions[] = []; + const newOptionsList: FeatureOptions[] = []; for (const options of optionsInput || []) { const feature = features.find( @@ -47,9 +50,9 @@ export const mapOptionsList = ({ // const ent = getPriceEntitlement(prepaidPrice, entitlements) - let config = prepaidPrice.config as UsagePriceConfig; + const config = prepaidPrice.config as UsagePriceConfig; - let dividedQuantity = new Decimal(options.quantity!) + const dividedQuantity = new Decimal(options.quantity!) .div(config.billing_units || 1) .ceil() .toNumber(); diff --git a/server/src/internal/customers/attach/handleSetupPayment.ts b/server/src/internal/customers/attach/handleSetupPayment.ts index bdc093266..2237673e3 100644 --- a/server/src/internal/customers/attach/handleSetupPayment.ts +++ b/server/src/internal/customers/attach/handleSetupPayment.ts @@ -1,11 +1,11 @@ -import { routeHandler } from "@/utils/routerUtils.js"; -import { getOrCreateCustomer } from "../cusUtils/getOrCreateCustomer.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; +import { ErrCode } from "@autumn/shared"; import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; import { createStripeCli } from "@/external/stripe/utils.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode } from "@/errors/errCodes.js"; import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { routeHandler } from "@/utils/routerUtils.js"; +import { getOrCreateCustomer } from "../cusUtils/getOrCreateCustomer.js"; export const handleSetupPayment = async (req: any, res: any) => routeHandler({ @@ -16,10 +16,14 @@ export const handleSetupPayment = async (req: any, res: any) => const { db, env, org } = req; const logger = req.logger; - let { customer_id, customer_data, success_url, checkout_session_params } = - req.body; + const { + customer_id, + customer_data, + success_url, + checkout_session_params, + } = req.body; - let customer = await getOrCreateCustomer({ + const customer = await getOrCreateCustomer({ req, customerId: customer_id, customerData: customer_data as any, diff --git a/server/src/internal/customers/cancel/cancelRouter.ts b/server/src/internal/customers/cancel/cancelRouter.ts index c979d82f3..d78d6b68f 100644 --- a/server/src/internal/customers/cancel/cancelRouter.ts +++ b/server/src/internal/customers/cancel/cancelRouter.ts @@ -1,4 +1,8 @@ -import { ErrCode, type FullCusProduct } from "@autumn/shared"; +import { + CusProductNotFoundError, + ErrCode, + type FullCusProduct, +} from "@autumn/shared"; import { Router } from "express"; import { CusService } from "@/internal/customers/CusService.js"; import RecaseError from "@/utils/errorUtils.js"; @@ -57,9 +61,10 @@ cancelRouter.post("", async (req, res) => ); if (!cusProduct) { - throw new RecaseError({ - code: ErrCode.ProductNotFound, - message: `Product ${product_id} not found for customer ${customer_id}`, + throw new CusProductNotFoundError({ + customerId: customer_id, + productId: product_id, + entityId: entity_id, }); } diff --git a/server/src/internal/customers/cusProducts/attachUtils.ts b/server/src/internal/customers/cusProducts/attachUtils.ts index ce4caf3ca..f0a83957e 100644 --- a/server/src/internal/customers/cusProducts/attachUtils.ts +++ b/server/src/internal/customers/cusProducts/attachUtils.ts @@ -1,42 +1,38 @@ import { - AppEnv, + type AppEnv, BillingType, CusProductStatus, - Customer, - CustomerData, - Entitlement, - EntitlementWithFeature, - Entity, - EntityData, - Feature, - FeatureOptions, - FreeTrial, - FullCusProduct, - Organization, - Price, - ProductItem, - UsagePriceConfig, + type Customer, + type CustomerData, + type Entitlement, + type EntitlementWithFeature, + type EntityData, + ErrCode, + type Feature, + type FeatureOptions, + type FreeTrial, + type FullCusProduct, + type Organization, + type Price, + type ProductItem, + type UsagePriceConfig, } from "@autumn/shared"; - -import { ErrCode } from "@/errors/errCodes.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { ProductService } from "@/internal/products/ProductService.js"; -import { notNullish, nullish } from "@/utils/genUtils.js"; - +import { Decimal } from "decimal.js"; +import { StatusCodes } from "http-status-codes"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js"; import { getFreeTrialAfterFingerprint, handleNewFreeTrial, } from "@/internal/products/free-trials/freeTrialUtils.js"; - -import { StatusCodes } from "http-status-codes"; -import { getExistingCusProducts } from "./cusProductUtils/getExistingCusProducts.js"; -import { getPricesForCusProduct } from "../change-product/scheduleUtils.js"; -import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js"; -import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js"; +import { ProductService } from "@/internal/products/ProductService.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js"; -import { Decimal } from "decimal.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; +import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { notNullish, nullish } from "@/utils/genUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { getPricesForCusProduct } from "../change-product/scheduleUtils.js"; +import { getExistingCusProducts } from "./cusProductUtils/getExistingCusProducts.js"; const getProducts = async ({ db, @@ -107,7 +103,7 @@ const getProducts = async ({ }); } - if (products.length != productIds.length) { + if (products.length !== productIds.length) { // Get product ids that were not found throw new RecaseError({ message: @@ -205,7 +201,7 @@ const getCustomerAndProducts = async ({ }), ]); - let cusProducts = customer.customer_products; + const cusProducts = customer.customer_products; return { customer, cusProducts, products }; }; @@ -228,7 +224,7 @@ const mapOptionsList = ({ features: Feature[]; prices: Price[]; }) => { - let newOptionsList: FeatureOptions[] = []; + const newOptionsList: FeatureOptions[] = []; for (const options of optionsListInput) { const feature = features.find( (feature) => feature.id === options.feature_id, @@ -246,8 +242,8 @@ const mapOptionsList = ({ if (!nullish(quantity)) { const prepaidPrice = prices.find( (p) => - getBillingType(p.config!) == BillingType.UsageInAdvance && - feature.internal_id == + getBillingType(p.config!) === BillingType.UsageInAdvance && + feature.internal_id === (p.config as UsagePriceConfig).internal_feature_id, ); @@ -259,9 +255,9 @@ const mapOptionsList = ({ }); } - let config = prepaidPrice.config as UsagePriceConfig; + const config = prepaidPrice.config as UsagePriceConfig; - let dividedQuantity = new Decimal(options.quantity!) + const dividedQuantity = new Decimal(options.quantity!) .div(config.billing_units || 1) .ceil() .toNumber(); @@ -336,7 +332,7 @@ export const getFullCusProductData = async ({ if (!isCustom) { let freeTrial = null; - let freeTrialProduct = products.find((p) => notNullish(p.free_trial)); + const freeTrialProduct = products.find((p) => notNullish(p.free_trial)); if (freeTrialProduct) { freeTrial = await getFreeTrialAfterFingerprint({ @@ -357,12 +353,12 @@ export const getFullCusProductData = async ({ optionsList: mapOptionsList({ optionsListInput, features, - prices: products.map((p) => p.prices).flat() as Price[], + prices: products.flatMap((p) => p.prices) as Price[], }), - prices: products.map((p) => p.prices).flat() as Price[], - entitlements: products - .map((p) => getEntsWithFeature(p.entitlements, features)) - .flat() as EntitlementWithFeature[], + prices: products.flatMap((p) => p.prices) as Price[], + entitlements: products.flatMap((p) => + getEntsWithFeature(p.entitlements, features), + ) as EntitlementWithFeature[], freeTrial, cusProducts, entities: customer.entities, @@ -407,7 +403,7 @@ export const getFullCusProductData = async ({ curEnts = curMainProduct!.customer_entitlements.map((e) => e.entitlement); } - let { prices, entitlements } = await handleNewProductItems({ + const { prices, entitlements } = await handleNewProductItems({ db, curPrices, curEnts, diff --git a/server/src/internal/customers/handlers/handleTransferProduct.ts b/server/src/internal/customers/handlers/handleTransferProduct.ts index afb65a791..27f23934f 100644 --- a/server/src/internal/customers/handlers/handleTransferProduct.ts +++ b/server/src/internal/customers/handlers/handleTransferProduct.ts @@ -1,8 +1,9 @@ -import { AttachScenario, ErrCode } from "@autumn/shared"; import { + AttachScenario, CusProductAlreadyExistsError, CusProductNotFoundError, -} from "@shared/api/errors/classes/cusProductErrClasses.js"; + ErrCode, +} from "@autumn/shared"; import { z } from "zod"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { ProductService } from "@/internal/products/ProductService.js"; diff --git a/server/src/internal/features/FeatureService.ts b/server/src/internal/features/FeatureService.ts index f6b5c83f4..640147509 100644 --- a/server/src/internal/features/FeatureService.ts +++ b/server/src/internal/features/FeatureService.ts @@ -1,10 +1,9 @@ -import RecaseError from "@/utils/errorUtils.js"; -import { AppEnv, Feature, features } from "@autumn/shared"; -import { ErrCode } from "@/errors/errCodes.js"; -import { clearOrgCache } from "../orgs/orgUtils/clearOrgCache.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { type AppEnv, ErrCode, type Feature, features } from "@autumn/shared"; import { and, eq } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import RecaseError from "@/utils/errorUtils.js"; import { notNullish } from "@/utils/genUtils.js"; +import { clearOrgCache } from "../orgs/orgUtils/clearOrgCache.js"; export class FeatureService { static async list({ @@ -136,13 +135,13 @@ export class FeatureService { logger: any; }) { try { - let insertedData = await db + const insertedData = await db .insert(features) .values(data as any) // DRIZZLE TYPE REFACTOR .returning(); if (insertedData && insertedData.length > 0) { - let orgId = insertedData[0].org_id; + const orgId = insertedData[0].org_id; await clearOrgCache({ db, orgId: orgId!, @@ -152,7 +151,7 @@ export class FeatureService { return insertedData as Feature[]; // DRIZZLE TYPE REFACTOR } catch (error: any) { if (error.code === "23505") { - let id = Array.isArray(data) ? data.map((f) => f.id) : data.id; + const id = Array.isArray(data) ? data.map((f) => f.id) : data.id; throw new RecaseError({ message: `Feature ${id} already exists`, code: ErrCode.DuplicateFeatureId, @@ -173,7 +172,7 @@ export class FeatureService { orgId: string; env: AppEnv; }) { - let deletedFeatures = await db + const deletedFeatures = await db .delete(features) .where( and( diff --git a/server/src/internal/features/handlers/handleDeleteFeature.ts b/server/src/internal/features/handlers/handleDeleteFeature.ts index 1c241e733..e70171dee 100644 --- a/server/src/internal/features/handlers/handleDeleteFeature.ts +++ b/server/src/internal/features/handlers/handleDeleteFeature.ts @@ -1,4 +1,4 @@ -import { ErrCode } from "@/errors/errCodes.js"; +import { ErrCode } from "@autumn/shared"; import { getCreditSystemsFromFeature } from "@/internal/features/creditSystemUtils.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js"; @@ -17,10 +17,10 @@ export const handleDeleteFeature = async (req: any, res: any) => handler: async (req: ExtendedRequest, res: ExtendedResponse) => { const { db, orgId } = req; - let { featureId } = req.params; - let features = await FeatureService.getFromReq(req); - let feature = features.find((f) => f.id === featureId); - let creditSystems = getCreditSystemsFromFeature({ + const { featureId } = req.params; + const features = await FeatureService.getFromReq(req); + const feature = features.find((f) => f.id === featureId); + const creditSystems = getCreditSystemsFromFeature({ featureId, features, }); diff --git a/server/src/internal/features/handlers/handleUpdateFeature.ts b/server/src/internal/features/handlers/handleUpdateFeature.ts index f7474b3a0..7a02e113a 100644 --- a/server/src/internal/features/handlers/handleUpdateFeature.ts +++ b/server/src/internal/features/handlers/handleUpdateFeature.ts @@ -1,33 +1,31 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { ErrCode } from "@/errors/errCodes.js"; +import { + type AppEnv, + EntInterval, + type Entitlement, + type EntitlementWithFeature, + ErrCode, + type Feature, + FeatureType, + FeatureUsageType, + notNullish, + type Price, + type UsagePriceConfig, +} from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; - import { FeatureService } from "@/internal/features/FeatureService.js"; import { getObjectsUsingFeature, validateCreditSystem, + validateMeteredConfig, } from "@/internal/features/featureUtils.js"; -import { validateMeteredConfig } from "@/internal/features/featureUtils.js"; -import { PriceService } from "@/internal/products/prices/PriceService.js"; import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js"; +import { PriceService } from "@/internal/products/prices/PriceService.js"; import { JobName } from "@/queue/JobName.js"; import { addTaskToQueue } from "@/queue/queueUtils.js"; import RecaseError from "@/utils/errorUtils.js"; import { keyToTitle } from "@/utils/genUtils.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { - Entitlement, - Feature, - FeatureType, - Price, - UsagePriceConfig, - AppEnv, - EntitlementWithFeature, - EntInterval, - FeatureUsageType, - notNullish, -} from "@autumn/shared"; -import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; import { toAPIFeature } from "../utils/mapFeatureUtils.js"; const handleFeatureIdChanged = async ({ @@ -54,7 +52,7 @@ const handleFeatureIdChanged = async ({ logger: any; }) => { // 1. Check if any customer entitlement linked to this feature - let cusEnts = await CusEntService.getByFeature({ + const cusEnts = await CusEntService.getByFeature({ db, internalFeatureId: feature.internal_id!, }); @@ -68,9 +66,9 @@ const handleFeatureIdChanged = async ({ } // 2. Update all linked objects - let batchUpdate = []; + const batchUpdate = []; - for (let entitlement of linkedEntitlements) { + for (const entitlement of linkedEntitlements) { batchUpdate.push( EntitlementService.update({ db, @@ -85,8 +83,8 @@ const handleFeatureIdChanged = async ({ await Promise.all(batchUpdate); // 3. Update all linked prices - let priceUpdate = []; - for (let price of prices) { + const priceUpdate = []; + for (const price of prices) { priceUpdate.push( PriceService.update({ db, @@ -104,9 +102,9 @@ const handleFeatureIdChanged = async ({ await Promise.all(priceUpdate); // 4. Update all linked credit systems - let creditSystemUpdate = []; - for (let creditSystem of creditSystems) { - let newSchema = structuredClone(creditSystem.config.schema); + const creditSystemUpdate = []; + for (const creditSystem of creditSystems) { + const newSchema = structuredClone(creditSystem.config.schema); for (let i = 0; i < newSchema.length; i++) { if (newSchema[i].metered_feature_id === feature.id) { newSchema[i].metered_feature_id = newId; @@ -131,9 +129,9 @@ const handleFeatureIdChanged = async ({ await Promise.all(creditSystemUpdate); // 5. Update all linked entitlements - let entitlementUpdate = []; + const entitlementUpdate = []; - for (let entitlement of entitlements) { + for (const entitlement of entitlements) { entitlementUpdate.push( EntitlementService.update({ db, @@ -165,7 +163,7 @@ const handleFeatureUsageTypeChanged = async ({ prices: Price[]; creditSystems: Feature[]; }) => { - let usageTypeTitle = keyToTitle(newUsageType).toLowerCase(); + const usageTypeTitle = keyToTitle(newUsageType).toLowerCase(); if (creditSystems.length > 0) { throw new RecaseError({ message: `Cannot set to ${usageTypeTitle} because it is used in credit system ${creditSystems[0].id}`, @@ -183,7 +181,7 @@ const handleFeatureUsageTypeChanged = async ({ } // Get cus product using feature... - let cusEnts = await CusEntService.getByFeature({ + const cusEnts = await CusEntService.getByFeature({ db, internalFeatureId: feature.internal_id!, }); @@ -201,8 +199,8 @@ const handleFeatureUsageTypeChanged = async ({ `Feature usage type changed to ${newUsageType}, updating entitlements and prices`, ); if (newUsageType == FeatureUsageType.Continuous) { - let batchEntUpdate = []; - for (let entitlement of entitlements) { + const batchEntUpdate = []; + for (const entitlement of entitlements) { batchEntUpdate.push( EntitlementService.update({ db, @@ -220,9 +218,9 @@ const handleFeatureUsageTypeChanged = async ({ } if (prices.length > 0) { - let batchPriceUpdate = []; - for (let price of prices) { - let priceConfig = price.config as UsagePriceConfig; + const batchPriceUpdate = []; + for (const price of prices) { + const priceConfig = price.config as UsagePriceConfig; batchPriceUpdate.push( PriceService.update({ @@ -259,13 +257,13 @@ export const handleUpdateFeature = async ( res, action: "Update feature", handler: async (req: any, res: any) => { - let featureId = req.params.feature_id; - let data = req.body; - let { db, orgId, env, logtail: logger } = req; + const featureId = req.params.feature_id; + const data = req.body; + const { db, orgId, env, logtail: logger } = req; // 1. Get feature by ID - let features = await FeatureService.getFromReq(req); - let feature = features.find((f) => f.id == featureId); + const features = await FeatureService.getFromReq(req); + const feature = features.find((f) => f.id == featureId); if (!feature) { throw new RecaseError({ @@ -278,7 +276,7 @@ export const handleUpdateFeature = async ( // If only archiving, skip other checks and just update if (data.archived !== undefined && Object.keys(data).length === 1) { console.log("Updating feature archived to: ", data.archived); - let updatedFeature = await FeatureService.update({ + const updatedFeature = await FeatureService.update({ db: req.db, id: featureId, orgId: req.orgId, @@ -301,19 +299,20 @@ export const handleUpdateFeature = async ( } // 1. Check if changing type... - let isChangingType = notNullish(data.type) && feature.type !== data.type; + const isChangingType = + notNullish(data.type) && feature.type !== data.type; - let isChangingId = notNullish(data.id) && feature.id !== data.id; + const isChangingId = notNullish(data.id) && feature.id !== data.id; - let isChangingUsageType = + const isChangingUsageType = feature.type != FeatureType.Boolean && data.type != FeatureType.Boolean && feature.config?.usage_type != data.config?.usage_type; - let isChangingName = feature.name !== data.name; + const isChangingName = feature.name !== data.name; if (isChangingType || isChangingId || isChangingUsageType) { - let { entitlements, prices, creditSystems, linkedEntitlements } = + const { entitlements, prices, creditSystems, linkedEntitlements } = await getObjectsUsingFeature({ db, orgId: req.orgId, @@ -374,7 +373,7 @@ export const handleUpdateFeature = async ( : data.config : feature.config; - let updatedFeature = await FeatureService.update({ + const updatedFeature = await FeatureService.update({ db: req.db, id: featureId, orgId: req.orgId, diff --git a/server/src/internal/orgs/handlers/handleConnectStripe.ts b/server/src/internal/orgs/handlers/handleConnectStripe.ts index f5da09fba..e7029af44 100644 --- a/server/src/internal/orgs/handlers/handleConnectStripe.ts +++ b/server/src/internal/orgs/handlers/handleConnectStripe.ts @@ -1,26 +1,19 @@ -import { routeHandler } from "@/utils/routerUtils.js"; +import { AppEnv, ErrCode } from "@autumn/shared"; import Stripe from "stripe"; -import RecaseError from "@/utils/errorUtils.js"; -import { encryptData } from "@/utils/encryptUtils.js"; - -import { ErrCode } from "@/errors/errCodes.js"; +import { z } from "zod"; +import { ensureStripeProductsWithEnv } from "@/external/stripe/stripeEnsureUtils.js"; import { checkKeyValid, createWebhookEndpoint, } from "@/external/stripe/stripeOnboardingUtils.js"; - +import { encryptData } from "@/utils/encryptUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { nullish } from "@/utils/genUtils.js"; +import { routeHandler } from "@/utils/routerUtils.js"; import { OrgService } from "../OrgService.js"; -import { AppEnv } from "@autumn/shared"; -import { notNullish, nullish } from "@/utils/genUtils.js"; import { clearOrgCache } from "../orgUtils/clearOrgCache.js"; -import { z } from "zod"; import { isStripeConnected } from "../orgUtils.js"; -import { - ensureStripeProducts, - ensureStripeProductsWithEnv, -} from "@/external/stripe/stripeEnsureUtils.js"; -import { toSuccessUrl } from "../orgUtils/convertOrgUtils.js"; export const connectStripe = async ({ orgId, @@ -34,9 +27,9 @@ export const connectStripe = async ({ // 1. Check if key is valid await checkKeyValid(apiKey); - let stripe = new Stripe(apiKey); + const stripe = new Stripe(apiKey); - let account = await stripe.accounts.retrieve(); + const account = await stripe.accounts.retrieve(); // 2. Disconnect existing webhook endpoints const curWebhooks = await stripe.webhookEndpoints.list(); @@ -47,7 +40,7 @@ export const connectStripe = async ({ } // 3. Create new webhook endpoint - let webhook = await createWebhookEndpoint(apiKey, env, orgId); + const webhook = await createWebhookEndpoint(apiKey, env, orgId); // 3. Return encrypted if (env === AppEnv.Sandbox) { @@ -96,9 +89,9 @@ export const connectAllStripe = async ({ await checkKeyValid(liveApiKey); // Get default currency from Stripe - let stripe = new Stripe(testApiKey); + const stripe = new Stripe(testApiKey); - let account = await stripe.accounts.retrieve(); + const account = await stripe.accounts.retrieve(); if (nullish(defaultCurrency) && nullish(account.default_currency)) { throw new RecaseError({ diff --git a/server/src/internal/products/handlers/handleCreateProduct.ts b/server/src/internal/products/handlers/handleCreateProduct.ts index 61292e904..dd8c00b1b 100644 --- a/server/src/internal/products/handlers/handleCreateProduct.ts +++ b/server/src/internal/products/handlers/handleCreateProduct.ts @@ -1,99 +1,80 @@ import { - APIProductSchema, - CreateProductSchema, - type Entitlement, - ErrCode, + type CreateProductParams, + CreateProductParamsSchema, type FreeTrial, - type FullProduct, - type Price, type Product, ProductAlreadyExistsError, type ProductItem, } from "@autumn/shared"; -import { - handleNewFreeTrial, - validateAndInitFreeTrial, -} from "@/internal/products/free-trials/freeTrialUtils.js"; -import { ProductService } from "@/internal/products/ProductService.js"; -import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js"; -import { validateProductItems } from "@/internal/products/product-items/validateProductItems.js"; -import { - constructProduct, - getGroupToDefaults, - initProductInStripe, -} from "@/internal/products/productUtils.js"; -import { JobName } from "@/queue/JobName.js"; -import { addTaskToQueue } from "@/queue/queueUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { - keyToTitle, - notNullish, - nullish, - validateId, -} from "@/utils/genUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; -import { routeHandler } from "@/utils/routerUtils.js"; -import { validateOneOffTrial } from "../free-trials/freeTrialUtils.js"; + +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { ProductService } from "../ProductService.js"; import { isDefaultTrial } from "../productUtils/classifyProduct.js"; +import { getGroupToDefaults } from "../productUtils.js"; -const validateCreateProduct = async ({ req }: { req: ExtendedRequest }) => { - const { free_trial, items } = req.body; - const { orgId, env, db, features } = req; +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); + // const productData = CreateProductSchema.parse(req.body); - validateId("Product", productData.id); + // validateId("Product", productData.id); - if (nullish(req.body.name)) { - productData.name = keyToTitle(productData.id); - } + // if (nullish(req.body.name)) { + // productData.name = keyToTitle(productData.id); + // } const existing = await ProductService.get({ db, - orgId, + orgId: org.id, env, - id: productData.id, + id: body.id, }); // 1. If existing product, throw error if (existing) { - throw new ProductAlreadyExistsError({ - productId: productData.id, - }); + 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, - }); - } + // 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); - } + // 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, + // freeTrial, + // productData, }; }; @@ -103,11 +84,10 @@ export const disableCurrentDefault = async ({ items, freeTrial, }: { - req: ExtendedRequest; + req: AutumnContext; newProduct: Product; items: ProductItem[]; - - freeTrial: FreeTrial | null; + freeTrial: FreeTrial; }) => { const { db, org, env, logger } = req; let defaultProds = await ProductService.listDefault({ @@ -158,98 +138,104 @@ export const disableCurrentDefault = async ({ } }; -export const handleCreateProduct = async (req: Request, res: any) => - routeHandler({ - req, - res, - action: "POST /products", - handler: async (req, res) => { - const { items } = req.body; +/** + * Route: POST /products - Create a product + */ +export const createProduct = createRoute({ + body: CreateProductParamsSchema, + handler: async (c) => { + const body = c.req.valid("json"); + const query = c.req.valid("query"); + const ctx = c.get("ctx"); - const { logtail: logger, org, features, env, db } = req; + const { logger, org, features, env, db } = ctx; + const { items } = body; - const { freeTrial, productData } = await validateCreateProduct({ - req, - }); + // const { features, freeTrial, productData } = await validateCreateProduct({ + // ctx, + // body, + // }); - const newProduct = constructProduct({ - productData, - orgId: org.id, - env, - }); + // const newProduct = constructProduct({ + // productData, + // orgId: org.id, + // env, + // }); - await disableCurrentDefault({ - req, - newProduct, - items, - freeTrial: freeTrial || null, - }); + return c.json({}); - const product = await ProductService.insert({ db, product: newProduct }); + // await disableCurrentDefault({ + // req, + // newProduct, + // items, + // freeTrial: freeTrial || null, + // }); - 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; - } + // const product = await ProductService.insert({ db, product: newProduct }); - await validateOneOffTrial({ - prices, - freeTrial: freeTrial || null, - }); + // 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; + // } - await initProductInStripe({ - db, - product: { - ...product, - prices, - entitlements, - } as FullProduct, - org, - env, - logger, - }); + // await validateOneOffTrial({ + // prices, + // freeTrial: freeTrial || null, + // }); - if (notNullish(freeTrial)) { - await handleNewFreeTrial({ - db, - newFreeTrial: freeTrial, - curFreeTrial: null, - internalProductId: product.internal_id, - isCustom: false, - }); - } + // await initProductInStripe({ + // db, + // product: { + // ...product, + // prices, + // entitlements, + // } as FullProduct, + // org, + // env, + // logger, + // }); - await addTaskToQueue({ - jobName: JobName.DetectBaseVariant, - payload: { - curProduct: { - ...product, - prices, - entitlements: [], - }, - }, - }); + // if (notNullish(freeTrial)) { + // await handleNewFreeTrial({ + // db, + // newFreeTrial: freeTrial, + // curFreeTrial: null, + // internalProductId: product.internal_id, + // isCustom: false, + // }); + // } - res.status(200).json( - APIProductSchema.parse({ - ...product, - autumn_id: product.internal_id, - items: items || [], - free_trial: freeTrial, - }), - ); - }, - }); + // await addTaskToQueue({ + // jobName: JobName.DetectBaseVariant, + // payload: { + // curProduct: { + // ...product, + // prices, + // entitlements: [], + // }, + // }, + // }); + + // res.status(200).json( + // APIProductSchema.parse({ + // ...product, + // autumn_id: product.internal_id, + // items: items || [], + // free_trial: freeTrial, + // }), + // ); + }, +}); diff --git a/server/src/internal/products/handlers/handleCreateProduct_old.ts b/server/src/internal/products/handlers/handleCreateProduct_old.ts new file mode 100644 index 000000000..61292e904 --- /dev/null +++ b/server/src/internal/products/handlers/handleCreateProduct_old.ts @@ -0,0 +1,255 @@ +import { + APIProductSchema, + CreateProductSchema, + type Entitlement, + ErrCode, + type FreeTrial, + type FullProduct, + type Price, + type Product, + ProductAlreadyExistsError, + type ProductItem, +} from "@autumn/shared"; +import { + handleNewFreeTrial, + validateAndInitFreeTrial, +} from "@/internal/products/free-trials/freeTrialUtils.js"; +import { ProductService } from "@/internal/products/ProductService.js"; +import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js"; +import { validateProductItems } from "@/internal/products/product-items/validateProductItems.js"; +import { + constructProduct, + getGroupToDefaults, + initProductInStripe, +} from "@/internal/products/productUtils.js"; +import { JobName } from "@/queue/JobName.js"; +import { addTaskToQueue } from "@/queue/queueUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { + keyToTitle, + notNullish, + nullish, + validateId, +} from "@/utils/genUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { routeHandler } from "@/utils/routerUtils.js"; +import { validateOneOffTrial } from "../free-trials/freeTrialUtils.js"; +import { isDefaultTrial } from "../productUtils/classifyProduct.js"; + +const validateCreateProduct = async ({ req }: { req: ExtendedRequest }) => { + const { free_trial, items } = req.body; + const { orgId, env, db, features } = req; + + 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, + env, + id: productData.id, + }); + + // 1. If existing product, throw error + if (existing) { + throw new ProductAlreadyExistsError({ + productId: productData.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, + }; +}; + +export const disableCurrentDefault = async ({ + req, + newProduct, + items, + freeTrial, +}: { + req: ExtendedRequest; + newProduct: Product; + items: ProductItem[]; + + freeTrial: FreeTrial | null; +}) => { + const { db, org, env, logger } = req; + let defaultProds = await ProductService.listDefault({ + db, + orgId: org.id, + env, + }); + + defaultProds = defaultProds.filter((prod) => prod.id !== newProduct.id); + + if (defaultProds.length === 0) return; + + const defaults = getGroupToDefaults({ + defaultProds, + })?.[newProduct.group]; + + const willBeDefaultTrial = isDefaultTrial({ + product: { + ...newProduct, + free_trial: freeTrial, + items: items || [], + }, + }); + + if (willBeDefaultTrial) { + // Disable current default trial + const curDefault = defaults?.defaultTrial; + if (curDefault) { + logger.info( + `Disabling trial on cur default trial product: ${curDefault.id}`, + ); + await ProductService.updateByInternalId({ + db, + internalId: curDefault.internal_id, + update: { is_default: false }, + }); + } + } else if (newProduct.is_default) { + const curDefault = defaults?.free; + if (curDefault) { + logger.info(`Disabling trial on cur default product: ${curDefault.id}`); + await ProductService.updateByInternalId({ + db, + internalId: curDefault.internal_id, + update: { is_default: false }, + }); + } + } +}; + +export const handleCreateProduct = async (req: Request, res: any) => + routeHandler({ + req, + res, + action: "POST /products", + handler: async (req, res) => { + const { items } = req.body; + + const { logtail: logger, org, features, env, db } = req; + + const { freeTrial, productData } = await validateCreateProduct({ + req, + }); + + const newProduct = constructProduct({ + productData, + orgId: org.id, + env, + }); + + await disableCurrentDefault({ + req, + newProduct, + items, + freeTrial: freeTrial || null, + }); + + const product = await ProductService.insert({ db, product: newProduct }); + + 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; + } + + await validateOneOffTrial({ + prices, + freeTrial: freeTrial || null, + }); + + await initProductInStripe({ + db, + product: { + ...product, + prices, + entitlements, + } as FullProduct, + org, + env, + logger, + }); + + if (notNullish(freeTrial)) { + await handleNewFreeTrial({ + db, + newFreeTrial: freeTrial, + curFreeTrial: null, + internalProductId: product.internal_id, + isCustom: false, + }); + } + + await addTaskToQueue({ + jobName: JobName.DetectBaseVariant, + payload: { + curProduct: { + ...product, + prices, + entitlements: [], + }, + }, + }); + + res.status(200).json( + APIProductSchema.parse({ + ...product, + autumn_id: product.internal_id, + items: items || [], + free_trial: freeTrial, + }), + ); + }, + }); diff --git a/server/src/internal/products/handlers/handleDeleteProduct.ts b/server/src/internal/products/handlers/handleDeleteProduct.ts index 0089eb5b4..5b4047009 100644 --- a/server/src/internal/products/handlers/handleDeleteProduct.ts +++ b/server/src/internal/products/handlers/handleDeleteProduct.ts @@ -1,4 +1,4 @@ -import { AutumnError, ProductNotFoundError } from "@autumn/shared"; +import { ProductNotFoundError, RecaseError } from "@autumn/shared"; import { CusProdReadService } from "@/internal/customers/cusProducts/CusProdReadService.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { routeHandler } from "@/utils/routerUtils.js"; @@ -41,9 +41,8 @@ export const handleDeleteProduct = (req: any, res: any) => const cusProdCount = deleteAllVersions ? allCounts.all : latestCounts.all; if (cusProdCount > 0) { - throw new AutumnError({ + throw new RecaseError({ message: `Product ${productId} has ${cusProdCount} customers (expired or active) on it and therefore cannot be deleted`, - statusCode: 400, }); } diff --git a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts index 3f36c92e5..6448f7e6f 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts @@ -22,7 +22,7 @@ import { mapToProductItems } from "../../productV2Utils.js"; import { disableCurrentDefault, handleCreateProduct, -} from "../handleCreateProduct.js"; +} from "../handleCreateProduct_old.js"; import { handleVersionProductV2 } from "../handleVersionProduct.js"; import { handleUpdateProductDetails } from "./updateProductDetails.js"; diff --git a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts index 3e2f011ab..e8c22d163 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts @@ -1,12 +1,12 @@ import { type AppEnv, - AutumnError, type FreeTrial, type FullProduct, isFreeProductV2, type Organization, type Product, type ProductItem, + RecaseError, type RewardProgram, type UpdateProduct, } from "@autumn/shared"; @@ -25,23 +25,23 @@ import { import { isFreeProduct } from "../../productUtils.js"; const productDetailsSame = (prod1: Product, prod2: UpdateProduct) => { - if (notNullish(prod2.id) && prod1.id != prod2.id) { + if (notNullish(prod2.id) && prod1.id !== prod2.id) { return false; } - if (notNullish(prod2.name) && prod1.name != prod2.name) { + if (notNullish(prod2.name) && prod1.name !== prod2.name) { return false; } - if (notNullish(prod2.group) && prod1.group != prod2.group) { + if (notNullish(prod2.group) && prod1.group !== prod2.group) { return false; } - if (notNullish(prod2.is_add_on) && prod1.is_add_on != prod2.is_add_on) { + if (notNullish(prod2.is_add_on) && prod1.is_add_on !== prod2.is_add_on) { return false; } - if (notNullish(prod2.is_default) && prod1.is_default != prod2.is_default) { + if (notNullish(prod2.is_default) && prod1.is_default !== prod2.is_default) { return false; } @@ -195,14 +195,14 @@ export const handleUpdateProductDetails = async ({ // 1. Check if there are items if (items) { if (items.some((item) => isFeaturePriceItem(item) || isPriceItem(item))) { - throw new AutumnError({ + throw new RecaseError({ message: "Cannot make a product default if it has fixed prices or paid features", }); } } else { if (!isFreeProduct(curProduct.prices)) { - throw new AutumnError({ + throw new RecaseError({ message: "Cannot make a product default if it has fixed prices or paid features", }); @@ -216,13 +216,13 @@ export const handleUpdateProductDetails = async ({ if (notNullish(newProduct.id) && newProduct.id !== curProduct.id) { if (customersOnAllVersions.length > 0) { - throw new AutumnError({ + throw new RecaseError({ message: "Cannot change product ID because it has existing customers", }); } if (rewardPrograms.length > 0) { - throw new AutumnError({ + throw new RecaseError({ message: "Cannot change product ID because existing reward programs are linked to it", }); diff --git a/server/src/internal/products/honoProductRouter.ts b/server/src/internal/products/honoProductRouter.ts index 3f6f1066b..90bff25d8 100644 --- a/server/src/internal/products/honoProductRouter.ts +++ b/server/src/internal/products/honoProductRouter.ts @@ -1,33 +1,9 @@ -// import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { createProduct } from "./handlers/handleCreateProduct.js"; -import { ProductNotFoundError } from "@autumn/shared"; -import type { Context } from "hono"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import type { HonoEnv } from "@/initHono.js"; +// Create a Hono app for products +export const honoProductRouter = new Hono(); -export const handleCreateProduct = async (c: Context) => { - const body1 = await c.req.json(); - const body2 = await c.req.json(); - - const ctx = c.get("ctx"); - const { org, env } = ctx; - - const stripe = createStripeCli({ org, env }); - - try { - const product = await stripe.products.retrieve("123"); - } catch (error: any) { - console.log(error.message, error.code); - } - - throw new ProductNotFoundError({ productId: "123" }); - // return c.json({ message: "Hello from Hono!" }); - - // throw new ProductNotFoundError({ productId: "123" }); - - // Get parsed body from context (already parsed by wrapExpressMiddleware) - // const body = c.get("parsedBody"); - // console.log("Body:", body); - - // return c.json({ message: "Hello from Hono!", receivedBody: body }); -}; +// POST /products - Create a product +honoProductRouter.post("", ...createProduct); diff --git a/server/src/internal/products/internalProductRouter.ts b/server/src/internal/products/internalProductRouter.ts index ff26b6b05..0955a5e09 100644 --- a/server/src/internal/products/internalProductRouter.ts +++ b/server/src/internal/products/internalProductRouter.ts @@ -65,13 +65,6 @@ productRouter.get("/product_counts", async (req: any, res) => { const counts = await Promise.all( products.map(async (product) => { - // if (latestVersion) { - // return CusProdReadService.getCounts({ - // db, - // internalProductId: product.internal_id, - // }); - // } - return CusProdReadService.getCountsForAllVersions({ db, productId: product.id, @@ -461,11 +454,10 @@ productRouter.post("/product_options", async (req: any, res: any) => { try { const { items } = req.body; - const features = await FeatureService.getFromReq(req); const featureToOptions: { [key: string]: FeatureOptions } = {}; for (const item of items) { - if (isFeaturePriceItem(item) && item.usage_model == UsageModel.Prepaid) { + if (isFeaturePriceItem(item) && item.usage_model === UsageModel.Prepaid) { featureToOptions[item.feature_id] = { feature_id: item.feature_id, quantity: 0, diff --git a/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts b/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts index 63b28053e..660cb9a54 100644 --- a/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts +++ b/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts @@ -35,6 +35,10 @@ export const subItemToFixedPrice = ({ interval, interval_count: intervalCount, stripe_price_id: price.id, + + stripe_product_id: undefined, + feature_id: undefined, + internal_feature_id: undefined, }, }); }; diff --git a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts index 1ecb0fc27..c8363b2d6 100644 --- a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts +++ b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts @@ -1,42 +1,41 @@ -import RecaseError from "@/utils/errorUtils.js"; import { AllowanceType, BillingInterval, BillingType, BillWhen, EntInterval, - Entitlement, + type Entitlement, ErrCode, - Feature, + type Feature, FeatureType, - FixedPriceConfig, - Infinite, - Price, - PriceType, - ProductItem, - UsageModel, - TierInfinite, - UsagePriceConfig, - OnIncrease, - OnDecrease, FeatureUsageType, + type FixedPriceConfig, + Infinite, + OnDecrease, + OnIncrease, + type Price, + PriceType, + type ProductItem, + TierInfinite, + UsageModel, + type UsagePriceConfig, } from "@autumn/shared"; -import { generateId, notNullish, nullish } from "@/utils/genUtils.js"; import { pricesAreSame } from "@/internal/products/prices/priceInitUtils.js"; -import { entsAreSame } from "../../entitlements/entitlementUtils.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js"; - -import { - isFeatureItem, - isFeaturePriceItem, - isPriceItem, -} from "./getItemType.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { generateId, notNullish, nullish } from "@/utils/genUtils.js"; +import { entsAreSame } from "../../entitlements/entitlementUtils.js"; +import { shouldProrate } from "../../prices/priceUtils/prorationConfigUtils.js"; import { itemToBillingInterval, itemToEntInterval, } from "../itemIntervalUtils.js"; import { itemCanBeProrated } from "./classifyItem.js"; -import { shouldProrate } from "../../prices/priceUtils/prorationConfigUtils.js"; +import { + isFeatureItem, + isFeaturePriceItem, + isPriceItem, +} from "./getItemType.js"; export const getResetUsage = ({ item, @@ -53,7 +52,7 @@ export const getResetUsage = ({ (isFeatureItem(item) || isFeaturePriceItem(item)) && feature ) { - return feature?.config?.usage_type == FeatureUsageType.Single; + return feature?.config?.usage_type === FeatureUsageType.Single; } return item.reset_usage_when_enabled; }; @@ -71,11 +70,14 @@ export const toPrice = ({ isCustom: boolean; newVersion?: boolean; }) => { - let config: FixedPriceConfig = { + const config: FixedPriceConfig = { type: PriceType.Fixed, amount: notNullish(item.price) ? item.price! : item.tiers![0].amount!, interval: itemToBillingInterval(item) as BillingInterval, interval_count: item.interval_count || 1, + stripe_product_id: null, + feature_id: null, + internal_feature_id: null, }; let price: Price = { @@ -116,9 +118,9 @@ export const toFeature = ({ newVersion?: boolean; feature?: Feature; }) => { - let isBoolean = feature?.type == FeatureType.Boolean; + const isBoolean = feature?.type == FeatureType.Boolean; - let resetUsage = getResetUsage({ item, feature }); + const resetUsage = getResetUsage({ item, feature }); let ent: Entitlement = { id: item.entitlement_id || generateId("ent"), @@ -178,7 +180,7 @@ export const toFeatureAndPrice = ({ newVersion?: boolean; features: Feature[]; }) => { - let resetUsage = getResetUsage({ + const resetUsage = getResetUsage({ item, feature: features.find((f) => f.id == item.feature_id), }); @@ -206,7 +208,7 @@ export const toFeatureAndPrice = ({ }; // Will only create new ent id if - let newEnt = !curEnt || (isCustom && !entsAreSame(curEnt, ent)); + const newEnt = !curEnt || (isCustom && !entsAreSame(curEnt, ent)); if (newEnt || newVersion) { ent = { ...ent, @@ -215,9 +217,9 @@ export const toFeatureAndPrice = ({ }; } - let entInterval = itemToEntInterval(item); + const entInterval = itemToEntInterval(item); - let config: UsagePriceConfig = { + const config: UsagePriceConfig = { type: PriceType.Usage, bill_when: @@ -244,7 +246,8 @@ export const toFeatureAndPrice = ({ let prorationConfig = null; if (itemCanBeProrated({ item, features })) { - let onIncrease = item.config?.on_increase || OnIncrease.ProrateImmediately; + const onIncrease = + item.config?.on_increase || OnIncrease.ProrateImmediately; let onDecrease = item.config?.on_decrease || OnDecrease.Prorate; // console.log("Item config:", item.config); @@ -274,7 +277,7 @@ export const toFeatureAndPrice = ({ proration_config: prorationConfig, }; - let billingType = getBillingType(price.config!); + const billingType = getBillingType(price.config!); if ( (billingType == BillingType.UsageInArrear || billingType == BillingType.InArrearProrated) && @@ -287,13 +290,13 @@ export const toFeatureAndPrice = ({ }); } - let priceOrEntDifferent = + const priceOrEntDifferent = (curPrice && !pricesAreSame(curPrice, price, true)) || (curEnt && !entsAreSame(curEnt, ent)); if (curPrice && (priceOrEntDifferent || newVersion)) { - let newConfig = price.config as UsagePriceConfig; - let curConfig = curPrice.config as UsagePriceConfig; + const newConfig = price.config as UsagePriceConfig; + const curConfig = curPrice.config as UsagePriceConfig; newConfig.stripe_meter_id = curConfig.stripe_meter_id; newConfig.stripe_product_id = curConfig.stripe_product_id; price.config = newConfig; @@ -341,7 +344,7 @@ export const itemToPriceAndEnt = ({ let sameEnt: Entitlement | null = null; if (isPriceItem(item)) { - let { price } = toPrice({ + const { price } = toPrice({ item, orgId, internalProductId, @@ -363,9 +366,9 @@ export const itemToPriceAndEnt = ({ code: ErrCode.InvalidRequest, }); } - let isBoolean = feature?.type == FeatureType.Boolean; + const isBoolean = feature?.type == FeatureType.Boolean; - let { ent } = toFeature({ + const { ent } = toFeature({ item, orgId, internalFeatureId: feature!.internal_id!, @@ -393,7 +396,7 @@ export const itemToPriceAndEnt = ({ }); } - let { price, ent } = toFeatureAndPrice({ + const { price, ent } = toFeatureAndPrice({ item, orgId, internalFeatureId: feature!.internal_id!, @@ -405,7 +408,7 @@ export const itemToPriceAndEnt = ({ features, }); - let entSame = curEnt && entsAreSame(curEnt, ent); + const entSame = curEnt && entsAreSame(curEnt, ent); // 1. If no curPrice, price is new if (!curPrice || newVersion) { diff --git a/server/src/utils/checkUtils/checkCustomerCorrect.ts b/server/src/utils/checkUtils/checkCustomerCorrect.ts index 809486c1c..f2f87c94c 100644 --- a/server/src/utils/checkUtils/checkCustomerCorrect.ts +++ b/server/src/utils/checkUtils/checkCustomerCorrect.ts @@ -1,4 +1,5 @@ import { expect } from "bun:test"; +import assert from "node:assert"; import { type AppEnv, CusProductStatus, @@ -8,7 +9,6 @@ import { type FullCustomer, type Organization, } from "@autumn/shared"; -import assert from "assert"; import type Stripe from "stripe"; import { defaultApiVersion } from "tests/constants.js"; import { cusProductToSubIds } from "tests/merged/mergeUtils.test.js"; @@ -187,10 +187,10 @@ const checkAllFreeProducts = async ({ const sub = subs.find( (sub) => sub.customer === fullCus.processor?.id && - (sub.status == "active" || sub.status == "past_due"), + (sub.status === "active" || sub.status === "past_due"), ); - if (fullCus.org_id == "6bWdIqEuRHBrReXbTb30l9beMFVZ3Ts3") return true; + if (fullCus.org_id === "6bWdIqEuRHBrReXbTb30l9beMFVZ3Ts3") return true; assert( !sub, @@ -289,7 +289,8 @@ export const checkCusSubCorrect = async ({ cusProduct.status === CusProductStatus.Scheduled && cusProductInPhase({ phaseStartMillis: unix, cusProduct }) ) { - return scheduleIndexes.push(index); + scheduleIndexes.push(index); + return; } if (cusProduct.status === CusProductStatus.Scheduled) return; @@ -311,7 +312,7 @@ export const checkCusSubCorrect = async ({ cp.product.group === product.group && cp.status === CusProductStatus.Scheduled && (cp.internal_entity_id - ? cp.internal_entity_id == cusProduct.internal_entity_id + ? cp.internal_entity_id === cusProduct.internal_entity_id : nullish(cp.internal_entity_id)), ); @@ -394,7 +395,6 @@ export const checkCusSubCorrect = async ({ ); if (existingIndex !== -1) { - // @ts-expect-error supposedSubItems[existingIndex].quantity += lineItem.quantity; } else { supposedSubItems.push({ diff --git a/server/src/utils/genUtils.ts b/server/src/utils/genUtils.ts index 6e453309e..2f454bed7 100644 --- a/server/src/utils/genUtils.ts +++ b/server/src/utils/genUtils.ts @@ -1,7 +1,7 @@ +import { ErrCode } from "@autumn/shared"; import { UTCDate } from "@date-fns/utc"; import { format } from "date-fns"; import KSUID from "ksuid"; -import { ErrCode } from "@/errors/errCodes.js"; import RecaseError from "./errorUtils.js"; export const generateId = (prefix: string) => { @@ -99,7 +99,7 @@ export const validateId = (type: string, id: string) => { if (!id.match(/^[a-zA-Z0-9_-]+$/)) { throw new RecaseError({ message: `${type} ID can only contain alphanumeric characters, underscores, and hyphens`, - code: ErrCode.InvalidId, + code: ErrCode.InvalidInputs, statusCode: 400, }); } diff --git a/server/src/utils/routerUtils.ts b/server/src/utils/routerUtils.ts index 8217a80d1..623a63616 100644 --- a/server/src/utils/routerUtils.ts +++ b/server/src/utils/routerUtils.ts @@ -318,7 +318,7 @@ export const routeHandler = async ({ if (error instanceof ZodError && req.originalUrl.includes("/attach")) { error = new RecaseError({ - message: formatZodError(error), + message: formatZodError(error as any), code: ErrCode.InvalidInputs, statusCode: StatusCodes.BAD_REQUEST, }); diff --git a/server/src/utils/scriptUtils/scriptUtils.ts b/server/src/utils/scriptUtils/scriptUtils.ts index 3e3532e14..9737ed90b 100644 --- a/server/src/utils/scriptUtils/scriptUtils.ts +++ b/server/src/utils/scriptUtils/scriptUtils.ts @@ -1,8 +1,8 @@ import "dotenv/config"; +import fs from "node:fs"; import type { AppEnv } from "@autumn/shared"; import { UTCDate } from "@date-fns/utc"; import { subHours } from "date-fns"; -import fs from "fs"; import type { Stripe } from "stripe"; import { db } from "@/db/initDrizzle.js"; import { createLogger } from "@/external/logtail/logtailUtils.js"; diff --git a/server/tests/advanced/referrals/referrals1.ts b/server/tests/advanced/referrals/referrals1.ts index f3ab1f077..2ed5f38ad 100644 --- a/server/tests/advanced/referrals/referrals1.ts +++ b/server/tests/advanced/referrals/referrals1.ts @@ -1,31 +1,28 @@ -import { products, referralPrograms } from "../../global.js"; +import { + type AppEnv, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; import { assert } from "chai"; import chalk from "chalk"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { setupBefore } from "tests/before.js"; -import { - AppEnv, - Customer, - ErrCode, - Organization, - ReferralCode, - RewardRedemption, -} from "@autumn/shared"; -import { timeout } from "tests/utils/genUtils.js"; - -import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addDays } from "date-fns"; -import { Stripe } from "stripe"; - -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import type { Stripe } from "stripe"; +import { addPrefixToProducts } from "tests/attach/utils.js"; +import { setupBefore } from "tests/before.js"; import { TestFeature } from "tests/setup/v2Features.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "tests/attach/utils.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { products, referralPrograms } from "../../global.js"; -let pro = constructProduct({ +const pro = constructProduct({ id: "pro", items: [constructFeatureItem({ featureId: TestFeature.Words })], type: "pro", @@ -36,15 +33,15 @@ let pro = constructProduct({ describe(`${chalk.yellowBright( "referrals1: Testing referrals (on checkout)", )}`, () => { - let mainCustomerId = "main-referral-1"; - let alternateCustomerId = "alternate-referral-1"; - let redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"]; - let autumn: AutumnInt = new AutumnInt(); + const mainCustomerId = "main-referral-1"; + const alternateCustomerId = "alternate-referral-1"; + const redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"]; + const autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; let testClockId: string; let referralCode: ReferralCode; - let redemptions: RewardRedemption[] = []; + const redemptions: RewardRedemption[] = []; let mainCustomer: any; let db: DrizzleCli; let org: Organization; @@ -89,8 +86,8 @@ describe(`${chalk.yellowBright( product_id: pro.id, }); - let batchCreate = []; - for (let redeemer of redeemers) { + const batchCreate = []; + for (const redeemer of redeemers) { batchCreate.push( initCustomer({ autumn: this.autumnJs, @@ -117,7 +114,7 @@ describe(`${chalk.yellowBright( await Promise.all(batchCreate); }); - it("should create code once", async function () { + it("should create code once", async () => { referralCode = await autumn.referrals.createCode({ customerId: mainCustomerId, referralId: referralPrograms.onCheckout.id, @@ -126,7 +123,7 @@ describe(`${chalk.yellowBright( assert.exists(referralCode.code); // Get referral code again - let referralCode2 = await autumn.referrals.createCode({ + const referralCode2 = await autumn.referrals.createCode({ customerId: mainCustomerId, referralId: referralPrograms.onCheckout.id, }); @@ -134,7 +131,7 @@ describe(`${chalk.yellowBright( assert.equal(referralCode2.code, referralCode.code); }); - it("should fail if same customer tries to redeem code again", async function () { + it("should fail if same customer tries to redeem code again", async () => { try { await autumn.referrals.redeem({ customerId: mainCustomerId, @@ -160,9 +157,9 @@ describe(`${chalk.yellowBright( } }); - it("should create redemption for each redeemer and fail if redeemed again", async function () { - for (let redeemer of redeemers) { - let redemption: RewardRedemption = await autumn.referrals.redeem({ + it("should create redemption for each redeemer and fail if redeemed again", async () => { + for (const redeemer of redeemers) { + const redemption: RewardRedemption = await autumn.referrals.redeem({ customerId: redeemer, code: referralCode.code, }); @@ -172,7 +169,7 @@ describe(`${chalk.yellowBright( // Try redeem for redeemer1 again try { - let redemption1 = await autumn.referrals.redeem({ + const redemption1 = await autumn.referrals.redeem({ customerId: redeemers[0], code: referralCode.code, }); @@ -185,9 +182,9 @@ describe(`${chalk.yellowBright( // return; - it("should be triggered (and applied) when redeemers check out", async function () { + it("should be triggered (and applied) when redeemers check out", async () => { for (let i = 0; i < redeemers.length; i++) { - let redeemer = redeemers[i]; + const redeemer = redeemers[i]; await autumn.attach({ customer_id: redeemer, @@ -197,21 +194,21 @@ describe(`${chalk.yellowBright( await timeout(3000); // Get redemption object - let redemption = await autumn.redemptions.get(redemptions[i].id); + const redemption = await autumn.redemptions.get(redemptions[i].id); // Check if redemption is triggered - let count = i + 1; + const count = i + 1; if (count > referralPrograms.onCheckout.max_redemptions) { assert.equal(redemption.triggered, false); assert.equal(redemption.applied, false); } else { assert.equal(redemption.triggered, true); - assert.equal(redemption.applied, i == 0); + assert.equal(redemption.applied, i === 0); } // Check stripe customer - let stripeCus = (await stripeCli.customers.retrieve( + const stripeCus = (await stripeCli.customers.retrieve( mainCustomer.processor?.id, )) as Stripe.Customer; @@ -220,7 +217,7 @@ describe(`${chalk.yellowBright( }); let curTime = new Date(); - it("customer should have discount for first purchase", async function () { + it("customer should have discount for first purchase", async () => { curTime = addDays(addDays(curTime, 7), 4); await advanceTestClock({ testClockId, @@ -229,7 +226,7 @@ describe(`${chalk.yellowBright( }); // 1. Get invoice - let { invoices } = await autumn.customers.get(mainCustomerId); + const { invoices } = await autumn.customers.get(mainCustomerId); assert.equal(invoices.length, 2); assert.equal(invoices[0].total, 0); }); diff --git a/server/tests/advanced/referrals/referrals2.ts b/server/tests/advanced/referrals/referrals2.ts index 6fdba167d..3fcda1718 100644 --- a/server/tests/advanced/referrals/referrals2.ts +++ b/server/tests/advanced/referrals/referrals2.ts @@ -1,33 +1,33 @@ -import { products, referralPrograms } from "../../global.js"; +import { + type Customer, + ErrCode, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; import { assert } from "chai"; import chalk from "chalk"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { addDays } from "date-fns"; +import type { Stripe } from "stripe"; import { setupBefore } from "tests/before.js"; -import { - Customer, - ErrCode, - ReferralCode, - RewardRedemption, -} from "@autumn/shared"; import { timeout } from "tests/utils/genUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addDays, addHours, addMonths } from "date-fns"; -import { Stripe } from "stripe"; import { initCustomer } from "tests/utils/init.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { products, referralPrograms } from "../../global.js"; // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( "referrals2: Testing referrals (immediate redemption)", )}`, () => { - let mainCustomerId = "main-referral-2"; - let redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"]; - let autumn: AutumnInt = new AutumnInt(); + const mainCustomerId = "main-referral-2"; + const redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"]; + const autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; let testClockId: string; let referralCode: ReferralCode; - let redemptions: RewardRedemption[] = []; + const redemptions: RewardRedemption[] = []; let mainCustomer: Customer; before(async function () { @@ -44,8 +44,8 @@ describe(`${chalk.yellowBright( testClockId = testClockId1; mainCustomer = customer; - let batchCreate = []; - for (let redeemer of redeemers) { + const batchCreate = []; + for (const redeemer of redeemers) { batchCreate.push( initCustomer({ customerId: redeemer, @@ -60,7 +60,7 @@ describe(`${chalk.yellowBright( await Promise.all(batchCreate); }); - it("should create code once", async function () { + it("should create code once", async () => { referralCode = await autumn.referrals.createCode({ customerId: mainCustomerId, referralId: referralPrograms.immediate.id, @@ -69,12 +69,12 @@ describe(`${chalk.yellowBright( assert.exists(referralCode.code); }); - it("should create redemption for each redeemer and fail if redeemed again", async function () { + it("should create redemption for each redeemer and fail if redeemed again", async () => { for (let i = 0; i < redeemers.length; i++) { - let redeemer = redeemers[i]; - let count = i + 1; + const redeemer = redeemers[i]; + const count = i + 1; try { - let redemption: RewardRedemption = await autumn.referrals.redeem({ + const redemption: RewardRedemption = await autumn.referrals.redeem({ customerId: redeemer, code: referralCode.code, }); @@ -95,7 +95,7 @@ describe(`${chalk.yellowBright( } // Check stripe customer - let stripeCus = (await stripeCli.customers.retrieve( + const stripeCus = (await stripeCli.customers.retrieve( mainCustomer.processor?.id, )) as Stripe.Customer; @@ -103,7 +103,7 @@ describe(`${chalk.yellowBright( }); let curTime = new Date(); - it("customer should have discount for first purchase", async function () { + it("customer should have discount for first purchase", async () => { await autumn.attach({ customer_id: mainCustomerId, product_id: products.proWithTrial.id, @@ -120,7 +120,7 @@ describe(`${chalk.yellowBright( }); // 1. Get invoice - let { invoices } = await autumn.customers.get(mainCustomerId); + const { invoices } = await autumn.customers.get(mainCustomerId); assert.equal(invoices!.length, 2); assert.equal(invoices![0].total, 0); diff --git a/server/tests/advanced/referrals/referrals3.ts b/server/tests/advanced/referrals/referrals3.ts index d4b229704..500f5294c 100644 --- a/server/tests/advanced/referrals/referrals3.ts +++ b/server/tests/advanced/referrals/referrals3.ts @@ -1,32 +1,32 @@ -import { features, products, referralPrograms } from "../../global.js"; +import { + type Customer, + ErrCode, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; import { assert } from "chai"; import chalk from "chalk"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import type { Stripe } from "stripe"; import { setupBefore } from "tests/before.js"; -import { - Customer, - ErrCode, - ReferralCode, - RewardRedemption, -} from "@autumn/shared"; -import { timeout } from "tests/utils/genUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { Stripe } from "stripe"; -import { initCustomer } from "tests/utils/init.js"; import { compareProductEntitlements } from "tests/utils/compare.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { initCustomer } from "tests/utils/init.js"; +import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { features, products, referralPrograms } from "../../global.js"; // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( "referrals3: Testing free product referrals", )}`, () => { - let mainCustomerId = "main-referral-3"; - let redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"]; + const mainCustomerId = "main-referral-3"; + const redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"]; let autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; let testClockId: string; let referralCode: ReferralCode; - let redemptions: RewardRedemption[] = []; + const redemptions: RewardRedemption[] = []; let mainCustomer: Customer; before(async function () { @@ -50,8 +50,8 @@ describe(`${chalk.yellowBright( product_id: products.proWithTrial.id, }); - let batchCreate = []; - for (let redeemer of redeemers) { + const batchCreate = []; + for (const redeemer of redeemers) { batchCreate.push( initCustomer({ customerId: redeemer, @@ -66,7 +66,7 @@ describe(`${chalk.yellowBright( await Promise.all(batchCreate); }); - it("should create code once", async function () { + it("should create code once", async () => { referralCode = await autumn.referrals.createCode({ customerId: mainCustomerId, referralId: referralPrograms.freeProduct.id, @@ -75,9 +75,9 @@ describe(`${chalk.yellowBright( assert.exists(referralCode.code); }); - it("should create redemption for each redeemer and fail if redeemed again", async function () { - for (let redeemer of redeemers) { - let redemption: RewardRedemption = await autumn.referrals.redeem({ + it("should create redemption for each redeemer and fail if redeemed again", async () => { + for (const redeemer of redeemers) { + const redemption: RewardRedemption = await autumn.referrals.redeem({ customerId: redeemer, code: referralCode.code, }); @@ -90,7 +90,7 @@ describe(`${chalk.yellowBright( // Try redeem for redeemer1 again try { - let redemption1 = await autumn.referrals.redeem({ + const redemption1 = await autumn.referrals.redeem({ customerId: redeemers[0], code: referralCode.code, }); @@ -101,9 +101,9 @@ describe(`${chalk.yellowBright( } }); - it("should be triggered (and applied) when redeemers check out", async function () { + it("should be triggered (and applied) when redeemers check out", async () => { for (let i = 0; i < redeemers.length; i++) { - let redeemer = redeemers[i]; + const redeemer = redeemers[i]; await autumn.attach({ customer_id: redeemer, @@ -113,10 +113,10 @@ describe(`${chalk.yellowBright( await timeout(3000); // Get redemption object - let redemption = await autumn.redemptions.get(redemptions[i].id); + const redemption = await autumn.redemptions.get(redemptions[i].id); // Check if redemption is triggered - let count = i + 1; + const count = i + 1; if (count > referralPrograms.freeProduct.max_redemptions) { assert.equal(redemption.triggered, false); diff --git a/server/tests/utils/expectUtils/expectErrUtils.ts b/server/tests/utils/expectUtils/expectErrUtils.ts index 636b3397b..4029c1460 100644 --- a/server/tests/utils/expectUtils/expectErrUtils.ts +++ b/server/tests/utils/expectUtils/expectErrUtils.ts @@ -1,5 +1,4 @@ import { assert, expect } from "chai"; -import { ErrCode } from "@autumn/shared"; import AutumnError from "@/external/autumn/autumnCli.js"; export const expectAutumnError = async ({ @@ -12,7 +11,7 @@ export const expectAutumnError = async ({ func: () => Promise; }) => { try { - let result = await func(); + const result = await func(); assert.fail( `Expected to receive autumn error ${errCode}, but received none`, diff --git a/shared/api/errors/base/AutumnError.ts b/shared/api/errors/base/RecaseError.ts similarity index 86% rename from shared/api/errors/base/AutumnError.ts rename to shared/api/errors/base/RecaseError.ts index 668a39fa5..27ea97820 100644 --- a/shared/api/errors/base/AutumnError.ts +++ b/shared/api/errors/base/RecaseError.ts @@ -2,7 +2,7 @@ * Base error class for all Autumn API errors * This should match the RecaseError interface from the server */ -export class AutumnError extends Error { +export class RecaseError extends Error { code: string; statusCode: number; data?: unknown; @@ -19,7 +19,7 @@ export class AutumnError extends Error { data?: unknown; }) { super(message); - this.name = "AutumnError"; + this.name = "RecaseError"; this.code = code || "invalid_request"; this.statusCode = statusCode; this.data = data; diff --git a/shared/api/errors/classes/cusProductErrClasses.ts b/shared/api/errors/classes/cusProductErrClasses.ts index 477daf169..f691b1108 100644 --- a/shared/api/errors/classes/cusProductErrClasses.ts +++ b/shared/api/errors/classes/cusProductErrClasses.ts @@ -1,10 +1,10 @@ -import { AutumnError } from "../base/AutumnError.js"; +import { RecaseError } from "../base/RecaseError.js"; import { CusProductErrorCode } from "../codes/cusProductErrCodes.js"; /** * Product not found error */ -export class CusProductNotFoundError extends AutumnError { +export class CusProductNotFoundError extends RecaseError { constructor(opts: { customerId: string; productId: string; @@ -23,7 +23,7 @@ export class CusProductNotFoundError extends AutumnError { } } -export class CusProductAlreadyExistsError extends AutumnError { +export class CusProductAlreadyExistsError extends RecaseError { constructor(opts: { productId: string; customerId?: string; diff --git a/shared/api/errors/classes/productErrClasses.ts b/shared/api/errors/classes/productErrClasses.ts index 6af8ad3f0..008014ed8 100644 --- a/shared/api/errors/classes/productErrClasses.ts +++ b/shared/api/errors/classes/productErrClasses.ts @@ -1,10 +1,10 @@ -import { AutumnError } from "../base/AutumnError.js"; +import { RecaseError } from "../base/RecaseError.js"; import { ProductErrorCode } from "../codes/productErrCodes.js"; /** * Product not found error */ -export class ProductNotFoundError extends AutumnError { +export class ProductNotFoundError extends RecaseError { constructor(opts: { productId: string; version?: string }) { super({ message: `Product ${opts.productId} ${opts.version ? ` (version ${opts.version})` : ""} not found`, @@ -18,7 +18,7 @@ export class ProductNotFoundError extends AutumnError { /** * Product already exists error */ -export class ProductAlreadyExistsError extends AutumnError { +export class ProductAlreadyExistsError extends RecaseError { constructor(opts: { productId: string; message?: string }) { super({ message: opts.message || `Product ${opts.productId} already exists`, diff --git a/shared/api/errors/index.ts b/shared/api/errors/index.ts index 4ab5a98c5..494cfae0c 100644 --- a/shared/api/errors/index.ts +++ b/shared/api/errors/index.ts @@ -1,5 +1,8 @@ -export * from "./base/AutumnError.js"; export * from "./base/InternalError.js"; +export * from "./base/RecaseError.js"; +export * from "./classes/cusProductErrClasses.js"; export * from "./classes/productErrClasses.js"; + +export * from "./codes/cusProductErrCodes.js"; export * from "./codes/productErrCodes.js"; diff --git a/shared/api/products/operations/createProductParams.ts b/shared/api/products/operations/createProductParams.ts index a98339a87..747a8ffa4 100644 --- a/shared/api/products/operations/createProductParams.ts +++ b/shared/api/products/operations/createProductParams.ts @@ -1,6 +1,10 @@ +import { CreateFreeTrialSchema } from "@models/productModels/freeTrialModels/freeTrialModels.js"; +import { ProductItemSchema } from "@models/productV2Models/productItemModels/productItemModels.js"; import { idRegex } from "@utils/utils.js"; import { z } from "zod/v4"; +export const CreateProductItemParamsSchema = ProductItemSchema; + export const CreateProductParamsSchema = z.object({ id: z.string().nonempty().regex(idRegex), @@ -12,4 +16,9 @@ export const CreateProductParamsSchema = z.object({ is_default: z.boolean().default(false), version: z.number().optional(), group: z.string().default(""), + + items: z.array(CreateProductItemParamsSchema).optional(), + free_trial: CreateFreeTrialSchema.optional(), }); + +export type CreateProductParams = z.infer; diff --git a/shared/models/cusModels/cusResModels/cusProductResponse.ts b/shared/models/cusModels/cusResModels/cusProductResponse.ts index 4d19af496..fe7bf4813 100644 --- a/shared/models/cusModels/cusResModels/cusProductResponse.ts +++ b/shared/models/cusModels/cusResModels/cusProductResponse.ts @@ -1,4 +1,4 @@ -import { APIProductItemSchema } from "@api/models.js"; +import { APIProductItemSchema } from "@api/products/apiProductItem.js"; import { z } from "zod/v4"; import { CusProductStatus } from "../../cusProductModels/cusProductEnums.js";