From 49d31fa27dcbc78a3e53388de6b6cfb458f6c7cd Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Mon, 4 May 2026 13:54:03 +0100 Subject: [PATCH] chore: cleanup logic --- ...ScheduledCustomerProductsToSubscription.ts | 17 ++++---- .../computeAttachNewCustomerProduct.ts | 39 +++---------------- .../attach/errors/handleStartDateErrors.ts | 17 ++++---- .../attach/setup/setupAttachBillingContext.ts | 6 +-- .../buildStripeSubscriptionAction.ts | 9 ++--- .../buildStripeSubscriptionScheduleAction.ts | 9 ++--- .../billing/v2/utils/startDateUtils.ts | 19 +++++++++ 7 files changed, 53 insertions(+), 63 deletions(-) create mode 100644 server/src/internal/billing/v2/utils/startDateUtils.ts diff --git a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/tasks/linkScheduledCustomerProductsToSubscription.ts b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/tasks/linkScheduledCustomerProductsToSubscription.ts index fbd622eb0..484d5a116 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/tasks/linkScheduledCustomerProductsToSubscription.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/tasks/linkScheduledCustomerProductsToSubscription.ts @@ -28,15 +28,16 @@ export const linkScheduledCustomerProductsToSubscription = async ({ env, }); - let updatedCount = 0; + const subscriptionStartMs = fromUnixTime( + subscription.start_date ?? subscription.created, + ).getTime(); + let linkedCount = 0; + for (const cusProduct of cusProducts) { const subscriptionIds = cusProduct.subscription_ids ?? []; if (subscriptionIds.includes(subscription.id)) continue; const nextSubscriptionIds = [...subscriptionIds, subscription.id]; - const subscriptionStartMs = fromUnixTime( - subscription.start_date ?? subscription.created, - ).getTime(); const hasStarted = hasCustomerProductStarted(cusProduct, { nowMs: subscriptionStartMs, }); @@ -51,7 +52,7 @@ export const linkScheduledCustomerProductsToSubscription = async ({ ? cusProduct.scheduled_ids : [scheduleId], }); - updatedCount++; + linkedCount++; continue; } @@ -69,12 +70,12 @@ export const linkScheduledCustomerProductsToSubscription = async ({ subscription_ids: nextSubscriptionIds, }, }); - updatedCount++; + linkedCount++; } - if (updatedCount > 0) { + if (linkedCount > 0) { logger.info( - `[sub.created] linked ${updatedCount} scheduled customer product(s) to subscription ${subscription.id}`, + `[sub.created] linked ${linkedCount} scheduled customer product(s) to subscription ${subscription.id}`, ); } }; diff --git a/server/src/internal/billing/v2/actions/attach/compute/computeAttachNewCustomerProduct.ts b/server/src/internal/billing/v2/actions/attach/compute/computeAttachNewCustomerProduct.ts index 0ab6ae8b8..00f6ee91b 100644 --- a/server/src/internal/billing/v2/actions/attach/compute/computeAttachNewCustomerProduct.ts +++ b/server/src/internal/billing/v2/actions/attach/compute/computeAttachNewCustomerProduct.ts @@ -26,31 +26,6 @@ const getScheduledBillingCycleAnchorResetAt = ({ return null; }; -const getStartsAt = ({ - startDate, - isScheduled, - endOfCycleMs, -}: { - startDate?: number; - isScheduled: boolean; - endOfCycleMs?: number; -}) => { - if (startDate) return startDate; - if (isScheduled) return endOfCycleMs; - return undefined; -}; - -const getResetCycleAnchor = ({ - startDate, - resetCycleAnchorMs, -}: { - startDate?: number; - resetCycleAnchorMs: number | "now"; -}) => { - if (resetCycleAnchorMs !== "now") return resetCycleAnchorMs; - return startDate ?? resetCycleAnchorMs; -}; - /** * Creates the new FullCusProduct to insert when attaching a product. * @@ -99,15 +74,11 @@ export const computeAttachNewCustomerProduct = ({ ); const isScheduled = planTiming === "end_of_cycle"; - const startsAt = getStartsAt({ - startDate: params.start_date, - isScheduled, - endOfCycleMs, - }); - const resetCycleAnchor = getResetCycleAnchor({ - startDate: params.start_date, - resetCycleAnchorMs, - }); + const startsAt = params.start_date ?? (isScheduled ? endOfCycleMs : undefined); + const resetCycleAnchor = + resetCycleAnchorMs === "now" && params.start_date !== undefined + ? params.start_date + : resetCycleAnchorMs; let existingUsagesConfig: ExistingUsagesConfig | undefined = !isScheduled && currentCustomerProduct diff --git a/server/src/internal/billing/v2/actions/attach/errors/handleStartDateErrors.ts b/server/src/internal/billing/v2/actions/attach/errors/handleStartDateErrors.ts index 568eedd03..ba13cf6e1 100644 --- a/server/src/internal/billing/v2/actions/attach/errors/handleStartDateErrors.ts +++ b/server/src/internal/billing/v2/actions/attach/errors/handleStartDateErrors.ts @@ -7,12 +7,13 @@ import { ErrCode, isFreeProduct, isOneOffProduct, - ms, RecaseError, } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; - -const START_DATE_TOLERANCE_MS = ms.minutes(1); +import { + isFutureStartDate, + isPastStartDate, +} from "@/internal/billing/v2/utils/startDateUtils"; const hasActivePaidRecurringSubscription = ({ billingContext, @@ -40,7 +41,7 @@ export const handleStartDateErrors = ({ }) => { if (params.start_date === undefined) return; - if (params.start_date < billingContext.currentEpochMs - START_DATE_TOLERANCE_MS) { + if (isPastStartDate(params.start_date, billingContext.currentEpochMs)) { throw new RecaseError({ message: "start_date cannot be set to a past timestamp. Use now or a future Unix timestamp in milliseconds.", @@ -67,9 +68,11 @@ export const handleStartDateErrors = ({ }); } - const isFutureStart = - params.start_date > billingContext.currentEpochMs + START_DATE_TOLERANCE_MS; - if (!isFutureStart) return; + if ( + !isFutureStartDate(params.start_date, billingContext.currentEpochMs) + ) { + return; + } if (params.invoice_mode?.enabled) { throw new RecaseError({ diff --git a/server/src/internal/billing/v2/actions/attach/setup/setupAttachBillingContext.ts b/server/src/internal/billing/v2/actions/attach/setup/setupAttachBillingContext.ts index 4dc3d1058..2d31e3a44 100644 --- a/server/src/internal/billing/v2/actions/attach/setup/setupAttachBillingContext.ts +++ b/server/src/internal/billing/v2/actions/attach/setup/setupAttachBillingContext.ts @@ -9,7 +9,6 @@ import { hasCustomItems, isFreeProduct, isOneOffProduct, - ms, notNullish, orgDisableStripeWrites, orgToReturnUrl, @@ -23,6 +22,7 @@ import { setupFullCustomerContext } from "@/internal/billing/v2/setup/setupFullC import { setupInvoiceModeContext } from "@/internal/billing/v2/setup/setupInvoiceModeContext"; import { setupResetCycleAnchor } from "@/internal/billing/v2/setup/setupResetCycleAnchor"; import { setupTransitionConfigs } from "@/internal/billing/v2/setup/setupTransitionConfigs"; +import { isFutureStartDate } from "@/internal/billing/v2/utils/startDateUtils"; import { setupAdjustableQuantities } from "../../../setup/setupAdjustableQuantities"; import { setupAnchorResetRefund } from "../../../setup/setupAnchorResetRefund"; import { setupAttachCheckoutMode } from "./setupAttachCheckoutMode"; @@ -198,9 +198,7 @@ export const setupAttachBillingContext = async ({ currentEpochMs, }); - const hasFutureStartDate = - params.start_date !== undefined && - params.start_date > currentEpochMs + ms.minutes(1); + const hasFutureStartDate = isFutureStartDate(params.start_date, currentEpochMs); const checkoutMode = setupAttachCheckoutMode({ paymentMethod, diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts index 8e4a88013..70980d963 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts @@ -5,11 +5,11 @@ import type { StripeSubscriptionAction, StripeSubscriptionScheduleAction, } from "@autumn/shared"; -import { msToSeconds } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; import { buildStripeSubscriptionItemsUpdate } from "@server/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate"; import { buildStripeSubscriptionCreateAction } from "@server/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionCreateAction"; import { buildStripeSubscriptionUpdateAction } from "@server/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction"; +import { stripePhaseStartsInFuture } from "@server/internal/billing/v2/utils/startDateUtils"; import { billingPlanToOneOffStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/stripeItemSpec/billingPlanToOneOffStripeItemSpecs"; const scheduleStartsInFuture = ({ @@ -21,10 +21,9 @@ const scheduleStartsInFuture = ({ }) => { if (stripeSubscriptionScheduleAction?.type !== "create") return false; - const startDate = stripeSubscriptionScheduleAction.params.phases?.[0]?.start_date; - return ( - typeof startDate === "number" && - startDate > msToSeconds(billingContext.currentEpochMs) + return stripePhaseStartsInFuture( + stripeSubscriptionScheduleAction.params.phases?.[0]?.start_date, + billingContext.currentEpochMs, ); }; diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts index 3fbb0ba72..ac201c5b1 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts @@ -8,10 +8,10 @@ import { cp, isCustomerProductOnStripeSubscription, isCustomerProductOnStripeSubscriptionSchedule, - msToSeconds, } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; import { buildStripePhasesUpdate } from "@server/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate"; +import { stripePhaseStartsInFuture } from "@server/internal/billing/v2/utils/startDateUtils"; import type Stripe from "stripe"; // ═══════════════════════════════════════════════════════════════════════════════ @@ -173,10 +173,9 @@ const shouldCreateFutureSchedule = ({ }) => { if (stripeSubscription) return false; - const startDate = scheduledPhases[0]?.start_date; - return ( - typeof startDate === "number" && - startDate > msToSeconds(billingContext.currentEpochMs) + return stripePhaseStartsInFuture( + scheduledPhases[0]?.start_date, + billingContext.currentEpochMs, ); }; diff --git a/server/src/internal/billing/v2/utils/startDateUtils.ts b/server/src/internal/billing/v2/utils/startDateUtils.ts new file mode 100644 index 000000000..ab786b548 --- /dev/null +++ b/server/src/internal/billing/v2/utils/startDateUtils.ts @@ -0,0 +1,19 @@ +import { ms, secondsToMs } from "@autumn/shared"; + +const START_DATE_TOLERANCE_MS = ms.minutes(1); + +export const isFutureStartDate = ( + startDate: number | undefined, + currentEpochMs: number, + toleranceMs = START_DATE_TOLERANCE_MS, +) => startDate !== undefined && startDate > currentEpochMs + toleranceMs; + +export const isPastStartDate = (startDate: number, currentEpochMs: number) => + startDate < currentEpochMs - START_DATE_TOLERANCE_MS; + +export const stripePhaseStartsInFuture = ( + startDate: number | "now" | undefined, + currentEpochMs: number, +) => + typeof startDate === "number" && + isFutureStartDate(secondsToMs(startDate), currentEpochMs, 0);