From 54e6531da22f935888be8b0caff4297cc9b1632b Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 29 Jun 2025 11:29:25 +0100 Subject: [PATCH] fix: adding custom feature from dashboard, multi product attach --- server/src/external/stripe/stripeCusUtils.ts | 6 ++- .../attachParamsUtils/getCusAndProducts.ts | 3 +- .../attach/attachUtils/getAttachBranch.ts | 4 +- .../internal/customers/attach/handleAttach.ts | 2 - .../attachParamsToPreview.ts | 1 + .../cusProducts/CusProductService.ts | 12 +++++- .../handlers/handleListProductsBeta.ts | 7 +++ .../updateProductDetails.ts | 2 + .../internal/products/prices/PriceService.ts | 6 +++ .../prices/priceUtils/copyPriceUtils.ts | 43 +++++++++++++++++++ .../product-items/compareItemUtils.ts | 1 + .../productItemModels/featureItem.ts | 3 +- 12 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 server/src/internal/products/prices/priceUtils/copyPriceUtils.ts diff --git a/server/src/external/stripe/stripeCusUtils.ts b/server/src/external/stripe/stripeCusUtils.ts index daf8d02a0..420bfde9a 100644 --- a/server/src/external/stripe/stripeCusUtils.ts +++ b/server/src/external/stripe/stripeCusUtils.ts @@ -47,7 +47,11 @@ export const createStripeCusIfNotExists = async ({ expand: ["test_clock", "invoice_settings.default_payment_method"], }, ); - return stripeCus as Stripe.Customer; + if (!stripeCus.deleted) { + return stripeCus as Stripe.Customer; + } else { + createNew = true; + } } catch (error) { createNew = true; } diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts index 78c5fbc98..a0a2ed180 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts @@ -26,7 +26,8 @@ const getProductsForAttach = async ({ if (notNullish(product_ids)) { let freeTrialProds = products.filter((prod) => notNullish(prod.free_trial)); - if (freeTrialProds.length > 0) { + console.log("freeTrialProds", freeTrialProds); + if (freeTrialProds.length > 1) { throw new RecaseError({ message: "When providing product_ids, can't have multiple free trial products", diff --git a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts index 3e6590499..c8088a570 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts @@ -23,7 +23,7 @@ import { hasPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePric import { attachParamToCusProducts } from "./convertAttachParams.js"; import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; -const checkMultiProductErrors = async ({ +const handleMultiProductErrors = async ({ attachParams, }: { attachParams: AttachParams; @@ -291,7 +291,7 @@ export const getAttachBranch = async ({ }) => { // 1. Multi product if (notNullish(attachBody.product_ids)) { - await checkMultiProductErrors({ attachParams }); + await handleMultiProductErrors({ attachParams }); return AttachBranch.MultiProduct; } diff --git a/server/src/internal/customers/attach/handleAttach.ts b/server/src/internal/customers/attach/handleAttach.ts index 7b9ec2006..d61d760ac 100644 --- a/server/src/internal/customers/attach/handleAttach.ts +++ b/server/src/internal/customers/attach/handleAttach.ts @@ -9,8 +9,6 @@ import { handleAttachErrors } from "./attachUtils/handleAttachErrors.js"; import { checkStripeConnections, createStripePrices } from "./attachRouter.js"; import { insertCustomItems } from "./attachUtils/insertCustomItems.js"; import { runAttachFunction } from "./attachUtils/getAttachFunction.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { FeatureSchema } from "@autumn/shared"; export const handleAttach = async (req: any, res: any) => routeHandler({ diff --git a/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts index 032ff4f07..a84202d51 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts @@ -23,6 +23,7 @@ export const attachParamsToPreview = async ({ logger: any; }) => { // Handle existing product + const branch = await getAttachBranch({ req, attachBody, diff --git a/server/src/internal/customers/cusProducts/CusProductService.ts b/server/src/internal/customers/cusProducts/CusProductService.ts index ebfa894e0..0ad3bfa3f 100644 --- a/server/src/internal/customers/cusProducts/CusProductService.ts +++ b/server/src/internal/customers/cusProducts/CusProductService.ts @@ -239,10 +239,14 @@ export class CusProductService { static async getByProductId({ db, productId, + orgId, + env, limit = 1, }: { db: DrizzleCli; productId: string; + orgId: string; + env: AppEnv; limit?: number; }) { let data = await db @@ -252,7 +256,13 @@ export class CusProductService { products, eq(customerProducts.internal_product_id, products.internal_id), ) - .where(eq(products.id, productId)) + .where( + and( + eq(products.id, productId), + eq(products.org_id, orgId), + eq(products.env, env), + ), + ) .limit(1); return data.map((d) => ({ diff --git a/server/src/internal/products/handlers/handleListProductsBeta.ts b/server/src/internal/products/handlers/handleListProductsBeta.ts index f5a491357..fba4ebc9a 100644 --- a/server/src/internal/products/handlers/handleListProductsBeta.ts +++ b/server/src/internal/products/handlers/handleListProductsBeta.ts @@ -30,6 +30,13 @@ export const handleListProductsBeta = async (req: any, res: any) => })(), ]); + if (req.query.v1_schema === "true") { + res.status(200).json({ + list: products, + }); + return; + } + sortFullProducts({ products }); let batchResponse = []; diff --git a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts index 0a2a80669..2c3bc75b1 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts @@ -121,6 +121,8 @@ export const handleUpdateProductDetails = async ({ const customersOnAllVersions = await CusProductService.getByProductId({ db, productId: curProduct.id, + orgId: org.id, + env: curProduct.env as AppEnv, }); if (productDetailsSame(curProduct, newProduct)) { diff --git a/server/src/internal/products/prices/PriceService.ts b/server/src/internal/products/prices/PriceService.ts index 6e658238e..6afc5cafb 100644 --- a/server/src/internal/products/prices/PriceService.ts +++ b/server/src/internal/products/prices/PriceService.ts @@ -4,6 +4,12 @@ import { Price, prices, Product } from "@autumn/shared"; import { and, eq, inArray } from "drizzle-orm"; export class PriceService { + static async get({ db, id }: { db: DrizzleCli; id: string }) { + return (await db.query.prices.findFirst({ + where: eq(prices.id, id), + })) as Price; + } + static async getCustomInEntIds({ db, entitlementIds, diff --git a/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts b/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts new file mode 100644 index 000000000..a7df8327f --- /dev/null +++ b/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts @@ -0,0 +1,43 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { PriceService } from "../PriceService.js"; +import { prices } from "@autumn/shared"; +import { eq } from "drizzle-orm"; +import { generateId } from "@/utils/genUtils.js"; +import { Price, UsagePriceConfig } from "@autumn/shared"; + +export const copyPrice = async ({ + db, + priceId, + usagePriceConfig, + isCustom, +}: { + db: DrizzleCli; + priceId: string; + usagePriceConfig?: Partial; + isCustom?: boolean; +}) => { + let price = (await db.query.prices.findFirst({ + where: eq(prices.id, priceId), + })) as Price; + + let newPrice = structuredClone(price); + + newPrice = { + ...newPrice, + id: generateId("pr"), + created_at: Date.now(), + is_custom: isCustom || newPrice.is_custom, + }; + + if (usagePriceConfig) { + newPrice = { + ...newPrice, + config: { + ...newPrice.config, + ...usagePriceConfig, + }, + }; + } + + return newPrice; +}; diff --git a/server/src/internal/products/product-items/compareItemUtils.ts b/server/src/internal/products/product-items/compareItemUtils.ts index ec66b0f6f..0a6d57304 100644 --- a/server/src/internal/products/product-items/compareItemUtils.ts +++ b/server/src/internal/products/product-items/compareItemUtils.ts @@ -186,6 +186,7 @@ export const itemsAreSame = ({ item1: FeatureItemSchema.parse(item1), item2: item2 as FeatureItem, }); + pricesChanged = false; } diff --git a/shared/models/productV2Models/productItemModels/featureItem.ts b/shared/models/productV2Models/productItemModels/featureItem.ts index d0d38f0bc..8b0de74e7 100644 --- a/shared/models/productV2Models/productItemModels/featureItem.ts +++ b/shared/models/productV2Models/productItemModels/featureItem.ts @@ -29,7 +29,8 @@ export const FeatureItemSchema = ProductItemSchema.pick({ } return num; - }), + }) + .nullish(), }); export type FeatureItem = z.infer;