reworking create product
This commit is contained in:
3
bun.lock
3
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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<HonoEnv>) => {
|
||||
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}]`,
|
||||
{
|
||||
|
||||
80
server/src/honoMiddlewares/routeHandler.ts
Normal file
80
server/src/honoMiddlewares/routeHandler.ts
Normal file
@@ -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<Body> : unknown;
|
||||
query: Query extends ZodType ? z.infer<Query> : unknown;
|
||||
};
|
||||
out: {
|
||||
json: Body extends ZodType ? z.infer<Body> : unknown;
|
||||
query: Query extends ZodType ? z.infer<Query> : 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<HonoEnv, Body, Query>,
|
||||
) => Response | Promise<Response>;
|
||||
}) {
|
||||
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<HonoEnv, Body, Query>) => {
|
||||
return await opts.handler(c);
|
||||
};
|
||||
|
||||
return [...middlewares, wrappedHandler as Handler] as const;
|
||||
}
|
||||
30
server/src/honoMiddlewares/validatorMiddleware.ts
Normal file
30
server/src/honoMiddlewares/validatorMiddleware.ts
Normal file
@@ -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 = <T extends ZodType>(
|
||||
target: "json" | "query" | "param" | "header" | "form",
|
||||
schema: T,
|
||||
) => {
|
||||
return zValidator(target, schema, (result, _c) => {
|
||||
if (!result.success) {
|
||||
throw result.error;
|
||||
}
|
||||
});
|
||||
};
|
||||
30
server/src/honoUtils/HonoEnv.ts
Normal file
30
server/src/honoUtils/HonoEnv.ts
Normal file
@@ -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 };
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
// }),
|
||||
// );
|
||||
},
|
||||
});
|
||||
|
||||
255
server/src/internal/products/handlers/handleCreateProduct_old.ts
Normal file
255
server/src/internal/products/handlers/handleCreateProduct_old.ts
Normal file
@@ -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,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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<HonoEnv>();
|
||||
|
||||
export const handleCreateProduct = async (c: Context<HonoEnv>) => {
|
||||
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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ export const routeHandler = async <TLoad = undefined>({
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<any>;
|
||||
}) => {
|
||||
try {
|
||||
let result = await func();
|
||||
const result = await func();
|
||||
|
||||
assert.fail(
|
||||
`Expected to receive autumn error ${errCode}, but received none`,
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<typeof CreateProductParamsSchema>;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user