Revert "feat: 🎸 stripe managed payments"

This commit is contained in:
amianthus
2026-06-09 17:20:48 +01:00
committed by GitHub
parent b9110f69fb
commit 68d50c5522
39 changed files with 573 additions and 709 deletions

View File

@@ -1,5 +1,5 @@
{ {
"setup": ["bun i", "bun dw setup"], "setup": ["bun dw setup"],
"run": ["bun dw run"], "run": ["bun dw run"],
"teardown": ["bun dw teardown"] "teardown": ["bun dw teardown"]
} }

View File

@@ -16,7 +16,7 @@
"autumn-js": "workspace:*", "autumn-js": "workspace:*",
"better-auth": "catalog:", "better-auth": "catalog:",
"kysely": "^0.28.11", "kysely": "^0.28.11",
"next": "16.2.6", "next": "16.1.6",
"pg": "^8.18.0", "pg": "^8.18.0",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.2.3", "react": "19.2.3",

View File

@@ -21,7 +21,7 @@
"lottie-react": "^2.4.1", "lottie-react": "^2.4.1",
"lottie-web": "^5.13.0", "lottie-web": "^5.13.0",
"motion": "^12.38.0", "motion": "^12.38.0",
"next": "16.2.6", "next": "16.2.4",
"ngrok": "^5.0.0-beta.2", "ngrok": "^5.0.0-beta.2",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",

