diff --git a/server/src/internal/billing/v2/billingContext.ts b/server/src/internal/billing/v2/billingContext.ts index 8d400e113..8e909063a 100644 --- a/server/src/internal/billing/v2/billingContext.ts +++ b/server/src/internal/billing/v2/billingContext.ts @@ -7,7 +7,7 @@ import type { Price, StripeDiscountWithCoupon, } from "@autumn/shared"; -import type { CancelMode } from "@shared/api/common/cancelMode"; +import type { CancelAction } from "@shared/api/common/cancelMode"; import type { FullCustomer } from "@shared/models/cusModels/fullCusModel"; import type Stripe from "stripe"; import { z } from "zod/v4"; @@ -55,12 +55,12 @@ export interface BillingContext { trialContext?: TrialContext; isCustom?: boolean; - // Cancel mode (used by update subscription for uncancel) - cancelMode?: CancelMode; + // Cancel action (used by update subscription for uncancel) + cancelAction?: CancelAction; } export interface UpdateSubscriptionBillingContext extends BillingContext { customerProduct: FullCusProduct; // target customer product defaultProduct?: FullProduct; // for cancel flows - cancelMode?: CancelMode; // for cancel flows + cancelAction?: CancelAction; // for cancel flows } diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts index 9f6c8b163..3ed22bb1d 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts @@ -32,17 +32,18 @@ export const executeStripeSubscriptionScheduleAction = async ({ const { org, env } = ctx; const stripeCli = createStripeCli({ org, env }); + logSubscriptionScheduleAction({ + ctx, + billingContext, + subscriptionScheduleAction, + }); + ctx.logger.debug( `[executeStripeSubscriptionScheduleAction] Executing subscription schedule operation: ${subscriptionScheduleAction.type}`, ); switch (subscriptionScheduleAction.type) { case "create": { - logSubscriptionScheduleAction({ - ctx, - billingContext, - subscriptionScheduleAction, - }); const { params } = subscriptionScheduleAction; // If there's an existing subscription, create from it first then update with phases @@ -71,11 +72,6 @@ export const executeStripeSubscriptionScheduleAction = async ({ } case "update": - logSubscriptionScheduleAction({ - ctx, - billingContext, - subscriptionScheduleAction, - }); return await stripeCli.subscriptionSchedules.update( subscriptionScheduleAction.stripeSubscriptionScheduleId, subscriptionScheduleAction.params, diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts index eaaf4b2eb..2dcc76820 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts @@ -43,10 +43,7 @@ export const logSubscriptionScheduleAction = ({ }: { ctx: AutumnContext; billingContext: BillingContext; - subscriptionScheduleAction: Extract< - StripeSubscriptionScheduleAction, - { type: "create" | "update" } - >; + subscriptionScheduleAction: StripeSubscriptionScheduleAction; }): void => { if (subscriptionScheduleAction.type === "release") { ctx.logger.debug( diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction.ts index 22bc819b9..9a52bdccc 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction.ts @@ -22,7 +22,7 @@ export const buildStripeSubscriptionUpdateAction = ({ stripeSubscriptionScheduleAction?: StripeSubscriptionScheduleAction; subscriptionCancelAt?: number; }): StripeSubscriptionAction | undefined => { - const { stripeSubscription, trialContext, cancelMode } = billingContext; + const { stripeSubscription, trialContext, cancelAction } = billingContext; if (!stripeSubscription) { throw new Error( @@ -49,11 +49,11 @@ export const buildStripeSubscriptionUpdateAction = ({ } // Determine cancel_at handling: - // 1. Clear cancel_at if uncancel mode and currently has a cancel_at + // 1. Clear cancel_at if uncancel action and currently has a cancel_at // 2. Set cancel_at if explicitly provided and differs from current value const currentCancelAt = stripeSubscription.cancel_at; const shouldClearCancelAt = - cancelMode === "uncancel" && currentCancelAt !== null; + cancelAction === "uncancel" && currentCancelAt !== null; const shouldSetCancelAt = !shouldClearCancelAt && subscriptionCancelAt !== undefined && diff --git a/server/src/internal/billing/v2/setup/setupCancelMode.ts b/server/src/internal/billing/v2/setup/setupCancelMode.ts index 0bf1e4c0a..5073e42ef 100644 --- a/server/src/internal/billing/v2/setup/setupCancelMode.ts +++ b/server/src/internal/billing/v2/setup/setupCancelMode.ts @@ -1,23 +1,20 @@ import type { UpdateSubscriptionV0Params } from "@shared/api/billing/updateSubscription/updateSubscriptionV0Params"; -import type { CancelMode } from "@shared/api/common/cancelMode"; +import type { CancelAction } from "@shared/api/common/cancelMode"; /** - * Setup cancel mode from params + * Setup cancel action from params * @param params - The params - * Converts cancel param to internal cancel mode - * - cancel: null means "uncancel" (remove scheduled cancellation) - * - cancel: "immediately" or "end_of_cycle" means cancel - * - cancel: undefined means no cancel operation - * @returns The cancel mode + * cancel_action param maps directly to internal cancel action + * - cancel_action: "cancel_immediately" means cancel immediately + * - cancel_action: "cancel_end_of_cycle" means cancel at end of cycle + * - cancel_action: "uncancel" means remove scheduled cancellation + * - cancel_action: undefined means no cancel operation + * @returns The cancel action */ -export const setupCancelMode = ({ +export const setupCancelAction = ({ params, }: { params: UpdateSubscriptionV0Params; -}): CancelMode | undefined => { - if (params.cancel === null) { - return "uncancel"; - } - - return params.cancel; +}): CancelAction | undefined => { + return params.cancel_action; }; diff --git a/server/src/internal/billing/v2/types/cancelTypes.ts b/server/src/internal/billing/v2/types/cancelTypes.ts index 462a5b0d7..d7a34a688 100644 --- a/server/src/internal/billing/v2/types/cancelTypes.ts +++ b/server/src/internal/billing/v2/types/cancelTypes.ts @@ -1,7 +1,7 @@ import type { CusProductStatus } from "@autumn/shared"; -// Re-export CancelMode from shared for convenience -export type { CancelMode } from "@shared/api/common/cancelMode"; +// Re-export CancelAction from shared for convenience +export type { CancelAction } from "@shared/api/common/cancelMode"; /** * Updates to apply to a customer product when canceling or uncanceling. diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts index 1d3975586..a0992f4e3 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts @@ -1,35 +1,6 @@ -import { - type FullCusProduct, - findMainScheduledCustomerProductByGroup, - isCustomerProductCanceling, - isCustomerProductMain, -} from "@autumn/shared"; import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; - -/** - * Finds the scheduled product to delete when uncanceling. - * Only applies to main products that are currently canceling. - */ -const findScheduledProductToDelete = ({ - billingContext, -}: { - billingContext: UpdateSubscriptionBillingContext; -}): FullCusProduct | undefined => { - const { customerProduct, fullCustomer } = billingContext; - - const isMain = isCustomerProductMain(customerProduct); - const isCanceling = isCustomerProductCanceling(customerProduct); - - if (!isMain || !isCanceling) { - return undefined; - } - - return findMainScheduledCustomerProductByGroup({ - fullCustomer, - productGroup: customerProduct.product.group, - }); -}; +import { computeCustomerProductToDelete } from "@/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete"; /** * Applies uncancel updates to an existing billing plan. @@ -43,11 +14,9 @@ export const applyUncancelToPlan = ({ billingContext: UpdateSubscriptionBillingContext; plan: AutumnBillingPlan; }): AutumnBillingPlan => { - const { cancelMode } = billingContext; + const { cancelAction } = billingContext; - if (cancelMode !== "uncancel") { - return plan; - } + if (cancelAction !== "uncancel") return plan; const cancelUpdates = { canceled: false, @@ -56,7 +25,7 @@ export const applyUncancelToPlan = ({ }; // Find scheduled product to delete (only for main canceling products) - const deleteCustomerProduct = findScheduledProductToDelete({ + const deleteCustomerProduct = computeCustomerProductToDelete({ billingContext, }); diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts index d8233cf19..ffd70f811 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts @@ -1,5 +1,5 @@ import { CusProductStatus, type FullCusProduct } from "@autumn/shared"; -import type { CancelMode } from "@/internal/billing/v2/types/cancelTypes"; +import type { CancelAction } from "@/internal/billing/v2/types/cancelTypes"; /** * Computes cancel-related fields for a new customer product. @@ -8,10 +8,10 @@ import type { CancelMode } from "@/internal/billing/v2/types/cancelTypes"; * Always preserves active status when replacing an active product. */ export const computeCancelFields = ({ - cancelMode, + cancelAction, currentCustomerProduct, }: { - cancelMode?: CancelMode; + cancelAction?: CancelAction; currentCustomerProduct: FullCusProduct; }): { canceledAt: number | undefined; @@ -25,7 +25,7 @@ export const computeCancelFields = ({ ? CusProductStatus.Active : undefined; - if (cancelMode === "uncancel") { + if (cancelAction === "uncancel") { return { canceledAt: undefined, endedAt: undefined, status }; } diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts index 0dc53f531..834eb56e3 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts @@ -14,7 +14,7 @@ export const computeCancelLineItems = ({ ctx: AutumnContext; billingContext: UpdateSubscriptionBillingContext; }): LineItem[] => { - if (billingContext.cancelMode !== "immediately") return []; + if (billingContext.cancelAction !== "cancel_immediately") return []; return buildAutumnLineItems({ ctx, diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts index 38b6369cc..e3f2c2f50 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts @@ -12,9 +12,9 @@ import { computeEndOfCycleMs } from "./computeEndOfCycleMs"; /** * Computes and applies the cancel plan for a subscription. * - * Handles two modes: - * - 'end_of_cycle': Schedule cancellation at cycle end, insert scheduled default product - * - 'immediately': Cancel now, insert active default product + * Handles two cancel actions: + * - 'cancel_end_of_cycle': Schedule cancellation at cycle end, insert scheduled default product + * - 'cancel_immediately': Cancel now, insert active default product */ export const computeCancelPlan = ({ ctx, @@ -25,9 +25,9 @@ export const computeCancelPlan = ({ billingContext: UpdateSubscriptionBillingContext; plan: AutumnBillingPlan; }): AutumnBillingPlan => { - if (!billingContext.cancelMode) return plan; + if (!billingContext.cancelAction) return plan; - if (billingContext.cancelMode === "uncancel") { + if (billingContext.cancelAction === "uncancel") { return applyUncancelToPlan({ billingContext, plan, @@ -38,7 +38,7 @@ export const computeCancelPlan = ({ const endOfCycleMs = computeEndOfCycleMs({ billingContext }); ctx.logger.debug( - `[computeCancelPlan] ${billingContext.cancelMode}: end of cycle at ${endOfCycleMs}`, + `[computeCancelPlan] ${billingContext.cancelAction}: end of cycle at ${endOfCycleMs}`, ); // Step 2: Build cancel updates for customer product diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts index 8c4b557ba..69e67ceb7 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts @@ -10,8 +10,8 @@ export interface CancelUpdates { /** * Builds the cancel field updates for a customer product. - * For 'immediately' mode, includes status: Expired. - * For 'end_of_cycle' mode, only sets cancel fields without status change. + * For 'cancel_immediately' mode, includes status: Expired. + * For 'cancel_end_of_cycle' mode, only sets cancel fields without status change. */ export const computeCancelUpdates = ({ billingContext, @@ -20,9 +20,9 @@ export const computeCancelUpdates = ({ billingContext: UpdateSubscriptionBillingContext; endOfCycleMs: number; }): CancelUpdates => { - const { cancelMode, currentEpochMs } = billingContext; + const { cancelAction, currentEpochMs } = billingContext; - if (cancelMode === "immediately") { + if (cancelAction === "cancel_immediately") { return { canceled: true, canceled_at: currentEpochMs, diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts index 9b397864f..6be3ebcb4 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts @@ -1,4 +1,5 @@ import { + cp, type FullCusProduct, findMainScheduledCustomerProductByGroup, } from "@autumn/shared"; @@ -15,7 +16,9 @@ export const computeCustomerProductToDelete = ({ }): FullCusProduct | undefined => { const { fullCustomer, customerProduct } = billingContext; - if (customerProduct.product.is_add_on) return undefined; + const { valid: isMainRecurring } = cp(customerProduct).main().recurring(); + + if (!isMainRecurring) return undefined; return findMainScheduledCustomerProductByGroup({ fullCustomer, diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts index c09f40330..247b791cd 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts @@ -6,8 +6,8 @@ import { initFullCustomerProduct } from "@/internal/billing/v2/utils/initFullCus /** * Creates the default customer product to insert when canceling. * Returns undefined for add-ons or when no default product exists. - * For 'immediately' mode, creates an active product. - * For 'end_of_cycle' mode, creates a scheduled product. + * For 'cancel_immediately' mode, creates an active product. + * For 'cancel_end_of_cycle' mode, creates a scheduled product. */ export const computeDefaultCustomerProduct = ({ ctx, @@ -19,7 +19,7 @@ export const computeDefaultCustomerProduct = ({ endOfCycleMs: number; }): FullCusProduct | undefined => { const { - cancelMode, + cancelAction, customerProduct, defaultProduct, fullCustomer, @@ -34,9 +34,10 @@ export const computeDefaultCustomerProduct = ({ // No default product configured if (!defaultProduct) return undefined; - const startsAt = cancelMode === "immediately" ? currentEpochMs : endOfCycleMs; + const startsAt = + cancelAction === "cancel_immediately" ? currentEpochMs : endOfCycleMs; const status = - cancelMode === "immediately" + cancelAction === "cancel_immediately" ? CusProductStatus.Active : CusProductStatus.Scheduled; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts index 810dd257f..6e743999d 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts @@ -4,18 +4,22 @@ import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceI /** * Calculates the end of cycle timestamp for cancellation. - * For 'immediately' mode, returns currentEpochMs. - * For 'end_of_cycle' mode, calculates the next cycle end based on billing interval. + * For 'cancel_immediately' mode, returns currentEpochMs. + * For 'cancel_end_of_cycle' mode, calculates the next cycle end based on billing interval. */ export const computeEndOfCycleMs = ({ billingContext, }: { billingContext: UpdateSubscriptionBillingContext; }): number => { - const { cancelMode, customerProduct, billingCycleAnchorMs, currentEpochMs } = - billingContext; + const { + cancelAction, + customerProduct, + billingCycleAnchorMs, + currentEpochMs, + } = billingContext; - if (cancelMode === "immediately") { + if (cancelAction === "cancel_immediately") { return currentEpochMs; } diff --git a/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts index 071428d3b..b4baaf28d 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts @@ -59,7 +59,7 @@ export const computeUpdateSubscriptionPlan = async ({ break; } - // Apply cancel plan if cancelMode is set in context + // Apply cancel plan if cancelAction is set in context plan = computeCancelPlan({ ctx, billingContext, plan }); plan = finalizeUpdateSubscriptionPlan({ diff --git a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts b/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts index dbcbad26e..600bc21f4 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts @@ -26,7 +26,7 @@ export const computeCustomPlanNewCustomerProduct = ({ currentEpochMs, featureQuantities, trialContext, - cancelMode, + cancelAction, } = updateSubscriptionContext; const existingUsages = cusProductToExistingUsages({ @@ -44,7 +44,7 @@ export const computeCustomPlanNewCustomerProduct = ({ ); const cancelFields = computeCancelFields({ - cancelMode, + cancelAction, currentCustomerProduct, }); diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts index a01e37059..4531bded8 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts @@ -17,7 +17,7 @@ export const handleCancelEndOfCycleErrors = ({ billingContext: UpdateSubscriptionBillingContext; params: UpdateSubscriptionV0Params; }) => { - if (params.cancel !== "end_of_cycle") return; + if (billingContext.cancelAction !== "cancel_end_of_cycle") return; const { customerProduct } = billingContext; diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts index 8284c0995..403938a08 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts @@ -12,7 +12,7 @@ export const handleUncancelErrors = ({ }: { billingContext: UpdateSubscriptionBillingContext; }) => { - if (billingContext.cancelMode !== "uncancel") { + if (billingContext.cancelAction !== "uncancel") { return; } diff --git a/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts b/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts index e559ec176..47ec7d961 100644 --- a/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts +++ b/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts @@ -22,7 +22,7 @@ export const logUpdateSubscriptionContext = ({ stripeSubscription, stripeSubscriptionSchedule, isCustom, - cancelMode, + cancelAction, } = billingContext; const fullProduct = fullProducts[0]; @@ -54,7 +54,7 @@ export const logUpdateSubscriptionContext = ({ : "undefined", defaultProduct: billingContext.defaultProduct?.name ?? "undefined", - cancelMode: cancelMode ? cancelMode : "no cancel operation", + cancelAction: cancelAction ? cancelAction : "no cancel operation", }, }, }); diff --git a/server/src/internal/billing/v2/updateSubscription/setup/setupDefaultProductContext.ts b/server/src/internal/billing/v2/updateSubscription/setup/setupDefaultProductContext.ts index 235165b1a..7563b0244 100644 --- a/server/src/internal/billing/v2/updateSubscription/setup/setupDefaultProductContext.ts +++ b/server/src/internal/billing/v2/updateSubscription/setup/setupDefaultProductContext.ts @@ -1,7 +1,7 @@ import { + cp, type FullCusProduct, type FullProduct, - notNullish, nullish, type UpdateSubscriptionV0Params, } from "@autumn/shared"; @@ -22,12 +22,14 @@ export const setupDefaultProductContext = async ({ customerProduct: FullCusProduct; }): Promise => { // Only fetch if cancel is requested (not null/undefined) - if (nullish(params.cancel)) return undefined; + if (nullish(params.cancel_action)) return undefined; // Add-ons don't trigger default products - if (customerProduct.product.is_add_on) return undefined; + const { valid: isMainAndCustomerScoped } = cp(customerProduct) + .main() + .customerScoped(); - if (notNullish(customerProduct.internal_entity_id)) return undefined; + if (!isMainAndCustomerScoped) return undefined; const defaultProduct = await getFreeDefaultProductByGroup({ ctx, diff --git a/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts b/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts index d70db38e8..6ec2fc209 100644 --- a/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts +++ b/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts @@ -2,7 +2,7 @@ import { notNullish, type UpdateSubscriptionV0Params } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { setupStripeBillingContext } from "@/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext"; import { setupBillingCycleAnchor } from "@/internal/billing/v2/setup/setupBillingCycleAnchor"; -import { setupCancelMode } from "@/internal/billing/v2/setup/setupCancelMode"; +import { setupCancelAction } from "@/internal/billing/v2/setup/setupCancelMode"; import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext"; import { setupFullCustomerContext } from "@/internal/billing/v2/setup/setupFullCustomerContext"; import { setupInvoiceModeContext } from "@/internal/billing/v2/setup/setupInvoiceModeContext"; @@ -97,14 +97,14 @@ export const setupUpdateSubscriptionBillingContext = async ({ customerProduct, }); - const cancelMode = setupCancelMode({ params }); + const cancelAction = setupCancelAction({ params }); return { fullCustomer, fullProducts: [fullProduct], customerProduct, defaultProduct, - cancelMode, + cancelAction, stripeSubscription, stripeSubscriptionSchedule, stripeDiscounts, diff --git a/server/tests/integration/billing/stripe-webhooks/subscription-deleted/subscription-deleted.test.ts b/server/tests/integration/billing/stripe-webhooks/subscription-deleted/subscription-deleted.test.ts index ee1c8a1e6..7a6a9265c 100644 --- a/server/tests/integration/billing/stripe-webhooks/subscription-deleted/subscription-deleted.test.ts +++ b/server/tests/integration/billing/stripe-webhooks/subscription-deleted/subscription-deleted.test.ts @@ -160,7 +160,7 @@ test(`${chalk.yellowBright("sub.deleted: cancel after end_of_cycle via Stripe")} await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling and free is scheduled diff --git a/server/tests/integration/billing/update-subscription/cancel/cancel-with-other-params.test.ts b/server/tests/integration/billing/update-subscription/cancel/cancel-with-other-params.test.ts index e9507c1f9..12ad55931 100644 --- a/server/tests/integration/billing/update-subscription/cancel/cancel-with-other-params.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/cancel-with-other-params.test.ts @@ -79,7 +79,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: with custom plan ite customer_id: customerId, product_id: pro.id, items: [updatedMessagesItem, newPriceItem], - cancel: "end_of_cycle" as const, + cancel_action: "cancel_end_of_cycle" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(updateParams); @@ -184,7 +184,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: with prepaid quantit customer_id: customerId, product_id: pro.id, options: [{ feature_id: TestFeature.Messages, quantity: 500 }], // 5 packs - cancel: "end_of_cycle" as const, + cancel_action: "cancel_end_of_cycle" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(updateParams); @@ -284,7 +284,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: with price increase customer_id: customerId, product_id: pro.id, items: [updatedMessagesItem, newPriceItem], - cancel: "end_of_cycle" as const, + cancel_action: "cancel_end_of_cycle" as const, }; const result = await autumnV1.subscriptions.update(updateParams); diff --git a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-addon.test.ts b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-addon.test.ts index 8f647a5dd..16dae970a 100644 --- a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-addon.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-addon.test.ts @@ -99,7 +99,7 @@ test(`${chalk.yellowBright("cancel addon EOC: addon canceling, pro active, free await autumnV1.subscriptions.update({ customer_id: customerId, product_id: addon.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify state after cancel @@ -223,7 +223,7 @@ test(`${chalk.yellowBright("cancel addon EOC: cancel pro, addon persists with fr await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify state after cancel @@ -335,7 +335,7 @@ test(`${chalk.yellowBright("cancel addon EOC: separate subscription (new_billing await autumnV1.subscriptions.update({ customer_id: customerId, product_id: addon.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify only the add-on subscription is canceling @@ -482,7 +482,7 @@ test(`${chalk.yellowBright("cancel addon EOC: multiple addons, cancel one, other await autumnV1.subscriptions.update({ customer_id: customerId, product_id: addon1.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify state after cancel @@ -590,7 +590,7 @@ test(`${chalk.yellowBright("cancel addon EOC: entity-level addon cancel")}`, asy customer_id: customerId, entity_id: entityId, product_id: addon.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify state after cancel diff --git a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-consumable.test.ts b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-consumable.test.ts index b55075b14..953fa192d 100644 --- a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-consumable.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-consumable.test.ts @@ -89,7 +89,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle consumable: customer await autumnV1Beta.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling @@ -192,7 +192,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle consumable: entity - customer_id: customerId, entity_id: entityId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling on entity @@ -304,14 +304,14 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle consumable: two entit customer_id: customerId, entity_id: entity1Id, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); await autumnV1.subscriptions.update({ customer_id: customerId, entity_id: entity2Id, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify both are canceling @@ -441,7 +441,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle consumable: two entit customer_id: customerId, entity_id: entity1Id, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify entity 1 is canceling, entity 2 is still active @@ -556,7 +556,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle consumable: entity + s.track({ featureId: TestFeature.Messages, value: 250 }), s.updateSubscription({ productId: customerPro.id, - cancel: "end_of_cycle", + cancelAction: "cancel_end_of_cycle", }), ], }); @@ -688,12 +688,12 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle consumable: entity + s.track({ featureId: TestFeature.Messages, value: 250 }), s.updateSubscription({ productId: customerPro.id, - cancel: "end_of_cycle", + cancelAction: "cancel_end_of_cycle", }), s.updateSubscription({ entityIndex: 0, productId: entityPro.id, - cancel: "end_of_cycle", + cancelAction: "cancel_end_of_cycle", }), ], }); diff --git a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial-entities.test.ts b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial-entities.test.ts index a52b2f66d..6b44a48c3 100644 --- a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial-entities.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial-entities.test.ts @@ -87,7 +87,7 @@ test(`${chalk.yellowBright("cancel trial EOC entities: cancel one entity, other customer_id: customerId, entity_id: entity1Id, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify entity 1 is canceling @@ -177,7 +177,7 @@ test(`${chalk.yellowBright("cancel trial EOC entities: cancel both entities EOC" customer_id: customerId, entity_id: entity1Id, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Cancel entity 2 at end of cycle @@ -185,7 +185,7 @@ test(`${chalk.yellowBright("cancel trial EOC entities: cancel both entities EOC" customer_id: customerId, entity_id: entity2Id, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify both entities are canceling @@ -316,7 +316,7 @@ test(`${chalk.yellowBright("cancel trial EOC entities: cancel one, attach pro to customer_id: customerId, entity_id: entity1Id, product_id: premiumTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Attach pro to entity 2 (downgrade - scheduled) @@ -442,7 +442,7 @@ test(`${chalk.yellowBright("cancel trial EOC entities: cancel entity 1, attach p customer_id: customerId, entity_id: entity1Id, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify entity 1 is canceling diff --git a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial.test.ts b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial.test.ts index 6ea2c3719..17f851730 100644 --- a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial.test.ts @@ -81,7 +81,7 @@ test(`${chalk.yellowBright("cancel trial EOC: basic cancel, preview.next_cycle n const cancelParams = { customer_id: customerId, product_id: proTrial.id, - cancel: "end_of_cycle" as const, + cancel_action: "cancel_end_of_cycle" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -200,7 +200,7 @@ test(`${chalk.yellowBright("cancel trial EOC: with consumable messages, no overa await autumnV1.subscriptions.update({ customer_id: customerId, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify product is canceling @@ -315,7 +315,7 @@ test(`${chalk.yellowBright("cancel trial EOC: premium trial with pro scheduled, await autumnV1.subscriptions.update({ customer_id: customerId, product_id: premiumTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro scheduled is removed @@ -418,7 +418,7 @@ test(`${chalk.yellowBright("cancel trial EOC: with free default, free scheduled" await autumnV1.subscriptions.update({ customer_id: customerId, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling and free is scheduled diff --git a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle.test.ts b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle.test.ts index 256bf1905..b76b9fc1d 100644 --- a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle.test.ts @@ -84,7 +84,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: with default free pr await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling and free is scheduled @@ -194,7 +194,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: no default product") await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling @@ -323,7 +323,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: downgrade then cance await autumnV1.subscriptions.update({ customer_id: customerId, product_id: premium.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify state after cancel @@ -408,7 +408,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: downgrade then cance await autumnV1.subscriptions.update({ customer_id: customerId, product_id: premium.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify state after cancel @@ -495,7 +495,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: multi-interval produ await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling @@ -595,7 +595,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: then cancel immediat await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling and free is scheduled @@ -625,7 +625,7 @@ test.concurrent(`${chalk.yellowBright("cancel end of cycle: then cancel immediat await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify pro is gone and free is active immediately diff --git a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-addon.test.ts b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-addon.test.ts index 00d6b1d79..c5079b628 100644 --- a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-addon.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-addon.test.ts @@ -80,7 +80,7 @@ test.concurrent(`${chalk.yellowBright("cancel addon immediately: basic refund")} await autumnV1.subscriptions.update({ customer_id: customerId, product_id: recurringAddon.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for async invoice processing @@ -163,7 +163,7 @@ test.concurrent(`${chalk.yellowBright("cancel addon immediately: usage overage n await autumnV1.subscriptions.update({ customer_id: customerId, product_id: usageAddon.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for async invoice processing @@ -245,7 +245,7 @@ test.concurrent(`${chalk.yellowBright("cancel addon immediately: free addon canc await autumnV1.subscriptions.update({ customer_id: customerId, product_id: freeAddon.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for processing @@ -326,7 +326,7 @@ test.concurrent(`${chalk.yellowBright("cancel addon immediately: with scheduled await autumnV1.subscriptions.update({ customer_id: customerId, product_id: recurringAddon.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify: premium active, pro still scheduled, addon gone @@ -410,7 +410,7 @@ test.concurrent(`${chalk.yellowBright("cancel addon immediately: entity product customer_id: customerId, entity_id: entities[0].id, product_id: recurringAddon.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for processing @@ -482,14 +482,14 @@ test.concurrent(`${chalk.yellowBright("cancel addon immediately: cancel both pro await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Cancel add-on immediately await autumnV1.subscriptions.update({ customer_id: customerId, product_id: recurringAddon.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify: pro canceling, addon removed @@ -583,7 +583,7 @@ test.concurrent(`${chalk.yellowBright("cancel addon immediately: multiple addons await autumnV1.subscriptions.update({ customer_id: customerId, product_id: proAddon.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for processing @@ -659,7 +659,7 @@ test.concurrent(`${chalk.yellowBright("cancel addon immediately: one-time addon await autumnV1.subscriptions.update({ customer_id: customerId, product_id: oneTimeAddon.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for processing diff --git a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-billing.test.ts b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-billing.test.ts index 87006c483..a16821c4c 100644 --- a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-billing.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-billing.test.ts @@ -98,7 +98,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately billing: base + prepai const cancelParams = { customer_id: customerId, product_id: pro.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -199,7 +199,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately billing: base + alloca const cancelParams = { customer_id: customerId, product_id: pro.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -331,7 +331,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately billing: base + prepai const cancelParams = { customer_id: customerId, product_id: pro.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -460,7 +460,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately billing: base + alloca const cancelParams = { customer_id: customerId, product_id: pro.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); diff --git a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-consumable.test.ts b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-consumable.test.ts index 8982b2593..179e8fb7a 100644 --- a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-consumable.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-consumable.test.ts @@ -88,7 +88,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately consumable: customer - const cancelParams = { customer_id: customerId, product_id: pro.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -182,7 +182,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately consumable: entity - t customer_id: customerId, entity_id: entityId, product_id: pro.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -294,7 +294,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately consumable: two entiti customer_id: customerId, entity_id: entity1Id, product_id: pro.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify entity 1 product removed immediately, entity 2 still active @@ -392,7 +392,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately consumable: entity - w customer_id: customerId, entity_id: entityId, product_id: pro.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify product removed diff --git a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-trial-entities.test.ts b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-trial-entities.test.ts index b5b4cba8d..916f1e70d 100644 --- a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-trial-entities.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-trial-entities.test.ts @@ -83,7 +83,7 @@ test(`${chalk.yellowBright("cancel trial immediately entity: one entity, other s customer_id: customerId, entity_id: entities[1].id, product_id: proTrial.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for processing @@ -158,7 +158,7 @@ test(`${chalk.yellowBright("cancel trial immediately entity: both entities, subs customer_id: customerId, entity_id: entities[0].id, product_id: proTrial.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for processing @@ -182,7 +182,7 @@ test(`${chalk.yellowBright("cancel trial immediately entity: both entities, subs customer_id: customerId, entity_id: entities[1].id, product_id: proTrial.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for processing @@ -261,7 +261,7 @@ test(`${chalk.yellowBright("cancel trial immediately entity: mixed EOC + immedia customer_id: customerId, entity_id: entities[1].id, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify subscription still trialing @@ -278,7 +278,7 @@ test(`${chalk.yellowBright("cancel trial immediately entity: mixed EOC + immedia customer_id: customerId, entity_id: entities[2].id, product_id: proTrial.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for processing @@ -305,7 +305,7 @@ test(`${chalk.yellowBright("cancel trial immediately entity: mixed EOC + immedia customer_id: customerId, entity_id: entities[0].id, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify subscription is trialing but canceled (all remaining entities are EOC canceling) @@ -368,7 +368,7 @@ test(`${chalk.yellowBright("cancel trial immediately entity: cancel then re-atta customer_id: customerId, entity_id: entities[0].id, product_id: proTrial.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for processing diff --git a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-trial.test.ts b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-trial.test.ts index 4fdcd6cdc..fa0e6fbc6 100644 --- a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-trial.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately-trial.test.ts @@ -80,7 +80,7 @@ test(`${chalk.yellowBright("cancel trial immediately: basic cancel")}`, async () const cancelParams = { customer_id: customerId, product_id: proTrial.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); expect(preview.total).toBe(0); @@ -167,7 +167,7 @@ test(`${chalk.yellowBright("cancel trial immediately: with free default")}`, asy await autumnV1.subscriptions.update({ customer_id: customerId, product_id: proTrial.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify pro is removed and free is active @@ -243,7 +243,7 @@ test(`${chalk.yellowBright("cancel trial immediately: re-attach charges full pri await autumnV1.subscriptions.update({ customer_id: customerId, product_id: proTrial.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify product is removed @@ -349,7 +349,7 @@ test(`${chalk.yellowBright("cancel trial immediately: with scheduled downgrade, await autumnV1.subscriptions.update({ customer_id: customerId, product_id: premiumTrial.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify both products are removed @@ -432,7 +432,7 @@ test(`${chalk.yellowBright("cancel trial immediately: with consumable usage, no await autumnV1.subscriptions.update({ customer_id: customerId, product_id: proTrial.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Wait for any async processing diff --git a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately.test.ts b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately.test.ts index cb1e07e3b..76bf00c89 100644 --- a/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/immediately/cancel-immediately.test.ts @@ -64,7 +64,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately: free product")}`, asy await autumnV1.subscriptions.update({ customer_id: customerId, product_id: free.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify free is gone, no products attached @@ -120,7 +120,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately: default free product" await autumnV1.subscriptions.update({ customer_id: customerId, product_id: free.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify free is gone, no products attached (default does not auto-reattach when canceled) @@ -190,7 +190,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately: pro with default free const cancelParams = { customer_id: customerId, product_id: pro.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -269,7 +269,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately: pro without default") const cancelParams = { customer_id: customerId, product_id: pro.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -361,7 +361,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately: downgrade then cancel const cancelParams = { customer_id: customerId, product_id: premium.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -463,7 +463,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately: downgrade then cancel const cancelParams = { customer_id: customerId, product_id: premium.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -555,7 +555,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately: multi-interval produc const cancelParams = { customer_id: customerId, product_id: pro.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); @@ -649,7 +649,7 @@ test.concurrent(`${chalk.yellowBright("cancel immediately: one-off prepaid produ const cancelParams = { customer_id: customerId, product_id: oneOffProduct.id, - cancel: "immediately" as const, + cancel_action: "cancel_immediately" as const, }; const preview = await autumnV1.subscriptions.previewUpdate(cancelParams); diff --git a/server/tests/integration/billing/update-subscription/errors/cancel-errors.test.ts b/server/tests/integration/billing/update-subscription/errors/cancel-errors.test.ts index 2b6d2ced2..4b941d242 100644 --- a/server/tests/integration/billing/update-subscription/errors/cancel-errors.test.ts +++ b/server/tests/integration/billing/update-subscription/errors/cancel-errors.test.ts @@ -77,7 +77,7 @@ test.concurrent(`${chalk.yellowBright("error: cannot cancel scheduled product")} await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); }, }); @@ -120,7 +120,7 @@ test.concurrent(`${chalk.yellowBright("error: cancel non-existent product")}`, a await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); // Verify pro is gone @@ -138,7 +138,7 @@ test.concurrent(`${chalk.yellowBright("error: cancel non-existent product")}`, a await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "immediately", + cancel_action: "cancel_immediately", }); }, }); @@ -187,7 +187,7 @@ test.concurrent(`${chalk.yellowBright("error: cancel immediately with options")} await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "immediately", + cancel_action: "cancel_immediately", options: [{ feature_id: "messages", quantity: 200 }], }); }, @@ -232,7 +232,7 @@ test.concurrent(`${chalk.yellowBright("error: cancel immediately with version")} await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "immediately", + cancel_action: "cancel_immediately", version: 2, }); }, @@ -277,7 +277,7 @@ test.concurrent(`${chalk.yellowBright("error: cancel immediately with items")}`, await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "immediately", + cancel_action: "cancel_immediately", items: [messagesItem], }); }, @@ -319,7 +319,7 @@ test.concurrent(`${chalk.yellowBright("error: cannot cancel free product with en await autumnV1.subscriptions.update({ customer_id: customerId, product_id: free.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); }, }); @@ -362,7 +362,7 @@ test.concurrent(`${chalk.yellowBright("error: cannot cancel one-time product wit await autumnV1.subscriptions.update({ customer_id: customerId, product_id: oneTime.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); }, }); @@ -406,7 +406,7 @@ test.concurrent(`${chalk.yellowBright("error: cannot pass free_trial when cancel await autumnV1.subscriptions.update({ customer_id: customerId, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", free_trial: { length: 14, duration: FreeTrialDuration.Day, diff --git a/server/tests/integration/billing/update-subscription/uncancel/uncancel-addon.test.ts b/server/tests/integration/billing/update-subscription/uncancel/uncancel-addon.test.ts index a294150f7..dd66a2ba3 100644 --- a/server/tests/integration/billing/update-subscription/uncancel/uncancel-addon.test.ts +++ b/server/tests/integration/billing/update-subscription/uncancel/uncancel-addon.test.ts @@ -45,7 +45,10 @@ test.concurrent(`${chalk.yellowBright("uncancel addon: main active")}`, async () actions: [ s.attach({ productId: pro.id }), s.attach({ productId: addon.id }), - s.updateSubscription({ productId: addon.id, cancel: "end_of_cycle" }), + s.updateSubscription({ + productId: addon.id, + cancelAction: "cancel_end_of_cycle", + }), ], }); @@ -65,7 +68,7 @@ test.concurrent(`${chalk.yellowBright("uncancel addon: main active")}`, async () await autumnV1.subscriptions.update({ customer_id: customerId, product_id: addon.id, - cancel: null, + cancel_action: "uncancel", }); // Verify addon is now active, pro unchanged @@ -125,8 +128,14 @@ test.concurrent(`${chalk.yellowBright("uncancel main: addon canceling")}`, async actions: [ s.attach({ productId: pro.id }), s.attach({ productId: addon.id }), - s.updateSubscription({ productId: pro.id, cancel: "end_of_cycle" }), - s.updateSubscription({ productId: addon.id, cancel: "end_of_cycle" }), + s.updateSubscription({ + productId: pro.id, + cancelAction: "cancel_end_of_cycle", + }), + s.updateSubscription({ + productId: addon.id, + cancelAction: "cancel_end_of_cycle", + }), ], }); @@ -146,7 +155,7 @@ test.concurrent(`${chalk.yellowBright("uncancel main: addon canceling")}`, async await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", }); // Verify main is active, addon still canceling @@ -197,8 +206,14 @@ test.concurrent(`${chalk.yellowBright("uncancel both: main and addon")}`, async actions: [ s.attach({ productId: pro.id }), s.attach({ productId: addon.id }), - s.updateSubscription({ productId: pro.id, cancel: "end_of_cycle" }), - s.updateSubscription({ productId: addon.id, cancel: "end_of_cycle" }), + s.updateSubscription({ + productId: pro.id, + cancelAction: "cancel_end_of_cycle", + }), + s.updateSubscription({ + productId: addon.id, + cancelAction: "cancel_end_of_cycle", + }), ], }); @@ -218,12 +233,12 @@ test.concurrent(`${chalk.yellowBright("uncancel both: main and addon")}`, async await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", }); await autumnV1.subscriptions.update({ customer_id: customerId, product_id: addon.id, - cancel: null, + cancel_action: "uncancel", }); // Verify both are active @@ -293,7 +308,7 @@ test.concurrent(`${chalk.yellowBright("uncancel: separate subscriptions")}`, asy await autumnV1.subscriptions.update({ customer_id: customerId, product_id: addon.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify addon is canceling, pro still active @@ -312,7 +327,7 @@ test.concurrent(`${chalk.yellowBright("uncancel: separate subscriptions")}`, asy await autumnV1.subscriptions.update({ customer_id: customerId, product_id: addon.id, - cancel: null, + cancel_action: "uncancel", }); // Verify both active diff --git a/server/tests/integration/billing/update-subscription/uncancel/uncancel-basic.test.ts b/server/tests/integration/billing/update-subscription/uncancel/uncancel-basic.test.ts index d3bed9ff2..ace5a5b3d 100644 --- a/server/tests/integration/billing/update-subscription/uncancel/uncancel-basic.test.ts +++ b/server/tests/integration/billing/update-subscription/uncancel/uncancel-basic.test.ts @@ -20,7 +20,7 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts"; * Uncancel Basic Tests * * Core uncancel functionality and error cases. - * Tests: cancel: null via subscriptions.update() + * Tests: cancel_action: "uncancel" via subscriptions.update() */ // ═══════════════════════════════════════════════════════════════════════════════ @@ -48,7 +48,10 @@ test.concurrent(`${chalk.yellowBright("uncancel: with scheduled default product" ], actions: [ s.attach({ productId: pro.id }), - s.updateSubscription({ productId: pro.id, cancel: "end_of_cycle" }), + s.updateSubscription({ + productId: pro.id, + cancelAction: "cancel_end_of_cycle", + }), ], }); @@ -64,11 +67,11 @@ test.concurrent(`${chalk.yellowBright("uncancel: with scheduled default product" productId: free.id, }); - // Uncancel via subscriptions.update with cancel: null + // Uncancel via subscriptions.update with cancel_action: "uncancel" await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", }); // Verify pro is now active (not canceling) @@ -130,7 +133,7 @@ test.concurrent(`${chalk.yellowBright("uncancel: already active (no-op)")}`, asy await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", }); // Verify pro is still active @@ -193,7 +196,7 @@ test.concurrent(`${chalk.yellowBright("uncancel: preserves usage")}`, async () = await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); await new Promise((resolve) => setTimeout(resolve, 4000)); @@ -211,7 +214,7 @@ test.concurrent(`${chalk.yellowBright("uncancel: preserves usage")}`, async () = await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", }); await new Promise((resolve) => setTimeout(resolve, 4000)); @@ -289,7 +292,7 @@ test.concurrent(`${chalk.yellowBright("error: uncancel scheduled product")}`, as await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", }); }, }); @@ -320,7 +323,10 @@ test.concurrent(`${chalk.yellowBright("error: uncancel expired product")}`, asyn ], actions: [ s.attach({ productId: pro.id }), - s.updateSubscription({ productId: pro.id, cancel: "end_of_cycle" }), + s.updateSubscription({ + productId: pro.id, + cancelAction: "cancel_end_of_cycle", + }), ], }); @@ -349,7 +355,7 @@ test.concurrent(`${chalk.yellowBright("error: uncancel expired product")}`, asyn await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", }); }, }); diff --git a/server/tests/integration/billing/update-subscription/uncancel/uncancel-combined.test.ts b/server/tests/integration/billing/update-subscription/uncancel/uncancel-combined.test.ts index e268aff31..183e44056 100644 --- a/server/tests/integration/billing/update-subscription/uncancel/uncancel-combined.test.ts +++ b/server/tests/integration/billing/update-subscription/uncancel/uncancel-combined.test.ts @@ -23,7 +23,7 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts"; * Uncancel Combined Tests * * Tests for uncancel combined with other update operations. - * Tests: cancel: null + options, cancel: null + items, cancel: null + trialing + * Tests: cancel_action: "uncancel" + options, cancel_action: "uncancel" + items, cancel_action: "uncancel" + trialing */ // =============================================================================== @@ -91,7 +91,7 @@ test.concurrent(`${chalk.yellowBright("uncancel + update quantity")}`, async () await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling and free is scheduled @@ -111,14 +111,14 @@ test.concurrent(`${chalk.yellowBright("uncancel + update quantity")}`, async () const preview = await autumnV1.subscriptions.previewUpdate({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", options: [{ feature_id: TestFeature.Messages, quantity: newQuantity }], }); await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", options: [{ feature_id: TestFeature.Messages, quantity: newQuantity }], }); @@ -203,7 +203,7 @@ test.concurrent(`${chalk.yellowBright("uncancel + custom plan (items)")}`, async await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling and free is scheduled @@ -225,14 +225,14 @@ test.concurrent(`${chalk.yellowBright("uncancel + custom plan (items)")}`, async const preview = await autumnV1.subscriptions.previewUpdate({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", items: [updatedMessagesItem, newPriceItem], }); await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", items: [updatedMessagesItem, newPriceItem], }); @@ -325,7 +325,7 @@ test.concurrent(`${chalk.yellowBright("uncancel trialing product")}`, async () = await autumnV1.subscriptions.update({ customer_id: customerId, product_id: proTrial.id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify product is canceling (canceled flag set, but still trialing) @@ -340,7 +340,7 @@ test.concurrent(`${chalk.yellowBright("uncancel trialing product")}`, async () = await autumnV1.subscriptions.update({ customer_id: customerId, product_id: proTrial.id, - cancel: null, + cancel_action: "uncancel", }); // Verify product is still trialing and no longer canceling diff --git a/server/tests/integration/billing/update-subscription/uncancel/uncancel-edge-cases.test.ts b/server/tests/integration/billing/update-subscription/uncancel/uncancel-edge-cases.test.ts index 5522a9b5e..0c1ac840a 100644 --- a/server/tests/integration/billing/update-subscription/uncancel/uncancel-edge-cases.test.ts +++ b/server/tests/integration/billing/update-subscription/uncancel/uncancel-edge-cases.test.ts @@ -58,7 +58,10 @@ test.concurrent(`${chalk.yellowBright("uncancel + version upgrade")}`, async () ], actions: [ s.attach({ productId: pro.id }), - s.updateSubscription({ productId: pro.id, cancel: "end_of_cycle" }), + s.updateSubscription({ + productId: pro.id, + cancelAction: "cancel_end_of_cycle", + }), ], }); @@ -84,7 +87,7 @@ test.concurrent(`${chalk.yellowBright("uncancel + version upgrade")}`, async () await autumnV1.subscriptions.previewUpdate({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", version: 2, }); @@ -92,7 +95,7 @@ test.concurrent(`${chalk.yellowBright("uncancel + version upgrade")}`, async () await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", version: 2, }); @@ -150,7 +153,10 @@ test.concurrent(`${chalk.yellowBright("uncancel + add trial")}`, async () => { ], actions: [ s.attach({ productId: pro.id }), - s.updateSubscription({ productId: pro.id, cancel: "end_of_cycle" }), + s.updateSubscription({ + productId: pro.id, + cancelAction: "cancel_end_of_cycle", + }), ], }); @@ -171,7 +177,7 @@ test.concurrent(`${chalk.yellowBright("uncancel + add trial")}`, async () => { const preview = await autumnV1.subscriptions.previewUpdate({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", free_trial: { length: trialDays, duration: FreeTrialDuration.Day, @@ -194,7 +200,7 @@ test.concurrent(`${chalk.yellowBright("uncancel + add trial")}`, async () => { await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", free_trial: { length: trialDays, duration: FreeTrialDuration.Day, @@ -266,7 +272,10 @@ test.concurrent(`${chalk.yellowBright("remove trial while canceling: cancel pres ], actions: [ s.attach({ productId: pro.id }), - s.updateSubscription({ productId: pro.id, cancel: "end_of_cycle" }), + s.updateSubscription({ + productId: pro.id, + cancelAction: "cancel_end_of_cycle", + }), ], }); @@ -400,7 +409,7 @@ test.concurrent(`${chalk.yellowBright("uncancel during downgrade")}`, async () = await autumnV1.subscriptions.update({ customer_id: customerId, product_id: premium.id, - cancel: null, + cancel_action: "uncancel", }); // Verify Premium is active, Pro is deleted @@ -464,7 +473,10 @@ test.concurrent(`${chalk.yellowBright("uncancel + items + invoice mode")}`, asyn ], actions: [ s.attach({ productId: pro.id }), - s.updateSubscription({ productId: pro.id, cancel: "end_of_cycle" }), + s.updateSubscription({ + productId: pro.id, + cancelAction: "cancel_end_of_cycle", + }), ], }); @@ -488,7 +500,7 @@ test.concurrent(`${chalk.yellowBright("uncancel + items + invoice mode")}`, asyn const preview = await autumnV1.subscriptions.previewUpdate({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", items: [customMessagesItem, customPriceItem], invoice: true, finalize_invoice: true, @@ -501,7 +513,7 @@ test.concurrent(`${chalk.yellowBright("uncancel + items + invoice mode")}`, asyn const updateResult = await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, - cancel: null, + cancel_action: "uncancel", items: [customMessagesItem, customPriceItem], invoice: true, finalize_invoice: true, diff --git a/server/tests/integration/billing/update-subscription/uncancel/uncancel-entities.test.ts b/server/tests/integration/billing/update-subscription/uncancel/uncancel-entities.test.ts index c829909e0..1ff34d808 100644 --- a/server/tests/integration/billing/update-subscription/uncancel/uncancel-entities.test.ts +++ b/server/tests/integration/billing/update-subscription/uncancel/uncancel-entities.test.ts @@ -48,7 +48,7 @@ test.concurrent(`${chalk.yellowBright("uncancel entity: other entity active")}`, customer_id: customerId, product_id: pro.id, entity_id: entities[0].id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify entity 1 is canceling, entity 2 is active @@ -74,7 +74,7 @@ test.concurrent(`${chalk.yellowBright("uncancel entity: other entity active")}`, customer_id: customerId, product_id: pro.id, entity_id: entities[0].id, - cancel: null, + cancel_action: "uncancel", }); // Verify both entities are now active @@ -141,13 +141,13 @@ test.concurrent(`${chalk.yellowBright("uncancel: all entities")}`, async () => { customer_id: customerId, product_id: pro.id, entity_id: entities[0].id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, entity_id: entities[1].id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify both are canceling @@ -173,13 +173,13 @@ test.concurrent(`${chalk.yellowBright("uncancel: all entities")}`, async () => { customer_id: customerId, product_id: pro.id, entity_id: entities[0].id, - cancel: null, + cancel_action: "uncancel", }); await autumnV1.subscriptions.update({ customer_id: customerId, product_id: pro.id, entity_id: entities[1].id, - cancel: null, + cancel_action: "uncancel", }); // Verify both are active @@ -237,7 +237,7 @@ test.concurrent(`${chalk.yellowBright("uncancel entity: no scheduled default for customer_id: customerId, product_id: pro.id, entity_id: entities[0].id, - cancel: "end_of_cycle", + cancel_action: "cancel_end_of_cycle", }); // Verify pro is canceling - entities do NOT get default products scheduled @@ -255,7 +255,7 @@ test.concurrent(`${chalk.yellowBright("uncancel entity: no scheduled default for customer_id: customerId, product_id: pro.id, entity_id: entities[0].id, - cancel: null, + cancel_action: "uncancel", }); // Verify pro is active diff --git a/server/tests/scenarios/update-subscription/uncancel-scenario.test.ts b/server/tests/scenarios/update-subscription/uncancel-scenario.test.ts index eae018297..7531bb51d 100644 --- a/server/tests/scenarios/update-subscription/uncancel-scenario.test.ts +++ b/server/tests/scenarios/update-subscription/uncancel-scenario.test.ts @@ -5,12 +5,12 @@ import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; import chalk from "chalk"; /** - * Uncancel Tests (cancel: null) + * Uncancel Tests (cancel_action: "uncancel") * * Tests the uncancel functionality which removes a scheduled cancellation * from a subscription via the update subscription API. * - * Usage: subscriptions.update({ customer_id, product_id, cancel: null }) + * Usage: subscriptions.update({ customer_id, product_id, cancel_action: "uncancel" }) */ test(`${chalk.yellowBright("uncancel: basic - canceling product → uncancel → active")}`, async () => { @@ -27,10 +27,10 @@ test(`${chalk.yellowBright("uncancel: basic - canceling product → uncancel → actions: [s.attach({ productId: pro.id }), s.cancel({ productId: pro.id })], }); - // Uncancel via subscriptions.update with cancel: null + // Uncancel via subscriptions.update with cancel_action: "uncancel" // await autumnV1.subscriptions.update({ // customer_id: customerId, // product_id: pro.id, - // cancel: null, + // cancel_action: "uncancel", // }); }); diff --git a/server/tests/utils/testInitUtils/initScenario.ts b/server/tests/utils/testInitUtils/initScenario.ts index 8c7cbe01f..208dc2e0a 100644 --- a/server/tests/utils/testInitUtils/initScenario.ts +++ b/server/tests/utils/testInitUtils/initScenario.ts @@ -76,7 +76,7 @@ type UpdateSubscriptionAction = { type: "updateSubscription"; productId: string; entityIndex?: number; - cancel?: "end_of_cycle" | "immediately"; + cancelAction?: "cancel_end_of_cycle" | "cancel_immediately" | "uncancel"; items?: ProductItem[]; }; @@ -413,21 +413,21 @@ const track = ({ * Update a subscription (e.g., cancel end of cycle, add items). * @param productId - The product ID (without prefix) * @param entityIndex - Optional entity index (0-based) for entity-level subscription - * @param cancel - Cancel mode: "end_of_cycle" or "immediately" + * @param cancelAction - Cancel action: "cancel_end_of_cycle", "cancel_immediately", or "uncancel" * @param items - Optional items to add/update on the subscription - * @example s.updateSubscription({ productId: "pro", cancel: "end_of_cycle" }) // customer-level - * @example s.updateSubscription({ productId: "pro", entityIndex: 0, cancel: "end_of_cycle" }) // entity-level + * @example s.updateSubscription({ productId: "pro", cancelAction: "cancel_end_of_cycle" }) // customer-level + * @example s.updateSubscription({ productId: "pro", entityIndex: 0, cancelAction: "cancel_end_of_cycle" }) // entity-level * @example s.updateSubscription({ productId: "pro", items: [consumableItem] }) // add items */ const updateSubscription = ({ productId, entityIndex, - cancel, + cancelAction, items, }: { productId: string; entityIndex?: number; - cancel?: "end_of_cycle" | "immediately"; + cancelAction?: "cancel_end_of_cycle" | "cancel_immediately" | "uncancel"; items?: ProductItem[]; }): ConfigFn => { return (config) => ({ @@ -438,7 +438,7 @@ const updateSubscription = ({ type: "updateSubscription" as const, productId, entityIndex, - cancel, + cancelAction, items, }, ], @@ -900,7 +900,7 @@ export async function initScenario({ customer_id: customerId, product_id: prefixedProductId, entity_id: entityId, - cancel: action.cancel, + cancel_action: action.cancelAction, items: action.items, }); } else if (action.type === "advanceToNextInvoice") { diff --git a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts index 7da7a9524..da911deb6 100644 --- a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts +++ b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts @@ -3,7 +3,7 @@ import { nullish } from "@utils/utils"; import { z } from "zod/v4"; import { FeatureOptionsSchema } from "../../../models/cusProductModels/cusProductModels"; import { ProductItemSchema } from "../../../models/productV2Models/productItemModels/productItemModels"; -import { CancelModeSchema } from "../../common/cancelMode"; +import { CancelActionSchema } from "../../common/cancelMode"; import { CustomerDataSchema } from "../../common/customerData"; import { EntityDataSchema } from "../../models"; @@ -26,8 +26,8 @@ export const ExtUpdateSubscriptionV0ParamsSchema = z.object({ items: z.array(ProductItemSchema).optional(), // used for custom configuration of a plan (in api - plan_override) free_trial: CreateFreeTrialSchema.nullable().optional(), - // Cancel: 'immediately' | 'end_of_cycle' | null (null = uncancel) - cancel: z.enum(["immediately", "end_of_cycle"]).nullable().optional(), + // Cancel action: 'cancel_immediately' | 'cancel_end_of_cycle' | 'uncancel' + cancel_action: CancelActionSchema.optional(), // Proration: defaults to true (charge for prorations). Set to false to skip proration charges. prorate_billing: z.boolean().optional(), @@ -70,7 +70,7 @@ export const UpdateSubscriptionV0ParamsSchema = }) .refine( (data) => { - if (data.cancel !== "immediately") return true; + if (data.cancel_action !== "cancel_immediately") return true; const forbiddenFields = [ "options", @@ -82,18 +82,19 @@ export const UpdateSubscriptionV0ParamsSchema = }, { message: - "Cannot pass options, items, version, or free_trial when cancel is 'immediately'. Immediate cancellation only processes a prorated refund.", + "Cannot pass options, items, version, or free_trial when cancel_action is 'cancel_immediately'. Immediate cancellation only processes a prorated refund.", }, ) .refine( (data) => { - if (data.cancel !== "end_of_cycle") return true; + if (data.cancel_action !== "cancel_end_of_cycle") return true; - // Cannot pass free_trial when cancel is 'end_of_cycle' + // Cannot pass free_trial when cancel_action is 'cancel_end_of_cycle' return data.free_trial === undefined; }, { - message: "Cannot pass free_trial when cancel is 'end_of_cycle'.", + message: + "Cannot pass free_trial when cancel_action is 'cancel_end_of_cycle'.", }, ); diff --git a/shared/api/common/cancelMode.ts b/shared/api/common/cancelMode.ts index a71540706..2d518a176 100644 --- a/shared/api/common/cancelMode.ts +++ b/shared/api/common/cancelMode.ts @@ -1,12 +1,12 @@ import { z } from "zod/v4"; /** - * Mode for canceling a subscription via update subscription API + * Action for canceling a subscription via update subscription API */ -export const CancelModeSchema = z.enum([ - "immediately", - "end_of_cycle", +export const CancelActionSchema = z.enum([ + "cancel_immediately", + "cancel_end_of_cycle", "uncancel", ]); -export type CancelMode = z.infer; +export type CancelAction = z.infer; diff --git a/shared/utils/cusProductUtils/classifyCustomerProduct/classifyCustomerProduct.ts b/shared/utils/cusProductUtils/classifyCustomerProduct/classifyCustomerProduct.ts index 27f8c69bf..cf2a94117 100644 --- a/shared/utils/cusProductUtils/classifyCustomerProduct/classifyCustomerProduct.ts +++ b/shared/utils/cusProductUtils/classifyCustomerProduct/classifyCustomerProduct.ts @@ -214,3 +214,10 @@ export const isCustomerProductEntityScoped = ( if (!customerProduct) return false; return notNullish(customerProduct.internal_entity_id); }; + +export const isCustomerProductCustomerScoped = ( + customerProduct?: FullCusProduct, +) => { + if (!customerProduct) return false; + return nullish(customerProduct.internal_entity_id); +}; diff --git a/shared/utils/cusProductUtils/classifyCustomerProduct/cpBuilder.ts b/shared/utils/cusProductUtils/classifyCustomerProduct/cpBuilder.ts index 12afc11cf..11b3653cd 100644 --- a/shared/utils/cusProductUtils/classifyCustomerProduct/cpBuilder.ts +++ b/shared/utils/cusProductUtils/classifyCustomerProduct/cpBuilder.ts @@ -8,6 +8,7 @@ import { isCusProductOnEntity, isCustomerProductAddOn, isCustomerProductCanceling, + isCustomerProductCustomerScoped, isCustomerProductFree, isCustomerProductMain, isCustomerProductOneOff, @@ -244,6 +245,12 @@ class CustomerProductChecker { this.pendingPredicates.push((cp) => cp.product.group === productGroup); return this; } + + /** Product is customer-scoped (not assigned to any entity) */ + customerScoped() { + this.pendingPredicates.push(isCustomerProductCustomerScoped); + return this; + } } /**