From 265b93f39c13e1517e2f43374ea43d6d314410c6 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 26 Nov 2025 14:58:01 +0000 Subject: [PATCH] chore: moved checkout to hono router --- server/biome.json | 4 +- .../check/handlers/getProductCheckPreview.ts | 56 ++--- .../attachPreview}/attachParamsToPreview.ts | 40 ++-- server/src/internal/billing/billingRouter.ts | 2 + .../billing/checkout/getHasProrations.ts | 27 +++ .../billing/checkout/handleCheckout.ts | 205 ++++++++++++++++++ .../billing/checkout/handleCheckoutV2.ts | 112 ++++++++++ .../checkout/previewToCheckoutRes.ts | 20 +- .../checkout/utils/checkoutToAttachContext.ts | 61 ++++++ .../checkout/utils/getCheckoutOptions.ts | 40 ++++ .../add-product/handleCreateCheckout.ts | 4 +- .../handleCreateInvoiceCheckout.ts | 25 +-- .../internal/customers/attach/attachRouter.ts | 25 ++- .../attachParamsUtils/getCusAndProducts.ts | 23 +- .../attachParamsUtils/getPricesAndEnts.ts | 8 +- .../attachParams/getAttachParams.ts | 18 +- .../attachParams/processAttachBody.ts | 26 +-- .../attach/attachUtils/getAttachBranch.ts | 7 +- .../attach/attachUtils/getAttachConfig.ts | 7 +- .../attach/checkout/getHasProrations.ts | 36 --- .../attach/checkout/handleCheckout.ts | 204 ----------------- .../internal/customers/attach/handleAttach.ts | 13 +- .../getMultiAttachPreview.ts | 12 +- .../getNewProductPreview.ts | 3 +- .../getUpgradeProductPreview.ts | 8 +- .../handleAttachPreview.ts | 11 +- .../handlers/handleMigrateProductV2.ts | 8 +- server/tests/attach/entities/entity2.test.ts | 17 +- server/tests/attach/entities/entity4.test.ts | 1 - server/tests/attach/prepaid/prepaid1.test.ts | 20 +- shared/api/billing/checkout/checkoutParams.ts | 19 ++ .../api/billing/checkout/checkoutResponse.ts | 32 +++ .../checkout/prevVersions/checkoutParamsV0.ts | 43 ++++ .../prevVersions/checkoutResponseV0.ts | 9 +- shared/api/core/checkoutModels.ts | 80 +++---- .../api/customers/cusPlans/apiSubscription.ts | 8 + shared/api/models.ts | 6 +- .../subscriptions/checkout/checkoutParams.ts | 19 -- .../checkout/checkoutResponse.ts | 32 --- .../versionChangeUtils/VersionChange.ts | 1 + 40 files changed, 790 insertions(+), 502 deletions(-) rename server/src/internal/{customers/attach/handleAttachPreview => billing/attachPreview}/attachParamsToPreview.ts (63%) create mode 100644 server/src/internal/billing/checkout/getHasProrations.ts create mode 100644 server/src/internal/billing/checkout/handleCheckout.ts create mode 100644 server/src/internal/billing/checkout/handleCheckoutV2.ts rename server/src/internal/{customers/attach => billing}/checkout/previewToCheckoutRes.ts (93%) create mode 100644 server/src/internal/billing/checkout/utils/checkoutToAttachContext.ts create mode 100644 server/src/internal/billing/checkout/utils/getCheckoutOptions.ts delete mode 100644 server/src/internal/customers/attach/checkout/getHasProrations.ts delete mode 100644 server/src/internal/customers/attach/checkout/handleCheckout.ts create mode 100644 shared/api/billing/checkout/checkoutParams.ts create mode 100644 shared/api/billing/checkout/checkoutResponse.ts create mode 100644 shared/api/billing/checkout/prevVersions/checkoutParamsV0.ts rename shared/api/{subscriptions => billing}/checkout/prevVersions/checkoutResponseV0.ts (73%) delete mode 100644 shared/api/subscriptions/checkout/checkoutParams.ts delete mode 100644 shared/api/subscriptions/checkout/checkoutResponse.ts diff --git a/server/biome.json b/server/biome.json index 463566616..a0b4b7ee7 100644 --- a/server/biome.json +++ b/server/biome.json @@ -21,8 +21,8 @@ "recommended": true, "suspicious": { "noExportsInTest": "off", - "noExplicitAny": "off", - "noImplicitAnyLet": "off" + "noExplicitAny": "on", + "noImplicitAnyLet": "on" }, "complexity": { "noStaticOnlyClass": "off" diff --git a/server/src/internal/api/check/handlers/getProductCheckPreview.ts b/server/src/internal/api/check/handlers/getProductCheckPreview.ts index efde1a413..e8a2da8dd 100644 --- a/server/src/internal/api/check/handlers/getProductCheckPreview.ts +++ b/server/src/internal/api/check/handlers/getProductCheckPreview.ts @@ -1,25 +1,25 @@ -import { checkToAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.js"; -import { FullCustomer, FullProduct } from "@autumn/shared"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { AttachBody } from "@autumn/shared"; -import { attachParamsToPreview } from "@/internal/customers/attach/handleAttachPreview/attachParamsToPreview.js"; - import { + type AttachBody, AttachFunction, - AttachPreview, - CheckProductPreview, - Feature, - Organization, + type AttachPreview, + type CheckProductPreview, + type Feature, + type FullCustomer, + type FullProduct, + type Organization, } from "@autumn/shared"; - -import { getAttachScenario } from "./attachToCheckPreview/getAttachScenario.js"; +import { Decimal } from "decimal.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { attachParamsToPreview } from "@/internal/billing/attachPreview/attachParamsToPreview.js"; +import { checkToAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js"; import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; import { formatAmount } from "@/utils/formatUtils.js"; -import { Decimal } from "decimal.js"; import { notNullish } from "@/utils/genUtils.js"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; +import { getAttachScenario } from "./attachToCheckPreview/getAttachScenario.js"; export const attachToCheckPreview = async ({ preview, @@ -39,26 +39,26 @@ export const attachToCheckPreview = async ({ fullCus: FullCustomer; }) => { // 1. If check - let attachFunc = preview.func; + const attachFunc = preview.func; if ( - attachFunc == AttachFunction.AddProduct && + attachFunc === AttachFunction.AddProduct && isFreeProduct(product.prices) ) { return null; } const noOptions = !preview.options || preview.options.length === 0; - if (attachFunc == AttachFunction.CreateCheckout && noOptions) { + if (attachFunc === AttachFunction.CreateCheckout && noOptions) { return null; } - let scenario = await getAttachScenario({ + const scenario = await getAttachScenario({ preview, product, }); - let items = preview.due_today?.line_items?.map((item) => { + const items = preview.due_today?.line_items?.map((item) => { return { price: notNullish(item.amount) ? formatAmount({ @@ -73,21 +73,26 @@ export const attachToCheckPreview = async ({ }; }); - let options = preview.options?.map((option: any) => { + const options = preview.options?.map((option: any) => { return { ...option, price: new Decimal(option.price).toDecimalPlaces(2).toNumber(), }; }); - let due_today = preview.due_today + const due_today = preview.due_today ? { price: preview.due_today.total, currency: org.default_currency || "usd", } : undefined; - let due_next_cycle = undefined; + let due_next_cycle: + | { + price: number; + currency: string; + } + | undefined; if (preview.due_next_cycle) { due_next_cycle = { @@ -101,7 +106,7 @@ export const attachToCheckPreview = async ({ }; } - let checkPreview: CheckProductPreview = { + const checkPreview: CheckProductPreview = { // title: "Check", // message: "Check", scenario, @@ -158,10 +163,9 @@ export const getProductCheckPreview = async ({ }; const preview = await attachParamsToPreview({ - req, + ctx: req as AutumnContext, attachParams, attachBody, - logger, }); const checkPreview = await attachToCheckPreview({ diff --git a/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts b/server/src/internal/billing/attachPreview/attachParamsToPreview.ts similarity index 63% rename from server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts rename to server/src/internal/billing/attachPreview/attachParamsToPreview.ts index ba57c28b5..fcc1ddce9 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts +++ b/server/src/internal/billing/attachPreview/attachParamsToPreview.ts @@ -3,43 +3,42 @@ import { AttachBranch, AttachFunction, cusProductToProduct, + notNullish, } from "@autumn/shared"; -import { notNullish } from "@/utils/genUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; -import type { AttachParams } from "../../cusProducts/AttachParams.js"; -import { attachParamToCusProducts } from "../attachUtils/convertAttachParams.js"; -import { getAttachBranch } from "../attachUtils/getAttachBranch.js"; -import { getAttachConfig } from "../attachUtils/getAttachConfig.js"; -import { getAttachFunction } from "../attachUtils/getAttachFunction.js"; -import { getDowngradeProductPreview } from "./getDowngradeProductPreview.js"; -import { getMultiAttachPreview } from "./getMultiAttachPreview.js"; -import { getNewProductPreview } from "./getNewProductPreview.js"; -import { getUpgradeProductPreview } from "./getUpgradeProductPreview.js"; +import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js"; +import { getAttachBranch } from "@/internal/customers/attach/attachUtils/getAttachBranch.js"; +import { getAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js"; +import { getAttachFunction } from "@/internal/customers/attach/attachUtils/getAttachFunction.js"; +import { getDowngradeProductPreview } from "@/internal/customers/attach/handleAttachPreview/getDowngradeProductPreview.js"; +import { getMultiAttachPreview } from "@/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.js"; +import { getNewProductPreview } from "@/internal/customers/attach/handleAttachPreview/getNewProductPreview.js"; +import { getUpgradeProductPreview } from "@/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import type { AutumnContext } from "../../../honoUtils/HonoEnv"; export const attachParamsToPreview = async ({ - req, + ctx, attachParams, attachBody, - logger, withPrepaid = false, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachParams: AttachParams; attachBody: AttachBody; - logger: any; withPrepaid?: boolean; }) => { + const { logger } = ctx; // Handle existing product const branch = await getAttachBranch({ - req, + ctx, attachBody, attachParams, fromPreview: true, }); - const { flags, config } = await getAttachConfig({ - req, + const { config } = await getAttachConfig({ + ctx, attachParams, attachBody, branch, @@ -65,10 +64,9 @@ export const attachParamsToPreview = async ({ notNullish(attachParams.productsList) ) { preview = await getMultiAttachPreview({ - req, + ctx, attachBody, attachParams, - logger, config, branch, }); @@ -103,7 +101,7 @@ export const attachParamsToPreview = async ({ func === AttachFunction.UpdatePrepaidQuantity ) { preview = await getUpgradeProductPreview({ - req, + ctx, attachParams, branch, now, diff --git a/server/src/internal/billing/billingRouter.ts b/server/src/internal/billing/billingRouter.ts index e3c3c5085..5e9dbd851 100644 --- a/server/src/internal/billing/billingRouter.ts +++ b/server/src/internal/billing/billingRouter.ts @@ -1,7 +1,9 @@ import { Hono } from "hono"; import type { HonoEnv } from "../../honoUtils/HonoEnv.js"; +import { handleCheckoutV2 } from "./checkout/handleCheckoutV2.js"; import { handleSetupPayment } from "./handlers/handleSetupPayment.js"; export const billingRouter = new Hono(); billingRouter.post("/setup_payment", ...handleSetupPayment); +billingRouter.post("/checkout", ...handleCheckoutV2); diff --git a/server/src/internal/billing/checkout/getHasProrations.ts b/server/src/internal/billing/checkout/getHasProrations.ts new file mode 100644 index 000000000..2bed0e91d --- /dev/null +++ b/server/src/internal/billing/checkout/getHasProrations.ts @@ -0,0 +1,27 @@ +import { AttachBranch, cusProductToPrices } from "@autumn/shared"; +import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { isFreeProduct } from "@/internal/products/productUtils.js"; + +export const getHasProrations = async ({ + branch, + attachParams, +}: { + branch: AttachBranch; + attachParams: AttachParams; +}) => { + const { curMainProduct } = attachParamToCusProducts({ attachParams }); + if (branch === AttachBranch.Upgrade) { + const curPrices = cusProductToPrices({ cusProduct: curMainProduct! }); + + if (!isFreeProduct(curPrices)) { + return true; + } + } + + if (branch === AttachBranch.UpdatePrepaidQuantity) { + return true; + } + + return false; +}; diff --git a/server/src/internal/billing/checkout/handleCheckout.ts b/server/src/internal/billing/checkout/handleCheckout.ts new file mode 100644 index 000000000..c3f15ea89 --- /dev/null +++ b/server/src/internal/billing/checkout/handleCheckout.ts @@ -0,0 +1,205 @@ +// import { +// type AttachBody, +// AttachBodySchema, +// AttachFunction, +// type FeatureOptions, +// } from "@autumn/shared"; +// import { attachParamsToPreview } from "@/internal/billing/attachPreview/attachParamsToPreview.js"; +// import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js"; +// import { handleCreateInvoiceCheckout } from "@/internal/customers/add-product/handleCreateInvoiceCheckout.js"; +// import { +// checkStripeConnections, +// handlePrepaidErrors, +// } from "@/internal/customers/attach/attachRouter.js"; +// import { getAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/getAttachParams.js"; +// import { attachParamsToProduct } from "@/internal/customers/attach/attachUtils/convertAttachParams.js"; +// import { getAttachBranch } from "@/internal/customers/attach/attachUtils/getAttachBranch.js"; +// import { getAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js"; +// import { getAttachFunction } from "@/internal/customers/attach/attachUtils/getAttachFunction.js"; +// import { handleCheckoutErrors } from "@/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.js"; +// import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +// import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js"; +// import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; +// import { getPriceOptions } from "@/internal/products/prices/priceUtils.js"; +// import type { +// ExtendedRequest, +// ExtendedResponse, +// } from "@/utils/models/Request.js"; +// import { routeHandler } from "@/utils/routerUtils.js"; +// import type { AutumnContext } from "../../../honoUtils/HonoEnv.js"; +// import { getHasProrations } from "./getHasProrations.js"; +// import { previewToCheckoutRes } from "./previewToCheckoutRes.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 getCheckoutOptions = async ({ +// req, +// attachParams, +// }: { +// req: ExtendedRequest; +// attachParams: AttachParams; +// }) => { +// const product = attachParamsToProduct({ attachParams }); +// const prepaidPrices = product.prices.filter((p) => +// isPrepaidPrice({ price: p }), +// ); + +// const newOptions: FeatureOptions[] = structuredClone( +// attachParams.optionsList, +// ); +// for (const prepaidPrice of prepaidPrices) { +// const feature = priceToFeature({ +// price: prepaidPrice, +// features: req.features, +// }); +// const option = getPriceOptions(prepaidPrice, attachParams.optionsList); +// if (!option) { +// newOptions.push({ +// feature_id: feature?.id ?? "", +// internal_feature_id: feature?.internal_id, +// quantity: 1, +// }); +// } +// } + +// attachParams.optionsList = newOptions; +// return newOptions; +// }; + +// export const handleCheckout = (req: any, res: any) => +// routeHandler({ +// req, +// res, +// action: "attach-preview", +// handler: async (req: ExtendedRequest, res: ExtendedResponse) => { +// const { logger } = req; + +// const attachBody = AttachBodySchema.parse(req.body); + +// const { attachParams, branch, func, config } = await getAttachVars({ +// req, +// attachBody, +// }); + +// let checkoutUrl = null; + +// handleCheckoutErrors({ +// attachParams, +// branch, +// }); + +// if (func === AttachFunction.CreateCheckout) { +// await checkStripeConnections({ +// ctx: req as AutumnContext, +// attachParams, +// createCus: true, +// useCheckout: true, +// }); + +// await handlePrepaidErrors({ +// attachParams, +// config, +// useCheckout: config.onlyCheckout, +// }); + +// if (config.invoiceCheckout) { +// const result = await handleCreateInvoiceCheckout({ +// req, +// attachParams, +// attachBody, +// branch, +// config, +// }); + +// checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url; +// } else { +// const checkout = await handleCreateCheckout({ +// req, +// res, +// attachParams, +// config, +// returnCheckout: true, +// }); + +// checkoutUrl = checkout?.url; +// } +// } + +// console.log(`Branch: ${branch}, Func: ${func}`); + +// await getCheckoutOptions({ +// req, +// attachParams, +// }); + +// const preview = await attachParamsToPreview({ +// req, +// attachParams, +// logger, +// attachBody, +// withPrepaid: true, +// }); + +// const checkoutRes = await previewToCheckoutRes({ +// req, +// attachParams, +// preview, +// branch, +// }); + +// // Get has prorations +// const hasProrations = await getHasProrations({ +// req, +// branch, +// attachParams, +// }); + +// res.status(200).json({ +// ...checkoutRes, +// url: checkoutUrl, +// has_prorations: hasProrations, +// }); + +// return; +// }, +// }); diff --git a/server/src/internal/billing/checkout/handleCheckoutV2.ts b/server/src/internal/billing/checkout/handleCheckoutV2.ts new file mode 100644 index 000000000..fcdd78925 --- /dev/null +++ b/server/src/internal/billing/checkout/handleCheckoutV2.ts @@ -0,0 +1,112 @@ +import { + AffectedResource, + ApiVersion, + AttachFunction, + CheckoutParamsV0Schema, +} from "@autumn/shared"; +import { createRoute } from "../../../honoMiddlewares/routeHandler"; +import type { ExtendedRequest } from "../../../utils/models/Request"; +import { handleCreateCheckout } from "../../customers/add-product/handleCreateCheckout"; +import { handleCreateInvoiceCheckout } from "../../customers/add-product/handleCreateInvoiceCheckout"; +import { + checkStripeConnections, + handlePrepaidErrors, +} from "../../customers/attach/attachRouter"; +import { handleCheckoutErrors } from "../../customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors"; +import { attachParamsToPreview } from "../attachPreview/attachParamsToPreview"; +import { getHasProrations } from "./getHasProrations"; +import { previewToCheckoutRes } from "./previewToCheckoutRes"; +import { checkoutToAttachContext } from "./utils/checkoutToAttachContext"; +import { getCheckoutOptions } from "./utils/getCheckoutOptions"; + +export const handleCheckoutV2 = createRoute({ + versionedBody: { + latest: CheckoutParamsV0Schema, + [ApiVersion.V1_Beta]: CheckoutParamsV0Schema, + }, + resource: AffectedResource.Checkout, + handler: async (c) => { + const ctx = c.get("ctx"); + const body = c.req.valid("json"); + + const { attachParams, branch, func, config } = + await checkoutToAttachContext({ + ctx, + checkoutParams: body, + }); + + let checkoutUrl = null; + + handleCheckoutErrors({ + attachParams, + branch, + }); + + if (func === AttachFunction.CreateCheckout) { + await checkStripeConnections({ + ctx, + attachParams, + createCus: true, + useCheckout: true, + }); + + await handlePrepaidErrors({ + attachParams, + config, + useCheckout: config.onlyCheckout, + }); + + if (config.invoiceCheckout) { + const result = await handleCreateInvoiceCheckout({ + req: ctx as ExtendedRequest, + attachParams, + attachBody: body, + branch, + config, + }); + + checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url; + } else { + const checkout = await handleCreateCheckout({ + req: ctx as ExtendedRequest, + attachParams, + config, + returnCheckout: true, + }); + + checkoutUrl = checkout?.url; + } + } + + await getCheckoutOptions({ + ctx, + attachParams, + }); + + const preview = await attachParamsToPreview({ + ctx, + attachParams, + attachBody: body, + withPrepaid: true, + }); + + const checkoutRes = await previewToCheckoutRes({ + req: ctx as ExtendedRequest, + attachParams, + preview, + branch, + }); + + // Get has prorations + const hasProrations = await getHasProrations({ + branch, + attachParams, + }); + + return c.json({ + ...checkoutRes, + url: checkoutUrl, + has_prorations: hasProrations, + }); + }, +}); diff --git a/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts b/server/src/internal/billing/checkout/previewToCheckoutRes.ts similarity index 93% rename from server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts rename to server/src/internal/billing/checkout/previewToCheckoutRes.ts index 9792ec733..1ba64b1b8 100644 --- a/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts +++ b/server/src/internal/billing/checkout/previewToCheckoutRes.ts @@ -1,8 +1,8 @@ import { type AttachBranch, type AttachPreview, - type CheckoutLine, - CheckoutResponseSchema, + type CheckoutLineV0, + CheckoutResponseV0Schema, cusProductToEnts, cusProductToPrices, cusProductToProduct, @@ -12,6 +12,11 @@ import { UsageModel, } from "@autumn/shared"; import { Decimal } from "decimal.js"; +import { + attachParamsToProduct, + attachParamToCusProducts, +} from "@/internal/customers/attach/attachUtils/convertAttachParams.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js"; import { isPriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js"; import { @@ -20,11 +25,6 @@ import { } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js"; import { notNullish } from "@/utils/genUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; -import type { AttachParams } from "../../cusProducts/AttachParams.js"; -import { - attachParamsToProduct, - attachParamToCusProducts, -} from "../attachUtils/convertAttachParams.js"; export const previewToCheckoutRes = async ({ req, @@ -52,7 +52,7 @@ export const previewToCheckoutRes = async ({ const newEnts = attachParams.entitlements; const allPrices = [...curPrices, ...newPrices]; const allEnts = [...curEnts, ...newEnts]; - let lines: CheckoutLine[] = []; + let lines: CheckoutLineV0[] = []; if (preview.due_today && preview.due_today.line_items.length > 0) { lines = preview.due_today.line_items @@ -77,7 +77,7 @@ export const previewToCheckoutRes = async ({ }), }; }) - .filter(notNullish) as CheckoutLine[]; + .filter(notNullish) as CheckoutLineV0[]; } const curProduct = curCusProduct @@ -172,7 +172,7 @@ export const previewToCheckoutRes = async ({ }) .filter(notNullish); - return CheckoutResponseSchema.parse({ + return CheckoutResponseV0Schema.parse({ customer_id: attachParams.customer.id, lines, product: newProduct, diff --git a/server/src/internal/billing/checkout/utils/checkoutToAttachContext.ts b/server/src/internal/billing/checkout/utils/checkoutToAttachContext.ts new file mode 100644 index 000000000..198081bc6 --- /dev/null +++ b/server/src/internal/billing/checkout/utils/checkoutToAttachContext.ts @@ -0,0 +1,61 @@ +import type { + AttachBranch, + AttachConfig, + AttachFunction, + CheckoutParamsV0, +} from "@autumn/shared"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv"; +import { getAttachParams } from "../../../customers/attach/attachUtils/attachParams/getAttachParams"; +import { getAttachBranch } from "../../../customers/attach/attachUtils/getAttachBranch"; +import { getAttachConfig } from "../../../customers/attach/attachUtils/getAttachConfig"; +import { getAttachFunction } from "../../../customers/attach/attachUtils/getAttachFunction"; +import type { AttachFlags } from "../../../customers/attach/models/AttachFlags"; +import type { AttachParams } from "../../../customers/cusProducts/AttachParams"; + +export const checkoutToAttachContext = async ({ + ctx, + checkoutParams, +}: { + ctx: AutumnContext; + checkoutParams: CheckoutParamsV0; +}): Promise<{ + attachParams: AttachParams; + flags: AttachFlags; + branch: AttachBranch; + config: AttachConfig; + func: AttachFunction; +}> => { + const { attachParams } = await getAttachParams({ + ctx, + attachBody: checkoutParams, + }); + + const branch = await getAttachBranch({ + ctx, + attachBody: checkoutParams, + attachParams, + fromPreview: true, + }); + + const { flags, config } = await getAttachConfig({ + ctx, + attachParams, + attachBody: checkoutParams, + branch, + }); + + const func = await getAttachFunction({ + branch, + attachParams, + attachBody: checkoutParams, + config, + }); + + return { + attachParams, + flags, + branch, + config, + func, + }; +}; diff --git a/server/src/internal/billing/checkout/utils/getCheckoutOptions.ts b/server/src/internal/billing/checkout/utils/getCheckoutOptions.ts new file mode 100644 index 000000000..f062f0919 --- /dev/null +++ b/server/src/internal/billing/checkout/utils/getCheckoutOptions.ts @@ -0,0 +1,40 @@ +import { type FeatureOptions, isPrepaidPrice } from "@autumn/shared"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv"; +import { attachParamsToProduct } from "../../../customers/attach/attachUtils/convertAttachParams"; +import type { AttachParams } from "../../../customers/cusProducts/AttachParams"; +import { getPriceOptions } from "../../../products/prices/priceUtils"; +import { priceToFeature } from "../../../products/prices/priceUtils/convertPrice"; + +export const getCheckoutOptions = async ({ + ctx, + attachParams, +}: { + ctx: AutumnContext; + attachParams: AttachParams; +}) => { + const product = attachParamsToProduct({ attachParams }); + const prepaidPrices = product.prices.filter((p) => + isPrepaidPrice({ price: p }), + ); + + const newOptions: FeatureOptions[] = structuredClone( + attachParams.optionsList, + ); + for (const prepaidPrice of prepaidPrices) { + const feature = priceToFeature({ + price: prepaidPrice, + features: ctx.features, + }); + const option = getPriceOptions(prepaidPrice, attachParams.optionsList); + if (!option) { + newOptions.push({ + feature_id: feature?.id ?? "", + internal_feature_id: feature?.internal_id, + quantity: 1, + }); + } + } + + attachParams.optionsList = newOptions; + return newOptions; +}; diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index b4d85f5d6..8f893eb04 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -27,7 +27,7 @@ export const handleCreateCheckout = async ({ returnCheckout = false, }: { req: any; - res: any; + res?: any; attachParams: AttachParams; config: AttachConfig; returnCheckout?: boolean; @@ -182,7 +182,7 @@ export const handleCreateCheckout = async ({ } } - if (returnCheckout) { + if (returnCheckout || !res) { return checkout; } diff --git a/server/src/internal/customers/add-product/handleCreateInvoiceCheckout.ts b/server/src/internal/customers/add-product/handleCreateInvoiceCheckout.ts index a4db8ed78..4f7f7bc2e 100644 --- a/server/src/internal/customers/add-product/handleCreateInvoiceCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateInvoiceCheckout.ts @@ -1,22 +1,19 @@ import { - AttachParams, - AttachResultSchema, -} from "../cusProducts/AttachParams.js"; - -import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js"; - -import { isOneOff } from "@/internal/products/productUtils.js"; - -import { handlePaidProduct } from "../attach/attachFunctions/addProductFlow/handlePaidProduct.js"; -import { - AttachBody, - AttachBranch, - AttachConfig, + type AttachBody, + type AttachBranch, + type AttachConfig, SuccessCode, } from "@autumn/shared"; -import Stripe from "stripe"; +import type Stripe from "stripe"; +import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js"; +import { isOneOff } from "@/internal/products/productUtils.js"; import { handleOneOffFunction } from "../attach/attachFunctions/addProductFlow/handleOneOffFunction.js"; +import { handlePaidProduct } from "../attach/attachFunctions/addProductFlow/handlePaidProduct.js"; import { handleMultiAttachFlow } from "../attach/attachFunctions/multiAttach/handleMultiAttachFlow.js"; +import { + type AttachParams, + AttachResultSchema, +} from "../cusProducts/AttachParams.js"; export const handleCreateInvoiceCheckout = async ({ req, diff --git a/server/src/internal/customers/attach/attachRouter.ts b/server/src/internal/customers/attach/attachRouter.ts index c0a40ce2e..bfdc81ca0 100644 --- a/server/src/internal/customers/attach/attachRouter.ts +++ b/server/src/internal/customers/attach/attachRouter.ts @@ -26,7 +26,7 @@ import { } from "@/internal/products/productUtils.js"; import RecaseError from "@/utils/errorUtils.js"; import { notNullish, nullOrUndefined } from "@/utils/genUtils.js"; -import { handleCheckout } from "./checkout/handleCheckout.js"; +import type { AutumnContext } from "../../../honoUtils/HonoEnv.js"; import { handleAttach } from "./handleAttach.js"; import { handleAttachPreview } from "./handleAttachPreview/handleAttachPreview.js"; @@ -125,18 +125,18 @@ export const handlePublicAttachErrors = async ({ }; export const checkStripeConnections = async ({ - req, + ctx, attachParams, createCus = true, useCheckout = false, }: { - req: any; + ctx: AutumnContext; attachParams: AttachParams; createCus?: boolean; useCheckout?: boolean; }) => { const { org, customer, products, stripeCus, stripeCli } = attachParams; - const logger = req.logger; + const { logger, db } = ctx; const env = customer.env; // 2. If invoice only and no email, save email @@ -144,7 +144,7 @@ export const checkStripeConnections = async ({ customer.email = `${customer.id}-${org.id}@invoices.useautumn.com`; await Promise.all([ CusService.update({ - db: req.db, + db, idOrInternalId: customer.internal_id, orgId: org.id, env, @@ -164,7 +164,7 @@ export const checkStripeConnections = async ({ if (createCus) { batchProductUpdates.push( createStripeCusIfNotExists({ - db: req.db, + db, org, env, customer, @@ -176,7 +176,7 @@ export const checkStripeConnections = async ({ for (const product of products) { batchProductUpdates.push( checkStripeProductExists({ - db: req.db, + db, org, env, product, @@ -189,23 +189,24 @@ export const checkStripeConnections = async ({ await createStripePrices({ attachParams, useCheckout, - req, + ctx, logger, }); }; export const createStripePrices = async ({ + ctx, attachParams, useCheckout, - req, logger, }: { + ctx: AutumnContext; attachParams: AttachParams; useCheckout: boolean; - req: any; logger: any; }) => { const { prices, entitlements, products, org, stripeCli } = attachParams; + const { db } = ctx; const batchPriceUpdates = []; @@ -216,7 +217,7 @@ export const createStripePrices = async ({ batchPriceUpdates.push( createStripePriceIFNotExist({ - db: req.db, + db, stripeCli, price, entitlements, @@ -250,4 +251,4 @@ export const customerHasPm = async ({ attachRouter.post("/attach", handleAttach); attachRouter.post("/attach/preview", handleAttachPreview); -attachRouter.post("/checkout", handleCheckout); +// attachRouter.post("/checkout", handleCheckout); 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 c42499a8c..9e2681d80 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts @@ -11,16 +11,15 @@ import { ProductService } from "@/internal/products/ProductService.js"; import { isOneOff } from "@/internal/products/productUtils.js"; import RecaseError from "@/utils/errorUtils.js"; import { notNullish } from "@/utils/genUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; import type { AutumnContext } from "../../../../../../honoUtils/HonoEnv"; import { getExistingCusProducts } from "../../../../cusProducts/cusProductUtils/getExistingCusProducts"; const getProductsForAttach = async ({ - req, + ctx, attachBody, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachBody: AttachBody; }) => { const { @@ -31,9 +30,9 @@ const getProductsForAttach = async ({ } = attachBody; const products = await ProductService.listFull({ - db: req.db, - orgId: req.orgId, - env: req.env, + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, inIds: inputProducts ? inputProducts.map((p) => p.product_id) : product_ids || [product_id!], @@ -74,15 +73,15 @@ const getProductsForAttach = async ({ }; export const getCustomerAndProducts = async ({ - req, + ctx, attachBody, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachBody: AttachBody; }) => { const [customer, products] = await Promise.all([ getOrCreateCustomer({ - ctx: req as unknown as AutumnContext, + ctx, customerId: attachBody.customer_id, customerData: { ...attachBody.customer_data, @@ -96,7 +95,7 @@ export const getCustomerAndProducts = async ({ entityId: attachBody.entity_id || undefined, entityData: attachBody.entity_data || undefined, }), - getProductsForAttach({ req, attachBody }), + getProductsForAttach({ ctx, attachBody }), ]); // if customer is on product v3, products[0] should just be the customer's product if version isn't explicitly passed in. @@ -109,13 +108,11 @@ export const getCustomerAndProducts = async ({ internalEntityId: customer.entity?.internal_id, }); - // console.log( - // `Product ${product.id} (${product.version}), curSameProduct: ${curSameProduct?.product.id} (version: ${curSameProduct?.product.version})`, - // ); if (curSameProduct) { products[i] = cusProductToProduct({ cusProduct: curSameProduct }); } } } + return { customer, products }; }; diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts index 6cee9d054..79f35e6de 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts @@ -16,22 +16,22 @@ import { } from "@/internal/products/free-trials/freeTrialUtils.js"; import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js"; import { notNullish } from "@/utils/genUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AutumnContext } from "../../../../../../honoUtils/HonoEnv.js"; import { mapOptionsList } from "../../mapOptionsList.js"; export const getPricesAndEnts = async ({ - req, + ctx, attachBody, customer, products, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachBody: AttachBody; customer: FullCustomer; products: FullProduct[]; }) => { const { options: optionsInput, is_custom, free_trial } = attachBody; - const { features, db, org, logger } = req; + const { features, db, org, logger } = ctx; const { curMainProduct, curSameProduct } = getExistingCusProducts({ product: products[0], diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts index bd89c7215..063b23734 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts @@ -1,14 +1,15 @@ import type { AttachBody } from "@autumn/shared"; import { nullish } from "@/utils/genUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js"; +import type { ExtendedRequest } from "../../../../../utils/models/Request.js"; import type { AttachParams } from "../../../cusProducts/AttachParams.js"; import { processAttachBody } from "./processAttachBody.js"; export const getAttachParams = async ({ - req, + ctx, attachBody, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachBody: AttachBody; }) => { const { @@ -23,7 +24,7 @@ export const getAttachParams = async ({ stripeVars, rewards, } = await processAttachBody({ - req, + ctx, attachBody, }); @@ -51,10 +52,11 @@ export const getAttachParams = async ({ replaceables: [], rewards, // From req - req, - org: req.org, + req: ctx as ExtendedRequest, + + org: ctx.org, entities: customer.entities, - features: req.features, + features: ctx.features, internalEntityId, entityId: entityId || undefined, cusProducts: customer.customer_products, @@ -72,7 +74,7 @@ export const getAttachParams = async ({ setupPayment: attachBody.setup_payment, // Others - apiVersion: req.apiVersion.value, + apiVersion: ctx.apiVersion.value, }; return { diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts b/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts index d74125db5..f2d26dc05 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts @@ -3,17 +3,17 @@ import type Stripe from "stripe"; import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import RecaseError from "@/utils/errorUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js"; import { getCustomerAndProducts } from "./attachParamsUtils/getCusAndProducts.js"; import { getPricesAndEnts } from "./attachParamsUtils/getPricesAndEnts.js"; import { getStripeCusData } from "./attachParamsUtils/getStripeCusData.js"; export const getRewards = async ({ - req, + ctx, attachBody, stripeCli, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachBody: AttachBody; stripeCli: Stripe; }) => { @@ -31,10 +31,10 @@ export const getRewards = async ({ // 1. Get reward by id or promo code const rewards = await RewardService.getByIdOrCode({ - db: req.db, + db: ctx.db, codes: rewardArray, - orgId: req.org.id, - env: req.env, + orgId: ctx.org.id, + env: ctx.env, }); for (const reward of rewardArray) { @@ -62,33 +62,33 @@ export const getRewards = async ({ }; export const processAttachBody = async ({ - req, + ctx, attachBody, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachBody: AttachBody; }) => { // 1. Get customer and products - const { org, env, logger } = req; + const { org, env, logger } = ctx; const stripeCli = createStripeCli({ org, env }); const { customer, products } = await getCustomerAndProducts({ - req, + ctx, attachBody, }); const [stripeCusData, rewardData] = await Promise.all([ getStripeCusData({ stripeCli, - db: req.db, + db: ctx.db, org, env, customer, logger, }), getRewards({ - req, + ctx, attachBody, stripeCli, }), @@ -104,7 +104,7 @@ export const processAttachBody = async ({ customPrices, customEnts, } = await getPricesAndEnts({ - req, + ctx, attachBody, customer, products, diff --git a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts index bbb3c87af..ad3e3c98d 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts @@ -19,7 +19,7 @@ import { isProductUpgrade, } from "@/internal/products/productUtils.js"; import { notNullish } from "@/utils/genUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import type { AttachParams } from "../../cusProducts/AttachParams.js"; import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js"; import { isMainTrialBranch } from "./attachUtils.js"; @@ -291,12 +291,13 @@ const getChangeProductBranch = async ({ }; export const getAttachBranch = async ({ - req, + // biome-ignore lint/correctness/noUnusedFunctionParameters: might be used in the future + ctx, attachBody, attachParams, fromPreview, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachBody: AttachBody; attachParams: AttachParams; fromPreview?: boolean; diff --git a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts index 140bd96cd..fa73c7b00 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts @@ -10,6 +10,7 @@ import { import { isDefaultTrialFullProduct } from "@/internal/products/productUtils/classifyProduct.js"; import { isFreeProduct } from "@/internal/products/productUtils.js"; import { notNullish, nullish } from "@/utils/genUtils.js"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import type { AttachParams } from "../../cusProducts/AttachParams.js"; import { willMergeSub } from "../mergeUtils/mergeUtils.js"; import type { AttachFlags } from "../models/AttachFlags.js"; @@ -55,12 +56,12 @@ export const intervalsAreSame = ({ }; export const getAttachConfig = async ({ - req, + ctx, attachParams, attachBody, branch, }: { - req: any; + ctx: AutumnContext; attachParams: AttachParams; attachBody: AttachBody; branch: AttachBranch; @@ -68,7 +69,7 @@ export const getAttachConfig = async ({ const { org, prices, paymentMethod } = attachParams; const flags: AttachFlags = { - isPublic: req.isPublic, + isPublic: ctx.isPublic, forceCheckout: attachBody.force_checkout || false, invoiceOnly: attachParams.invoiceOnly || false, isFree: isFreeProduct(prices), diff --git a/server/src/internal/customers/attach/checkout/getHasProrations.ts b/server/src/internal/customers/attach/checkout/getHasProrations.ts deleted file mode 100644 index 2f96643d8..000000000 --- a/server/src/internal/customers/attach/checkout/getHasProrations.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { AttachParams } from "../../cusProducts/AttachParams.js"; -import { AttachBranch } from "@autumn/shared"; -import { - attachParamsToCurCusProduct, - attachParamToCusProducts, -} from "../attachUtils/convertAttachParams.js"; -import { cusProductToPrices } from "@autumn/shared"; -import { isFreeProduct } from "@/internal/products/productUtils.js"; - -export const getHasProrations = async ({ - req, - branch, - attachParams, -}: { - req: ExtendedRequest; - branch: AttachBranch; - attachParams: AttachParams; -}) => { - let hasProrations = false; - - let { curMainProduct } = attachParamToCusProducts({ attachParams }); - if (branch == AttachBranch.Upgrade) { - let curPrices = cusProductToPrices({ cusProduct: curMainProduct! }); - - if (!isFreeProduct(curPrices)) { - return true; - } - } - - if (branch == AttachBranch.UpdatePrepaidQuantity) { - return true; - } - - return false; -}; diff --git a/server/src/internal/customers/attach/checkout/handleCheckout.ts b/server/src/internal/customers/attach/checkout/handleCheckout.ts deleted file mode 100644 index d7151fe11..000000000 --- a/server/src/internal/customers/attach/checkout/handleCheckout.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { - type AttachBody, - AttachBodySchema, - AttachFunction, - type FeatureOptions, -} from "@autumn/shared"; -import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js"; -import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; -import { getPriceOptions } from "@/internal/products/prices/priceUtils.js"; -import type { - ExtendedRequest, - ExtendedResponse, -} from "@/utils/models/Request.js"; -import { routeHandler } from "@/utils/routerUtils.js"; -import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js"; -import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js"; -import type { AttachParams } from "../../cusProducts/AttachParams.js"; -import { - checkStripeConnections, - handlePrepaidErrors, -} from "../attachRouter.js"; -import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js"; -import { attachParamsToProduct } from "../attachUtils/convertAttachParams.js"; -import { getAttachBranch } from "../attachUtils/getAttachBranch.js"; -import { getAttachConfig } from "../attachUtils/getAttachConfig.js"; -import { getAttachFunction } from "../attachUtils/getAttachFunction.js"; -import { handleCheckoutErrors } from "../attachUtils/handleAttachErrors/handleCheckoutErrors.js"; -import { attachParamsToPreview } from "../handleAttachPreview/attachParamsToPreview.js"; -import { getHasProrations } from "./getHasProrations.js"; -import { previewToCheckoutRes } from "./previewToCheckoutRes.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 getCheckoutOptions = async ({ - req, - attachParams, -}: { - req: ExtendedRequest; - attachParams: AttachParams; -}) => { - const product = attachParamsToProduct({ attachParams }); - const prepaidPrices = product.prices.filter((p) => - isPrepaidPrice({ price: p }), - ); - - const newOptions: FeatureOptions[] = structuredClone( - attachParams.optionsList, - ); - for (const prepaidPrice of prepaidPrices) { - const feature = priceToFeature({ - price: prepaidPrice, - features: req.features, - }); - const option = getPriceOptions(prepaidPrice, attachParams.optionsList); - if (!option) { - newOptions.push({ - feature_id: feature?.id ?? "", - internal_feature_id: feature?.internal_id, - quantity: 1, - }); - } - } - - attachParams.optionsList = newOptions; - return newOptions; -}; - -export const handleCheckout = (req: any, res: any) => - routeHandler({ - req, - res, - action: "attach-preview", - handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - const { logger, features } = req; - - const attachBody = AttachBodySchema.parse(req.body); - - const { attachParams, branch, func, config } = await getAttachVars({ - req, - attachBody, - }); - - let checkoutUrl = null; - - handleCheckoutErrors({ - attachParams, - branch, - }); - - if (func === AttachFunction.CreateCheckout) { - await checkStripeConnections({ - req, - attachParams, - createCus: true, - useCheckout: true, - }); - - await handlePrepaidErrors({ - attachParams, - config, - useCheckout: config.onlyCheckout, - }); - - if (config.invoiceCheckout) { - const result = await handleCreateInvoiceCheckout({ - req, - attachParams, - attachBody, - branch, - config, - }); - - checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url; - } else { - const checkout = await handleCreateCheckout({ - req, - res, - attachParams, - config, - returnCheckout: true, - }); - - checkoutUrl = checkout?.url; - } - } - - console.log(`Branch: ${branch}, Func: ${func}`); - - await getCheckoutOptions({ - req, - attachParams, - }); - - const preview = await attachParamsToPreview({ - req, - attachParams, - logger, - attachBody, - withPrepaid: true, - }); - - const checkoutRes = await previewToCheckoutRes({ - req, - attachParams, - preview, - branch, - }); - - // Get has prorations - const hasProrations = await getHasProrations({ - req, - branch, - attachParams, - }); - - res.status(200).json({ - ...checkoutRes, - url: checkoutUrl, - has_prorations: hasProrations, - }); - - return; - }, - }); diff --git a/server/src/internal/customers/attach/handleAttach.ts b/server/src/internal/customers/attach/handleAttach.ts index 505b4aa7b..19bc67fb9 100644 --- a/server/src/internal/customers/attach/handleAttach.ts +++ b/server/src/internal/customers/attach/handleAttach.ts @@ -5,6 +5,7 @@ import type { ExtendedResponse, } from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; +import type { AutumnContext } from "../../../honoUtils/HonoEnv.js"; import { checkStripeConnections } from "./attachRouter.js"; import { getAttachParams } from "./attachUtils/attachParams/getAttachParams.js"; import { getAttachBranch } from "./attachUtils/getAttachBranch.js"; @@ -23,20 +24,22 @@ export const handleAttach = async (req: any, res: any) => const attachBody = AttachBodySchema.parse(req.body); + const ctx = req as AutumnContext; + const { attachParams, customPrices, customEnts } = await getAttachParams({ - req, + ctx, attachBody, }); // Handle existing product const branch = await getAttachBranch({ - req, + ctx, attachBody, attachParams, }); const { flags, config } = await getAttachConfig({ - req, + ctx, attachParams, attachBody, branch, @@ -51,7 +54,7 @@ export const handleAttach = async (req: any, res: any) => }); await checkStripeConnections({ - req, + ctx, attachParams, useCheckout: config.onlyCheckout, }); @@ -82,7 +85,7 @@ export const handleAttach = async (req: any, res: any) => freeTrial: attachParams.freeTrial, }, }); - } catch (error) {} + } catch (_error) {} await runAttachFunction({ req, diff --git a/server/src/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.ts index fb2675d4a..baf92b068 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.ts @@ -1,6 +1,7 @@ import { type AttachBody, type AttachBranch, + type AttachConfig, cusProductsToPrices, type PreviewLineItem, } from "@autumn/shared"; @@ -9,7 +10,7 @@ import type Stripe from "stripe"; import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; import { notNullish } from "@/utils/genUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import type { AttachParams } from "../../cusProducts/AttachParams.js"; import { getAddAndRemoveProducts } from "../attachFunctions/multiAttach/getAddAndRemoveProducts.js"; import { priceToNewPreviewItem } from "../attachPreviewUtils/priceToNewPreviewItem.js"; @@ -18,18 +19,17 @@ import { getCustomerSub } from "../attachUtils/convertAttachParams.js"; import { handleMultiAttachErrors } from "../attachUtils/handleAttachErrors/handleMultiAttachErrors.js"; export const getMultiAttachPreview = async ({ - req, + // biome-ignore lint/correctness/noUnusedFunctionParameters: might be used in the future + ctx, attachBody, attachParams, - logger, config, branch, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachBody: AttachBody; attachParams: AttachParams; - logger: any; - config: any; + config: AttachConfig; branch: AttachBranch; }) => { await handleMultiAttachErrors({ attachParams, attachBody, branch }); diff --git a/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts index 070ee4ab3..e6bedbfca 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts @@ -12,6 +12,7 @@ import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingInter import { getAlignedUnix } from "@/internal/products/prices/billingIntervalUtils2.js"; import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; import { mapToProductItems } from "@/internal/products/productV2Utils.js"; +import type { Logger } from "../../../../external/logtail/logtailUtils.js"; import type { AttachParams } from "../../cusProducts/AttachParams.js"; import { attachParamsToProduct, @@ -86,7 +87,7 @@ export const getNewProductPreview = async ({ }: { branch: AttachBranch; attachParams: AttachParams; - logger: any; + logger: Logger; config: AttachConfig; withPrepaid?: boolean; }) => { diff --git a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts index 76bd9c3f4..a1245eee7 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts @@ -25,7 +25,7 @@ import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePrice import { isFreeProduct } from "@/internal/products/productUtils.js"; import { mapToProductItems } from "@/internal/products/productV2Utils.js"; import { nullish } from "@/utils/genUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; import type { AttachParams } from "../../cusProducts/AttachParams.js"; import { attachParamsToProduct, @@ -132,21 +132,21 @@ const filterNoProratePrepaidItems = ({ }; export const getUpgradeProductPreview = async ({ - req, + ctx, attachParams, branch, now, withPrepaid = false, config, }: { - req: ExtendedRequest; + ctx: AutumnContext; attachParams: AttachParams; branch: AttachBranch; now: number; withPrepaid?: boolean; config: AttachConfig; }) => { - const { logger } = req; + const { logger } = ctx; const { curMainProduct, curSameProduct } = attachParamToCusProducts({ attachParams, diff --git a/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts index c16959938..4fcb000c7 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts @@ -4,30 +4,29 @@ import type { ExtendedResponse, } from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; +import { attachParamsToPreview } from "../../../billing/attachPreview/attachParamsToPreview.js"; import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js"; -import { attachParamsToPreview } from "./attachParamsToPreview.js"; - export const handleAttachPreview = (req: any, res: any) => routeHandler({ req, res, action: "attach-preview", handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - const { logger } = req; const attachBody = AttachBodySchema.parse(req.body); // console.log("attachBody", attachBody); + const ctx = req as AutumnContext; const { attachParams } = await getAttachParams({ - req, + ctx, attachBody, }); const attachPreview = await attachParamsToPreview({ - req, + ctx, attachParams, attachBody, - logger, }); res.status(200).json(attachPreview); diff --git a/server/src/internal/products/handlers/handleMigrateProductV2.ts b/server/src/internal/products/handlers/handleMigrateProductV2.ts index 71b74db24..163fefa32 100644 --- a/server/src/internal/products/handlers/handleMigrateProductV2.ts +++ b/server/src/internal/products/handlers/handleMigrateProductV2.ts @@ -54,7 +54,13 @@ export const handleMigrateProductV2 = createRoute({ env, }); - if (currentMigrations.length > 0) { + if ( + currentMigrations.find( + (m) => + m.from_internal_product_id === fromProduct.internal_id && + m.to_internal_product_id === toProduct.internal_id, + ) + ) { throw new RecaseError({ message: "Another migration is ongoing, cannot create a new migration", }); diff --git a/server/tests/attach/entities/entity2.test.ts b/server/tests/attach/entities/entity2.test.ts index c78822842..cc083b05d 100644 --- a/server/tests/attach/entities/entity2.test.ts +++ b/server/tests/attach/entities/entity2.test.ts @@ -6,13 +6,15 @@ import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCo import { expectInvoiceAfterUsage } from "@tests/utils/expectUtils/expectSingleUse/expectUsageInvoice.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { advanceToNextInvoice } from "../../utils/testAttachUtils/testAttachUtils"; +import { hoursToFinalizeInvoice } from "../../utils/constants"; +import { advanceTestClock } from "../../utils/stripeUtils"; const testCase = "aentity2"; @@ -115,9 +117,20 @@ describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro annual to }); test("should have correct invoice after cycle", async () => { - curUnix = await advanceToNextInvoice({ + // curUnix = await advanceToNextInvoice({ + // stripeCli: ctx.stripeCli, + // testClockId, + // }); + curUnix = await advanceTestClock({ stripeCli: ctx.stripeCli, testClockId, + advanceTo: addMonths(new Date(), 1).getTime(), + }); + + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(), }); await expectInvoiceAfterUsage({ diff --git a/server/tests/attach/entities/entity4.test.ts b/server/tests/attach/entities/entity4.test.ts index b35a1098f..f2b00f170 100644 --- a/server/tests/attach/entities/entity4.test.ts +++ b/server/tests/attach/entities/entity4.test.ts @@ -149,7 +149,6 @@ describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro diff enti product: pro, }); }); - return; const entity2Usage = Math.random() * 1000000; test("should track usage on entity 2", async () => { diff --git a/server/tests/attach/prepaid/prepaid1.test.ts b/server/tests/attach/prepaid/prepaid1.test.ts index 19823e8e3..4497e3122 100644 --- a/server/tests/attach/prepaid/prepaid1.test.ts +++ b/server/tests/attach/prepaid/prepaid1.test.ts @@ -1,20 +1,24 @@ -import { type Customer, LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; -import chalk from "chalk"; -import { addHours, addMonths } from "date-fns"; -import type Stripe from "stripe"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import { + type Customer, + LegacyVersion, + OnDecrease, + OnIncrease, +} from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import { hoursToFinalizeInvoice } from "@tests/utils/constants.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; const testCase = "prepaid1"; @@ -154,9 +158,7 @@ describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proratio }); const autumnCus = await autumn.customers.get(customerId); - expect(autumnCus.features[TestFeature.Messages].balance).toBe( - newQuantity, - ); + expect(autumnCus.features[TestFeature.Messages].balance).toBe(newQuantity); expect(autumnCus.invoices.length).toBe(3); expect(autumnCus.invoices[0].total).toBe((newQuantity / 100) * 12.5); diff --git a/shared/api/billing/checkout/checkoutParams.ts b/shared/api/billing/checkout/checkoutParams.ts new file mode 100644 index 000000000..ccaadc054 --- /dev/null +++ b/shared/api/billing/checkout/checkoutParams.ts @@ -0,0 +1,19 @@ +// import { z } from "zod/v4"; +// import { +// AttachBodySchema, +// ExtAttachBodySchema, +// } from "../../core/attachModels.js"; + +// export const ExtCheckoutParamsSchema = ExtAttachBodySchema.extend({ +// setup_payment: z.boolean().optional(), +// }).meta({ +// description: +// "Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.", +// }); + +// export const CheckoutParamsSchema = AttachBodySchema.extend({ +// // skip_checkout: z.boolean().optional(), +// setup_payment: z.boolean().optional(), +// }); + +// export type CheckoutParams = z.infer; diff --git a/shared/api/billing/checkout/checkoutResponse.ts b/shared/api/billing/checkout/checkoutResponse.ts new file mode 100644 index 000000000..145221c86 --- /dev/null +++ b/shared/api/billing/checkout/checkoutResponse.ts @@ -0,0 +1,32 @@ +// import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js"; +// import { z } from "zod/v4"; +// import { ApiPlanSchema } from "../../products/apiPlan.js"; + +// // RESULT +// export const CheckoutLineSchema = z.object({ +// description: z.string(), +// amount: z.number(), +// // item: ApiProductItemSchema.nullish(), +// }); + +// export const CheckoutResponseSchema = z.object({ +// url: z.string().nullish(), +// customer_id: z.string(), +// lines: z.array(CheckoutLineSchema), + +// plan: ApiPlanSchema.nullish(), +// current_plan: ApiPlanSchema.nullish(), + +// options: z.array(FeatureOptionsSchema).nullish(), +// total: z.number().nullish(), +// currency: z.string().nullish(), +// has_prorations: z.boolean().nullish(), +// next_cycle: z +// .object({ +// starts_at: z.number(), +// total: z.number(), +// }) +// .nullish(), +// }); + +// export type CheckoutLine = z.infer; diff --git a/shared/api/billing/checkout/prevVersions/checkoutParamsV0.ts b/shared/api/billing/checkout/prevVersions/checkoutParamsV0.ts new file mode 100644 index 000000000..33c82bbbf --- /dev/null +++ b/shared/api/billing/checkout/prevVersions/checkoutParamsV0.ts @@ -0,0 +1,43 @@ +import { + AttachBodySchema, + ExtAttachBodySchema, +} from "@api/core/attachModels.js"; +import { z } from "zod/v4"; + +export const ExtCheckoutParamsV0Schema = ExtAttachBodySchema.extend({ + setup_payment: z.boolean().optional(), +}).meta({ + description: + "Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.", +}); + +export const CheckoutParamsV0Schema = AttachBodySchema.extend({ + setup_payment: z.boolean().optional(), +}); + +// // RESULT +// export const CheckoutLineV0Schema = z.object({ +// description: z.string(), +// amount: z.number(), +// item: ApiProductItemSchema.nullish(), +// }); + +// export const CheckoutResponseV0Schema = z.object({ +// url: z.string().nullish(), +// customer_id: z.string(), +// lines: z.array(CheckoutLineV0Schema), +// product: ApiProductSchema.nullish(), +// current_product: ApiProductSchema.nullish(), +// options: z.array(FeatureOptionsSchema).nullish(), +// total: z.number().nullish(), +// currency: z.string().nullish(), +// has_prorations: z.boolean().nullish(), +// next_cycle: z +// .object({ +// starts_at: z.number(), +// total: z.number(), +// }) +// .nullish(), +// }); + +export type CheckoutParamsV0 = z.infer; diff --git a/shared/api/subscriptions/checkout/prevVersions/checkoutResponseV0.ts b/shared/api/billing/checkout/prevVersions/checkoutResponseV0.ts similarity index 73% rename from shared/api/subscriptions/checkout/prevVersions/checkoutResponseV0.ts rename to shared/api/billing/checkout/prevVersions/checkoutResponseV0.ts index 8481d1d20..60e65ecaa 100644 --- a/shared/api/subscriptions/checkout/prevVersions/checkoutResponseV0.ts +++ b/shared/api/billing/checkout/prevVersions/checkoutResponseV0.ts @@ -4,16 +4,16 @@ import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels. import { z } from "zod/v4"; // RESULT -export const CheckoutLineSchema = z.object({ +export const CheckoutLineV0Schema = z.object({ description: z.string(), amount: z.number(), item: ApiProductItemSchema.nullish(), }); -export const CheckoutResponseSchema = z.object({ +export const CheckoutResponseV0Schema = z.object({ url: z.string().nullish(), customer_id: z.string(), - lines: z.array(CheckoutLineSchema), + lines: z.array(CheckoutLineV0Schema), product: ApiProductSchema.nullish(), current_product: ApiProductSchema.nullish(), @@ -30,4 +30,5 @@ export const CheckoutResponseSchema = z.object({ .nullish(), }); -export type CheckoutLine = z.infer; +export type CheckoutLineV0 = z.infer; +export type CheckoutResponseV0 = z.infer; diff --git a/shared/api/core/checkoutModels.ts b/shared/api/core/checkoutModels.ts index 9ddda59f9..5c0742e0a 100644 --- a/shared/api/core/checkoutModels.ts +++ b/shared/api/core/checkoutModels.ts @@ -1,45 +1,45 @@ -import { ApiProductItemSchema } from "@api/products/planFeature/previousVersions/apiProductItem.js"; -import { ApiProductSchema } from "@api/products/previousVersions/apiProduct.js"; -import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js"; -import { z } from "zod/v4"; -import { AttachBodySchema, ExtAttachBodySchema } from "./attachModels.js"; +// import { ApiProductItemSchema } from "@api/products/planFeature/previousVersions/apiProductItem.js"; +// import { ApiProductSchema } from "@api/products/previousVersions/apiProduct.js"; +// import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js"; +// import { z } from "zod/v4"; +// import { AttachBodySchema, ExtAttachBodySchema } from "./attachModels.js"; -export const ExtCheckoutParamsSchema = ExtAttachBodySchema.extend({ - setup_payment: z.boolean().optional(), -}).meta({ - description: - "Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.", -}); +// export const ExtCheckoutParamsSchema = ExtAttachBodySchema.extend({ +// setup_payment: z.boolean().optional(), +// }).meta({ +// description: +// "Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.", +// }); -export const CheckoutParamsSchema = AttachBodySchema.extend({ - // skip_checkout: z.boolean().optional(), - setup_payment: z.boolean().optional(), -}); +// export const CheckoutParamsSchema = AttachBodySchema.extend({ +// // skip_checkout: z.boolean().optional(), +// setup_payment: z.boolean().optional(), +// }); -// RESULT -export const CheckoutLineSchema = z.object({ - description: z.string(), - amount: z.number(), - item: ApiProductItemSchema.nullish(), -}); +// // RESULT +// export const CheckoutLineSchema = z.object({ +// description: z.string(), +// amount: z.number(), +// item: ApiProductItemSchema.nullish(), +// }); -export const CheckoutResponseSchema = z.object({ - url: z.string().nullish(), - customer_id: z.string(), - lines: z.array(CheckoutLineSchema), - product: ApiProductSchema.nullish(), - current_product: ApiProductSchema.nullish(), - options: z.array(FeatureOptionsSchema).nullish(), - total: z.number().nullish(), - currency: z.string().nullish(), - has_prorations: z.boolean().nullish(), - next_cycle: z - .object({ - starts_at: z.number(), - total: z.number(), - }) - .nullish(), -}); +// export const CheckoutResponseSchema = z.object({ +// url: z.string().nullish(), +// customer_id: z.string(), +// lines: z.array(CheckoutLineSchema), +// product: ApiProductSchema.nullish(), +// current_product: ApiProductSchema.nullish(), +// options: z.array(FeatureOptionsSchema).nullish(), +// total: z.number().nullish(), +// currency: z.string().nullish(), +// has_prorations: z.boolean().nullish(), +// next_cycle: z +// .object({ +// starts_at: z.number(), +// total: z.number(), +// }) +// .nullish(), +// }); -export type CheckoutLine = z.infer; -export type CheckoutParams = z.infer; +// export type CheckoutLine = z.infer; +// export type CheckoutParams = z.infer; diff --git a/shared/api/customers/cusPlans/apiSubscription.ts b/shared/api/customers/cusPlans/apiSubscription.ts index 9170b7e7c..03d4cc8ad 100644 --- a/shared/api/customers/cusPlans/apiSubscription.ts +++ b/shared/api/customers/cusPlans/apiSubscription.ts @@ -19,6 +19,14 @@ export const ApiSubscriptionSchema = z.object({ current_period_start: z.number().nullable(), current_period_end: z.number().nullable(), quantity: z.number(), + + // feature_quantities: z.array( + // z.object({ + // feature_id: z.string(), + // quantity: z.number(), + // upcoming_quantity: z.number().nullable(), + // }), + // ), }); export type ApiSubscription = z.infer; diff --git a/shared/api/models.ts b/shared/api/models.ts index c0fbadeb6..e454154d1 100644 --- a/shared/api/models.ts +++ b/shared/api/models.ts @@ -1,6 +1,6 @@ // Core export * from "./core/attachModels.js"; -export * from "./core/checkoutModels.js"; + // NOTE: coreOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation export * from "./core/coreOpModels.js"; @@ -70,6 +70,10 @@ export * from "./balances/track/trackParams.js"; export * from "./balances/track/trackResponseV2.js"; export * from "./balances/track/trackTypes/pgDeductionUpdate.js"; export * from "./balances/usageModels.js"; +export * from "./billing/checkout/prevVersions/checkoutParamsV0.js"; +export * from "./billing/checkout/prevVersions/checkoutParamsV0.js"; +export * from "./billing/checkout/prevVersions/checkoutResponseV0.js"; + export * from "./common/customerData.js"; export * from "./common/entityData.js"; export * from "./customers/cusFeatures/cusFeatureLegacyData.js"; diff --git a/shared/api/subscriptions/checkout/checkoutParams.ts b/shared/api/subscriptions/checkout/checkoutParams.ts deleted file mode 100644 index 54089549e..000000000 --- a/shared/api/subscriptions/checkout/checkoutParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from "zod/v4"; -import { - AttachBodySchema, - ExtAttachBodySchema, -} from "../../core/attachModels.js"; - -export const ExtCheckoutParamsSchema = ExtAttachBodySchema.extend({ - setup_payment: z.boolean().optional(), -}).meta({ - description: - "Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.", -}); - -export const CheckoutParamsSchema = AttachBodySchema.extend({ - // skip_checkout: z.boolean().optional(), - setup_payment: z.boolean().optional(), -}); - -export type CheckoutParams = z.infer; diff --git a/shared/api/subscriptions/checkout/checkoutResponse.ts b/shared/api/subscriptions/checkout/checkoutResponse.ts deleted file mode 100644 index d3a82fb9f..000000000 --- a/shared/api/subscriptions/checkout/checkoutResponse.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js"; -import { z } from "zod/v4"; -import { ApiPlanSchema } from "../../products/apiPlan.js"; - -// RESULT -export const CheckoutLineSchema = z.object({ - description: z.string(), - amount: z.number(), - // item: ApiProductItemSchema.nullish(), -}); - -export const CheckoutResponseSchema = z.object({ - url: z.string().nullish(), - customer_id: z.string(), - lines: z.array(CheckoutLineSchema), - - plan: ApiPlanSchema.nullish(), - current_plan: ApiPlanSchema.nullish(), - - options: z.array(FeatureOptionsSchema).nullish(), - total: z.number().nullish(), - currency: z.string().nullish(), - has_prorations: z.boolean().nullish(), - next_cycle: z - .object({ - starts_at: z.number(), - total: z.number(), - }) - .nullish(), -}); - -export type CheckoutLine = z.infer; diff --git a/shared/api/versionUtils/versionChangeUtils/VersionChange.ts b/shared/api/versionUtils/versionChangeUtils/VersionChange.ts index a75f2ac08..ae3ab6814 100644 --- a/shared/api/versionUtils/versionChangeUtils/VersionChange.ts +++ b/shared/api/versionUtils/versionChangeUtils/VersionChange.ts @@ -15,6 +15,7 @@ export enum AffectedResource { Feature = "feature", Check = "check", Track = "track", + Checkout = "checkout", // Add more as needed }