diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts index 9fcdcd278..173ec87f4 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts @@ -126,8 +126,10 @@ export const handleSubscriptionUpdated = async ({ subscription.latest_invoice, ); - logger.info("Latest invoice billing reason", latestInvoice.billing_reason); - logger.info("Latest invoice status", latestInvoice.status); + logger.info( + `Latest invoice billing reason: ${latestInvoice.billing_reason}`, + ); + logger.info(`Latest invoice status: ${latestInvoice.status}`); if ( latestInvoice.status !== "open" || diff --git a/server/src/internal/api/apiRouter.ts b/server/src/internal/api/apiRouter.ts index 04abfedcb..a0babcfbf 100644 --- a/server/src/internal/api/apiRouter.ts +++ b/server/src/internal/api/apiRouter.ts @@ -46,7 +46,7 @@ apiRouter.use("/referrals", referralRouter); apiRouter.use("/redemptions", redemptionRouter); // Cus Product -apiRouter.use("/attach", attachRouter); +apiRouter.use("", attachRouter); apiRouter.use("/cancel", expireRouter); apiRouter.use("/entitled", checkRouter); apiRouter.use("/check", checkRouter); diff --git a/server/src/internal/api/entitled/handlers/getProductCheckPreview.ts b/server/src/internal/api/entitled/handlers/getProductCheckPreview.ts index 8cef2e152..d2ea278d4 100644 --- a/server/src/internal/api/entitled/handlers/getProductCheckPreview.ts +++ b/server/src/internal/api/entitled/handlers/getProductCheckPreview.ts @@ -21,14 +21,6 @@ import { notNullish } from "@/utils/genUtils.js"; import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; -const getNextCycle = (preview: AttachPreview) => { - if (!preview.due_next_cycle && !preview.due_today) { - return undefined; - } - - if (!preview.due_today && preview.due_next_cycle) { - } -}; export const attachToCheckPreview = async ({ preview, params, diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index 6f60cd890..2e14122b9 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -20,10 +20,12 @@ export const handleCreateCheckout = async ({ req, res, attachParams, + returnCheckout = false, }: { req: any; res: any; attachParams: AttachParams; + returnCheckout?: boolean; }) => { const { db, logtail: logger } = req; @@ -113,6 +115,10 @@ export const handleCreateCheckout = async ({ logger.info(`✅ Successfully created checkout for customer ${customer.id}`); + if (returnCheckout) { + return checkout; + } + let apiVersion = attachParams.apiVersion || APIVersion.v1; if (apiVersion >= APIVersion.v1_1) { res.status(200).json( diff --git a/server/src/internal/customers/attach/attachRouter.ts b/server/src/internal/customers/attach/attachRouter.ts index 968612d06..f01b6814b 100644 --- a/server/src/internal/customers/attach/attachRouter.ts +++ b/server/src/internal/customers/attach/attachRouter.ts @@ -33,6 +33,8 @@ import { AttachBodySchema } from "./models/AttachBody.js"; import { processAttachBody } from "./attachUtils/attachParams/processAttachBody.js"; import { handleAttachPreview } from "./handleAttachPreview/handleAttachPreview.js"; import { handleAttach } from "./handleAttach.js"; +import { handleCheckout } from "./checkout/handleCheckout.js"; +import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; export const attachRouter: Router = Router(); @@ -128,9 +130,13 @@ export const handlePublicAttachErrors = async ({ export const checkStripeConnections = async ({ req, attachParams, + createCus = true, + useCheckout = false, }: { req: any; attachParams: AttachParams; + createCus?: boolean; + useCheckout?: boolean; }) => { const { org, customer, products, stripeCus, stripeCli } = attachParams; const logger = req.logtail; @@ -154,15 +160,18 @@ export const checkStripeConnections = async ({ ]); } - const batchProductUpdates = [ - // createStripeCusIfNotExists({ - // db: req.db, - // org, - // env, - // customer, - // logger, - // }), - ]; + const batchProductUpdates = []; + if (createCus) { + batchProductUpdates.push( + createStripeCusIfNotExists({ + db: req.db, + org, + env, + customer, + logger, + }), + ); + } for (const product of products) { batchProductUpdates.push( checkStripeProductExists({ @@ -175,6 +184,13 @@ export const checkStripeConnections = async ({ ); } await Promise.all(batchProductUpdates); + + await createStripePrices({ + attachParams, + useCheckout, + req, + logger, + }); }; export const createStripePrices = async ({ @@ -228,205 +244,6 @@ export const customerHasPm = async ({ return notNullOrUndefined(paymentMethod) ? true : false; }; -const handleAttachOld = async (req: any, res: any) => - routeHandler({ - action: "attach", - req, - res, - handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - await handleAttachRaceCondition({ req, res }); - - const attachBody = AttachBodySchema.parse(req.body); - - const logger = req.logtail; - - // PUBLIC STUFF - let forceCheckout = req.isPublic || attachBody.force_checkout || false; - let isCustom = attachBody.is_custom || false; - if (req.isPublic) { - isCustom = false; - } - - logger.info("--------------------------------"); - - const { - customer, - products, - optionsList, - prices, - entitlements, - freeTrial, - } = await processAttachBody({ - req, - attachBody, - }); - - const apiVersion = - orgToVersion({ - org: req.org, - reqApiVersion: req.apiVersion, - }) || APIVersion.v1; - - const internalEntityId = attachBody.entity_id - ? customer.entities.find( - (e) => - e.id === attachBody.entity_id || - e.internal_id === attachBody.entity_id, - )?.internal_id - : undefined; - - const stripeCli = createStripeCli({ org: req.org, env: req.env }); - const paymentMethod = await getCusPaymentMethod({ - stripeCli, - stripeId: customer.processor?.id, - }); - - // const attachParams: AttachParams = { - // stripeCli, - // paymentMethod, - - // customer, - // products, - // optionsList, - // prices, - // entitlements, - // freeTrial, - - // // From req - // req, - // org: req.org, - // entities: customer.entities, - // features: req.features, - // internalEntityId, - // cusProducts: customer.customer_products, - - // // Others - // apiVersion, - // successUrl: attachBody.success_url, - // invoiceOnly: attachBody.invoice_only, - // billingAnchor: attachBody.billing_cycle_anchor, - // metadata: attachBody.metadata, - // disableFreeTrial: attachBody.free_trial === false || false, - // checkoutSessionParams: attachBody.checkout_session_params, - // isCustom, - // }; - - // attachParams.apiVersion = - // orgToVersion({ - // org, - // reqApiVersion: req.apiVersion, - // }) || APIVersion.v1; - - // attachParams.req = req; - // attachParams.successUrl = attachBody.success_url; - // attachParams.invoiceOnly = attachBody.invoice_only; - // attachParams.billingAnchor = attachBody.billing_cycle_anchor; - // attachParams.metadata = attachBody.metadata; - // attachParams.isCustom = isCustom || false; - // attachParams.disableFreeTrial = attachBody.free_trial === false || false; - // attachParams.checkoutSessionParams = checkout_session_params; - - // logger.info( - // `Customer: ${chalk.yellow( - // `${attachParams.customer.id} (${attachParams.customer.name})`, - // )}, Products: ${chalk.yellow( - // attachParams.products.map((p) => p.id).join(", "), - // )}`, - // ); - - // // 3. Check for stripe connection - - // let hasPm = await customerHasPm({ attachParams }); - // const useCheckout = !hasPm || forceCheckout; - // await createStripePrices({ - // attachParams, - // useCheckout, - // req, - // logger, - // }); - - // logger.info( - // `Has PM: ${chalk.yellow(hasPm)}, Force Checkout: ${chalk.yellow( - // forceCheckout, - // )}`, - // ); - // logger.info( - // `Use Checkout: ${chalk.yellow(useCheckout)}, Is Custom: ${chalk.yellow( - // isCustom, - // )}, Invoice Only: ${chalk.yellow(invoiceOnly)}`, - // { - // details: { hasPm, forceCheckout, useCheckout, isCustom, invoiceOnly }, - // }, - // ); - - // -------------------- ERROR CHECKING -------------------- - - // 1. Check for normal errors (eg. options, different recurring intervals) - - // const { curCusProduct, done } = await handleExistingProduct({ - // req, - // res, - // attachParams, - // useCheckout, - // invoiceOnly: attachBody.invoice_only, - // isCustom, - // }); - - // await handlePrepaidErrors({ - // attachParams, - // useCheckout, - // }); - - // await handlePublicAttachErrors({ - // curCusProduct, - // isPublic: req.isPublic || false, - // }); - - // if (done) return; - - // // -------------------- ATTACH PRODUCT -------------------- - - // SCENARIO 1: Free product, no existing product - // const newProductsFree = isFreeProduct(attachParams.prices); - // const allAddOns = attachParams.products.every((p) => p.is_add_on); - - // if ( - // (!curCusProduct && newProductsFree) || - // (allAddOns && newProductsFree) - // ) { - // logger.info("SCENARIO 1: FREE PRODUCT"); - - // if (useCheckout && !newProductsFree && !attachParams.invoiceOnly) { - // logger.info("SCENARIO 2: USING CHECKOUT"); - // await handleCreateCheckout({ - // req, - // res, - // attachParams, - // }); - // return; - // } - - // // SCENARIO 4: Switching product - // if (curCusProduct) { - // logger.info("SCENARIO 3: SWITCHING PRODUCT"); - // await handleChangeProduct({ - // req, - // res, - // attachParams, - // curCusProduct, - // }); - // return; - // } - - // SCENARIO 5: No existing product, not free product - // logger.info("SCENARIO 4: ADDING PRODUCT"); - // await handleAddProduct({ - // req, - // res, - // attachParams, - // }); - }, - }); - -attachRouter.post("", handleAttach); -attachRouter.post("/preview", handleAttachPreview); +attachRouter.post("/attach", handleAttach); +attachRouter.post("/attach/preview", handleAttachPreview); +attachRouter.post("/checkout", handleCheckout); diff --git a/server/src/internal/customers/attach/checkout/handleCheckout.ts b/server/src/internal/customers/attach/checkout/handleCheckout.ts new file mode 100644 index 000000000..3316718a7 --- /dev/null +++ b/server/src/internal/customers/attach/checkout/handleCheckout.ts @@ -0,0 +1,115 @@ +import { + AttachFunction, + AttachScenario, + FreeTrialResponseSchema, + ProductItemResponseSchema, + ProductResponseSchema, +} from "@autumn/shared"; +import { routeHandler } from "@/utils/routerUtils.js"; +import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js"; +import { AttachBody, AttachBodySchema } from "../models/AttachBody.js"; +import { ExtendedResponse } from "@/utils/models/Request.js"; +import { ExtendedRequest } from "@/utils/models/Request.js"; +import { getAttachBranch } from "../attachUtils/getAttachBranch.js"; +import { getAttachConfig } from "../attachUtils/getAttachConfig.js"; +import { getAttachFunction } from "../attachUtils/getAttachFunction.js"; +import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js"; +import { z } from "zod"; +import { checkStripeConnections } from "../attachRouter.js"; + +const getAttachVars = async ({ + req, + attachBody, +}: { + req: ExtendedRequest; + attachBody: AttachBody; +}) => { + const { attachParams } = await getAttachParams({ + req, + attachBody, + }); + + const branch = await getAttachBranch({ + req, + attachBody, + attachParams, + fromPreview: true, + }); + + const { flags, config } = await getAttachConfig({ + req, + attachParams, + attachBody, + branch, + }); + + const func = await getAttachFunction({ + branch, + attachParams, + attachBody, + config, + }); + + return { + attachParams, + flags, + branch, + config, + func, + }; +}; + +const CheckoutResponseSchema = z.object({ + url: z.string().nullish(), + customer_id: z.string().nullish(), + scenario: z.nativeEnum(AttachScenario), + lines: z.array( + z.object({ + description: z.string(), + amount: z.number(), + item: ProductItemResponseSchema, + }), + ), + // next_cycle: { + // lines + // } + product: ProductResponseSchema, +}); + +export const handleCheckout = (req: any, res: any) => + routeHandler({ + req, + res, + action: "attach-preview", + handler: async (req: ExtendedRequest, res: ExtendedResponse) => { + const { logtail: logger } = req; + const attachBody = AttachBodySchema.parse(req.body); + + const { attachParams, flags, branch, config, func } = await getAttachVars( + { req, attachBody }, + ); + + if (func == AttachFunction.CreateCheckout) { + await checkStripeConnections({ + req, + attachParams, + createCus: true, + useCheckout: true, + }); + + const checkout = await handleCreateCheckout({ + req, + res, + attachParams, + returnCheckout: true, + }); + + res.status(200).json(CheckoutResponseSchema.parse(checkout)); + return; + } + + res.status(200).json("ok"); + + return; + }, + }); diff --git a/server/src/internal/customers/attach/handleAttach.ts b/server/src/internal/customers/attach/handleAttach.ts index d61d760ac..74ac689f0 100644 --- a/server/src/internal/customers/attach/handleAttach.ts +++ b/server/src/internal/customers/attach/handleAttach.ts @@ -49,13 +49,12 @@ export const handleAttach = async (req: any, res: any) => config, }); - await checkStripeConnections({ req, attachParams }); - await createStripePrices({ + await checkStripeConnections({ + req, attachParams, useCheckout: config.onlyCheckout, - req, - logger, }); + await insertCustomItems({ db: req.db, customPrices: customPrices || [], diff --git a/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts index e57daa219..a2066b7c5 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts @@ -32,83 +32,5 @@ export const handleAttachPreview = (req: any, res: any) => res.status(200).json(attachPreview); return; - - // // Handle existing product - // const branch = await getAttachBranch({ - // req, - // attachBody, - // attachParams, - // fromPreview: true, - // }); - - // const { flags, config } = await getAttachConfig({ - // req, - // attachParams, - // attachBody, - // branch, - // }); - - // const func = await getAttachFunction({ - // branch, - // attachParams, - // attachBody, - // config, - // }); - - // logger.info("--------------------------------"); - // logger.info(`ATTACH PREVIEW (org: ${attachParams.org.id})`); - // logger.info(`Branch: ${branch}, Function: ${func}`); - - // let now = attachParams.now || Date.now(); - - // let preview: any = null; - - // if ( - // func == AttachFunction.AddProduct || - // func == AttachFunction.CreateCheckout || - // func == AttachFunction.OneOff - // ) { - // preview = await getNewProductPreview({ - // attachParams, - // now, - // logger, - // }); - // } - - // if (func == AttachFunction.ScheduleProduct) { - // preview = await getDowngradeProductPreview({ - // attachParams, - // now, - // logger, - // }); - // } - - // if ( - // func == AttachFunction.UpgradeDiffInterval || - // func == AttachFunction.UpdatePrepaidQuantity || - // func == AttachFunction.UpgradeSameInterval - // ) { - // preview = await getUpgradeProductPreview({ - // req, - // attachParams, - // branch, - // now, - // }); - // } - - // const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({ - // attachParams, - // }); - - // res.status(200).json({ - // branch, - // ...preview, - // current_product: curMainProduct - // ? cusProductToProduct({ - // cusProduct: curMainProduct, - // }) - // : null, - // scheduled_product: curScheduledProduct, - // }); }, }); diff --git a/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts b/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts index 37dbe9cd6..43bc56aa0 100644 --- a/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts +++ b/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts @@ -175,6 +175,7 @@ export const getCusProductResponse = async ({ canceled_at: cusProduct.canceled_at, is_default: fullProduct.is_default || false, is_add_on: fullProduct.is_add_on || false, + version: fullProduct.version, // stripe_subscription_ids: cusProduct.subscription_ids || [], started_at: cusProduct.starts_at, diff --git a/shared/models/cusModels/cusResModels/cusProductResponse.ts b/shared/models/cusModels/cusResModels/cusProductResponse.ts index 9cbd8fad1..2c66e5bde 100644 --- a/shared/models/cusModels/cusResModels/cusProductResponse.ts +++ b/shared/models/cusModels/cusResModels/cusProductResponse.ts @@ -12,6 +12,7 @@ export const CusProductResponseSchema = z.object({ started_at: z.number(), is_default: z.boolean(), is_add_on: z.boolean(), + version: z.number().nullish(), stripe_subscription_ids: z.array(z.string()).nullish(),