843
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -25,10 +25,10 @@
"packages/stripe-sync" "packages/stripe-sync"
], ],
"catalog": { "catalog": {
"stripe": "22.1.0", "stripe": "19.3.0-beta.1",
"drizzle-orm": "0.43.1", "drizzle-orm": "0.43.1",
"drizzle-kit": "^0.31.1", "drizzle-kit": "^0.31.1",
"@typescript/native-preview": "7.0.0-dev.20260527.2", "@typescript/native-preview": "7.0.0-dev.20260220.1",
"@tailwindcss/vite": "4.2.1", "@tailwindcss/vite": "4.2.1",
"@vitejs/plugin-react": "5.2.0", "@vitejs/plugin-react": "5.2.0",
"@sentry/bun": "10.38.0", "@sentry/bun": "10.38.0",

View File

@@ -108,7 +108,7 @@
"@useautumn/sdk": "workspace:*", "@useautumn/sdk": "workspace:*",
"esbuild-plugin-path-alias": "^1.0.7", "esbuild-plugin-path-alias": "^1.0.7",
"hono": "4.12.7", "hono": "4.12.7",
"next": "^15.5.18", "next": "^15.2.3",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"tsup": "^8.4.0", "tsup": "^8.4.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@@ -88,7 +88,7 @@
"@trigger.dev/sdk": "4.4.6", "@trigger.dev/sdk": "4.4.6",
"@types/qs": "^6.14.0", "@types/qs": "^6.14.0",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@typescript/native-preview": "^7.0.0-dev.20260527.2", "@typescript/native-preview": "^7.0.0-dev.20251114.1",
"@upstash/ratelimit": "^2.0.7", "@upstash/ratelimit": "^2.0.7",
"@upstash/redis": "^1.35.6", "@upstash/redis": "^1.35.6",
"@vercel/sdk": "^1.17.0", "@vercel/sdk": "^1.17.0",

View File

@@ -13,7 +13,7 @@ const calculateTieredAmount = ({
let quantityCursor = quantity; let quantityCursor = quantity;
for (const tier of tiers) { for (const tier of tiers) {
const unitAmount = new Decimal( const unitAmount = new Decimal(
tier.unit_amount_decimal?.toString() ?? tier.unit_amount ?? 0, tier.unit_amount_decimal || tier.unit_amount!,
); );
if (notNullish(tier.up_to)) { if (notNullish(tier.up_to)) {
@@ -54,9 +54,7 @@ export const getSubItemAmount = ({
if (price.billing_scheme === "per_unit") { if (price.billing_scheme === "per_unit") {
if (price.unit_amount_decimal) { if (price.unit_amount_decimal) {
return new Decimal(price.unit_amount_decimal.toString()) return new Decimal(price.unit_amount_decimal).mul(quantity).toNumber();
.mul(quantity)
.toNumber();
} else { } else {
return new Decimal(price.unit_amount || 0).mul(quantity).toNumber(); return new Decimal(price.unit_amount || 0).mul(quantity).toNumber();
} }

View File

@@ -258,7 +258,7 @@ export const handleErrorSkip = (err: Error, c: Context<HonoEnv>) => {
// 4. Check Stripe-specific rules // 4. Check Stripe-specific rules
for (const rule of STRIPE_RULES) { for (const rule of STRIPE_RULES) {
if (rule.match(err, c)) { if (rule.match(err, c)) {
const stripeErr = err as Stripe.ErrorType.StripeError; const stripeErr = err as Stripe.errors.StripeError;
logger.warn(`${rule.name}, org: ${ctx.org?.slug || "unknown"}`); logger.warn(`${rule.name}, org: ${ctx.org?.slug || "unknown"}`);
return createErrorResponse({ return createErrorResponse({
c, c,

View File

@@ -1,5 +1,5 @@
import { Scopes } from "@autumn/shared";
import { initMasterStripe } from "@/external/connect/initStripeCli"; import { initMasterStripe } from "@/external/connect/initStripeCli";
import { Scopes } from "@autumn/shared";
import { createRoute } from "../../honoMiddlewares/routeHandler"; import { createRoute } from "../../honoMiddlewares/routeHandler";
export const handleGetMasterStripeAccount = createRoute({ export const handleGetMasterStripeAccount = createRoute({
@@ -10,7 +10,7 @@ export const handleGetMasterStripeAccount = createRoute({
try { try {
const masterStripe = initMasterStripe({ env }); const masterStripe = initMasterStripe({ env });
const account = await masterStripe.accounts.retrieve(null); const account = await masterStripe.accounts.retrieve();
return c.json({ return c.json({
id: account.id, id: account.id,

View File

@@ -10,7 +10,6 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv";
import { buildStripeCheckoutSessionItems } from "@/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildStripeCheckoutSessionItems"; import { buildStripeCheckoutSessionItems } from "@/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildStripeCheckoutSessionItems";
import { buildAutumnSubscriptionMetadata } from "@/internal/billing/v2/providers/stripe/utils/common/autumnStripeMetadata"; import { buildAutumnSubscriptionMetadata } from "@/internal/billing/v2/providers/stripe/utils/common/autumnStripeMetadata";
import { stripeDiscountsToCheckoutParams } from "@/internal/billing/v2/providers/stripe/utils/discounts/stripeDiscountsToParams"; 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 = ({ export const buildStripeCheckoutSessionAction = ({
ctx, ctx,
@@ -46,11 +45,11 @@ export const buildStripeCheckoutSessionAction = ({
// Payment-mode checkout has no top-level default_tax_rates, so one-off items take per-line tax_rates. // Payment-mode checkout has no top-level default_tax_rates, so one-off items take per-line tax_rates.
const taxRateId = billingContext.taxRateId; const taxRateId = billingContext.taxRateId;
const applyTaxRateToLineItem = ( const applyTaxRateToLineItem = (
item: CheckoutSessions.SessionCreateParams.LineItem, item: Stripe.Checkout.SessionCreateParams.LineItem,
): CheckoutSessions.SessionCreateParams.LineItem => ): Stripe.Checkout.SessionCreateParams.LineItem =>
taxRateId ? { ...item, tax_rates: [taxRateId] } : item; taxRateId ? { ...item, tax_rates: [taxRateId] } : item;
const lineItems: CheckoutSessions.SessionCreateParams.LineItem[] = [ const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = [
...recurringLineItems.filter((item) => item.quantity !== 0), ...recurringLineItems.filter((item) => item.quantity !== 0),
...oneOffLineItems ...oneOffLineItems
.filter((item) => item.quantity !== 0) .filter((item) => item.quantity !== 0)
@@ -65,7 +64,7 @@ export const buildStripeCheckoutSessionAction = ({
// 5. Build subscription_data (only for subscription mode) // 5. Build subscription_data (only for subscription mode)
const subscriptionData: const subscriptionData:
| CheckoutSessions.SessionCreateParams.SubscriptionData | Stripe.Checkout.SessionCreateParams.SubscriptionData
| undefined = | undefined =
mode === "subscription" mode === "subscription"
? { ? {
@@ -89,7 +88,7 @@ export const buildStripeCheckoutSessionAction = ({
// 7. Build params. Tax policy is baked in here (not at execute time) so // 7. Build params. Tax policy is baked in here (not at execute time) so
// the action object is self-describing in logs/EXTRA_LOGS. // the action object is self-describing in logs/EXTRA_LOGS.
const autumnAutoTax: Partial<CheckoutSessions.SessionCreateParams> = org.config const autumnAutoTax: Partial<Stripe.Checkout.SessionCreateParams> = org.config
.automatic_tax .automatic_tax
? { ? {
automatic_tax: { enabled: true }, automatic_tax: { enabled: true },
@@ -99,7 +98,7 @@ export const buildStripeCheckoutSessionAction = ({
} }
: {}; : {};
const params: CheckoutSessions.SessionCreateParams = { const params: Stripe.Checkout.SessionCreateParams = {
customer: stripeCustomer?.id ?? "none", customer: stripeCustomer?.id ?? "none",
mode, mode,
line_items: lineItems, line_items: lineItems,
@@ -113,6 +112,6 @@ export const buildStripeCheckoutSessionAction = ({
type: "create", type: "create",
params, params,
checkoutSessionParams: checkoutSessionParams:
checkoutSessionParams as Partial<CheckoutSessions.SessionCreateParams>, checkoutSessionParams as Partial<Stripe.Checkout.SessionCreateParams>,
}; };
}; };

View File

@@ -1,7 +1,6 @@
import type Stripe from "stripe"; import type Stripe from "stripe";
import { mergeStripeMetadata } from "@/internal/billing/v2/providers/stripe/utils/common/mergeStripeMetadata"; import { mergeStripeMetadata } from "@/internal/billing/v2/providers/stripe/utils/common/mergeStripeMetadata";
import { buildCheckoutSessionMetadata } from "./buildCheckoutSessionMetadata"; 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) * Deep-merges subscription_data so user-provided fields (e.g. metadata)
@@ -13,9 +12,9 @@ const mergeSubscriptionData = ({
userSubscriptionData, userSubscriptionData,
}: { }: {
userMetadata?: Record<string, string>; userMetadata?: Record<string, string>;
paramsSubscriptionData?: CheckoutSessions.SessionCreateParams.SubscriptionData; paramsSubscriptionData?: Stripe.Checkout.SessionCreateParams.SubscriptionData;
userSubscriptionData?: CheckoutSessions.SessionCreateParams.SubscriptionData; userSubscriptionData?: Stripe.Checkout.SessionCreateParams.SubscriptionData;
}): CheckoutSessions.SessionCreateParams.SubscriptionData | undefined => { }): Stripe.Checkout.SessionCreateParams.SubscriptionData | undefined => {
if (!paramsSubscriptionData && !userSubscriptionData && !userMetadata) { if (!paramsSubscriptionData && !userSubscriptionData && !userMetadata) {
return undefined; return undefined;
} }
@@ -42,16 +41,16 @@ export const buildCheckoutSessionParams = ({
autumnMetadataId, autumnMetadataId,
userMetadata, userMetadata,
}: { }: {
params: CheckoutSessions.SessionCreateParams; params: Stripe.Checkout.SessionCreateParams;
checkoutSessionParams?: Partial<CheckoutSessions.SessionCreateParams>; checkoutSessionParams?: Partial<Stripe.Checkout.SessionCreateParams>;
currency?: string; currency?: string;
defaultAllowPromotionCodes?: boolean; defaultAllowPromotionCodes?: boolean;
defaultInvoiceCreation?: CheckoutSessions.SessionCreateParams.InvoiceCreation; defaultInvoiceCreation?: Stripe.Checkout.SessionCreateParams.InvoiceCreation;
defaultSavedPaymentMethodOptions?: CheckoutSessions.SessionCreateParams.SavedPaymentMethodOptions; defaultSavedPaymentMethodOptions?: Stripe.Checkout.SessionCreateParams.SavedPaymentMethodOptions;
autumnMetadataId?: string; autumnMetadataId?: string;
userMetadata?: Record<string, string>; userMetadata?: Record<string, string>;
}): CheckoutSessions.SessionCreateParams => { }): Stripe.Checkout.SessionCreateParams => {
const mergedParams: CheckoutSessions.SessionCreateParams = { const mergedParams: Stripe.Checkout.SessionCreateParams = {
...(checkoutSessionParams ?? {}), ...(checkoutSessionParams ?? {}),
...params, ...params,
}; };
@@ -84,11 +83,11 @@ export const buildCheckoutSessionParams = ({
: mergeSubscriptionData({ : mergeSubscriptionData({
userMetadata, userMetadata,
paramsSubscriptionData: params.subscription_data as paramsSubscriptionData: params.subscription_data as
| CheckoutSessions.SessionCreateParams.SubscriptionData | Stripe.Checkout.SessionCreateParams.SubscriptionData
| undefined, | undefined,
userSubscriptionData: userSubscriptionData:
checkoutSessionParams?.subscription_data as checkoutSessionParams?.subscription_data as
| CheckoutSessions.SessionCreateParams.SubscriptionData | Stripe.Checkout.SessionCreateParams.SubscriptionData
| undefined, | undefined,
}), }),
}; };

View File

@@ -6,7 +6,7 @@ import {
priceUtils, priceUtils,
type StripeItemSpec, type StripeItemSpec,
} from "@autumn/shared"; } from "@autumn/shared";
import type { Checkout as CheckoutSessions } from "stripe/resources/Checkout/Sessions.js"; import type Stripe from "stripe";
import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { AutumnContext } from "@/honoUtils/HonoEnv";
import { customerProductsToOneOffStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/stripeItemSpec/customerProductsToOneOffStripeItemSpecs"; import { customerProductsToOneOffStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/stripeItemSpec/customerProductsToOneOffStripeItemSpecs";
import { customerProductsToRecurringStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/stripeItemSpec/customerProductsToRecurringStripeItemSpecs"; import { customerProductsToRecurringStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/stripeItemSpec/customerProductsToRecurringStripeItemSpecs";
@@ -17,13 +17,13 @@ import { updateOneOffTieredItems } from "./updateOneOffTieredItems";
const isZeroAmountInlineLineItem = ({ const isZeroAmountInlineLineItem = ({
lineItem, lineItem,
}: { }: {
lineItem: CheckoutSessions.SessionCreateParams.LineItem; lineItem: Stripe.Checkout.SessionCreateParams.LineItem;
}) => { }) => {
if (!("price_data" in lineItem) || !lineItem.price_data) return false; if (!("price_data" in lineItem) || !lineItem.price_data) return false;
return ( return (
lineItem.price_data.unit_amount === 0 || lineItem.price_data.unit_amount === 0 ||
lineItem.price_data.unit_amount_decimal?.toNumber() === 0 lineItem.price_data.unit_amount_decimal === "0"
); );
}; };
@@ -34,7 +34,7 @@ const isZeroAmountInlineRecurringStripeItemSpec = ({
}) => { }) => {
if (!stripeItemSpec.stripeInlinePrice?.recurring) return false; if (!stripeItemSpec.stripeInlinePrice?.recurring) return false;
return stripeItemSpec.stripeInlinePrice.unit_amount_decimal.toNumber() === 0; return stripeItemSpec.stripeInlinePrice.unit_amount_decimal === "0";
}; };
const getRecurringCadenceKey = ({ const getRecurringCadenceKey = ({
@@ -94,10 +94,10 @@ const applyAdjustableQuantityToPrepaidLineItem = ({
spec, spec,
billingContext, billingContext,
}: { }: {
lineItem: CheckoutSessions.SessionCreateParams.LineItem; lineItem: Stripe.Checkout.SessionCreateParams.LineItem;
spec: StripeItemSpec; spec: StripeItemSpec;
billingContext: BillingContext; billingContext: BillingContext;
}): CheckoutSessions.SessionCreateParams.LineItem => { }): Stripe.Checkout.SessionCreateParams.LineItem => {
const { autumnPrice, autumnEntitlement, autumnProduct } = spec; const { autumnPrice, autumnEntitlement, autumnProduct } = spec;
if (!autumnPrice || !autumnEntitlement || !isPrepaidPrice(autumnPrice)) { if (!autumnPrice || !autumnEntitlement || !isPrepaidPrice(autumnPrice)) {
@@ -132,7 +132,7 @@ const applyAdjustableQuantityToPrepaidLineItem = ({
}), }),
maximum: 999999, maximum: 999999,
}, },
} as CheckoutSessions.SessionCreateParams.LineItem; } as Stripe.Checkout.SessionCreateParams.LineItem;
}; };
export const buildStripeCheckoutSessionItems = ({ export const buildStripeCheckoutSessionItems = ({
@@ -144,8 +144,8 @@ export const buildStripeCheckoutSessionItems = ({
billingContext: BillingContext; billingContext: BillingContext;
newCustomerProducts: FullCusProduct[]; newCustomerProducts: FullCusProduct[];
}): { }): {
recurringLineItems: CheckoutSessions.SessionCreateParams.LineItem[]; recurringLineItems: Stripe.Checkout.SessionCreateParams.LineItem[];
oneOffLineItems: CheckoutSessions.SessionCreateParams.LineItem[]; oneOffLineItems: Stripe.Checkout.SessionCreateParams.LineItem[];
} => { } => {
// 1. Filter customer products by active statuses // 1. Filter customer products by active statuses
const activeCustomerProducts = filterCustomerProductsByActiveStatuses({ const activeCustomerProducts = filterCustomerProductsByActiveStatuses({

View File

@@ -10,7 +10,6 @@ import {
} from "@autumn/shared"; } from "@autumn/shared";
import type Stripe from "stripe"; import type Stripe from "stripe";
import { stripeItemSpecToCheckoutLineItem } from "../stripeItemSpec/stripeItemSpecToStripeParam"; 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. * Update one-off items to use inline price_data if they're tiered.
@@ -23,7 +22,7 @@ export const updateOneOffTieredItems = ({
}: { }: {
oneOffItemSpecs: StripeItemSpec[]; oneOffItemSpecs: StripeItemSpec[];
org: Organization; org: Organization;
}): CheckoutSessions.SessionCreateParams.LineItem[] => { }): Stripe.Checkout.SessionCreateParams.LineItem[] => {
const currency = orgToCurrency({ org }); const currency = orgToCurrency({ org });
return oneOffItemSpecs.map((item) => { return oneOffItemSpecs.map((item) => {

View File

@@ -256,10 +256,7 @@ const mergeStripeAndBillingLineItems = ({
stripeLineItem.parent?.invoice_item_details?.invoice_item ?? null, stripeLineItem.parent?.invoice_item_details?.invoice_item ?? null,
stripe_subscription_item_id: stripeSubscriptionItemId, stripe_subscription_item_id: stripeSubscriptionItemId,
stripe_product_id: (priceDetails?.product as string) ?? null, stripe_product_id: (priceDetails?.product as string) ?? null,
stripe_price_id: stripe_price_id: priceDetails?.price ?? null,
typeof priceDetails?.price === "string"
? priceDetails?.price
: (priceDetails?.price?.id ?? null),
stripe_discountable: stripeLineItem.discountable, stripe_discountable: stripeLineItem.discountable,
// Amounts (from Stripe or Autumn depending on discountable flag) // Amounts (from Stripe or Autumn depending on discountable flag)
@@ -343,10 +340,7 @@ const createDbLineItemFromStripeOnly = ({
stripeLineItem.parent?.invoice_item_details?.invoice_item ?? null, stripeLineItem.parent?.invoice_item_details?.invoice_item ?? null,
stripe_subscription_item_id: stripeSubscriptionItemId, stripe_subscription_item_id: stripeSubscriptionItemId,
stripe_product_id: (priceDetails?.product as string) ?? null, stripe_product_id: (priceDetails?.product as string) ?? null,
stripe_price_id: stripe_price_id: priceDetails?.price ?? null,
typeof priceDetails?.price === "string"
? priceDetails?.price
: (priceDetails?.price?.id ?? null),
stripe_discountable: stripeLineItem.discountable, stripe_discountable: stripeLineItem.discountable,
amount, amount,

View File

@@ -12,7 +12,7 @@ import type { PayInvoiceResult } from "./payStripeInvoice";
const getFailureCodeFromStripeError = ({ const getFailureCodeFromStripeError = ({
stripeError, stripeError,
}: { }: {
stripeError: Stripe.ErrorType.StripeError; stripeError: Stripe.errors.StripeError;
}): "3ds_required" | "payment_failed" => { }): "3ds_required" | "payment_failed" => {
const authCodes = ["authentication_required", "authentication_not_handled"]; const authCodes = ["authentication_required", "authentication_not_handled"];
@@ -61,7 +61,7 @@ export const handleInvoicePaymentFailure = ({
} }
// 2. Check if it's a Stripe error // 2. Check if it's a Stripe error
const stripeError = error as Stripe.ErrorType.StripeError; const stripeError = error as Stripe.errors.StripeError;
const isStripeError = stripeError.type !== undefined; const isStripeError = stripeError.type !== undefined;
if (!isStripeError) throw error; if (!isStripeError) throw error;

View File

@@ -13,7 +13,7 @@ export type PayInvoiceResult = {
code: PaymentFailureCode; code: PaymentFailureCode;
reason: string; reason: string;
}; };
stripeError?: Stripe.ErrorType.StripeError; stripeError?: Stripe.errors.StripeError;
}; };
type PayStripeInvoiceParams = { type PayStripeInvoiceParams = {

View File

@@ -13,11 +13,7 @@ type InlinePriceLike = {
divide_by?: number; divide_by?: number;
round?: string; round?: string;
} | null; } | null;
unit_amount_decimal?: unit_amount_decimal?: string | number | null;
| string
| number
| ReturnType<typeof Stripe.Decimal.from>
| null;
}; };
export type StripePriceShape = { export type StripePriceShape = {
@@ -69,7 +65,7 @@ export const stripePriceToShape = ({
intervalCount: price.recurring?.interval_count, intervalCount: price.recurring?.interval_count,
tiersMode: price.tiers_mode ?? undefined, tiersMode: price.tiers_mode ?? undefined,
transformQuantity: transformQuantityKey(price.transform_quantity), transformQuantity: transformQuantityKey(price.transform_quantity),
unitAmountDecimal: decimalAmount(price.unit_amount_decimal?.toString()), unitAmountDecimal: decimalAmount(price.unit_amount_decimal),
}); });
export const inlinePriceToShape = ({ export const inlinePriceToShape = ({
@@ -84,7 +80,7 @@ export const inlinePriceToShape = ({
interval: price.recurring?.interval, interval: price.recurring?.interval,
intervalCount: price.recurring?.interval_count, intervalCount: price.recurring?.interval_count,
transformQuantity: transformQuantityKey(price.transform_quantity), transformQuantity: transformQuantityKey(price.transform_quantity),
unitAmountDecimal: decimalAmount(price.unit_amount_decimal?.toString()), unitAmountDecimal: decimalAmount(price.unit_amount_decimal),
}); });
export const stripePriceShapesEqual = ( export const stripePriceShapesEqual = (

View File

@@ -3,21 +3,13 @@ import {
type StripeInlinePrice, type StripeInlinePrice,
type StripeItemSpec, type StripeItemSpec,
} from "@autumn/shared"; } from "@autumn/shared";
import Stripe from "stripe"; import type Stripe from "stripe";
import type { Checkout as CheckoutSessions } from "stripe/resources/Checkout/Sessions.js";
type StoredPriceParam = { price: string }; type StoredPriceParam = { price: string };
type RecurringInlinePriceParam = { type RecurringInlinePriceParam = {
price_data: Stripe.SubscriptionCreateParams.Item["price_data"]; 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. * 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). * For inline prices, asserts that `recurring` is present (one-off items should not reach this path).
@@ -37,7 +29,7 @@ const toRecurringPriceParam = ({
} }
return { return {
price_data: { price_data: {
...toStripeInlinePriceData(spec.stripeInlinePrice), ...spec.stripeInlinePrice,
recurring: spec.stripeInlinePrice.recurring, recurring: spec.stripeInlinePrice.recurring,
}, },
}; };
@@ -64,11 +56,9 @@ const toPriceParam = ({
spec, spec,
}: { }: {
spec: StripeItemSpec; spec: StripeItemSpec;
}): }): StoredPriceParam | { price_data: StripeInlinePrice } => {
| StoredPriceParam
| { price_data: ReturnType<typeof toStripeInlinePriceData> } => {
if (spec.stripeInlinePrice) { if (spec.stripeInlinePrice) {
return { price_data: toStripeInlinePriceData(spec.stripeInlinePrice) }; return { price_data: spec.stripeInlinePrice };
} }
return { price: spec.stripePriceId! }; return { price: spec.stripePriceId! };
}; };
@@ -78,7 +68,7 @@ export const stripeItemSpecToCheckoutLineItem = ({
spec, spec,
}: { }: {
spec: StripeItemSpec; spec: StripeItemSpec;
}): CheckoutSessions.SessionCreateParams.LineItem => { }): Stripe.Checkout.SessionCreateParams.LineItem => {
return { return {
...toPriceParam({ spec }), ...toPriceParam({ spec }),
quantity: spec.quantity, quantity: spec.quantity,

View File

@@ -77,7 +77,7 @@ export const storeInvoiceLineItems = async ({
if (info?.isMetered) return false; if (info?.isMetered) return false;
// Filter $0 empty price placeholders (e.g. stripe_empty_price_id) // Filter $0 empty price placeholders (e.g. stripe_empty_price_id)
if (li.pricing?.unit_amount_decimal?.toNumber() === 0) return false; if (li.pricing?.unit_amount_decimal === "0") return false;
return true; return true;
}); });

View File

@@ -18,7 +18,6 @@ import { notNullish } from "@/utils/genUtils.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js"; import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { attachParamsToMetadata } from "../../billing/attach/utils/attachParamsToMetadata.js"; import { attachParamsToMetadata } from "../../billing/attach/utils/attachParamsToMetadata.js";
import type { AttachParams } from "../cusProducts/AttachParams.js"; import type { AttachParams } from "../cusProducts/AttachParams.js";
import type { Checkout as CheckoutSessions } from "stripe/resources/Checkout/Sessions.js";
export const handleCreateCheckout = async ({ export const handleCreateCheckout = async ({
ctx, ctx,
@@ -86,10 +85,10 @@ export const handleCreateCheckout = async ({
} }
const checkoutParams = attachParams.checkoutSessionParams as const checkoutParams = attachParams.checkoutSessionParams as
| Partial<CheckoutSessions.SessionCreateParams> | Partial<Stripe.Checkout.SessionCreateParams>
| undefined; | undefined;
const checkoutSubscriptionData = checkoutParams?.subscription_data as const checkoutSubscriptionData = checkoutParams?.subscription_data as
| CheckoutSessions.SessionCreateParams.SubscriptionData | Stripe.Checkout.SessionCreateParams.SubscriptionData
| undefined; | undefined;
const trialEnd = const trialEnd =
freeTrial && !attachParams.disableFreeTrial freeTrial && !attachParams.disableFreeTrial
@@ -105,7 +104,7 @@ export const handleCreateCheckout = async ({
: undefined; : undefined;
const subscriptionData: const subscriptionData:
| CheckoutSessions.SessionCreateParams.SubscriptionData | Stripe.Checkout.SessionCreateParams.SubscriptionData
| undefined = isRecurring | undefined = isRecurring
? { ? {
...(checkoutSubscriptionData ?? {}), ...(checkoutSubscriptionData ?? {}),
@@ -140,7 +139,7 @@ export const handleCreateCheckout = async ({
notNullish(checkoutParams?.payment_method_types) || notNullish(checkoutParams?.payment_method_types) ||
notNullish(checkoutParams?.payment_method_configuration); notNullish(checkoutParams?.payment_method_configuration);
let sessionParams: CheckoutSessions.SessionCreateParams = { let sessionParams: Stripe.Checkout.SessionCreateParams = {
customer: customer.processor.id, customer: customer.processor.id,
line_items: items, line_items: items,
mode: isRecurring ? "subscription" : "payment", mode: isRecurring ? "subscription" : "payment",

View File

@@ -1,17 +1,13 @@
import { import {
AppEnv, AppEnv,
customerEntitlements,
customerPrices,
customerProducts,
customers, customers,
ErrCode, ErrCode,
member, member,
organizations, organizations,
type Organization, type Organization,
products,
RecaseError, RecaseError,
} from "@autumn/shared"; } from "@autumn/shared";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import type { DrizzleCli } from "@server/db/initDrizzle.js"; import type { DrizzleCli } from "@server/db/initDrizzle.js";
import type { Logger } from "@/external/logtail/logtailUtils.js"; import type { Logger } from "@/external/logtail/logtailUtils.js";
import { import {
@@ -70,40 +66,6 @@ export const deletePlatformSubOrg = async ({
logger.info("5. Deleting org memberships"); logger.info("5. Deleting org memberships");
await db.delete(member).where(eq(member.organizationId, org.id)); await db.delete(member).where(eq(member.organizationId, org.id));
// products.org_id cascades on org delete, but customer_products RESTRICTs logger.info("6. Deleting organization");
// the product delete — so tear down the customer-side rows first, deepest
// FK child to shallowest: cusEnts/cusPrices → customer_products → products.
logger.info("6. Deleting customer products and products");
const orgProductIds = (
await db
.select({ internalId: products.internal_id })
.from(products)
.where(eq(products.org_id, org.id))
).map((p) => p.internalId);
if (orgProductIds.length > 0) {
const cusProductIds = (
await db
.select({ id: customerProducts.id })
.from(customerProducts)
.where(inArray(customerProducts.internal_product_id, orgProductIds))
).map((cp) => cp.id);
if (cusProductIds.length > 0) {
await db
.delete(customerEntitlements)
.where(inArray(customerEntitlements.customer_product_id, cusProductIds));
await db
.delete(customerPrices)
.where(inArray(customerPrices.customer_product_id, cusProductIds));
await db
.delete(customerProducts)
.where(inArray(customerProducts.id, cusProductIds));
}
await db.delete(products).where(eq(products.org_id, org.id));
}
logger.info("7. Deleting organization");
await db.delete(organizations).where(eq(organizations.id, org.id)); await db.delete(organizations).where(eq(organizations.id, org.id));
}; };

View File

@@ -1,5 +1,5 @@
import { Scopes } from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { Scopes } from "@autumn/shared";
import { createRoute } from "@/honoMiddlewares/routeHandler.js"; import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { isStripeConnected } from "../../orgUtils.js"; import { isStripeConnected } from "../../orgUtils.js";
@@ -15,7 +15,7 @@ export const handleGetStripeAccount = createRoute({
try { try {
const stripeCli = createStripeCli({ org, env }); const stripeCli = createStripeCli({ org, env });
const accountDetails = await stripeCli.accounts.retrieve(null); const accountDetails = await stripeCli.accounts.retrieve();
return c.json(accountDetails); return c.json(accountDetails);
} catch (error) { } catch (error) {
logger.warn( logger.warn(

View File

@@ -46,8 +46,8 @@ export const shouldReconnectStripe = async ({
const stripeCli = createStripeCli({ org, env }); const stripeCli = createStripeCli({ org, env });
const newKey = new Stripe(stripeKey); const newKey = new Stripe(stripeKey);
const oldAccount = await stripeCli.accounts.retrieve(null); const oldAccount = await stripeCli.accounts.retrieve();
const newAccount = await newKey.accounts.retrieve(null); const newAccount = await newKey.accounts.retrieve();
return oldAccount.id !== newAccount.id; return oldAccount.id !== newAccount.id;
} catch (error) { } catch (error) {

View File

@@ -19,7 +19,7 @@ export const handleStripeSecretKey = async ({
// 1. Check if key is valid // 1. Check if key is valid
await checkKeyValid(secretKey); await checkKeyValid(secretKey);
const stripe = new Stripe(secretKey); const stripe = new Stripe(secretKey);
const account = await stripe.accounts.retrieve(null); const account = await stripe.accounts.retrieve();
// 2. Disconnect existing webhook endpoints // 2. Disconnect existing webhook endpoints
const curWebhooks = await stripe.webhookEndpoints.list(); const curWebhooks = await stripe.webhookEndpoints.list();

View File

@@ -6,14 +6,13 @@ import {
trace, trace,
} from "@opentelemetry/api"; } from "@opentelemetry/api";
import type Stripe from "stripe"; import type Stripe from "stripe";
import type { StripeConfig } from "stripe/lib.js";
import { otelConfig } from "./otelConfig.js"; import { otelConfig } from "./otelConfig.js";
const TRACER_NAME = "autumn.stripe"; const TRACER_NAME = "autumn.stripe";
const SPAN_NAME = "stripe.api"; const SPAN_NAME = "stripe.api";
const INSTRUMENTED = new WeakSet<object>(); const INSTRUMENTED = new WeakSet<object>();
type StripeHttpClient = NonNullable<StripeConfig["httpClient"]>; type StripeHttpClient = NonNullable<Stripe.StripeConfig["httpClient"]>;
type StripeApiLike = { type StripeApiLike = {
httpClient?: StripeHttpClient; httpClient?: StripeHttpClient;

View File

@@ -84,7 +84,7 @@ export const triggerSubscriptionCreated = async ({
created: getUnixTime(subscriptionCreatedAtMs ?? Date.now()), created: getUnixTime(subscriptionCreatedAtMs ?? Date.now()),
schedule: scheduleId ?? null, schedule: scheduleId ?? null,
} as Stripe.Subscription; } as Stripe.Subscription;
const retrieveSubscription: Stripe.SubscriptionResource["retrieve"] = const retrieveSubscription: Stripe.SubscriptionsResource["retrieve"] =
async () => async () =>
stripeResponse({ object: subscription, requestId: `req_${stripeSubId}` }); stripeResponse({ object: subscription, requestId: `req_${stripeSubId}` });

View File

@@ -16,7 +16,7 @@ export const normalizeActualSubItem = ({
quantity: item.quantity ?? 0, quantity: item.quantity ?? 0,
isInline: !!autumnCusPriceId, isInline: !!autumnCusPriceId,
unitAmountDecimal: autumnCusPriceId unitAmountDecimal: autumnCusPriceId
? (item.price.unit_amount_decimal?.toString() ?? undefined) ? (item.price.unit_amount_decimal ?? undefined)
: undefined, : undefined,
}; };
}; };
@@ -42,7 +42,7 @@ export const normalizeActualPhaseItem = ({
autumnCustomerPriceId: autumnCusPriceId || undefined, autumnCustomerPriceId: autumnCusPriceId || undefined,
quantity: item.quantity ?? 0, quantity: item.quantity ?? 0,
isInline: !!autumnCusPriceId, isInline: !!autumnCusPriceId,
unitAmountDecimal: unitAmountDecimal?.toString(), unitAmountDecimal,
}; };
}; };
@@ -58,7 +58,7 @@ export const normalizeExpectedPhaseItem = ({
let unitAmountDecimal: string | undefined; let unitAmountDecimal: string | undefined;
if (hasInlinePrice) { if (hasInlinePrice) {
const priceData = (item as { price_data: StripeInlinePrice }).price_data; const priceData = (item as { price_data: StripeInlinePrice }).price_data;
unitAmountDecimal = priceData.unit_amount_decimal.toString(); unitAmountDecimal = priceData.unit_amount_decimal;
} }
return { return {

View File

@@ -58,10 +58,7 @@ export const expectStripeInvoiceLineItemPeriodCorrect = async ({
Boolean( Boolean(
findPriceFromStripeId({ findPriceFromStripeId({
prices: usagePrices, prices: usagePrices,
stripePriceId: stripePriceId: line.pricing?.price_details?.price ?? "",
typeof line.pricing?.price_details?.price === "string"
? line.pricing?.price_details?.price
: (line.pricing?.price_details?.price?.id ?? ""),
}), }),
), ),
), ),

View File

@@ -11,8 +11,6 @@ import { expireAllCusEntsForReset } from "@tests/utils/cusProductUtils/resetTest
import { products } from "@tests/utils/fixtures/products.js"; import { products } from "@tests/utils/fixtures/products.js";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
import chalk from "chalk"; import chalk from "chalk";
import { sql } from "drizzle-orm";
import { syncItemV4 } from "@/internal/balances/utils/sync/syncItemV4.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
// ───────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────
@@ -51,40 +49,6 @@ test.concurrent(`${chalk.yellowBright("customer aggregated balance: rollover bal
actions: [s.attach({ productId: base.id })], actions: [s.attach({ productId: base.id })],
}); });
const syncMessagesBalance = async () => {
const cusEntRows = await ctx.db.execute(
sql`SELECT ce.id
FROM customer_entitlements ce
JOIN customers c ON c.internal_id = ce.internal_customer_id
WHERE c.id = ${customerId}
AND ce.feature_id = ${TestFeature.Messages}`,
);
const cusEntIds = cusEntRows.map((row) => row.id as string);
const rolloverRows = cusEntIds.length
? await ctx.db.execute(
sql`SELECT id FROM rollovers WHERE cus_ent_id IN (${sql.join(
cusEntIds.map((id) => sql`${id}`),
sql`, `,
)})`,
)
: [];
await syncItemV4({
ctx,
payload: {
customerId,
orgId: ctx.org.id,
env: ctx.env,
timestamp: Date.now(),
rolloverIds: rolloverRows.map((row) => row.id as string),
modifiedCusEntIdsByFeatureId: {
[TestFeature.Messages]: cusEntIds,
},
},
});
};
// ── Step 1: Track 60 on ent-0, 30 on ent-1 ── // ── Step 1: Track 60 on ent-0, 30 on ent-1 ──
// Expected per-entity balance: ent-0 = 40, ent-1 = 70. // Expected per-entity balance: ent-0 = 40, ent-1 = 70.
// Customer aggregated: remaining = 110, usage = 90. // Customer aggregated: remaining = 110, usage = 90.
@@ -94,14 +58,12 @@ test.concurrent(`${chalk.yellowBright("customer aggregated balance: rollover bal
feature_id: TestFeature.Messages, feature_id: TestFeature.Messages,
value: 60, value: 60,
}); });
await syncMessagesBalance();
await autumnV1.track({ await autumnV1.track({
customer_id: customerId, customer_id: customerId,
entity_id: entities[1].id, entity_id: entities[1].id,
feature_id: TestFeature.Messages, feature_id: TestFeature.Messages,
value: 30, value: 30,
}); });
await syncMessagesBalance();
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -170,7 +132,6 @@ test.concurrent(`${chalk.yellowBright("customer aggregated balance: rollover bal
feature_id: TestFeature.Messages, feature_id: TestFeature.Messages,
value: 50, value: 50,
}); });
await syncMessagesBalance();
await new Promise((resolve) => setTimeout(resolve, 1500)); await new Promise((resolve) => setTimeout(resolve, 1500));

View File

@@ -1,6 +1,6 @@
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test"; import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
import type { BillingContext } from "@autumn/shared"; import type { BillingContext } from "@autumn/shared";
import Stripe from "stripe"; import type Stripe from "stripe";
import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { AutumnContext } from "@/honoUtils/HonoEnv";
const mockState = { const mockState = {
@@ -112,7 +112,7 @@ describe("executeStripeSubscriptionScheduleAction", () => {
product: "stripe_prod_inline", product: "stripe_prod_inline",
currency: "usd", currency: "usd",
recurring: { interval: "month" as const, interval_count: 1 }, recurring: { interval: "month" as const, interval_count: 1 },
unit_amount_decimal: Stripe.Decimal.from("1000"), unit_amount_decimal: "1000",
}; };
const params = { const params = {
phases: [ phases: [
@@ -152,7 +152,7 @@ describe("executeStripeSubscriptionScheduleAction", () => {
product: "stripe_prod_inline", product: "stripe_prod_inline",
currency: "usd", currency: "usd",
recurring: { interval: "month", interval_count: 1 }, recurring: { interval: "month", interval_count: 1 },
unit_amount_decimal: Stripe.Decimal.from("1000"), unit_amount_decimal: "1000",
}, },
}, },
], ],
@@ -189,7 +189,7 @@ describe("executeStripeSubscriptionScheduleAction", () => {
product: "stripe_prod_inline", product: "stripe_prod_inline",
currency: "usd", currency: "usd",
recurring: { interval: "month" as const, interval_count: 1 }, recurring: { interval: "month" as const, interval_count: 1 },
unit_amount_decimal: Stripe.Decimal.from("1000"), unit_amount_decimal: "1000",
}; };
const params = { const params = {
phases: [ phases: [
@@ -242,7 +242,7 @@ describe("executeStripeSubscriptionScheduleAction", () => {
product: "stripe_prod_inline", product: "stripe_prod_inline",
currency: "usd", currency: "usd",
recurring: { interval: "month", interval_count: 1 }, recurring: { interval: "month", interval_count: 1 },
unit_amount_decimal: Stripe.Decimal.from("1000"), unit_amount_decimal: "1000",
}, },
}, },
], ],

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import Stripe from "stripe"; import type Stripe from "stripe";
import { import {
findMatchingInlinePriceIdForPhaseItem, findMatchingInlinePriceIdForPhaseItem,
findMatchingInlineSubscriptionItem, findMatchingInlineSubscriptionItem,
@@ -10,7 +10,7 @@ const inlinePrice = {
product: "stripe_prod_inline", product: "stripe_prod_inline",
currency: "usd", currency: "usd",
recurring: { interval: "month" as const, interval_count: 1 }, recurring: { interval: "month" as const, interval_count: 1 },
unit_amount_decimal: Stripe.Decimal.from("1000"), unit_amount_decimal: "1000",
}; };
const subscriptionItem = ({ const subscriptionItem = ({

View File

@@ -15,7 +15,7 @@ import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
import { prices } from "@tests/utils/fixtures/db/prices"; import { prices } from "@tests/utils/fixtures/db/prices";
import { stripeSubscriptions } from "@tests/utils/fixtures/stripe/subscriptions"; import { stripeSubscriptions } from "@tests/utils/fixtures/stripe/subscriptions";
import chalk from "chalk"; import chalk from "chalk";
import Stripe from "stripe"; import type Stripe from "stripe";
import { buildStripeSubscriptionItemsUpdate } from "@/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate"; import { buildStripeSubscriptionItemsUpdate } from "@/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate";
import { import {
createCustomerPricesForProduct, createCustomerPricesForProduct,
@@ -968,7 +968,7 @@ describe(
const changedItem = currentItems.find( const changedItem = currentItems.find(
(item) => item.metadata.inline_price === "true", (item) => item.metadata.inline_price === "true",
)!; )!;
changedItem.price.unit_amount_decimal = Stripe.Decimal.from("999999"); changedItem.price.unit_amount_decimal = "999999";
const result = buildUpdateWithItems({ currentItems }); const result = buildUpdateWithItems({ currentItems });

View File

@@ -7,7 +7,6 @@ import {
} from "@autumn/shared"; } from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js"; import type { DrizzleCli } from "@/db/initDrizzle.js";
import type { AutumnInt } from "@/external/autumn/autumnCli.js"; import type { AutumnInt } from "@/external/autumn/autumnCli.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { ProductService } from "@/internal/products/ProductService.js"; import { ProductService } from "@/internal/products/ProductService.js";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -48,20 +47,12 @@ export const createProduct = async ({
const batchDelete = []; const batchDelete = [];
for (const prod of products) { for (const prod of products) {
batchDelete.push( batchDelete.push(
(async () => { ProductService.deleteByInternalId({
await CusProductService.deleteByProduct({
db,
internalProductId: prod.internal_id,
orgId,
env,
});
await ProductService.deleteByInternalId({
db, db,
internalId: prod.internal_id, internalId: prod.internal_id,
orgId, orgId,
env, env,
}); }),
})(),
); );
} }
@@ -128,15 +119,7 @@ export const createProducts = async ({
const batchCreate = []; const batchCreate = [];
for (const product of products) { for (const product of products) {
batchCreate.push( batchCreate.push(
createProduct({ createProduct({ db, orgId, env, autumn, product, prefix, createInStripe }),
db,
orgId,
env,
autumn,
product,
prefix,
createInStripe,
}),
); );
} }

View File

@@ -8,11 +8,12 @@
"target": "ES2020", "target": "ES2020",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"module": "Preserve", "module": "Preserve",
"rootDirs": [".", ".."],
"typeRoots": ["./node_modules/@types", "../node_modules/@types"], "typeRoots": ["./node_modules/@types", "../node_modules/@types"],
"types": ["bun", "node"], "types": ["bun", "node"],
// "declaration": true, // "declaration": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"outDir": "./dist",
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
@@ -40,9 +41,7 @@
// Explicitly set drizzle-orm path to avoid monorepo issues // 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/*"], "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"], "include": ["src", "tests", "scripts", "experiments", "perf"],

View File

@@ -13,7 +13,7 @@ export type StripeInlinePrice = {
product: string; product: string;
currency: string; currency: string;
recurring?: Stripe.PriceCreateParams.Recurring; recurring?: Stripe.PriceCreateParams.Recurring;
unit_amount_decimal: ReturnType<typeof Stripe.Decimal.from>; unit_amount_decimal: string;
}; };
/** /**

View File

@@ -15,9 +15,7 @@
"@api/*": ["./api/*"], "@api/*": ["./api/*"],
"@models/*": ["./models/*"], "@models/*": ["./models/*"],
"@utils/*": ["./utils/*"], "@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": ["./**/*"], "include": ["./**/*"],

View File

@@ -1,8 +1,5 @@
import { Decimal as DecimalJS } from "decimal.js"; import { Decimal } from "decimal.js";
import type Stripe from "stripe";
type DecimalLike = { toString(): string };
type StripeDecimal = string & ReturnType<typeof Stripe.Decimal.from>;
/** /**
* Zero-decimal currencies that Stripe handles without decimal places. * Zero-decimal currencies that Stripe handles without decimal places.
* These currencies don't require multiplying/dividing by 100. * These currencies don't require multiplying/dividing by 100.
@@ -41,11 +38,11 @@ export const atmnToStripeAmount = ({
if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) { if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) {
return amount; return amount;
} }
return new DecimalJS(amount).mul(100).round().toNumber(); return new Decimal(amount).mul(100).round().toNumber();
}; };
/** /**
* Converts an Autumn amount to a Stripe Decimal class. * Converts an Autumn amount to a Stripe decimal string.
* For most currencies, multiplies by 100 and returns as string with decimal places. * 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. * 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. * Used for Stripe API calls that require unit_amount_decimal as a string.
@@ -55,19 +52,16 @@ export const atmnToStripeAmountDecimal = ({
currency = "USD", currency = "USD",
decimalPlaces = 10, decimalPlaces = 10,
}: { }: {
amount: number | DecimalJS; amount: number | Decimal;
currency?: string; currency?: string;
decimalPlaces?: number; decimalPlaces?: number;
}): StripeDecimal => { }): string => {
const decimal = amount instanceof DecimalJS ? amount : new DecimalJS(amount); const decimal = amount instanceof Decimal ? amount : new Decimal(amount);
if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) { if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) {
return decimal.toDecimalPlaces(decimalPlaces).toString() as StripeDecimal; return decimal.toDecimalPlaces(decimalPlaces).toString();
} }
return decimal return decimal.mul(100).toDecimalPlaces(decimalPlaces).toString();
.mul(100)
.toDecimalPlaces(decimalPlaces)
.toString() as StripeDecimal;
}; };
/** /**
@@ -81,26 +75,24 @@ export const stripeToAtmnAmount = ({
decimalPlaces = 10, decimalPlaces = 10,
round = true, round = true,
}: { }: {
amount: number | string | DecimalLike; amount: number;
currency?: string; currency?: string;
decimalPlaces?: number; decimalPlaces?: number;
round?: boolean; round?: boolean;
}): number => { }): number => {
let finalAmount = new DecimalJS( let finalAmount = amount;
typeof amount === "number" ? amount : amount.toString(),
);
if (!ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) { if (!ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) {
finalAmount = finalAmount.div(100); finalAmount = new Decimal(amount).div(100).toNumber();
} }
if (round) { if (round) {
return finalAmount.toDecimalPlaces(decimalPlaces).toNumber(); return new Decimal(finalAmount).toDecimalPlaces(decimalPlaces).toNumber();
} }
if (decimalPlaces) { if (decimalPlaces) {
return finalAmount.toDecimalPlaces(decimalPlaces).toNumber(); return new Decimal(finalAmount).toDecimalPlaces(decimalPlaces).toNumber();
} }
return finalAmount.toNumber(); return finalAmount;
}; };

View File

@@ -11,8 +11,6 @@ import { atmnToStripeAmountDecimal } from "@utils/productUtils/priceUtils/conver
import { Decimal } from "decimal.js"; import { Decimal } from "decimal.js";
import type Stripe from "stripe"; import type Stripe from "stripe";
type StripeDecimal = string & ReturnType<typeof Stripe.Decimal.from>;
/** /**
* Builds the Stripe tier array for a V2 prepaid price. * Builds the Stripe tier array for a V2 prepaid price.
* *
@@ -35,7 +33,7 @@ export const priceToStripePrepaidV2Tiers = ({
price: Price; price: Price;
entitlement: Entitlement; entitlement: Entitlement;
org: Organization; org: Organization;
}): Stripe.PriceCreateParams.Tier[] => { }) => {
const config = price.config as UsagePriceConfig; const config = price.config as UsagePriceConfig;
const tiers: Stripe.PriceCreateParams.Tier[] = []; const tiers: Stripe.PriceCreateParams.Tier[] = [];
@@ -44,7 +42,7 @@ export const priceToStripePrepaidV2Tiers = ({
// allowance. Applies to both graduated and volume pricing. // allowance. Applies to both graduated and volume pricing.
if (entitlement.allowance) { if (entitlement.allowance) {
tiers.push({ tiers.push({
unit_amount_decimal: "0" as StripeDecimal, unit_amount_decimal: "0",
up_to: entitlement.allowance, up_to: entitlement.allowance,
}); });
} }
@@ -86,15 +84,15 @@ export const priceToStripePrepaidV2Tiers = ({
up_to: up_to:
index === tiers.length - 1 index === tiers.length - 1
? ("inf" as const) ? "inf"
: new Decimal(tier.up_to ?? 0) : new Decimal(tier.up_to ?? 0)
.div(config.billing_units ?? 1) .div(config.billing_units ?? 1)
.ceil() .ceil()
.toNumber(), .toNumber(),
unit_amount_decimal: new Decimal(tier.unit_amount_decimal?.toString() ?? "0") unit_amount_decimal: new Decimal(tier.unit_amount_decimal ?? 0)
.mul(config.billing_units ?? 1) .mul(config.billing_units ?? 1)
.toString() as StripeDecimal, .toString(),
})); }));
return dividedTiers; return dividedTiers;