Files
cfw-autumn/server/src/internal/products/internalProductRouter.ts
2025-09-26 16:33:19 +01:00

525 lines
12 KiB
TypeScript

import { ErrCode, type FeatureOptions, UsageModel } from "@autumn/shared";
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
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 { 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 });
// Get list of products
productRouter.get("/products", async (req: any, res) => {
try {
const { db } = req;
const products = await ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
});
sortFullProducts({ products });
const groupToDefaults = getGroupToDefaults({
defaultProds: products,
});
res.status(200).json({
products: products.map((p) =>
mapToProductV2({ product: p, features: req.features }),
),
groupToDefaults,
});
} catch (error) {
console.error("Failed to get products", error);
res.status(500).send(error);
}
});
// Get counts for all products
productRouter.get("/product_counts", async (req: any, res) => {
try {
const { db } = req;
const products = await ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
});
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,
orgId: req.orgId,
env: req.env,
});
}),
);
const result: { [key: string]: any } = {};
for (let i = 0; i < products.length; i++) {
if (!result[products[i].id]) {
result[products[i].id] = counts[i];
}
}
res.status(200).send(result);
} catch (error) {
console.error("Failed to get products", error);
res.status(500).send(error);
}
});
// Get list of features
productRouter.get("/features", async (req: any, res) => {
try {
res.status(200).json({ features: req.features });
} catch (error) {
console.error("Failed to get features", error);
res.status(500).send(error);
}
});
// Get list of rewards
productRouter.get("/rewards", async (req: any, res) => {
try {
const { db, orgId, env } = req;
const rewards = await RewardService.list({ db, orgId, env });
const rewardPrograms = await RewardProgramService.list({
db,
orgId,
env,
});
res.status(200).send({ rewards, rewardPrograms });
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get rewards",
});
}
});
// Get single product data
productRouter.get("/:productId/data2", async (req: any, res) => {
try {
const { productId } = req.params;
const { version } = req.query;
const { db, orgId, env } = req;
const [product, latestProduct] = await Promise.all([
ProductService.getFull({
db,
idOrInternalId: productId,
orgId,
env,
version: version ? parseInt(version) : undefined,
}),
ProductService.getFull({
db,
idOrInternalId: productId,
orgId,
env,
}),
]);
if (!product) {
throw new RecaseError({
message: `Product ${productId} ${
version ? `(v${version})` : ""
} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
const productV2 = mapToProductV2({
product: product,
features: req.features,
});
res
.status(200)
.json({ product: productV2, numVersions: latestProduct.version });
} catch (error) {
console.error("Failed to get product", error);
res.status(500).send(error);
}
});
// Get counts for a single product
productRouter.get("/:productId/count", async (req: any, res) => {
try {
const { db, orgId, env } = req;
const { productId } = req.params;
const { version } = req.query;
const product = await ProductService.get({
db,
id: productId,
orgId,
env,
version: version ? parseInt(version) : undefined,
});
if (!product) {
throw new RecaseError({
message: `Product ${productId} ${
version ? `(v${version})` : ""
} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
// Get counts from postgres
const counts = await CusProdReadService.getCounts({
db,
internalProductId: product.internal_id,
});
res.status(200).send(counts);
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get product counts (internal)",
});
}
});
// Get list of migrations
productRouter.get("/migrations", async (req: any, res) => {
try {
const { db, orgId, env } = req;
const migrations = await MigrationService.getExistingJobs({
db,
orgId,
env,
});
res.status(200).send({ migrations });
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get migrations",
});
}
});
productRouter.get("/data", async (req: any, res) => {
try {
const { db } = req;
const allVersions = req.query.all_versions === "true";
const [products, features, org, coupons, rewardPrograms, defaultProds] =
await Promise.all([
ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
archived: false,
returnAll: allVersions,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
RewardService.list({ db, orgId: req.orgId, env: req.env }),
RewardProgramService.list({
db,
orgId: req.orgId,
env: req.env,
}),
ProductService.listDefault({
db,
orgId: req.orgId,
env: req.env,
}),
]);
sortFullProducts({
products: getLatestProducts(products),
});
const groupToDefaultProd = getGroupToDefaults({
defaultProds,
});
res.status(200).json({
products: products.map((product) => {
return mapToProductV2({ product, features });
}),
versionCounts: getProductVersionCounts(products),
features,
org: createOrgResponse({ org, env: req.env }),
rewards: coupons,
rewardPrograms,
groupToDefaults: groupToDefaultProd,
});
} catch (error) {
console.error("Failed to get products", error);
res.status(500).send(error);
}
});
productRouter.post("/data", async (req: any, res) => {
try {
const { db } = req;
const { showArchived } = req.body;
const [products, defaultProds, features, org, coupons, rewardPrograms] =
await Promise.all([
ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
// returnAll: true,
archived: showArchived,
}),
ProductService.listDefault({
db,
orgId: req.orgId,
env: req.env,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
RewardService.list({ db, orgId: req.orgId, env: req.env }),
RewardProgramService.list({
db,
orgId: req.orgId,
env: req.env,
}),
]);
// Group to default product
const groupToDefaultProd = getGroupToDefaults({
defaultProds,
});
res.status(200).json({
products: sortFullProducts({ products }).map((product) => {
return mapToProductV2({ product, features });
}),
groupToDefaults: groupToDefaultProd,
versionCounts: getProductVersionCounts(products),
features,
org: createOrgResponse({ org, env: req.env }),
rewards: coupons,
rewardPrograms,
});
} catch (error) {
console.error("Failed to get products", error);
res.status(500).send(error);
}
});
productRouter.get("/counts", async (req: any, res) => {
try {
const { db } = req;
const products = await ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
// returnAll: true,
});
const latestVersion = req.query.latest_version === "true";
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,
orgId: req.orgId,
env: req.env,
});
}),
);
const result: { [key: string]: any } = {};
for (let i = 0; i < products.length; i++) {
if (!result[products[i].id]) {
result[products[i].id] = counts[i];
}
}
res.status(200).send(result);
} catch (error) {
console.error("Failed to get product counts", error);
res.status(500).send(error);
}
});
productRouter.get("/:productId/data", async (req: any, res) => {
try {
const { productId } = req.params;
const { version } = req.query;
const { db, orgId, env } = req;
const [product, features, org, numVersions, existingMigrations] =
await Promise.all([
ProductService.getFull({
db,
idOrInternalId: productId,
orgId,
env,
version: version ? parseInt(version) : undefined,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
ProductService.getProductVersionCount({
db,
productId,
orgId,
env,
}),
MigrationService.getExistingJobs({
db,
orgId,
env,
}),
]);
if (!product) {
throw new RecaseError({
message: `Product ${productId} ${
version ? `(v${version})` : ""
} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
const defaultProds = await ProductService.listDefault({
db,
orgId: req.orgId,
env: req.env,
group: product.group,
});
const groupDefaults = getGroupToDefaults({
defaultProds,
})?.[product.group];
let entitlements = product.entitlements;
let prices = product.prices;
entitlements = entitlements.sort((a: any, b: any) => {
return b.feature.id.localeCompare(a.feature.id);
});
prices = prices.sort((a: any, b: any) => {
return b.id.localeCompare(a.id);
});
const productV2 = mapToProductV2({ product, features });
res.status(200).send({
product: productV2,
entitlements,
prices,
features,
org: {
id: org.id,
name: org.name,
test_pkey: org.test_pkey,
live_pkey: org.live_pkey,
default_currency: org.default_currency,
},
numVersions,
existingMigrations,
groupDefaults: groupDefaults,
});
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get product data (internal)",
});
}
});
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) {
featureToOptions[item.feature_id] = {
feature_id: item.feature_id,
quantity: 0,
};
}
}
res.status(200).send({ options: Object.values(featureToOptions) });
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get product options",
});
}
});
productRouter.get("/:productId/info", handleGetProductDeleteInfo);
productRouter.get("/rewards", async (req: any, res: any) => {
try {
const { db, orgId, env } = req;
const rewards = await RewardService.list({
db,
orgId,
env,
});
res.status(200).send({ rewards });
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get rewards",
});
}
});