feat: created api product and api feature zod schemas
This commit is contained in:
@@ -9,8 +9,7 @@
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"experimentalScannerIgnores": ["dist/**"],
|
||||
"includes": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"ignore": ["dist/**"]
|
||||
"includes": ["src/**/*.ts", "tests/**/*.ts"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
@@ -20,9 +19,6 @@
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noNonNullAssertion": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExportsInTest": "off",
|
||||
"noExplicitAny": "off",
|
||||
@@ -30,6 +26,12 @@
|
||||
},
|
||||
"complexity": {
|
||||
"noStaticOnlyClass": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"useParseIntRadix": "off"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,14 +3,14 @@ import { config } from "dotenv";
|
||||
config();
|
||||
|
||||
import "./instrumentation.js";
|
||||
import cluster from "node:cluster";
|
||||
import http from "node:http";
|
||||
import os from "node:os";
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { context, trace } from "@opentelemetry/api";
|
||||
import { toNodeHandler } from "better-auth/node";
|
||||
import cluster from "cluster";
|
||||
import cors from "cors";
|
||||
import express from "express";
|
||||
import http from "http";
|
||||
import os from "os";
|
||||
import { client, db } from "./db/initDrizzle.js";
|
||||
import { CacheManager } from "./external/caching/CacheManager.js";
|
||||
import { ClickHouseManager } from "./external/clickhouse/ClickHouseManager.js";
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import express, { Router } from "express";
|
||||
import { FeatureService } from "./FeatureService.js";
|
||||
import { fromAPIFeature, toAPIFeature } from "./utils/mapFeatureUtils.js";
|
||||
import {
|
||||
APIFeatureSchema,
|
||||
APIFeatureType,
|
||||
ErrCode,
|
||||
Feature,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
UpdateAPIFeatureSchema,
|
||||
type FeatureUsageType,
|
||||
UpdateFeatureParamsSchema,
|
||||
} from "@autumn/shared";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
|
||||
import express, { type Router } from "express";
|
||||
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 { handleUpdateFeature } from "./handlers/handleUpdateFeature.js";
|
||||
import { handleDeleteFeature } from "./handlers/handleDeleteFeature.js";
|
||||
import { keyToTitle, notNullOrUndefined } from "@/utils/genUtils.js";
|
||||
import { FeatureService } from "./FeatureService.js";
|
||||
import { validateFeatureId } from "./featureUtils.js";
|
||||
import { handleDeleteFeature } from "./handlers/handleDeleteFeature.js";
|
||||
import { handleGetFeatureDeletionInfo } from "./handlers/handleGetFeatureDeletionInfo.js";
|
||||
import { handleUpdateFeature } from "./handlers/handleUpdateFeature.js";
|
||||
import { fromAPIFeature, toAPIFeature } from "./utils/mapFeatureUtils.js";
|
||||
|
||||
export const featureRouter: Router = express.Router();
|
||||
|
||||
@@ -29,7 +31,7 @@ featureRouter.get("", async (req: any, res: any) =>
|
||||
action: "list features",
|
||||
handler: async () => {
|
||||
const includeArchived = req.query.include_archived === "true";
|
||||
let features = await FeatureService.list({
|
||||
const features = await FeatureService.list({
|
||||
db: req.db,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
@@ -51,7 +53,7 @@ featureRouter.get("/:featureId", async (req: any, res: any) =>
|
||||
action: "Get feature",
|
||||
handler: async () => {
|
||||
const feature = req.features.find(
|
||||
(f: Feature) => f.id == req.params.featureId,
|
||||
(f: Feature) => f.id === req.params.featureId,
|
||||
);
|
||||
|
||||
if (!feature) {
|
||||
@@ -73,14 +75,14 @@ featureRouter.post("", async (req: any, res: any) =>
|
||||
res,
|
||||
action: "Create feature",
|
||||
handler: async () => {
|
||||
let apiFeature = APIFeatureSchema.parse(req.body);
|
||||
const apiFeature = APIFeatureSchema.parse(req.body);
|
||||
if (!apiFeature.name) {
|
||||
apiFeature.name = keyToTitle(apiFeature.id);
|
||||
}
|
||||
|
||||
validateFeatureId(apiFeature.id);
|
||||
|
||||
let feature = fromAPIFeature({
|
||||
const feature = fromAPIFeature({
|
||||
apiFeature,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
@@ -88,7 +90,7 @@ featureRouter.post("", async (req: any, res: any) =>
|
||||
|
||||
const { db, logger, features: curFeatures } = req;
|
||||
|
||||
let curFeature = curFeatures.find((f: Feature) => f.id == feature.id);
|
||||
const curFeature = curFeatures.find((f: Feature) => f.id === feature.id);
|
||||
|
||||
if (curFeature) {
|
||||
throw new RecaseError({
|
||||
@@ -116,11 +118,14 @@ featureRouter.post("/:feature_id", async (req: any, res: any) =>
|
||||
res,
|
||||
action: "Update feature",
|
||||
handler: async (req: any, res: any) => {
|
||||
let { feature_id: featureId } = req.params;
|
||||
let { features: curFeatures } = req;
|
||||
let apiFeature = UpdateAPIFeatureSchema.parse(req.body);
|
||||
const { feature_id: featureId } = req.params;
|
||||
const { features: curFeatures } = req;
|
||||
const apiFeature = UpdateFeatureParamsSchema.parse(req.body);
|
||||
|
||||
const originalFeature = curFeatures.find(
|
||||
(f: Feature) => f.id === featureId,
|
||||
);
|
||||
|
||||
let originalFeature = curFeatures.find((f: Feature) => f.id == featureId);
|
||||
if (!originalFeature) {
|
||||
throw new RecaseError({
|
||||
message: `Feature with id ${featureId} not found`,
|
||||
@@ -131,16 +136,16 @@ featureRouter.post("/:feature_id", async (req: any, res: any) =>
|
||||
|
||||
// Replace body...
|
||||
let featureType = apiFeature.type as unknown as FeatureType;
|
||||
let usageType = undefined;
|
||||
let usageType: FeatureUsageType | undefined;
|
||||
if (
|
||||
apiFeature.type == APIFeatureType.SingleUsage ||
|
||||
apiFeature.type == APIFeatureType.ContinuousUse
|
||||
apiFeature.type === APIFeatureType.SingleUsage ||
|
||||
apiFeature.type === APIFeatureType.ContinuousUse
|
||||
) {
|
||||
featureType = FeatureType.Metered;
|
||||
usageType = apiFeature.type;
|
||||
usageType = apiFeature.type as unknown as FeatureUsageType;
|
||||
}
|
||||
|
||||
let newConfig = originalFeature.config;
|
||||
const newConfig = originalFeature.config;
|
||||
if (usageType) {
|
||||
newConfig.usage_type = usageType;
|
||||
}
|
||||
@@ -152,7 +157,7 @@ featureRouter.post("/:feature_id", async (req: any, res: any) =>
|
||||
}));
|
||||
}
|
||||
|
||||
let newBody = {
|
||||
const newBody = {
|
||||
id: req.body.id || undefined,
|
||||
name: req.body.name || undefined,
|
||||
type: featureType,
|
||||
@@ -163,15 +168,6 @@ featureRouter.post("/:feature_id", async (req: any, res: any) =>
|
||||
req.body = newBody;
|
||||
|
||||
await handleUpdateFeature(req, res, true);
|
||||
|
||||
// let newFeature = await FeatureService.get({
|
||||
// db: req.db,
|
||||
// id: featureId,
|
||||
// orgId: req.orgId,
|
||||
// env: req.env,
|
||||
// });
|
||||
|
||||
// res.status(200).json(toAPIFeature({ feature: newFeature }));
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,48 +1,45 @@
|
||||
import { validateProductItems } from "@/internal/products/product-items/validateProductItems.js";
|
||||
import {
|
||||
APIProductSchema,
|
||||
CreateProductSchema,
|
||||
type Entitlement,
|
||||
ErrCode,
|
||||
type FreeTrial,
|
||||
type FullProduct,
|
||||
type Price,
|
||||
type Product,
|
||||
type ProductItem,
|
||||
} from "@autumn/shared";
|
||||
import {
|
||||
handleNewFreeTrial,
|
||||
validateAndInitFreeTrial,
|
||||
} from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.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 {
|
||||
CreateProductSchema,
|
||||
Entitlement,
|
||||
ErrCode,
|
||||
FreeTrial,
|
||||
FullProduct,
|
||||
Price,
|
||||
Product,
|
||||
ProductItem,
|
||||
ProductResponseSchema,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
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 { ProductService } from "@/internal/products/ProductService.js";
|
||||
import {
|
||||
constructProduct,
|
||||
getGroupToDefaults,
|
||||
initProductInStripe,
|
||||
} from "@/internal/products/productUtils.js";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
import { isDefaultTrial } from "../productUtils/classifyProduct.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 }) => {
|
||||
let { free_trial, items } = req.body;
|
||||
let { orgId, env, db, features } = req;
|
||||
const { free_trial, items } = req.body;
|
||||
const { orgId, env, db, features } = req;
|
||||
|
||||
let productData = CreateProductSchema.parse(req.body);
|
||||
const productData = CreateProductSchema.parse(req.body);
|
||||
|
||||
validateId("Product", productData.id);
|
||||
|
||||
@@ -169,14 +166,15 @@ export const handleCreateProduct = async (req: Request, res: any) =>
|
||||
res,
|
||||
action: "POST /products",
|
||||
handler: async (req, res) => {
|
||||
let { items } = req.body;
|
||||
let { logtail: logger, org, features, env, db } = req;
|
||||
const { items } = req.body;
|
||||
|
||||
let { freeTrial, productData } = await validateCreateProduct({
|
||||
const { logtail: logger, org, features, env, db } = req;
|
||||
|
||||
const { freeTrial, productData } = await validateCreateProduct({
|
||||
req,
|
||||
});
|
||||
|
||||
let newProduct = constructProduct({
|
||||
const newProduct = constructProduct({
|
||||
productData,
|
||||
orgId: org.id,
|
||||
env,
|
||||
@@ -189,7 +187,7 @@ export const handleCreateProduct = async (req: Request, res: any) =>
|
||||
freeTrial: freeTrial || null,
|
||||
});
|
||||
|
||||
let product = await ProductService.insert({ db, product: newProduct });
|
||||
const product = await ProductService.insert({ db, product: newProduct });
|
||||
|
||||
let prices: Price[] = [];
|
||||
let entitlements: Entitlement[] = [];
|
||||
@@ -248,7 +246,7 @@ export const handleCreateProduct = async (req: Request, res: any) =>
|
||||
});
|
||||
|
||||
res.status(200).json(
|
||||
ProductResponseSchema.parse({
|
||||
APIProductSchema.parse({
|
||||
...product,
|
||||
autumn_id: product.internal_id,
|
||||
items: items || [],
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import type {
|
||||
ExtendedRequest,
|
||||
ExtendedResponse,
|
||||
} from "@/utils/models/Request.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
export const handleGetProduct = async (req: any, res: any) =>
|
||||
routeHandler({
|
||||
@@ -14,7 +17,7 @@ export const handleGetProduct = async (req: any, res: any) =>
|
||||
action: "get product",
|
||||
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
|
||||
const { productId } = req.params;
|
||||
let { schemaVersion } = req.query as { schemaVersion: string };
|
||||
const { schemaVersion } = req.query as { schemaVersion: string };
|
||||
|
||||
const { db, orgId, env } = req;
|
||||
|
||||
@@ -25,7 +28,7 @@ export const handleGetProduct = async (req: any, res: any) =>
|
||||
});
|
||||
}
|
||||
|
||||
let [product, features] = await Promise.all([
|
||||
const [product, features] = await Promise.all([
|
||||
ProductService.getFull({
|
||||
db,
|
||||
orgId,
|
||||
@@ -43,9 +46,9 @@ export const handleGetProduct = async (req: any, res: any) =>
|
||||
});
|
||||
}
|
||||
|
||||
let schemaVersionInt = schemaVersion ? parseInt(schemaVersion) : 2;
|
||||
const schemaVersionInt = schemaVersion ? parseInt(schemaVersion) : 2;
|
||||
|
||||
if (schemaVersionInt == 1) {
|
||||
if (schemaVersionInt === 1) {
|
||||
res.status(200).json(product);
|
||||
} else {
|
||||
res.status(200).json(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import type {
|
||||
ExtendedRequest,
|
||||
ExtendedResponse,
|
||||
} from "@/utils/models/Request.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { getProductResponse } from "../productUtils/productResponseUtils/getProductResponse.js";
|
||||
|
||||
export const handleListProducts = async (req: any, res: any) =>
|
||||
@@ -21,7 +24,7 @@ export const handleListProducts = async (req: any, res: any) =>
|
||||
}),
|
||||
]);
|
||||
|
||||
let prods = await Promise.all(
|
||||
const prods = await Promise.all(
|
||||
products.map((p) => getProductResponse({ product: p, features })),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import { ErrCode, type FeatureOptions, UsageModel } from "@autumn/shared";
|
||||
import { Router } from "express";
|
||||
import { FeatureService } from "../features/FeatureService.js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { ProductService } from "./ProductService.js";
|
||||
import { ErrCode, UsageModel } from "@autumn/shared";
|
||||
import { FeatureOptions } from "@autumn/shared";
|
||||
import { OrgService } from "../orgs/OrgService.js";
|
||||
import { RewardService } from "../rewards/RewardService.js";
|
||||
import { getGroupToDefaults, getProductVersionCounts } from "./productUtils.js";
|
||||
import { getLatestProducts } from "./productUtils.js";
|
||||
import { CusProdReadService } from "../customers/cusProducts/CusProdReadService.js";
|
||||
import { MigrationService } from "../migrations/MigrationService.js";
|
||||
import { RewardProgramService } from "../rewards/RewardProgramService.js";
|
||||
import { mapToProductV2 } from "./productV2Utils.js";
|
||||
import { isFeaturePriceItem } from "./product-items/productItemUtils/getItemType.js";
|
||||
|
||||
import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
|
||||
|
||||
import { CusProdReadService } from "../customers/cusProducts/CusProdReadService.js";
|
||||
import { FeatureService } from "../features/FeatureService.js";
|
||||
import { MigrationService } from "../migrations/MigrationService.js";
|
||||
import { OrgService } from "../orgs/OrgService.js";
|
||||
import { createOrgResponse } from "../orgs/orgUtils.js";
|
||||
import {
|
||||
sortFullProducts,
|
||||
sortProductsByPrice,
|
||||
} from "./productUtils/sortProductUtils.js";
|
||||
import { RewardProgramService } from "../rewards/RewardProgramService.js";
|
||||
import { RewardService } from "../rewards/RewardService.js";
|
||||
import { handleGetProductDeleteInfo } from "./handlers/handleGetProductDeleteInfo.js";
|
||||
import { ProductService } from "./ProductService.js";
|
||||
import { isFeaturePriceItem } from "./product-items/productItemUtils/getItemType.js";
|
||||
import { sortFullProducts } from "./productUtils/sortProductUtils.js";
|
||||
import {
|
||||
getGroupToDefaults,
|
||||
getLatestProducts,
|
||||
getProductVersionCounts,
|
||||
} from "./productUtils.js";
|
||||
import { mapToProductV2 } from "./productV2Utils.js";
|
||||
|
||||
export const productRouter: Router = Router({ mergeParams: true });
|
||||
|
||||
@@ -56,14 +53,14 @@ productRouter.get("/products", async (req: any, res) => {
|
||||
// Get counts for all products
|
||||
productRouter.get("/product_counts", async (req: any, res) => {
|
||||
try {
|
||||
let { db } = req;
|
||||
let products = await ProductService.listFull({
|
||||
const { db } = req;
|
||||
const products = await ProductService.listFull({
|
||||
db,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
});
|
||||
|
||||
let counts = await Promise.all(
|
||||
const counts = await Promise.all(
|
||||
products.map(async (product) => {
|
||||
// if (latestVersion) {
|
||||
// return CusProdReadService.getCounts({
|
||||
@@ -81,7 +78,7 @@ productRouter.get("/product_counts", async (req: any, res) => {
|
||||
}),
|
||||
);
|
||||
|
||||
let result: { [key: string]: any } = {};
|
||||
const result: { [key: string]: any } = {};
|
||||
for (let i = 0; i < products.length; i++) {
|
||||
if (!result[products[i].id]) {
|
||||
result[products[i].id] = counts[i];
|
||||
@@ -159,7 +156,7 @@ productRouter.get("/:productId/data2", async (req: any, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
let productV2 = mapToProductV2({
|
||||
const productV2 = mapToProductV2({
|
||||
product: product,
|
||||
features: req.features,
|
||||
});
|
||||
@@ -237,7 +234,7 @@ productRouter.get("/migrations", async (req: any, res) => {
|
||||
|
||||
productRouter.get("/data", async (req: any, res) => {
|
||||
try {
|
||||
let { db } = req;
|
||||
const { db } = req;
|
||||
|
||||
const allVersions = req.query.all_versions === "true";
|
||||
|
||||
@@ -292,8 +289,8 @@ productRouter.get("/data", async (req: any, res) => {
|
||||
|
||||
productRouter.post("/data", async (req: any, res) => {
|
||||
try {
|
||||
let { db } = req;
|
||||
let { showArchived } = req.body;
|
||||
const { db } = req;
|
||||
const { showArchived } = req.body;
|
||||
|
||||
const [products, defaultProds, features, org, coupons, rewardPrograms] =
|
||||
await Promise.all([
|
||||
@@ -343,8 +340,8 @@ productRouter.post("/data", async (req: any, res) => {
|
||||
|
||||
productRouter.get("/counts", async (req: any, res) => {
|
||||
try {
|
||||
let { db } = req;
|
||||
let products = await ProductService.listFull({
|
||||
const { db } = req;
|
||||
const products = await ProductService.listFull({
|
||||
db,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
@@ -353,7 +350,7 @@ productRouter.get("/counts", async (req: any, res) => {
|
||||
|
||||
const latestVersion = req.query.latest_version === "true";
|
||||
|
||||
let counts = await Promise.all(
|
||||
const counts = await Promise.all(
|
||||
products.map(async (product) => {
|
||||
if (latestVersion) {
|
||||
return CusProdReadService.getCounts({
|
||||
@@ -371,7 +368,7 @@ productRouter.get("/counts", async (req: any, res) => {
|
||||
}),
|
||||
);
|
||||
|
||||
let result: { [key: string]: any } = {};
|
||||
const result: { [key: string]: any } = {};
|
||||
for (let i = 0; i < products.length; i++) {
|
||||
if (!result[products[i].id]) {
|
||||
result[products[i].id] = counts[i];
|
||||
@@ -447,7 +444,7 @@ productRouter.get("/:productId/data", async (req: any, res) => {
|
||||
return b.id.localeCompare(a.id);
|
||||
});
|
||||
|
||||
let productV2 = mapToProductV2({ product, features });
|
||||
const productV2 = mapToProductV2({ product, features });
|
||||
|
||||
res.status(200).send({
|
||||
product: productV2,
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
|
||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
|
||||
|
||||
import {
|
||||
ProductV2,
|
||||
Feature,
|
||||
ProductItem,
|
||||
Organization,
|
||||
ProductItemFeatureType,
|
||||
ErrCode,
|
||||
Infinite,
|
||||
FullCusProduct,
|
||||
numberWithCommas,
|
||||
AttachScenario,
|
||||
FullProduct,
|
||||
FullCustomer,
|
||||
cusProductToProduct,
|
||||
ErrCode,
|
||||
type Feature,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
type FullProduct,
|
||||
Infinite,
|
||||
numberWithCommas,
|
||||
type Organization,
|
||||
type ProductItem,
|
||||
ProductItemFeatureType,
|
||||
type ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { isPriceItem } from "../product-items/productItemUtils/getItemType.js";
|
||||
import { isFeaturePriceItem } from "../product-items/productItemUtils/getItemType.js";
|
||||
import { cusProductToProduct } from "@autumn/shared";
|
||||
import { isProductUpgrade } from "../productUtils.js";
|
||||
import { getLargestInterval } from "../prices/priceUtils/priceIntervalUtils.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import { getFreeTrialAfterFingerprint } from "../free-trials/freeTrialUtils.js";
|
||||
import { getLargestInterval } from "../prices/priceUtils/priceIntervalUtils.js";
|
||||
import {
|
||||
isFeaturePriceItem,
|
||||
isPriceItem,
|
||||
} from "../product-items/productItemUtils/getItemType.js";
|
||||
import { isProductUpgrade } from "../productUtils.js";
|
||||
|
||||
export const sortProductItems = (items: ProductItem[], features: Feature[]) => {
|
||||
items.sort((a, b) => {
|
||||
let aIsPriceItem = isPriceItem(a);
|
||||
let bIsPriceItem = isPriceItem(b);
|
||||
const aIsPriceItem = isPriceItem(a);
|
||||
const bIsPriceItem = isPriceItem(b);
|
||||
|
||||
if (aIsPriceItem && bIsPriceItem) {
|
||||
return 0;
|
||||
@@ -43,8 +43,8 @@ export const sortProductItems = (items: ProductItem[], features: Feature[]) => {
|
||||
}
|
||||
|
||||
// 2. Put feature price next
|
||||
let aIsFeatureItem = isFeaturePriceItem(a);
|
||||
let bIsFeatureItem = isFeaturePriceItem(b);
|
||||
const aIsFeatureItem = isFeaturePriceItem(a);
|
||||
const bIsFeatureItem = isFeaturePriceItem(b);
|
||||
|
||||
if (aIsFeatureItem && !bIsFeatureItem) {
|
||||
return -1;
|
||||
@@ -55,9 +55,9 @@ export const sortProductItems = (items: ProductItem[], features: Feature[]) => {
|
||||
}
|
||||
|
||||
// 3. Put feature price items in alphabetical order
|
||||
let feature = features.find((f) => f.id == a.feature_id);
|
||||
let aFeatureName = feature?.name;
|
||||
let bFeatureName = features.find((f) => f.id == b.feature_id)?.name;
|
||||
const feature = features.find((f) => f.id == a.feature_id);
|
||||
const aFeatureName = feature?.name;
|
||||
const bFeatureName = features.find((f) => f.id == b.feature_id)?.name;
|
||||
|
||||
if (!aFeatureName || !bFeatureName) {
|
||||
return 0;
|
||||
@@ -101,14 +101,14 @@ export const getPriceText = ({
|
||||
return formatAmount(item.price as number);
|
||||
}
|
||||
|
||||
let tiers = item.tiers;
|
||||
const tiers = item.tiers;
|
||||
if (tiers) {
|
||||
if (tiers.length == 1) {
|
||||
return formatAmount(tiers[0].amount);
|
||||
}
|
||||
|
||||
let firstPrice = tiers[0].amount;
|
||||
let lastPrice = tiers[tiers.length - 1].amount;
|
||||
const firstPrice = tiers[0].amount;
|
||||
const lastPrice = tiers[tiers.length - 1].amount;
|
||||
|
||||
return `${formatAmount(firstPrice)} - ${formatAmount(lastPrice)}`;
|
||||
}
|
||||
@@ -126,7 +126,9 @@ export const getPricecnPrice = ({
|
||||
items: ProductItem[];
|
||||
isMainPrice?: boolean;
|
||||
}) => {
|
||||
let priceExists = items.some((i) => isPriceItem(i) || isFeaturePriceItem(i));
|
||||
const priceExists = items.some(
|
||||
(i) => isPriceItem(i) || isFeaturePriceItem(i),
|
||||
);
|
||||
|
||||
if (!priceExists) {
|
||||
return {
|
||||
@@ -135,7 +137,7 @@ export const getPricecnPrice = ({
|
||||
};
|
||||
}
|
||||
|
||||
let priceItem = items[0];
|
||||
const priceItem = items[0];
|
||||
|
||||
if (isPriceItem(priceItem)) {
|
||||
return {
|
||||
@@ -144,8 +146,8 @@ export const getPricecnPrice = ({
|
||||
secondaryText: priceItem.interval ? `per ${priceItem.interval}` : " ",
|
||||
};
|
||||
} else {
|
||||
let feature = features.find((f) => f.id == priceItem.feature_id);
|
||||
let texts = featurePricetoPricecnItem({
|
||||
const feature = features.find((f) => f.id == priceItem.feature_id);
|
||||
const texts = featurePricetoPricecnItem({
|
||||
feature,
|
||||
item: priceItem,
|
||||
org,
|
||||
@@ -180,12 +182,12 @@ export const featureToPricecnItem = ({
|
||||
};
|
||||
}
|
||||
|
||||
let featureName = getIncludedFeatureName({
|
||||
const featureName = getIncludedFeatureName({
|
||||
feature,
|
||||
item,
|
||||
});
|
||||
|
||||
let includedUsageTxt =
|
||||
const includedUsageTxt =
|
||||
item.included_usage == Infinite
|
||||
? "Unlimited "
|
||||
: nullish(item.included_usage) || item.included_usage == 0
|
||||
@@ -219,14 +221,14 @@ export const featurePricetoPricecnItem = ({
|
||||
}
|
||||
|
||||
// 1. Get included usage
|
||||
let includedFeatureName = getIncludedFeatureName({
|
||||
const includedFeatureName = getIncludedFeatureName({
|
||||
feature,
|
||||
item,
|
||||
});
|
||||
|
||||
let includedUsageStr = "";
|
||||
if (notNullish(item.included_usage) && (item.included_usage as number) > 0) {
|
||||
let includedUsage = numberWithCommas(item.included_usage as number);
|
||||
const includedUsage = numberWithCommas(item.included_usage as number);
|
||||
if (withNameAfterIncluded) {
|
||||
includedUsageStr = `${includedUsage} ${includedFeatureName}`;
|
||||
} else {
|
||||
@@ -234,8 +236,8 @@ export const featurePricetoPricecnItem = ({
|
||||
}
|
||||
}
|
||||
|
||||
let priceStr = getPriceText({ item, org });
|
||||
let billingFeatureName = getFeatureName({
|
||||
const priceStr = getPriceText({ item, org });
|
||||
const billingFeatureName = getFeatureName({
|
||||
feature,
|
||||
plural: typeof item.billing_units == "number" && item.billing_units > 1,
|
||||
});
|
||||
@@ -247,7 +249,8 @@ export const featurePricetoPricecnItem = ({
|
||||
priceStr2 = `${billingFeatureName}`;
|
||||
}
|
||||
|
||||
let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : "";
|
||||
const intervalStr =
|
||||
isMainPrice && item.interval ? ` per ${item.interval}` : "";
|
||||
|
||||
if (includedUsageStr) {
|
||||
return {
|
||||
@@ -288,9 +291,9 @@ export const getAttachScenario = ({
|
||||
return AttachScenario.Scheduled;
|
||||
}
|
||||
|
||||
let curFullProduct = cusProductToProduct({ cusProduct: curMainProduct });
|
||||
const curFullProduct = cusProductToProduct({ cusProduct: curMainProduct });
|
||||
|
||||
let isUpgrade = isProductUpgrade({
|
||||
const isUpgrade = isProductUpgrade({
|
||||
prices1: curFullProduct.prices,
|
||||
prices2: fullProduct.prices,
|
||||
});
|
||||
@@ -319,29 +322,31 @@ export const toPricecnProduct = async ({
|
||||
curScheduledProduct?: FullCusProduct | null;
|
||||
fullCus?: FullCustomer;
|
||||
}) => {
|
||||
let items = structuredClone(product.items);
|
||||
const items = structuredClone(product.items);
|
||||
|
||||
sortProductItems(items, features);
|
||||
|
||||
let price = getPricecnPrice({ org, items, features });
|
||||
let priceExists = items.some((i) => isPriceItem(i) || isFeaturePriceItem(i));
|
||||
let itemsWithoutPrice = priceExists ? items.slice(1) : items;
|
||||
const price = getPricecnPrice({ org, items, features });
|
||||
const priceExists = items.some(
|
||||
(i) => isPriceItem(i) || isFeaturePriceItem(i),
|
||||
);
|
||||
const itemsWithoutPrice = priceExists ? items.slice(1) : items;
|
||||
|
||||
let pricecnItems = itemsWithoutPrice.map((i) => {
|
||||
const pricecnItems = itemsWithoutPrice.map((i) => {
|
||||
let data: {
|
||||
primaryText?: string;
|
||||
secondaryText?: string;
|
||||
};
|
||||
|
||||
if (isPriceItem(i)) {
|
||||
let priceTxt = getPriceText({ item: i, org });
|
||||
const priceTxt = getPriceText({ item: i, org });
|
||||
data = {
|
||||
primaryText: priceTxt,
|
||||
secondaryText: i.interval ? `per ${i.interval}` : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
let feature = features.find((f) => f.id == i.feature_id);
|
||||
const feature = features.find((f) => f.id == i.feature_id);
|
||||
if (isFeaturePriceItem(i)) {
|
||||
data = featurePricetoPricecnItem({
|
||||
feature,
|
||||
@@ -364,25 +369,25 @@ export const toPricecnProduct = async ({
|
||||
};
|
||||
});
|
||||
|
||||
let isCurrent = curMainProduct?.product.id == product.id;
|
||||
let isScheduled = curScheduledProduct?.product.id == product.id;
|
||||
const isCurrent = curMainProduct?.product.id == product.id;
|
||||
const isScheduled = curScheduledProduct?.product.id == product.id;
|
||||
|
||||
let buttonText = "Get Started";
|
||||
|
||||
if (isCurrent) {
|
||||
let isCanceled = curMainProduct!.canceled_at != null;
|
||||
const isCanceled = curMainProduct!.canceled_at != null;
|
||||
buttonText = isCanceled ? "Renew" : "Current Plan";
|
||||
} else if (isScheduled) {
|
||||
buttonText = "Scheduled";
|
||||
}
|
||||
|
||||
let scenario = getAttachScenario({
|
||||
const scenario = getAttachScenario({
|
||||
curMainProduct,
|
||||
curScheduledProduct,
|
||||
fullProduct,
|
||||
});
|
||||
|
||||
let freeTrial = fullProduct.free_trial;
|
||||
const freeTrial = fullProduct.free_trial;
|
||||
|
||||
let baseVariant = null;
|
||||
if (fullProduct.base_variant_id) {
|
||||
@@ -401,7 +406,7 @@ export const toPricecnProduct = async ({
|
||||
baseVariant ||
|
||||
otherProducts.some((p) => p.base_variant_id == product.id)
|
||||
) {
|
||||
let intervalSet = getLargestInterval({ prices: fullProduct.prices });
|
||||
const intervalSet = getLargestInterval({ prices: fullProduct.prices });
|
||||
intervalGroup = intervalSet?.interval;
|
||||
}
|
||||
|
||||
@@ -435,16 +440,7 @@ export const toPricecnProduct = async ({
|
||||
: null,
|
||||
items: pricecnItems,
|
||||
scenario,
|
||||
// free_trial: freeTrial
|
||||
// ? FreeTrialResponseSchema.parse({
|
||||
// ...freeTrial,
|
||||
// trial_available: trialAvailable,
|
||||
// })
|
||||
// : null,
|
||||
|
||||
// interval_group: intervalGroup,
|
||||
|
||||
// To deprecate
|
||||
buttonText,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {
|
||||
type APIFreeTrial,
|
||||
APIFreeTrialSchema,
|
||||
APIProductItemSchema,
|
||||
APIProductPropertiesSchema,
|
||||
APIProductSchema,
|
||||
AttachScenario,
|
||||
BillingInterval,
|
||||
type Feature,
|
||||
type FeatureOptions,
|
||||
type FreeTrialResponse,
|
||||
FreeTrialResponseSchema,
|
||||
type FullCustomer,
|
||||
type FullProduct,
|
||||
type Price,
|
||||
type ProductItem,
|
||||
ProductItemResponseSchema,
|
||||
ProductPropertiesSchema,
|
||||
ProductResponseSchema,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
@@ -71,7 +71,7 @@ export const getProductItemResponse = ({
|
||||
}
|
||||
|
||||
const feature = features.find((f) => f.id === item.feature_id);
|
||||
return ProductItemResponseSchema.parse({
|
||||
return APIProductItemSchema.parse({
|
||||
type,
|
||||
...item,
|
||||
feature: feature ? toAPIFeature({ feature }) : null,
|
||||
@@ -106,7 +106,7 @@ export const getFreeTrialResponse = async ({
|
||||
});
|
||||
|
||||
if (attachScenario === AttachScenario.Downgrade) trial = null;
|
||||
return FreeTrialResponseSchema.parse({
|
||||
return APIFreeTrialSchema.parse({
|
||||
duration: product.free_trial?.duration,
|
||||
length: product.free_trial?.length,
|
||||
unique_fingerprint: product.free_trial?.unique_fingerprint,
|
||||
@@ -116,7 +116,7 @@ export const getFreeTrialResponse = async ({
|
||||
}
|
||||
|
||||
if (product.free_trial) {
|
||||
return FreeTrialResponseSchema.parse({
|
||||
return APIFreeTrialSchema.parse({
|
||||
duration: product.free_trial?.duration,
|
||||
length: product.free_trial?.length,
|
||||
unique_fingerprint: product.free_trial?.unique_fingerprint,
|
||||
@@ -132,7 +132,7 @@ export const getProductProperties = ({
|
||||
freeTrial,
|
||||
}: {
|
||||
product: FullProduct;
|
||||
freeTrial?: FreeTrialResponse | null;
|
||||
freeTrial?: APIFreeTrial | null;
|
||||
}) => {
|
||||
const largestInterval = getLargestInterval({
|
||||
prices: product.prices,
|
||||
@@ -142,7 +142,7 @@ export const getProductProperties = ({
|
||||
const hasFreeTrial =
|
||||
notNullish(freeTrial) && freeTrial?.trial_available !== false;
|
||||
|
||||
return ProductPropertiesSchema.parse({
|
||||
return APIProductPropertiesSchema.parse({
|
||||
is_free: isFreeProduct(product.prices) || false,
|
||||
is_one_off: isOneOff(product.prices) || false,
|
||||
interval_group: largestInterval?.interval,
|
||||
@@ -205,9 +205,9 @@ export const getProductResponse = async ({
|
||||
product,
|
||||
fullCus,
|
||||
attachScenario,
|
||||
})) as FreeTrialResponse;
|
||||
})) as APIFreeTrial;
|
||||
|
||||
return ProductResponseSchema.parse({
|
||||
return APIProductSchema.parse({
|
||||
...product,
|
||||
name: product.name || null,
|
||||
group: product.group || null,
|
||||
@@ -215,6 +215,5 @@ export const getProductResponse = async ({
|
||||
free_trial: freeTrial || null,
|
||||
scenario: attachScenario,
|
||||
properties: getProductProperties({ product, freeTrial }),
|
||||
archived: product.archived ? true : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ErrCode } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import Stripe from "stripe";
|
||||
import { ZodError } from "zod";
|
||||
import { ZodError, type ZodIssue } from "zod/v4";
|
||||
|
||||
export const isPaymentDeclined = (error: any) => {
|
||||
return (
|
||||
@@ -47,52 +47,49 @@ export default class RecaseError extends Error {
|
||||
}
|
||||
|
||||
export function formatZodError(error: ZodError): string {
|
||||
return error.errors
|
||||
.map((err) =>
|
||||
err.path.length ? `${err.path.join(".")}: ${err.message}` : err.message,
|
||||
)
|
||||
.join(", ");
|
||||
}
|
||||
const formatMessage = (issue: ZodIssue): string => {
|
||||
const path = issue.path.length ? issue.path.join(".") : "input";
|
||||
|
||||
const getJsonBody = (body: any) => {
|
||||
if (Buffer.isBuffer(body)) {
|
||||
try {
|
||||
return JSON.parse(body.toString());
|
||||
} catch (e) {
|
||||
return `[Invalid JSON] Raw body: ${body.toString()}`;
|
||||
// Clean up common Zod error messages
|
||||
let message = issue.message;
|
||||
|
||||
// Handle common patterns and make them more user-friendly
|
||||
if (
|
||||
message.includes("Too small") &&
|
||||
message.includes("expected string to have >=1 characters")
|
||||
) {
|
||||
message = "cannot be empty";
|
||||
} else if (message.includes("Invalid string: must match pattern")) {
|
||||
// Extract the pattern and make it more readable
|
||||
if (message.includes("/^[a-zA-Z0-9_-]+$/")) {
|
||||
message =
|
||||
"must contain only letters, numbers, underscores, and hyphens";
|
||||
} else {
|
||||
message = "has invalid format";
|
||||
}
|
||||
} else if (message.includes("Invalid input: expected string, received")) {
|
||||
const receivedType = message.split("received ")[1];
|
||||
message = `must be a string (received ${receivedType})`;
|
||||
} else if (message.includes("Invalid input: expected number, received")) {
|
||||
const receivedType = message.split("received ")[1];
|
||||
message = `must be a number (received ${receivedType})`;
|
||||
} else if (message.includes("Invalid input: expected boolean, received")) {
|
||||
const receivedType = message.split("received ")[1];
|
||||
message = `must be a boolean (received ${receivedType})`;
|
||||
}
|
||||
}
|
||||
return body;
|
||||
};
|
||||
|
||||
const logRequestBody = (logger: any, req: any, level: "warn" | "error") => {
|
||||
if (
|
||||
req.body &&
|
||||
typeof req.body === "object" &&
|
||||
Object.keys(req.body).length > 0
|
||||
) {
|
||||
logger[level]("Request body:");
|
||||
logger[level](getJsonBody(req.body));
|
||||
}
|
||||
};
|
||||
return `${path}: ${message}`;
|
||||
};
|
||||
|
||||
const logReqUrl = (logger: any, req: any, level: "warn" | "error") => {
|
||||
if (req.originalUrl.includes("/webhooks/stripe")) {
|
||||
logger[level](`Stripe webhook: ${req.originalUrl}`);
|
||||
let body = req.body;
|
||||
try {
|
||||
body = Buffer.isBuffer(req.body)
|
||||
? JSON.parse(req.body.toString())
|
||||
: req.body;
|
||||
const formattedIssues = error.issues.map(formatMessage);
|
||||
|
||||
logger[level](`Event type: ${body.type}, ID: ${body.id}`);
|
||||
} catch (error) {
|
||||
logger[level](`Invalid JSON body`);
|
||||
}
|
||||
// If there are multiple issues, format them nicely
|
||||
if (formattedIssues.length === 1) {
|
||||
return formattedIssues[0];
|
||||
} else {
|
||||
logger[level](`${req.method} ${req.originalUrl}`);
|
||||
return `[Validation Errors] ${formattedIssues.join("; ")}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const handleRequestError = ({
|
||||
error,
|
||||
@@ -194,7 +191,7 @@ export const handleFrontendReqError = ({
|
||||
const logger = req.logger;
|
||||
if (
|
||||
error instanceof RecaseError &&
|
||||
error.statusCode == StatusCodes.NOT_FOUND
|
||||
error.statusCode === StatusCodes.NOT_FOUND
|
||||
) {
|
||||
// Temporarily disable logger to prevent thread-stream crashes
|
||||
console.log(`(frontend) ${req.method} ${req.originalUrl}: not found`);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { AppEnv, ErrCode, Organization } from "@autumn/shared";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import qs from "qs";
|
||||
import Stripe from "stripe";
|
||||
import { ZodAny, ZodError, ZodObject } from "zod";
|
||||
import { withSpan as withSpanTracer } from "@/internal/analytics/tracer/spanUtils.js";
|
||||
import RecaseError, {
|
||||
formatZodError,
|
||||
handleRequestError,
|
||||
} from "./errorUtils.js";
|
||||
import { ZodAny, ZodError, ZodObject } from "zod";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { ExtendedRequest } from "./models/Request.js";
|
||||
import { withSpan as withSpanTracer } from "@/internal/analytics/tracer/spanUtils.js";
|
||||
import qs from "qs";
|
||||
import type { ExtendedRequest } from "./models/Request.js";
|
||||
|
||||
/**
|
||||
* Parses query parameters with proper type coercion for validation
|
||||
@@ -264,21 +263,19 @@ export const routeHandler = async <TLoad = undefined>({
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
try {
|
||||
if (error instanceof RecaseError) {
|
||||
if (error.code === ErrCode.EntityNotFound) {
|
||||
req.logtail.warn(
|
||||
`${error.message}, org: ${req.org?.slug || req.orgId}`,
|
||||
);
|
||||
return res.status(404).json({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
}
|
||||
if (error instanceof RecaseError) {
|
||||
if (error.code === ErrCode.EntityNotFound) {
|
||||
req.logtail.warn(
|
||||
`${error.message}, org: ${req.org?.slug || req.orgId}`,
|
||||
);
|
||||
return res.status(404).json({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
let originalUrl = req.originalUrl;
|
||||
const originalUrl = req.originalUrl;
|
||||
if (error instanceof Stripe.errors.StripeError) {
|
||||
if (
|
||||
originalUrl.includes("/exchange") &&
|
||||
|
||||
@@ -1,148 +1,148 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import yaml from "yaml";
|
||||
import { z } from "zod/v4";
|
||||
import { createDocument } from "zod-openapi";
|
||||
import { AttachBodySchema } from "../models/attachModels/attachBody.js";
|
||||
import { CusResponseSchema } from "../models/cusModels/cusResponseModels.js";
|
||||
// import { writeFileSync } from "node:fs";
|
||||
// import yaml from "yaml";
|
||||
// import { z } from "zod/v4";
|
||||
// import { createDocument } from "zod-openapi";
|
||||
// import { AttachBodySchema } from "../models/attachModels/attachBody.js";
|
||||
// import { CusResponseSchema } from "../models/cusModels/cusResponseModels.js";
|
||||
|
||||
const customerId = z.string().meta({
|
||||
description: "Your internal ID for the customer",
|
||||
example: "cus_123",
|
||||
id: "customer_id",
|
||||
});
|
||||
// const customerId = z.string().meta({
|
||||
// description: "Your internal ID for the customer",
|
||||
// example: "cus_123",
|
||||
// id: "customer_id",
|
||||
// });
|
||||
|
||||
const featureId = z.string().meta({
|
||||
description: "Feature ID as defined in the dashboard (eg. 'messages')",
|
||||
example: "messages",
|
||||
id: "feature_id",
|
||||
});
|
||||
// const featureId = z.string().meta({
|
||||
// description: "Feature ID as defined in the dashboard (eg. 'messages')",
|
||||
// example: "messages",
|
||||
// id: "feature_id",
|
||||
// });
|
||||
|
||||
const AttachResult = z
|
||||
.object({
|
||||
message: z.string().meta({
|
||||
description: "A short description on the result of the operation",
|
||||
example: "Successfully downgraded from Product A to Product B",
|
||||
id: "message",
|
||||
}),
|
||||
product_ids: z.array(z.string()).meta({
|
||||
description: "The IDs of the products that were attached",
|
||||
example: ["pro", "one_off"],
|
||||
id: "product_ids",
|
||||
}),
|
||||
customer_id: customerId,
|
||||
})
|
||||
.meta({ id: "AttachResult" });
|
||||
// const AttachResult = z
|
||||
// .object({
|
||||
// message: z.string().meta({
|
||||
// description: "A short description on the result of the operation",
|
||||
// example: "Successfully downgraded from Product A to Product B",
|
||||
// id: "message",
|
||||
// }),
|
||||
// product_ids: z.array(z.string()).meta({
|
||||
// description: "The IDs of the products that were attached",
|
||||
// example: ["pro", "one_off"],
|
||||
// id: "product_ids",
|
||||
// }),
|
||||
// customer_id: customerId,
|
||||
// })
|
||||
// .meta({ id: "AttachResult" });
|
||||
|
||||
const attachDefinition = {
|
||||
summary: "Attach Product",
|
||||
tags: ["core"],
|
||||
"x-speakeasy-name-override": "attach",
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": { schema: AttachBodySchema },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: {
|
||||
"application/json": { schema: AttachResult },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
// const attachDefinition = {
|
||||
// summary: "Attach Product",
|
||||
// tags: ["core"],
|
||||
// "x-speakeasy-name-override": "attach",
|
||||
// requestBody: {
|
||||
// content: {
|
||||
// "application/json": { schema: AttachBodySchema },
|
||||
// },
|
||||
// },
|
||||
// responses: {
|
||||
// "200": {
|
||||
// description: "200 OK",
|
||||
// content: {
|
||||
// "application/json": { schema: AttachResult },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
|
||||
// Define Customer schema as a reusable component
|
||||
const CustomerV1 = CusResponseSchema.meta({ id: "Customer" });
|
||||
// // Define Customer schema as a reusable component
|
||||
// const CustomerV1 = CusResponseSchema.meta({ id: "Customer" });
|
||||
|
||||
const document = createDocument({
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "My API",
|
||||
version: "1.0.0",
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "https://api.useautumn.com",
|
||||
description: "Production server",
|
||||
},
|
||||
],
|
||||
security: [
|
||||
{
|
||||
secretKey: [],
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
secretKey: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
},
|
||||
},
|
||||
},
|
||||
// const document = createDocument({
|
||||
// openapi: "3.1.0",
|
||||
// info: {
|
||||
// title: "My API",
|
||||
// version: "1.0.0",
|
||||
// },
|
||||
// servers: [
|
||||
// {
|
||||
// url: "https://api.useautumn.com",
|
||||
// description: "Production server",
|
||||
// },
|
||||
// ],
|
||||
// security: [
|
||||
// {
|
||||
// secretKey: [],
|
||||
// },
|
||||
// ],
|
||||
// components: {
|
||||
// securitySchemes: {
|
||||
// secretKey: {
|
||||
// type: "http",
|
||||
// scheme: "bearer",
|
||||
// bearerFormat: "JWT",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
|
||||
paths: {
|
||||
"/core/attach": {
|
||||
post: attachDefinition,
|
||||
},
|
||||
"/customers/{customer_id}": {
|
||||
get: {
|
||||
summary: "Get customer",
|
||||
"x-speakeasy-name-override": "get",
|
||||
tags: ["customers"],
|
||||
requestParams: { path: z.object({ customer_id: customerId }) },
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: {
|
||||
"application/json": { schema: CustomerV1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/customers/{customer_id}/features/{feature_id}": {
|
||||
patch: {
|
||||
summary: "Update customer feature",
|
||||
tags: ["customer.features"],
|
||||
"x-speakeasy-name-override": "update",
|
||||
requestParams: {
|
||||
path: z.object({ customer_id: customerId, feature_id: featureId }),
|
||||
},
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
usage: z.number(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
usage: z.number(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// paths: {
|
||||
// "/core/attach": {
|
||||
// post: attachDefinition,
|
||||
// },
|
||||
// "/customers/{customer_id}": {
|
||||
// get: {
|
||||
// summary: "Get customer",
|
||||
// "x-speakeasy-name-override": "get",
|
||||
// tags: ["customers"],
|
||||
// requestParams: { path: z.object({ customer_id: customerId }) },
|
||||
// responses: {
|
||||
// "200": {
|
||||
// description: "200 OK",
|
||||
// content: {
|
||||
// "application/json": { schema: CustomerV1 },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// "/customers/{customer_id}/features/{feature_id}": {
|
||||
// patch: {
|
||||
// summary: "Update customer feature",
|
||||
// tags: ["customer.features"],
|
||||
// "x-speakeasy-name-override": "update",
|
||||
// requestParams: {
|
||||
// path: z.object({ customer_id: customerId, feature_id: featureId }),
|
||||
// },
|
||||
// requestBody: {
|
||||
// content: {
|
||||
// "application/json": {
|
||||
// schema: z.object({
|
||||
// usage: z.number(),
|
||||
// }),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// responses: {
|
||||
// "200": {
|
||||
// description: "200 OK",
|
||||
// content: {
|
||||
// "application/json": {
|
||||
// schema: z.object({
|
||||
// usage: z.number(),
|
||||
// }),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// Export to YAML file during build
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
try {
|
||||
const yamlContent = yaml.stringify(document);
|
||||
writeFileSync("./openapi.yaml", yamlContent, "utf8");
|
||||
console.log("OpenAPI document exported to openapi-customer.yaml");
|
||||
} catch (error) {
|
||||
console.error("Failed to export OpenAPI document:", error);
|
||||
}
|
||||
}
|
||||
// // Export to YAML file during build
|
||||
// if (process.env.NODE_ENV !== "production") {
|
||||
// try {
|
||||
// const yamlContent = yaml.stringify(document);
|
||||
// writeFileSync("./openapi.yaml", yamlContent, "utf8");
|
||||
// console.log("OpenAPI document exported to openapi-customer.yaml");
|
||||
// } catch (error) {
|
||||
// console.error("Failed to export OpenAPI document:", error);
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// import { ProductResV2Schema } from "@api/products/prodResV2/prodResV2.js";
|
||||
// import { z } from "zod/v4";
|
||||
|
||||
// // import { CusProductStatus } from "../../../models/cusProductModels/cusProductEnums.js";
|
||||
// // import { ProductItemResponseSchema } from "../../../models/productV2Models/productItemModels/prodItemResponseModels.js";
|
||||
|
||||
// const PartialProduct = ProductResV2Schema.pick({
|
||||
// id: true,
|
||||
// name: true,
|
||||
// is_default: true,
|
||||
// is_add_on: true,
|
||||
// version: true,
|
||||
// group: true,
|
||||
// items: true,
|
||||
// });
|
||||
|
||||
// export const CusProductResSchema = z.object({
|
||||
// ...PartialProduct.shape,
|
||||
// group: z.string().nullable(),
|
||||
// status: z.enum(["active", "past_due", "expired"]),
|
||||
|
||||
// canceled_at: z.number().nullable(),
|
||||
// started_at: z.number(),
|
||||
// current_period_start: z.number().nullish(),
|
||||
// current_period_end: z.number().nullish(),
|
||||
// entity_id: z.string().nullish(),
|
||||
|
||||
// quantity: z.number().optional(),
|
||||
// });
|
||||
|
||||
// // Product related fields
|
||||
// // id: z.string(),
|
||||
// // name: z.string().nullable(),
|
||||
// // is_default: z.boolean(),
|
||||
// // is_add_on: z.boolean(),
|
||||
// // version: z.number().nullish(),
|
||||
// // items: z.array(ProductItemResponseSchema).nullish(),
|
||||
// // product: {
|
||||
|
||||
// // },
|
||||
|
||||
// // entity_id: z.string().nullish(),
|
||||
|
||||
// // stripe_subscription_ids: z.array(z.string()).nullish(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export enum FeatureResType {
|
||||
export enum APIFeatureType {
|
||||
Boolean = "boolean",
|
||||
SingleUsage = "single_use",
|
||||
ContinuousUse = "continuous_use",
|
||||
@@ -10,7 +10,7 @@ export enum FeatureResType {
|
||||
export const APIFeatureSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
type: z.enum(FeatureResType),
|
||||
type: z.enum(APIFeatureType),
|
||||
display: z
|
||||
.object({
|
||||
singular: z.string(),
|
||||
|
||||
6
shared/api/features/updateFeatureParams.ts
Normal file
6
shared/api/features/updateFeatureParams.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { APIFeatureSchema } from "@api/features/apiFeature.js";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
export const UpdateFeatureParamsSchema = APIFeatureSchema.partial();
|
||||
|
||||
export type UpdateFeatureParams = z.infer<typeof UpdateFeatureParamsSchema>;
|
||||
@@ -1,3 +1,11 @@
|
||||
export * from "./features/apiFeature.js";
|
||||
|
||||
// Product
|
||||
export * from "./products/apiFreeTrial.js";
|
||||
export * from "./products/apiProduct.js";
|
||||
export * from "./products/apiProductItem.js";
|
||||
export * from "./products/operations/createProductParams.js";
|
||||
|
||||
// export * from "./products/apiFreeTrial.js";
|
||||
// export * from "./products/apiProduct.js";
|
||||
// export * from "./products/apiProductItem.js";
|
||||
|
||||
68
shared/api/openapi.ts
Normal file
68
shared/api/openapi.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import yaml from "yaml";
|
||||
import { createDocument } from "zod-openapi";
|
||||
import { APIProductSchema } from "./products/apiProduct.js";
|
||||
import { CreateProductParamsSchema } from "./products/operations/createProductParams.js";
|
||||
|
||||
const API_VERSION = "1.2.0";
|
||||
|
||||
const document = createDocument({
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "Autumn API",
|
||||
version: API_VERSION,
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "https://api.useautumn.com",
|
||||
description: "Production server",
|
||||
},
|
||||
],
|
||||
security: [
|
||||
{
|
||||
secretKey: [],
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
secretKey: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
paths: {
|
||||
"/products": {
|
||||
post: {
|
||||
summary: "Create Product",
|
||||
tags: ["products"],
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": { schema: CreateProductParamsSchema },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: {
|
||||
"application/json": { schema: APIProductSchema },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Export to YAML file during build
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
try {
|
||||
const yamlContent = yaml.stringify(document);
|
||||
writeFileSync("./openapi.yaml", yamlContent, "utf8");
|
||||
console.log("OpenAPI document exported to openapi-customer.yaml");
|
||||
} catch (error) {
|
||||
console.error("Failed to export OpenAPI document:", error);
|
||||
}
|
||||
}
|
||||
1
shared/api/operations.ts
Normal file
1
shared/api/operations.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./features/updateFeatureParams.js";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FreeTrialDuration } from "@models/productModels/freeTrialModels/freeTrialEnums.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const APIFreeTrial = z.object({
|
||||
export const APIFreeTrialSchema = z.object({
|
||||
duration: z.enum(FreeTrialDuration),
|
||||
length: z.number(),
|
||||
unique_fingerprint: z.boolean(),
|
||||
@@ -10,3 +10,5 @@ export const APIFreeTrial = z.object({
|
||||
// For Cus Product
|
||||
trial_available: z.boolean().nullish().default(true),
|
||||
});
|
||||
|
||||
export type APIFreeTrial = z.infer<typeof APIFreeTrialSchema>;
|
||||
|
||||
@@ -1,41 +1,143 @@
|
||||
import { AttachScenario } from "@models/checkModels/checkPreviewModels.js";
|
||||
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||
import { ProductItemResponseSchema } from "@models/productV2Models/productItemModels/prodItemResponseModels.js";
|
||||
import { z } from "zod/v4";
|
||||
import { APIFreeTrial } from "./apiFreeTrial.js";
|
||||
// import { AttachScenario } from "../checkModels/checkPreviewModels.js";
|
||||
// import { AppEnv } from "../genModels/genEnums.js";
|
||||
// import { FreeTrialResponseSchema } from "../productModels/freeTrialModels/freeTrialModels.js";
|
||||
// import { ProductItemResponseSchema } from "./productItemModels/prodItemResponseModels.js";
|
||||
import { APIFreeTrialSchema } from "./apiFreeTrial.js";
|
||||
import { APIProductItemSchema } from "./apiProductItem.js";
|
||||
|
||||
export const APIProductPropertiesSchema = z.object({
|
||||
is_free: z.boolean(),
|
||||
is_one_off: z.boolean(),
|
||||
interval_group: z.string().nullish(),
|
||||
has_trial: z.boolean().nullish(),
|
||||
updateable: z.boolean().nullish(),
|
||||
is_free: z.boolean().meta({
|
||||
description: "True if the product has no base price or usage prices",
|
||||
example: false,
|
||||
}),
|
||||
is_one_off: z.boolean().meta({
|
||||
description: "True if the product only contains a one-time price",
|
||||
example: false,
|
||||
}),
|
||||
interval_group: z.string().nullish().meta({
|
||||
description:
|
||||
"The billing interval group for recurring products (e.g., 'monthly', 'yearly')",
|
||||
example: "monthly",
|
||||
}),
|
||||
has_trial: z.boolean().nullish().meta({
|
||||
description: "True if the product includes a free trial",
|
||||
example: true,
|
||||
}),
|
||||
updateable: z.boolean().nullish().meta({
|
||||
description:
|
||||
"True if the product can be updated after creation (only applicable if there are prepaid recurring prices)",
|
||||
example: true,
|
||||
}),
|
||||
});
|
||||
|
||||
export const APIProductSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
group: z.string().nullable(),
|
||||
export const APIProductSchema = z
|
||||
.object({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the product you set when creating the product",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
|
||||
env: z.enum(AppEnv),
|
||||
is_add_on: z.boolean(),
|
||||
is_default: z.boolean(),
|
||||
version: z.number(),
|
||||
created_at: z.number(),
|
||||
name: z.string().meta({
|
||||
description: "The name of the product",
|
||||
example: "Pro Plan",
|
||||
}),
|
||||
|
||||
items: z.array(ProductItemResponseSchema),
|
||||
free_trial: APIFreeTrial.nullable(),
|
||||
base_variant_id: z.string().nullable(),
|
||||
group: z.string().nullable().meta({
|
||||
description: "The group of the product",
|
||||
example: "product_set_1",
|
||||
}),
|
||||
|
||||
scenario: z.enum(AttachScenario).optional(),
|
||||
properties: APIProductPropertiesSchema.optional(),
|
||||
archived: z.boolean().optional(),
|
||||
});
|
||||
env: z.enum(AppEnv).meta({
|
||||
description: "The environment of the product",
|
||||
example: "production",
|
||||
}),
|
||||
|
||||
is_add_on: z.boolean().meta({
|
||||
description:
|
||||
"Whether the product is an add-on and can be purchased alongside other products",
|
||||
example: true,
|
||||
}),
|
||||
|
||||
is_default: z.boolean().meta({
|
||||
description: "Whether the product is the default product",
|
||||
example: true,
|
||||
}),
|
||||
|
||||
archived: z.boolean({ message: "archived should be a boolean" }).meta({
|
||||
description:
|
||||
"Whether this product has been archived and is no longer available",
|
||||
example: false,
|
||||
}),
|
||||
|
||||
version: z.number().meta({
|
||||
description: "The version of the product",
|
||||
example: 1,
|
||||
}),
|
||||
|
||||
created_at: z.number().meta({
|
||||
description:
|
||||
"The timestamp of when the product was created in milliseconds since epoch",
|
||||
example: 1759247877000,
|
||||
}),
|
||||
|
||||
items: z.array(APIProductItemSchema).meta({
|
||||
description:
|
||||
"Array of product items that define the features and pricing",
|
||||
example: [
|
||||
{
|
||||
feature_id: "<string>",
|
||||
feature_type: "single_use",
|
||||
included_usage: 123,
|
||||
interval: "<string>",
|
||||
usage_model: "prepaid",
|
||||
price: 123,
|
||||
billing_units: 123,
|
||||
entity_feature_id: "<string>",
|
||||
reset_usage_when_enabled: true,
|
||||
tiers: [
|
||||
{
|
||||
to: 123,
|
||||
amount: 123,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
free_trial: APIFreeTrialSchema.nullable().meta({
|
||||
description: "Free trial configuration for this product, if available",
|
||||
example: {
|
||||
duration: "<string>",
|
||||
length: 123,
|
||||
unique_fingerprint: true,
|
||||
},
|
||||
}),
|
||||
|
||||
base_variant_id: z.string().nullable().meta({
|
||||
description: "ID of the base variant this product is derived from",
|
||||
example: "var_1234567890abcdef",
|
||||
}),
|
||||
|
||||
scenario: z.enum(AttachScenario).optional().meta({
|
||||
description:
|
||||
"Scenario context for when this product is used in attach flows",
|
||||
example: "upgrade",
|
||||
}),
|
||||
|
||||
properties: APIProductPropertiesSchema.optional().meta({
|
||||
description: "Additional properties and metadata for the product",
|
||||
example: {
|
||||
is_free: false,
|
||||
is_one_off: false,
|
||||
interval_group: "monthly",
|
||||
has_trial: true,
|
||||
updateable: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
description: "A product",
|
||||
id: "Product",
|
||||
});
|
||||
|
||||
export type APIProduct = z.infer<typeof APIProductSchema>;
|
||||
export type APIProductProperties = z.infer<typeof APIProductPropertiesSchema>;
|
||||
// export type ProductResponse = z.infer<typeof ProductResponseSchema>;
|
||||
|
||||
@@ -9,35 +9,47 @@ import {
|
||||
} from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const APIProductItemSchema = z.object({
|
||||
// Feature stuff
|
||||
type: z.enum(ProductItemType).nullish(),
|
||||
feature_id: z.string().nullish(),
|
||||
feature_type: z.enum(ProductItemFeatureType).nullish(),
|
||||
export const APIProductItemSchema = z
|
||||
.object({
|
||||
// Feature stuff
|
||||
type: z.enum(ProductItemType).nullish(),
|
||||
feature_id: z.string().nullish(),
|
||||
feature_type: z.enum(ProductItemFeatureType).nullish(),
|
||||
|
||||
// Feature response
|
||||
feature: APIFeatureSchema.nullish(),
|
||||
// Feature response
|
||||
feature: APIFeatureSchema.nullish(),
|
||||
|
||||
included_usage: z.number().or(z.literal(Infinite)).nullish(),
|
||||
interval: z.enum(ProductItemInterval).nullish(),
|
||||
interval_count: z.number().nullish(),
|
||||
included_usage: z.number().or(z.literal(Infinite)).nullish(),
|
||||
interval: z.enum(ProductItemInterval).nullish(),
|
||||
interval_count: z.number().nullish(),
|
||||
|
||||
// Price config
|
||||
price: z.number().nullish(),
|
||||
tiers: z.array(PriceTierSchema).nullish(),
|
||||
usage_model: z.enum(UsageModel).nullish(),
|
||||
billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units)
|
||||
reset_usage_when_enabled: z.boolean().nullish(),
|
||||
quantity: z.number().nullish(),
|
||||
next_cycle_quantity: z.number().nullish(),
|
||||
entity_feature_id: z.string().nullish(),
|
||||
// Price config
|
||||
price: z.number().nullish(),
|
||||
tiers: z.array(PriceTierSchema).nullish(),
|
||||
usage_model: z.enum(UsageModel).nullish(),
|
||||
billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units)
|
||||
reset_usage_when_enabled: z.boolean().nullish(),
|
||||
quantity: z.number().nullish(),
|
||||
next_cycle_quantity: z.number().nullish(),
|
||||
entity_feature_id: z.string().nullish(),
|
||||
|
||||
display: z
|
||||
.object({
|
||||
primary_text: z.string(),
|
||||
secondary_text: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
display: z
|
||||
.object({
|
||||
primary_text: z.string(),
|
||||
secondary_text: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
})
|
||||
.meta({
|
||||
id: "ProductItem",
|
||||
description: "A product item that defines a feature",
|
||||
example: {
|
||||
feature_id: "feature_1",
|
||||
feature_type: "single_use",
|
||||
included_usage: 123,
|
||||
interval: "monthly",
|
||||
usage_model: "prepaid",
|
||||
},
|
||||
});
|
||||
|
||||
export type APIProductItem = z.infer<typeof APIProductItemSchema>;
|
||||
|
||||
15
shared/api/products/operations/createProductParams.ts
Normal file
15
shared/api/products/operations/createProductParams.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { idRegex } from "@utils/utils.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const CreateProductParamsSchema = z.object({
|
||||
id: z.string().nonempty().regex(idRegex),
|
||||
|
||||
name: z.string().refine((val) => val.length > 0, {
|
||||
message: "name must be a non-empty string",
|
||||
}),
|
||||
|
||||
is_add_on: z.boolean().default(false),
|
||||
is_default: z.boolean().default(false),
|
||||
version: z.number().optional(),
|
||||
group: z.string().default(""),
|
||||
});
|
||||
24
shared/api/products/productsOpenApi.ts
Normal file
24
shared/api/products/productsOpenApi.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { APIProductSchema } from "./apiProduct.js";
|
||||
import { CreateProductParamsSchema } from "./operations/createProductParams.js";
|
||||
|
||||
export const productOps = {
|
||||
"/products": {
|
||||
post: {
|
||||
summary: "Create Product",
|
||||
tags: ["products"],
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": { schema: CreateProductParamsSchema },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: {
|
||||
"application/json": { schema: APIProductSchema },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -4,6 +4,7 @@ export { schemas };
|
||||
|
||||
// API MODELS
|
||||
export * from "./api/models.js";
|
||||
export * from "./api/operations.js";
|
||||
|
||||
// Auth Models
|
||||
export * from "./db/auth-schema.js";
|
||||
@@ -38,9 +39,9 @@ export * from "./models/cusModels/cusExpand.js";
|
||||
|
||||
// 8. Customer Models
|
||||
export * from "./models/cusModels/cusModels.js";
|
||||
export * from "./models/cusModels/cusResModels/cusFeatureResponse.js";
|
||||
export * from "./models/cusModels/cusResModels/cusProductResponse.js";
|
||||
export * from "./models/cusModels/cusResModels/cusReferralsResponse.js";
|
||||
|
||||
// Cus response
|
||||
export * from "./models/cusModels/cusResponseModels.js";
|
||||
export * from "./models/cusModels/cusTable.js";
|
||||
@@ -115,10 +116,10 @@ export * from "./models/productModels/productTable.js";
|
||||
export * from "./models/productV2Models/productItemModels/featureItem.js";
|
||||
export * from "./models/productV2Models/productItemModels/featurePriceItem.js";
|
||||
export * from "./models/productV2Models/productItemModels/priceItem.js";
|
||||
export * from "./models/productV2Models/productItemModels/prodItemResponseModels.js";
|
||||
|
||||
export * from "./models/productV2Models/productItemModels/productItemEnums.js";
|
||||
export * from "./models/productV2Models/productItemModels/productItemModels.js";
|
||||
export * from "./models/productV2Models/productResponseModels.js";
|
||||
|
||||
// 6. Product V2 Models
|
||||
export * from "./models/productV2Models/productV2Models.js";
|
||||
export * from "./models/rewardModels/referralModels/referralCodeTable.js";
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { APIProductSchema } from "@api/products/apiProduct.js";
|
||||
import { APIProductItemSchema } from "@api/products/apiProductItem.js";
|
||||
|
||||
import { z } from "zod/v4";
|
||||
import { FeatureOptionsSchema } from "../cusProductModels/cusProductModels.js";
|
||||
import { ProductItemResponseSchema } from "../productV2Models/productItemModels/prodItemResponseModels.js";
|
||||
import { ProductResponseSchema } from "../productV2Models/productResponseModels.js";
|
||||
|
||||
export const CheckoutLineSchema = z.object({
|
||||
description: z.string(),
|
||||
amount: z.number(),
|
||||
item: ProductItemResponseSchema.nullish(),
|
||||
item: APIProductItemSchema.nullish(),
|
||||
});
|
||||
|
||||
export const CheckoutResponseSchema = z.object({
|
||||
url: z.string().nullish(),
|
||||
customer_id: z.string(),
|
||||
lines: z.array(CheckoutLineSchema),
|
||||
product: ProductResponseSchema.nullish(),
|
||||
current_product: ProductResponseSchema.nullish(),
|
||||
product: APIProductSchema.nullish(),
|
||||
current_product: APIProductSchema.nullish(),
|
||||
options: z.array(FeatureOptionsSchema).nullish(),
|
||||
total: z.number().nullish(),
|
||||
currency: z.string().nullish(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { APIProduct } from "@api/products/apiProduct.js";
|
||||
import type { Infinite } from "../productModels/productEnums.js";
|
||||
import type { UsageModel } from "../productV2Models/productItemModels/productItemModels.js";
|
||||
import type { ProductResponse } from "../productV2Models/productResponseModels.js";
|
||||
|
||||
export enum AttachScenario {
|
||||
Scheduled = "scheduled",
|
||||
@@ -59,7 +59,7 @@ export interface CheckProductPreview {
|
||||
currency: string;
|
||||
};
|
||||
|
||||
product?: ProductResponse;
|
||||
product?: APIProduct;
|
||||
payment_method?: any;
|
||||
}
|
||||
|
||||
@@ -76,5 +76,5 @@ export interface CheckFeaturePreview {
|
||||
feature_id: string;
|
||||
feature_name: string;
|
||||
|
||||
products: ProductResponse[];
|
||||
products: APIProduct[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { APIProductItemSchema } from "@api/models.js";
|
||||
import { z } from "zod/v4";
|
||||
import { CusProductStatus } from "../../cusProductModels/cusProductEnums.js";
|
||||
import { ProductItemResponseSchema } from "../../productV2Models/productItemModels/prodItemResponseModels.js";
|
||||
|
||||
export const CusProductResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -22,7 +22,7 @@ export const CusProductResponseSchema = z.object({
|
||||
current_period_start: z.number().nullish(),
|
||||
current_period_end: z.number().nullish(),
|
||||
entity_id: z.string().nullish(),
|
||||
items: z.array(ProductItemResponseSchema).nullish(),
|
||||
items: z.array(APIProductItemSchema).nullish(),
|
||||
quantity: z.number().optional(),
|
||||
prepaid_quantities: z
|
||||
.array(
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export enum APIFeatureType {
|
||||
Boolean = "boolean",
|
||||
SingleUsage = "single_use",
|
||||
ContinuousUse = "continuous_use",
|
||||
CreditSystem = "credit_system",
|
||||
}
|
||||
export const APIFeatureSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
type: z.nativeEnum(APIFeatureType),
|
||||
display: z
|
||||
.object({
|
||||
singular: z.string(),
|
||||
plural: z.string(),
|
||||
})
|
||||
.nullish(),
|
||||
|
||||
credit_schema: z
|
||||
.array(
|
||||
z.object({
|
||||
metered_feature_id: z.string(),
|
||||
credit_cost: z.number(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
archived: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export const UpdateAPIFeatureSchema = APIFeatureSchema.extend({
|
||||
archived: z.boolean().optional(),
|
||||
}).partial();
|
||||
|
||||
export type APIFeature = z.infer<typeof APIFeatureSchema>;
|
||||
export type UpdateAPIFeature = z.infer<typeof UpdateAPIFeatureSchema>;
|
||||
@@ -23,15 +23,5 @@ export const CreateFreeTrialSchema = z.object({
|
||||
card_required: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const FreeTrialResponseSchema = z.object({
|
||||
// id: z.string(),
|
||||
duration: z.nativeEnum(FreeTrialDuration),
|
||||
length: z.number(),
|
||||
unique_fingerprint: z.boolean(),
|
||||
trial_available: z.boolean().nullish().default(true),
|
||||
card_required: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export type FreeTrial = z.infer<typeof FreeTrialSchema>;
|
||||
export type CreateFreeTrial = z.infer<typeof CreateFreeTrialSchema>;
|
||||
export type FreeTrialResponse = z.infer<typeof FreeTrialResponseSchema>;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
import { APIFeatureSchema } from "../../featureModels/featureResModels.js";
|
||||
import { Infinite } from "../../productModels/productEnums.js";
|
||||
import {
|
||||
PriceTierSchema,
|
||||
ProductItemFeatureType,
|
||||
ProductItemInterval,
|
||||
ProductItemType,
|
||||
UsageModel,
|
||||
} from "./productItemModels.js";
|
||||
|
||||
export const ProductItemResponseSchema = z.object({
|
||||
// Feature stuff
|
||||
type: z.nativeEnum(ProductItemType).nullish(),
|
||||
feature_id: z.string().nullish(),
|
||||
feature_type: z.nativeEnum(ProductItemFeatureType).nullish(),
|
||||
|
||||
// Feature response
|
||||
feature: APIFeatureSchema.nullish(),
|
||||
|
||||
included_usage: z.number().or(z.literal(Infinite)).nullish(),
|
||||
interval: z.nativeEnum(ProductItemInterval).nullish(),
|
||||
interval_count: z.number().nullish(),
|
||||
|
||||
// Price config
|
||||
price: z.number().nullish(),
|
||||
tiers: z.array(PriceTierSchema).nullish(),
|
||||
usage_model: z.nativeEnum(UsageModel).nullish(),
|
||||
billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units)
|
||||
reset_usage_when_enabled: z.boolean().nullish(),
|
||||
quantity: z.number().nullish(),
|
||||
next_cycle_quantity: z.number().nullish(),
|
||||
entity_feature_id: z.string().nullish(),
|
||||
|
||||
display: z
|
||||
.object({
|
||||
primary_text: z.string(),
|
||||
secondary_text: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export type ProductItemResponse = z.infer<typeof ProductItemResponseSchema>;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
import { AttachScenario } from "../checkModels/checkPreviewModels.js";
|
||||
import { AppEnv } from "../genModels/genEnums.js";
|
||||
import { FreeTrialResponseSchema } from "../productModels/freeTrialModels/freeTrialModels.js";
|
||||
import { ProductItemResponseSchema } from "./productItemModels/prodItemResponseModels.js";
|
||||
|
||||
export const ProductPropertiesSchema = z.object({
|
||||
is_free: z.boolean(),
|
||||
is_one_off: z.boolean(),
|
||||
interval_group: z.string().nullish(),
|
||||
has_trial: z.boolean().nullish(),
|
||||
updateable: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export const ProductResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullable(),
|
||||
group: z.string().nullable(),
|
||||
env: z.nativeEnum(AppEnv),
|
||||
is_add_on: z.boolean(),
|
||||
is_default: z.boolean(),
|
||||
version: z.number(),
|
||||
created_at: z.number(),
|
||||
|
||||
items: z.array(ProductItemResponseSchema),
|
||||
free_trial: FreeTrialResponseSchema.nullable(),
|
||||
base_variant_id: z.string().nullable(),
|
||||
|
||||
scenario: z.nativeEnum(AttachScenario).optional(),
|
||||
properties: ProductPropertiesSchema.optional(),
|
||||
archived: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type ProductResponse = z.infer<typeof ProductResponseSchema>;
|
||||
export type ProductProperties = z.infer<typeof ProductPropertiesSchema>;
|
||||
1739
shared/openapi.yaml
1739
shared/openapi.yaml
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@
|
||||
"author": "Recase Inc.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build:tsc": "tsc && bun api/customer.ts",
|
||||
"build:tsc": "tsc && bun api/openapi.ts",
|
||||
"build": "bun build ./index.ts --outdir dist --format esm --target bun --external zod",
|
||||
"dev": "bunx nodemon --ext ts --ignore dist --exec \"bun run build && bun run dev:dts\"",
|
||||
"dev:dts": "tsc --emitDeclarationOnly --outDir dist --project tsconfig.json",
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"rootDir": ".",
|
||||
"paths": {
|
||||
"@api/*": ["./api/*"],
|
||||
"@models/*": ["./models/*"]
|
||||
"@models/*": ["./models/*"],
|
||||
"@utils/*": ["./utils/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*"],
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { CreditSchemaItem } from "../models/featureModels/featureConfig/creditConfig.js";
|
||||
import { APIFeatureSchema } from "@api/features/apiFeature.js";
|
||||
import type { CreditSchemaItem } from "../models/featureModels/featureConfig/creditConfig.js";
|
||||
import { FeatureType } from "../models/featureModels/featureEnums.js";
|
||||
import { Feature } from "../models/featureModels/featureModels.js";
|
||||
import { APIFeatureSchema } from "../models/featureModels/featureResModels.js";
|
||||
import type { Feature } from "../models/featureModels/featureModels.js";
|
||||
|
||||
export const toAPIFeature = ({ feature }: { feature: Feature }) => {
|
||||
// return FeatureResponseSchema.parse(feature);
|
||||
// 1. Get feature type
|
||||
let featureType = feature.type;
|
||||
if (feature.type == FeatureType.Metered) {
|
||||
if (feature.type === FeatureType.Metered) {
|
||||
featureType = feature.config.usage_type;
|
||||
}
|
||||
|
||||
let creditSchema = undefined;
|
||||
if (feature.type == FeatureType.CreditSystem) {
|
||||
let creditSchema: CreditSchemaItem[] | undefined;
|
||||
if (feature.type === FeatureType.CreditSystem) {
|
||||
creditSchema = feature.config.schema.map((s: CreditSchemaItem) => ({
|
||||
metered_feature_id: s.metered_feature_id,
|
||||
credit_cost: s.credit_amount,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { FeatureOptions } from "../../models/cusProductModels/cusProductModels.js";
|
||||
import { Feature } from "../../models/featureModels/featureModels.js";
|
||||
import { ProductItemResponseSchema } from "../../models/productV2Models/productItemModels/prodItemResponseModels.js";
|
||||
import { APIProductItemSchema } from "@api/products/apiProductItem.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type { FeatureOptions } from "../../models/cusProductModels/cusProductModels.js";
|
||||
import type { Feature } from "../../models/featureModels/featureModels.js";
|
||||
import {
|
||||
ProductItem,
|
||||
type ProductItem,
|
||||
UsageModel,
|
||||
} from "../../models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { toAPIFeature } from "../featureUtils.js";
|
||||
import { getProductItemDisplay } from "../productDisplayUtils.js";
|
||||
import { notNullish } from "../utils.js";
|
||||
import { getItemType } from "./getItemType.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
|
||||
export const calculateProrationAmount = ({
|
||||
periodEnd,
|
||||
@@ -109,22 +109,22 @@ export const getProductItemResponse = ({
|
||||
options?: FeatureOptions[];
|
||||
}) => {
|
||||
// 1. Get item type
|
||||
let type = getItemType(item);
|
||||
const type = getItemType(item);
|
||||
|
||||
// 2. Get display
|
||||
let display = getProductItemDisplay({
|
||||
const display = getProductItemDisplay({
|
||||
item,
|
||||
features,
|
||||
currency,
|
||||
});
|
||||
|
||||
let priceData = itemToPriceOrTiers({ item });
|
||||
const priceData = itemToPriceOrTiers({ item });
|
||||
|
||||
let quantity = undefined;
|
||||
let upcomingQuantity = undefined;
|
||||
let quantity: number | undefined;
|
||||
let upcomingQuantity: number | undefined;
|
||||
|
||||
if (item.usage_model == UsageModel.Prepaid && notNullish(options)) {
|
||||
let option = options!.find((o) => o.feature_id == item.feature_id);
|
||||
if (item.usage_model === UsageModel.Prepaid && notNullish(options)) {
|
||||
const option = options!.find((o) => o.feature_id === item.feature_id);
|
||||
quantity = option?.quantity
|
||||
? option?.quantity * (item.billing_units ?? 1)
|
||||
: undefined;
|
||||
@@ -134,8 +134,8 @@ export const getProductItemResponse = ({
|
||||
: undefined;
|
||||
}
|
||||
|
||||
let feature = features.find((f) => f.id == item.feature_id);
|
||||
return ProductItemResponseSchema.parse({
|
||||
const feature = features.find((f) => f.id === item.feature_id);
|
||||
return APIProductItemSchema.parse({
|
||||
type,
|
||||
...item,
|
||||
feature: feature ? toAPIFeature({ feature }) : null,
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { Decimal } from "decimal.js";
|
||||
import { type ProductItem, type ProductV2 } from "../index.js";
|
||||
import {
|
||||
BillingType,
|
||||
Price,
|
||||
ProductItem,
|
||||
ProductResponse,
|
||||
ProductV2,
|
||||
} from "../index.js";
|
||||
import { nullish } from "./utils.js";
|
||||
import {
|
||||
isFeatureItem,
|
||||
isFeaturePriceItem,
|
||||
isPriceItem,
|
||||
} from "./productDisplayUtils/getItemType.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { nullish } from "./utils.js";
|
||||
|
||||
export const isFreeProductV2 = ({ items }: { items: ProductItem[] }) => {
|
||||
return items.every((item) => nullish(item.price) && nullish(item.tiers));
|
||||
@@ -44,7 +37,7 @@ export const isProductUpgradeV2 = ({
|
||||
for (const item of items) {
|
||||
if (item.price) totalPrice = totalPrice.plus(item.price);
|
||||
if (item.tiers) {
|
||||
let tierTotal = item.tiers.reduce(
|
||||
const tierTotal = item.tiers.reduce(
|
||||
(acc, tier) => acc.plus(tier.amount),
|
||||
new Decimal(0),
|
||||
);
|
||||
@@ -75,7 +68,7 @@ export const sortProductsV2 = ({ products }: { products: ProductV2[] }) => {
|
||||
}
|
||||
|
||||
// Primary sort: by price (using upgrade logic)
|
||||
let isUpgrade = isProductUpgradeV2({
|
||||
const isUpgrade = isProductUpgradeV2({
|
||||
items1: a.items,
|
||||
items2: b.items,
|
||||
});
|
||||
|
||||
@@ -6,3 +6,5 @@ export const nullish = <T>(
|
||||
|
||||
export const notNullish = <T>(value: T | null | undefined): value is T =>
|
||||
value !== null && value !== undefined;
|
||||
|
||||
export const idRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import FieldLabel from "@/components/general/modal-components/FieldLabel";
|
||||
import { SelectType } from "@/components/general/SelectType";
|
||||
import {
|
||||
APIFeatureType,
|
||||
CreateFeature,
|
||||
type CreateFeature,
|
||||
FeatureType,
|
||||
FeatureUsageType,
|
||||
} from "@autumn/shared";
|
||||
import { Zap, Clock, ArrowUp01, Flag } from "lucide-react";
|
||||
import { ArrowUp01, Flag } from "lucide-react";
|
||||
import FieldLabel from "@/components/general/modal-components/FieldLabel";
|
||||
import { SelectType } from "@/components/general/SelectType";
|
||||
import { defaultMeteredConfig } from "../utils/defaultFeatureConfig";
|
||||
|
||||
export const SelectFeatureType = ({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
APIFeatureType,
|
||||
CreateFeature,
|
||||
type CreateFeature,
|
||||
FeatureType,
|
||||
FeatureUsageType,
|
||||
} from "@autumn/shared";
|
||||
import { Clock, Zap } from "lucide-react";
|
||||
import { defaultMeteredConfig } from "../utils/defaultFeatureConfig";
|
||||
import FieldLabel from "@/components/general/modal-components/FieldLabel";
|
||||
import { SelectType } from "@/components/general/SelectType";
|
||||
import { defaultMeteredConfig } from "../utils/defaultFeatureConfig";
|
||||
|
||||
export const SelectFeatureUsageType = ({
|
||||
feature,
|
||||
|
||||
Reference in New Issue
Block a user