updated cancel action param

This commit is contained in:
John Yeo
2026-01-23 08:43:48 +00:00
parent c503f0072c
commit 3f4247e453
46 changed files with 282 additions and 265 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ export const handleUncancelErrors = ({
}: {
billingContext: UpdateSubscriptionBillingContext;
}) => {
if (billingContext.cancelMode !== "uncancel") {
if (billingContext.cancelAction !== "uncancel") {
return;
}

View File

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

View File

@@ -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<FullProduct | undefined> => {
// 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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") {

View File

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

View File

@@ -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<typeof CancelModeSchema>;
export type CancelAction = z.infer<typeof CancelActionSchema>;

View File

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

View File

@@ -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;
}
}
/**