From 8e530888e38aaca3af5ffbd6df97b3d8b321d173 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:07:47 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20upgrade=20all=20Stripe?= =?UTF-8?q?=20types=20for=20v22=20compat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stripe/stripeSubUtils/getSubItemAmount.ts | 6 ++-- .../honoMiddlewares/errorSkipMiddleware.ts | 2 +- .../admin/handleGetMasterStripeAccount.ts | 4 +-- .../buildStripeCheckoutSessionAction.ts | 15 +++++---- .../buildCheckoutSessionParams.ts | 23 ++++++------- .../buildStripeCheckoutSessionItems.ts | 18 +++++------ .../updateOneOffTieredItems.ts | 3 +- .../stripeLineItemGroupToDbLineItems.ts | 10 ++++-- .../invoices/handleInvoicePaymentFailure.ts | 4 +-- .../stripe/utils/invoices/payStripeInvoice.ts | 2 +- .../utils/matchUtils/stripePriceShape.ts | 10 ++++-- .../stripeItemSpecToStripeParam.ts | 20 +++++++++--- .../storeInvoiceLineItems.ts | 2 +- .../add-product/handleCreateCheckout.ts | 9 +++--- .../stripeHandlers/handleGetStripeAccount.ts | 4 +-- server/src/internal/orgs/orgUtils.ts | 4 +-- .../orgs/orgUtils/handleStripeSecretKey.ts | 2 +- server/src/utils/otel/instrumentStripe.ts | 3 +- .../billing/attach/params/start-date/utils.ts | 2 +- .../helpers/compareItems.ts | 6 ++-- ...xpectStripeInvoiceLineItemPeriodCorrect.ts | 5 ++- .../match-stripe-inline-price.spec.ts | 4 +-- .../build-subscription-items-update.spec.ts | 4 +-- server/tsconfig.json | 4 ++- .../billingModels/stripe/stripeItemSpec.ts | 2 +- shared/tsconfig.json | 4 ++- .../priceUtils/convertAmountUtils.ts | 32 +++++++++++-------- .../priceToStripePrepaidV2Tiers.ts | 16 ++++++---- 28 files changed, 131 insertions(+), 89 deletions(-) diff --git a/server/src/external/stripe/stripeSubUtils/getSubItemAmount.ts b/server/src/external/stripe/stripeSubUtils/getSubItemAmount.ts index f96e43f09..97ee7481a 100644 --- a/server/src/external/stripe/stripeSubUtils/getSubItemAmount.ts +++ b/server/src/external/stripe/stripeSubUtils/getSubItemAmount.ts @@ -13,7 +13,7 @@ const calculateTieredAmount = ({ let quantityCursor = quantity; for (const tier of tiers) { const unitAmount = new Decimal( - tier.unit_amount_decimal || tier.unit_amount!, + tier.unit_amount_decimal?.toNumber() || tier.unit_amount!, ); if (notNullish(tier.up_to)) { @@ -54,7 +54,9 @@ export const getSubItemAmount = ({ if (price.billing_scheme === "per_unit") { if (price.unit_amount_decimal) { - return new Decimal(price.unit_amount_decimal).mul(quantity).toNumber(); + return new Decimal(price.unit_amount_decimal.toNumber()) + .mul(quantity) + .toNumber(); } else { return new Decimal(price.unit_amount || 0).mul(quantity).toNumber(); } diff --git a/server/src/honoMiddlewares/errorSkipMiddleware.ts b/server/src/honoMiddlewares/errorSkipMiddleware.ts index ae76f76a1..44bcfe68d 100644 --- a/server/src/honoMiddlewares/errorSkipMiddleware.ts +++ b/server/src/honoMiddlewares/errorSkipMiddleware.ts @@ -258,7 +258,7 @@ export const handleErrorSkip = (err: Error, c: Context) => { // 4. Check Stripe-specific rules for (const rule of STRIPE_RULES) { if (rule.match(err, c)) { - const stripeErr = err as Stripe.errors.StripeError; + const stripeErr = err as Stripe.ErrorType.StripeError; logger.warn(`${rule.name}, org: ${ctx.org?.slug || "unknown"}`); return createErrorResponse({ c, diff --git a/server/src/internal/admin/handleGetMasterStripeAccount.ts b/server/src/internal/admin/handleGetMasterStripeAccount.ts index 51279fe77..c412b0c06 100644 --- a/server/src/internal/admin/handleGetMasterStripeAccount.ts +++ b/server/src/internal/admin/handleGetMasterStripeAccount.ts @@ -1,5 +1,5 @@ -import { initMasterStripe } from "@/external/connect/initStripeCli"; import { Scopes } from "@autumn/shared"; +import { initMasterStripe } from "@/external/connect/initStripeCli"; import { createRoute } from "../../honoMiddlewares/routeHandler"; export const handleGetMasterStripeAccount = createRoute({ @@ -10,7 +10,7 @@ export const handleGetMasterStripeAccount = createRoute({ try { const masterStripe = initMasterStripe({ env }); - const account = await masterStripe.accounts.retrieve(); + const account = await masterStripe.accounts.retrieve(null); return c.json({ id: account.id, diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts index 40736eee8..4e2776bfb 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts @@ -10,6 +10,7 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { buildStripeCheckoutSessionItems } from "@/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildStripeCheckoutSessionItems"; import { buildAutumnSubscriptionMetadata } from "@/internal/billing/v2/providers/stripe/utils/common/autumnStripeMetadata"; import { stripeDiscountsToCheckoutParams } from "@/internal/billing/v2/providers/stripe/utils/discounts/stripeDiscountsToParams"; +import type { Checkout as CheckoutSessions } from "stripe/resources/Checkout/Sessions.js"; export const buildStripeCheckoutSessionAction = ({ ctx, @@ -45,11 +46,11 @@ export const buildStripeCheckoutSessionAction = ({ // Payment-mode checkout has no top-level default_tax_rates, so one-off items take per-line tax_rates. const taxRateId = billingContext.taxRateId; const applyTaxRateToLineItem = ( - item: Stripe.Checkout.SessionCreateParams.LineItem, - ): Stripe.Checkout.SessionCreateParams.LineItem => + item: CheckoutSessions.SessionCreateParams.LineItem, + ): CheckoutSessions.SessionCreateParams.LineItem => taxRateId ? { ...item, tax_rates: [taxRateId] } : item; - const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = [ + const lineItems: CheckoutSessions.SessionCreateParams.LineItem[] = [ ...recurringLineItems.filter((item) => item.quantity !== 0), ...oneOffLineItems .filter((item) => item.quantity !== 0) @@ -64,7 +65,7 @@ export const buildStripeCheckoutSessionAction = ({ // 5. Build subscription_data (only for subscription mode) const subscriptionData: - | Stripe.Checkout.SessionCreateParams.SubscriptionData + | CheckoutSessions.SessionCreateParams.SubscriptionData | undefined = mode === "subscription" ? { @@ -88,7 +89,7 @@ export const buildStripeCheckoutSessionAction = ({ // 7. Build params. Tax policy is baked in here (not at execute time) so // the action object is self-describing in logs/EXTRA_LOGS. - const autumnAutoTax: Partial = org.config + const autumnAutoTax: Partial = org.config .automatic_tax ? { automatic_tax: { enabled: true }, @@ -98,7 +99,7 @@ export const buildStripeCheckoutSessionAction = ({ } : {}; - const params: Stripe.Checkout.SessionCreateParams = { + const params: CheckoutSessions.SessionCreateParams = { customer: stripeCustomer?.id ?? "none", mode, line_items: lineItems, @@ -112,6 +113,6 @@ export const buildStripeCheckoutSessionAction = ({ type: "create", params, checkoutSessionParams: - checkoutSessionParams as Partial, + checkoutSessionParams as Partial, }; }; diff --git a/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildCheckoutSessionParams.ts b/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildCheckoutSessionParams.ts index 2bb185e97..27dabf87d 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildCheckoutSessionParams.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildCheckoutSessionParams.ts @@ -1,6 +1,7 @@ import type Stripe from "stripe"; import { mergeStripeMetadata } from "@/internal/billing/v2/providers/stripe/utils/common/mergeStripeMetadata"; import { buildCheckoutSessionMetadata } from "./buildCheckoutSessionMetadata"; +import type { Checkout as CheckoutSessions } from "stripe/resources/Checkout/Sessions.js"; /** * Deep-merges subscription_data so user-provided fields (e.g. metadata) @@ -12,9 +13,9 @@ const mergeSubscriptionData = ({ userSubscriptionData, }: { userMetadata?: Record; - paramsSubscriptionData?: Stripe.Checkout.SessionCreateParams.SubscriptionData; - userSubscriptionData?: Stripe.Checkout.SessionCreateParams.SubscriptionData; -}): Stripe.Checkout.SessionCreateParams.SubscriptionData | undefined => { + paramsSubscriptionData?: CheckoutSessions.SessionCreateParams.SubscriptionData; + userSubscriptionData?: CheckoutSessions.SessionCreateParams.SubscriptionData; +}): CheckoutSessions.SessionCreateParams.SubscriptionData | undefined => { if (!paramsSubscriptionData && !userSubscriptionData && !userMetadata) { return undefined; } @@ -41,16 +42,16 @@ export const buildCheckoutSessionParams = ({ autumnMetadataId, userMetadata, }: { - params: Stripe.Checkout.SessionCreateParams; - checkoutSessionParams?: Partial; + params: CheckoutSessions.SessionCreateParams; + checkoutSessionParams?: Partial; currency?: string; defaultAllowPromotionCodes?: boolean; - defaultInvoiceCreation?: Stripe.Checkout.SessionCreateParams.InvoiceCreation; - defaultSavedPaymentMethodOptions?: Stripe.Checkout.SessionCreateParams.SavedPaymentMethodOptions; + defaultInvoiceCreation?: CheckoutSessions.SessionCreateParams.InvoiceCreation; + defaultSavedPaymentMethodOptions?: CheckoutSessions.SessionCreateParams.SavedPaymentMethodOptions; autumnMetadataId?: string; userMetadata?: Record; -}): Stripe.Checkout.SessionCreateParams => { - const mergedParams: Stripe.Checkout.SessionCreateParams = { +}): CheckoutSessions.SessionCreateParams => { + const mergedParams: CheckoutSessions.SessionCreateParams = { ...(checkoutSessionParams ?? {}), ...params, }; @@ -83,11 +84,11 @@ export const buildCheckoutSessionParams = ({ : mergeSubscriptionData({ userMetadata, paramsSubscriptionData: params.subscription_data as - | Stripe.Checkout.SessionCreateParams.SubscriptionData + | CheckoutSessions.SessionCreateParams.SubscriptionData | undefined, userSubscriptionData: checkoutSessionParams?.subscription_data as - | Stripe.Checkout.SessionCreateParams.SubscriptionData + | CheckoutSessions.SessionCreateParams.SubscriptionData | undefined, }), }; diff --git a/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildStripeCheckoutSessionItems.ts b/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildStripeCheckoutSessionItems.ts index 47be1c21c..69ebe0bfe 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildStripeCheckoutSessionItems.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildStripeCheckoutSessionItems.ts @@ -6,7 +6,7 @@ import { priceUtils, type StripeItemSpec, } from "@autumn/shared"; -import type Stripe from "stripe"; +import type { Checkout as CheckoutSessions } from "stripe/resources/Checkout/Sessions.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { customerProductsToOneOffStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/stripeItemSpec/customerProductsToOneOffStripeItemSpecs"; import { customerProductsToRecurringStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/stripeItemSpec/customerProductsToRecurringStripeItemSpecs"; @@ -17,13 +17,13 @@ import { updateOneOffTieredItems } from "./updateOneOffTieredItems"; const isZeroAmountInlineLineItem = ({ lineItem, }: { - lineItem: Stripe.Checkout.SessionCreateParams.LineItem; + lineItem: CheckoutSessions.SessionCreateParams.LineItem; }) => { if (!("price_data" in lineItem) || !lineItem.price_data) return false; return ( lineItem.price_data.unit_amount === 0 || - lineItem.price_data.unit_amount_decimal === "0" + lineItem.price_data.unit_amount_decimal?.toNumber() === 0 ); }; @@ -34,7 +34,7 @@ const isZeroAmountInlineRecurringStripeItemSpec = ({ }) => { if (!stripeItemSpec.stripeInlinePrice?.recurring) return false; - return stripeItemSpec.stripeInlinePrice.unit_amount_decimal === "0"; + return stripeItemSpec.stripeInlinePrice.unit_amount_decimal.toNumber() === 0; }; const getRecurringCadenceKey = ({ @@ -94,10 +94,10 @@ const applyAdjustableQuantityToPrepaidLineItem = ({ spec, billingContext, }: { - lineItem: Stripe.Checkout.SessionCreateParams.LineItem; + lineItem: CheckoutSessions.SessionCreateParams.LineItem; spec: StripeItemSpec; billingContext: BillingContext; -}): Stripe.Checkout.SessionCreateParams.LineItem => { +}): CheckoutSessions.SessionCreateParams.LineItem => { const { autumnPrice, autumnEntitlement, autumnProduct } = spec; if (!autumnPrice || !autumnEntitlement || !isPrepaidPrice(autumnPrice)) { @@ -132,7 +132,7 @@ const applyAdjustableQuantityToPrepaidLineItem = ({ }), maximum: 999999, }, - } as Stripe.Checkout.SessionCreateParams.LineItem; + } as CheckoutSessions.SessionCreateParams.LineItem; }; export const buildStripeCheckoutSessionItems = ({ @@ -144,8 +144,8 @@ export const buildStripeCheckoutSessionItems = ({ billingContext: BillingContext; newCustomerProducts: FullCusProduct[]; }): { - recurringLineItems: Stripe.Checkout.SessionCreateParams.LineItem[]; - oneOffLineItems: Stripe.Checkout.SessionCreateParams.LineItem[]; + recurringLineItems: CheckoutSessions.SessionCreateParams.LineItem[]; + oneOffLineItems: CheckoutSessions.SessionCreateParams.LineItem[]; } => { // 1. Filter customer products by active statuses const activeCustomerProducts = filterCustomerProductsByActiveStatuses({ diff --git a/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/updateOneOffTieredItems.ts b/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/updateOneOffTieredItems.ts index 2e5c0e2be..364f33511 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/updateOneOffTieredItems.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/checkoutSessions/updateOneOffTieredItems.ts @@ -10,6 +10,7 @@ import { } from "@autumn/shared"; import type Stripe from "stripe"; import { stripeItemSpecToCheckoutLineItem } from "../stripeItemSpec/stripeItemSpecToStripeParam"; +import type { Checkout as CheckoutSessions } from "stripe/resources/Checkout/Sessions.js"; /** * Update one-off items to use inline price_data if they're tiered. @@ -22,7 +23,7 @@ export const updateOneOffTieredItems = ({ }: { oneOffItemSpecs: StripeItemSpec[]; org: Organization; -}): Stripe.Checkout.SessionCreateParams.LineItem[] => { +}): CheckoutSessions.SessionCreateParams.LineItem[] => { const currency = orgToCurrency({ org }); return oneOffItemSpecs.map((item) => { diff --git a/server/src/internal/billing/v2/providers/stripe/utils/invoiceLines/convertToDbLineItem/stripeLineItemGroupToDbLineItems.ts b/server/src/internal/billing/v2/providers/stripe/utils/invoiceLines/convertToDbLineItem/stripeLineItemGroupToDbLineItems.ts index 2921e340d..c9dd5eb96 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/invoiceLines/convertToDbLineItem/stripeLineItemGroupToDbLineItems.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/invoiceLines/convertToDbLineItem/stripeLineItemGroupToDbLineItems.ts @@ -252,7 +252,10 @@ const mergeStripeAndBillingLineItems = ({ stripeLineItem.parent?.invoice_item_details?.invoice_item ?? null, stripe_subscription_item_id: stripeSubscriptionItemId, stripe_product_id: (priceDetails?.product as string) ?? null, - stripe_price_id: priceDetails?.price ?? null, + stripe_price_id: + typeof priceDetails?.price === "string" + ? priceDetails?.price + : (priceDetails?.price?.id ?? null), stripe_discountable: stripeLineItem.discountable, // Amounts (from Stripe or Autumn depending on discountable flag) @@ -336,7 +339,10 @@ const createDbLineItemFromStripeOnly = ({ stripeLineItem.parent?.invoice_item_details?.invoice_item ?? null, stripe_subscription_item_id: stripeSubscriptionItemId, stripe_product_id: (priceDetails?.product as string) ?? null, - stripe_price_id: priceDetails?.price ?? null, + stripe_price_id: + typeof priceDetails?.price === "string" + ? priceDetails?.price + : (priceDetails?.price?.id ?? null), stripe_discountable: stripeLineItem.discountable, amount, diff --git a/server/src/internal/billing/v2/providers/stripe/utils/invoices/handleInvoicePaymentFailure.ts b/server/src/internal/billing/v2/providers/stripe/utils/invoices/handleInvoicePaymentFailure.ts index 3589b6b42..a53932efc 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/invoices/handleInvoicePaymentFailure.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/invoices/handleInvoicePaymentFailure.ts @@ -12,7 +12,7 @@ import type { PayInvoiceResult } from "./payStripeInvoice"; const getFailureCodeFromStripeError = ({ stripeError, }: { - stripeError: Stripe.errors.StripeError; + stripeError: Stripe.ErrorType.StripeError; }): "3ds_required" | "payment_failed" => { const authCodes = ["authentication_required", "authentication_not_handled"]; @@ -61,7 +61,7 @@ export const handleInvoicePaymentFailure = ({ } // 2. Check if it's a Stripe error - const stripeError = error as Stripe.errors.StripeError; + const stripeError = error as Stripe.ErrorType.StripeError; const isStripeError = stripeError.type !== undefined; if (!isStripeError) throw error; diff --git a/server/src/internal/billing/v2/providers/stripe/utils/invoices/payStripeInvoice.ts b/server/src/internal/billing/v2/providers/stripe/utils/invoices/payStripeInvoice.ts index 7f55b3e74..d39a24721 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/invoices/payStripeInvoice.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/invoices/payStripeInvoice.ts @@ -13,7 +13,7 @@ export type PayInvoiceResult = { code: PaymentFailureCode; reason: string; }; - stripeError?: Stripe.errors.StripeError; + stripeError?: Stripe.ErrorType.StripeError; }; type PayStripeInvoiceParams = { diff --git a/server/src/internal/billing/v2/providers/stripe/utils/matchUtils/stripePriceShape.ts b/server/src/internal/billing/v2/providers/stripe/utils/matchUtils/stripePriceShape.ts index 5dd503003..3700eb6ab 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/matchUtils/stripePriceShape.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/matchUtils/stripePriceShape.ts @@ -13,7 +13,11 @@ type InlinePriceLike = { divide_by?: number; round?: string; } | null; - unit_amount_decimal?: string | number | null; + unit_amount_decimal?: + | string + | number + | ReturnType + | null; }; export type StripePriceShape = { @@ -65,7 +69,7 @@ export const stripePriceToShape = ({ intervalCount: price.recurring?.interval_count, tiersMode: price.tiers_mode ?? undefined, transformQuantity: transformQuantityKey(price.transform_quantity), - unitAmountDecimal: decimalAmount(price.unit_amount_decimal), + unitAmountDecimal: decimalAmount(price.unit_amount_decimal?.toNumber()), }); export const inlinePriceToShape = ({ @@ -80,7 +84,7 @@ export const inlinePriceToShape = ({ interval: price.recurring?.interval, intervalCount: price.recurring?.interval_count, transformQuantity: transformQuantityKey(price.transform_quantity), - unitAmountDecimal: decimalAmount(price.unit_amount_decimal), + unitAmountDecimal: decimalAmount(price.unit_amount_decimal?.toString()), }); export const stripePriceShapesEqual = ( diff --git a/server/src/internal/billing/v2/providers/stripe/utils/stripeItemSpec/stripeItemSpecToStripeParam.ts b/server/src/internal/billing/v2/providers/stripe/utils/stripeItemSpec/stripeItemSpecToStripeParam.ts index 21aee37f3..3b5ad8bd4 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/stripeItemSpec/stripeItemSpecToStripeParam.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/stripeItemSpec/stripeItemSpecToStripeParam.ts @@ -3,13 +3,21 @@ import { type StripeInlinePrice, type StripeItemSpec, } from "@autumn/shared"; -import type Stripe from "stripe"; +import Stripe from "stripe"; +import type { Checkout as CheckoutSessions } from "stripe/resources/Checkout/Sessions.js"; type StoredPriceParam = { price: string }; type RecurringInlinePriceParam = { price_data: Stripe.SubscriptionCreateParams.Item["price_data"]; }; +const toStripeInlinePriceData = (stripeInlinePrice: StripeInlinePrice) => ({ + ...stripeInlinePrice, + unit_amount_decimal: Stripe.Decimal.from( + stripeInlinePrice.unit_amount_decimal.toString(), + ), +}); + /** * Returns the price param for a StripeItemSpec — either a stored price ID or inline price_data. * For inline prices, asserts that `recurring` is present (one-off items should not reach this path). @@ -29,7 +37,7 @@ const toRecurringPriceParam = ({ } return { price_data: { - ...spec.stripeInlinePrice, + ...toStripeInlinePriceData(spec.stripeInlinePrice), recurring: spec.stripeInlinePrice.recurring, }, }; @@ -56,9 +64,11 @@ const toPriceParam = ({ spec, }: { spec: StripeItemSpec; -}): StoredPriceParam | { price_data: StripeInlinePrice } => { +}): + | StoredPriceParam + | { price_data: ReturnType } => { if (spec.stripeInlinePrice) { - return { price_data: spec.stripeInlinePrice }; + return { price_data: toStripeInlinePriceData(spec.stripeInlinePrice) }; } return { price: spec.stripePriceId! }; }; @@ -68,7 +78,7 @@ export const stripeItemSpecToCheckoutLineItem = ({ spec, }: { spec: StripeItemSpec; -}): Stripe.Checkout.SessionCreateParams.LineItem => { +}): CheckoutSessions.SessionCreateParams.LineItem => { return { ...toPriceParam({ spec }), quantity: spec.quantity, diff --git a/server/src/internal/billing/v2/workflows/storeInvoiceLineItems/storeInvoiceLineItems.ts b/server/src/internal/billing/v2/workflows/storeInvoiceLineItems/storeInvoiceLineItems.ts index 143a67445..a6d17fdbd 100644 --- a/server/src/internal/billing/v2/workflows/storeInvoiceLineItems/storeInvoiceLineItems.ts +++ b/server/src/internal/billing/v2/workflows/storeInvoiceLineItems/storeInvoiceLineItems.ts @@ -77,7 +77,7 @@ export const storeInvoiceLineItems = async ({ if (info?.isMetered) return false; // Filter $0 empty price placeholders (e.g. stripe_empty_price_id) - if (li.pricing?.unit_amount_decimal === "0") return false; + if (li.pricing?.unit_amount_decimal?.toNumber() === 0) return false; return true; }); diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index 7894a18ca..90dc6dede 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -18,6 +18,7 @@ import { notNullish } from "@/utils/genUtils.js"; import type { AutumnContext } from "../../../honoUtils/HonoEnv.js"; import { attachParamsToMetadata } from "../../billing/attach/utils/attachParamsToMetadata.js"; import type { AttachParams } from "../cusProducts/AttachParams.js"; +import type { Checkout as CheckoutSessions } from "stripe/resources/Checkout/Sessions.js"; export const handleCreateCheckout = async ({ ctx, @@ -85,10 +86,10 @@ export const handleCreateCheckout = async ({ } const checkoutParams = attachParams.checkoutSessionParams as - | Partial + | Partial | undefined; const checkoutSubscriptionData = checkoutParams?.subscription_data as - | Stripe.Checkout.SessionCreateParams.SubscriptionData + | CheckoutSessions.SessionCreateParams.SubscriptionData | undefined; const trialEnd = freeTrial && !attachParams.disableFreeTrial @@ -104,7 +105,7 @@ export const handleCreateCheckout = async ({ : undefined; const subscriptionData: - | Stripe.Checkout.SessionCreateParams.SubscriptionData + | CheckoutSessions.SessionCreateParams.SubscriptionData | undefined = isRecurring ? { ...(checkoutSubscriptionData ?? {}), @@ -139,7 +140,7 @@ export const handleCreateCheckout = async ({ notNullish(checkoutParams?.payment_method_types) || notNullish(checkoutParams?.payment_method_configuration); - let sessionParams: Stripe.Checkout.SessionCreateParams = { + let sessionParams: CheckoutSessions.SessionCreateParams = { customer: customer.processor.id, line_items: items, mode: isRecurring ? "subscription" : "payment", diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleGetStripeAccount.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleGetStripeAccount.ts index de8f8ec02..98a033433 100644 --- a/server/src/internal/orgs/handlers/stripeHandlers/handleGetStripeAccount.ts +++ b/server/src/internal/orgs/handlers/stripeHandlers/handleGetStripeAccount.ts @@ -1,5 +1,5 @@ -import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { Scopes } from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; import { isStripeConnected } from "../../orgUtils.js"; @@ -15,7 +15,7 @@ export const handleGetStripeAccount = createRoute({ try { const stripeCli = createStripeCli({ org, env }); - const accountDetails = await stripeCli.accounts.retrieve(); + const accountDetails = await stripeCli.accounts.retrieve(null); return c.json(accountDetails); } catch (error) { logger.warn( diff --git a/server/src/internal/orgs/orgUtils.ts b/server/src/internal/orgs/orgUtils.ts index dde3156fe..743c74286 100644 --- a/server/src/internal/orgs/orgUtils.ts +++ b/server/src/internal/orgs/orgUtils.ts @@ -46,8 +46,8 @@ export const shouldReconnectStripe = async ({ const stripeCli = createStripeCli({ org, env }); const newKey = new Stripe(stripeKey); - const oldAccount = await stripeCli.accounts.retrieve(); - const newAccount = await newKey.accounts.retrieve(); + const oldAccount = await stripeCli.accounts.retrieve(null); + const newAccount = await newKey.accounts.retrieve(null); return oldAccount.id !== newAccount.id; } catch (error) { diff --git a/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts b/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts index 522b8e3db..a42244a39 100644 --- a/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts +++ b/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts @@ -19,7 +19,7 @@ export const handleStripeSecretKey = async ({ // 1. Check if key is valid await checkKeyValid(secretKey); const stripe = new Stripe(secretKey); - const account = await stripe.accounts.retrieve(); + const account = await stripe.accounts.retrieve(null); // 2. Disconnect existing webhook endpoints const curWebhooks = await stripe.webhookEndpoints.list(); diff --git a/server/src/utils/otel/instrumentStripe.ts b/server/src/utils/otel/instrumentStripe.ts index a7fab2e21..96ba3d32e 100644 --- a/server/src/utils/otel/instrumentStripe.ts +++ b/server/src/utils/otel/instrumentStripe.ts @@ -6,13 +6,14 @@ import { trace, } from "@opentelemetry/api"; import type Stripe from "stripe"; +import type { StripeConfig } from "stripe/lib.js"; import { otelConfig } from "./otelConfig.js"; const TRACER_NAME = "autumn.stripe"; const SPAN_NAME = "stripe.api"; const INSTRUMENTED = new WeakSet(); -type StripeHttpClient = NonNullable; +type StripeHttpClient = NonNullable; type StripeApiLike = { httpClient?: StripeHttpClient; diff --git a/server/tests/integration/billing/attach/params/start-date/utils.ts b/server/tests/integration/billing/attach/params/start-date/utils.ts index 116584bed..223d261b5 100644 --- a/server/tests/integration/billing/attach/params/start-date/utils.ts +++ b/server/tests/integration/billing/attach/params/start-date/utils.ts @@ -84,7 +84,7 @@ export const triggerSubscriptionCreated = async ({ created: getUnixTime(subscriptionCreatedAtMs ?? Date.now()), schedule: scheduleId ?? null, } as Stripe.Subscription; - const retrieveSubscription: Stripe.SubscriptionsResource["retrieve"] = + const retrieveSubscription: Stripe.SubscriptionResource["retrieve"] = async () => stripeResponse({ object: subscription, requestId: `req_${stripeSubId}` }); diff --git a/server/tests/integration/billing/utils/expectStripeSubCorrect/helpers/compareItems.ts b/server/tests/integration/billing/utils/expectStripeSubCorrect/helpers/compareItems.ts index 70467bd79..607573e46 100644 --- a/server/tests/integration/billing/utils/expectStripeSubCorrect/helpers/compareItems.ts +++ b/server/tests/integration/billing/utils/expectStripeSubCorrect/helpers/compareItems.ts @@ -16,7 +16,7 @@ export const normalizeActualSubItem = ({ quantity: item.quantity ?? 0, isInline: !!autumnCusPriceId, unitAmountDecimal: autumnCusPriceId - ? (item.price.unit_amount_decimal ?? undefined) + ? (item.price.unit_amount_decimal?.toString() ?? undefined) : undefined, }; }; @@ -42,7 +42,7 @@ export const normalizeActualPhaseItem = ({ autumnCustomerPriceId: autumnCusPriceId || undefined, quantity: item.quantity ?? 0, isInline: !!autumnCusPriceId, - unitAmountDecimal, + unitAmountDecimal: unitAmountDecimal?.toString(), }; }; @@ -58,7 +58,7 @@ export const normalizeExpectedPhaseItem = ({ let unitAmountDecimal: string | undefined; if (hasInlinePrice) { const priceData = (item as { price_data: StripeInlinePrice }).price_data; - unitAmountDecimal = priceData.unit_amount_decimal; + unitAmountDecimal = priceData.unit_amount_decimal.toString(); } return { diff --git a/server/tests/integration/billing/utils/stripe/expectStripeInvoiceLineItemPeriodCorrect.ts b/server/tests/integration/billing/utils/stripe/expectStripeInvoiceLineItemPeriodCorrect.ts index e0aad6006..083582d4c 100644 --- a/server/tests/integration/billing/utils/stripe/expectStripeInvoiceLineItemPeriodCorrect.ts +++ b/server/tests/integration/billing/utils/stripe/expectStripeInvoiceLineItemPeriodCorrect.ts @@ -58,7 +58,10 @@ export const expectStripeInvoiceLineItemPeriodCorrect = async ({ Boolean( findPriceFromStripeId({ prices: usagePrices, - stripePriceId: line.pricing?.price_details?.price ?? "", + stripePriceId: + typeof line.pricing?.price_details?.price === "string" + ? line.pricing?.price_details?.price + : (line.pricing?.price_details?.price?.id ?? ""), }), ), ), diff --git a/server/tests/unit/billing/stripe/match-utils/match-stripe-inline-price.spec.ts b/server/tests/unit/billing/stripe/match-utils/match-stripe-inline-price.spec.ts index e0add57d0..5bf3dc22e 100644 --- a/server/tests/unit/billing/stripe/match-utils/match-stripe-inline-price.spec.ts +++ b/server/tests/unit/billing/stripe/match-utils/match-stripe-inline-price.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import type Stripe from "stripe"; +import Stripe from "stripe"; import { findMatchingInlinePriceIdForPhaseItem, findMatchingInlineSubscriptionItem, @@ -10,7 +10,7 @@ const inlinePrice = { product: "stripe_prod_inline", currency: "usd", recurring: { interval: "month" as const, interval_count: 1 }, - unit_amount_decimal: "1000", + unit_amount_decimal: Stripe.Decimal.from("1000"), }; const subscriptionItem = ({ diff --git a/server/tests/unit/billing/stripe/subscriptions/build-subscription-items-update.spec.ts b/server/tests/unit/billing/stripe/subscriptions/build-subscription-items-update.spec.ts index 4f1fbaea9..fde2dd9cc 100644 --- a/server/tests/unit/billing/stripe/subscriptions/build-subscription-items-update.spec.ts +++ b/server/tests/unit/billing/stripe/subscriptions/build-subscription-items-update.spec.ts @@ -15,7 +15,7 @@ import { customerProducts } from "@tests/utils/fixtures/db/customerProducts"; import { prices } from "@tests/utils/fixtures/db/prices"; import { stripeSubscriptions } from "@tests/utils/fixtures/stripe/subscriptions"; import chalk from "chalk"; -import type Stripe from "stripe"; +import Stripe from "stripe"; import { buildStripeSubscriptionItemsUpdate } from "@/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate"; import { createCustomerPricesForProduct, @@ -968,7 +968,7 @@ describe( const changedItem = currentItems.find( (item) => item.metadata.inline_price === "true", )!; - changedItem.price.unit_amount_decimal = "999999"; + changedItem.price.unit_amount_decimal = Stripe.Decimal.from("999999"); const result = buildUpdateWithItems({ currentItems }); diff --git a/server/tsconfig.json b/server/tsconfig.json index bc7248cc8..b23925e79 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -40,7 +40,9 @@ // Explicitly set drizzle-orm path to avoid monorepo issues "drizzle-orm": ["../node_modules/drizzle-orm"], - "drizzle-orm/*": ["../node_modules/drizzle-orm/*"] + "drizzle-orm/*": ["../node_modules/drizzle-orm/*"], + "stripe": ["../node_modules/.bun/node_modules/stripe/esm/stripe.esm.node"], + "stripe/*": ["../node_modules/.bun/node_modules/stripe/esm/*"] } }, "include": ["src", "tests", "scripts", "experiments", "perf"], diff --git a/shared/models/billingModels/stripe/stripeItemSpec.ts b/shared/models/billingModels/stripe/stripeItemSpec.ts index cadb93fd2..5650f04d5 100644 --- a/shared/models/billingModels/stripe/stripeItemSpec.ts +++ b/shared/models/billingModels/stripe/stripeItemSpec.ts @@ -13,7 +13,7 @@ export type StripeInlinePrice = { product: string; currency: string; recurring?: Stripe.PriceCreateParams.Recurring; - unit_amount_decimal: string; + unit_amount_decimal: ReturnType; }; /** diff --git a/shared/tsconfig.json b/shared/tsconfig.json index b7dfbec6d..1ca10762d 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -15,7 +15,9 @@ "@api/*": ["./api/*"], "@models/*": ["./models/*"], "@utils/*": ["./utils/*"], - "@autumn/ksuid": ["../packages/ksuid/src/index.ts"] + "@autumn/ksuid": ["../packages/ksuid/src/index.ts"], + "stripe": ["../node_modules/.bun/node_modules/stripe/esm/stripe.esm.node"], + "stripe/*": ["../node_modules/.bun/node_modules/stripe/esm/*"] } }, "include": ["./**/*"], diff --git a/shared/utils/productUtils/priceUtils/convertAmountUtils.ts b/shared/utils/productUtils/priceUtils/convertAmountUtils.ts index ad67a7808..dcfece6a6 100644 --- a/shared/utils/productUtils/priceUtils/convertAmountUtils.ts +++ b/shared/utils/productUtils/priceUtils/convertAmountUtils.ts @@ -1,5 +1,7 @@ -import { Decimal } from "decimal.js"; +import { Decimal as DecimalJS } from "decimal.js"; +import Stripe from "stripe"; +type StripeDecimal = ReturnType; /** * Zero-decimal currencies that Stripe handles without decimal places. * These currencies don't require multiplying/dividing by 100. @@ -38,11 +40,11 @@ export const atmnToStripeAmount = ({ if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) { return amount; } - return new Decimal(amount).mul(100).round().toNumber(); + return new DecimalJS(amount).mul(100).round().toNumber(); }; /** - * Converts an Autumn amount to a Stripe decimal string. + * Converts an Autumn amount to a Stripe Decimal class. * For most currencies, multiplies by 100 and returns as string with decimal places. * For zero-decimal currencies like JPY, returns the amount as-is with decimal places. * Used for Stripe API calls that require unit_amount_decimal as a string. @@ -52,16 +54,20 @@ export const atmnToStripeAmountDecimal = ({ currency = "USD", decimalPlaces = 10, }: { - amount: number | Decimal; + amount: number | DecimalJS; currency?: string; decimalPlaces?: number; -}): string => { - const decimal = amount instanceof Decimal ? amount : new Decimal(amount); +}): StripeDecimal => { + const decimal = amount instanceof DecimalJS ? amount : new DecimalJS(amount); if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) { - return decimal.toDecimalPlaces(decimalPlaces).toString(); + return Stripe.Decimal.from( + decimal.toDecimalPlaces(decimalPlaces).toString(), + ); } - return decimal.mul(100).toDecimalPlaces(decimalPlaces).toString(); + return Stripe.Decimal.from( + decimal.mul(100).toDecimalPlaces(decimalPlaces).toString(), + ); }; /** @@ -75,23 +81,23 @@ export const stripeToAtmnAmount = ({ decimalPlaces = 10, round = true, }: { - amount: number; + amount: number | StripeDecimal; currency?: string; decimalPlaces?: number; round?: boolean; }): number => { - let finalAmount = amount; + let finalAmount = typeof amount === "number" ? amount : amount.toNumber(); if (!ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) { - finalAmount = new Decimal(amount).div(100).toNumber(); + finalAmount = new DecimalJS(finalAmount).div(100).toNumber(); } if (round) { - return new Decimal(finalAmount).toDecimalPlaces(decimalPlaces).toNumber(); + return new DecimalJS(finalAmount).toDecimalPlaces(decimalPlaces).toNumber(); } if (decimalPlaces) { - return new Decimal(finalAmount).toDecimalPlaces(decimalPlaces).toNumber(); + return new DecimalJS(finalAmount).toDecimalPlaces(decimalPlaces).toNumber(); } return finalAmount; diff --git a/shared/utils/productUtils/priceUtils/convertPrice/priceToStripePrepaidV2Tiers.ts b/shared/utils/productUtils/priceUtils/convertPrice/priceToStripePrepaidV2Tiers.ts index 81a9e2d3f..e67091d53 100644 --- a/shared/utils/productUtils/priceUtils/convertPrice/priceToStripePrepaidV2Tiers.ts +++ b/shared/utils/productUtils/priceUtils/convertPrice/priceToStripePrepaidV2Tiers.ts @@ -9,7 +9,7 @@ import { } from "@utils/productUtils/priceUtils/classifyPriceUtils"; import { atmnToStripeAmountDecimal } from "@utils/productUtils/priceUtils/convertAmountUtils"; import { Decimal } from "decimal.js"; -import type Stripe from "stripe"; +import Stripe from "stripe"; /** * Builds the Stripe tier array for a V2 prepaid price. @@ -33,7 +33,7 @@ export const priceToStripePrepaidV2Tiers = ({ price: Price; entitlement: Entitlement; org: Organization; -}) => { +}): Stripe.PriceCreateParams.Tier[] => { const config = price.config as UsagePriceConfig; const tiers: Stripe.PriceCreateParams.Tier[] = []; @@ -42,7 +42,7 @@ export const priceToStripePrepaidV2Tiers = ({ // allowance. Applies to both graduated and volume pricing. if (entitlement.allowance) { tiers.push({ - unit_amount_decimal: "0", + unit_amount_decimal: Stripe.Decimal.zero, up_to: entitlement.allowance, }); } @@ -90,10 +90,12 @@ export const priceToStripePrepaidV2Tiers = ({ .ceil() .toNumber(), - unit_amount_decimal: new Decimal(tier.unit_amount_decimal ?? 0) - .mul(config.billing_units ?? 1) - .toString(), + unit_amount_decimal: Stripe.Decimal.from( + new Decimal(tier.unit_amount_decimal?.toNumber() ?? 0) + .mul(config.billing_units ?? 1) + .toString(), + ), })); - return dividedTiers; + return dividedTiers satisfies unknown as Stripe.PriceCreateParams.Tier[]; };