chore: cleanup logic

This commit is contained in:
Charlie Lamb
2026-05-04 13:54:03 +01:00
parent f42895448a
commit 49d31fa27d
7 changed files with 53 additions and 63 deletions

View File

@@ -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}`,
);
}
};

View File

@@ -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

View File

@@ -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({

View File

@@ -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,

View File

@@ -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,
);
};

View File

@@ -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,
);
};

View File

@@ -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);