fix: patch schedules
This commit is contained in:
1
bun.lock
1
bun.lock
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "autumn",
|
||||
|
||||
@@ -1,12 +1,100 @@
|
||||
import type {
|
||||
AttachParamsV1,
|
||||
BillingContextOverride,
|
||||
Entitlement,
|
||||
FullCustomer,
|
||||
FullProduct,
|
||||
MultiAttachParamsV0,
|
||||
UpdateSubscriptionV1Params,
|
||||
} from "@autumn/shared";
|
||||
import {
|
||||
BillingVersion,
|
||||
cusProductToProduct,
|
||||
isCustomizePlanPatchStyle,
|
||||
type PatchContext,
|
||||
} from "@autumn/shared";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { setupPatchContext } from "@/internal/billing/v2/setup/patch";
|
||||
import { initFullCustomerProduct } from "@/internal/billing/v2/utils/initFullCustomerProduct/initFullCustomerProduct";
|
||||
import { getEntsWithFeature } from "@/internal/products/entitlements/entitlementUtils";
|
||||
import { ProductService } from "@/internal/products/ProductService";
|
||||
import { setupCustomFullProduct } from "../../../setup/setupCustomFullProduct";
|
||||
|
||||
const patchContextToFullProduct = ({
|
||||
ctx,
|
||||
patchContext,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
patchContext: PatchContext;
|
||||
}): FullProduct => {
|
||||
const fullProduct = cusProductToProduct({
|
||||
cusProduct: patchContext.finalCustomerProduct,
|
||||
});
|
||||
|
||||
return {
|
||||
...fullProduct,
|
||||
prices: [...fullProduct.prices, ...patchContext.customPrices],
|
||||
entitlements: getEntsWithFeature({
|
||||
ents: [
|
||||
...fullProduct.entitlements,
|
||||
...(patchContext.customEntitlements as Entitlement[]),
|
||||
],
|
||||
features: ctx.features,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const setupAttachPatchProductContext = ({
|
||||
ctx,
|
||||
params,
|
||||
fullCustomer,
|
||||
fullProduct,
|
||||
currentEpochMs,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
params: AttachParamsV1 | MultiAttachParamsV0["plans"][number];
|
||||
fullCustomer: FullCustomer;
|
||||
fullProduct: FullProduct;
|
||||
currentEpochMs?: number;
|
||||
}) => {
|
||||
if (!isCustomizePlanPatchStyle(params.customize)) return undefined;
|
||||
|
||||
const baseCustomerProduct = initFullCustomerProduct({
|
||||
ctx,
|
||||
initContext: {
|
||||
fullCustomer,
|
||||
fullProduct,
|
||||
featureQuantities: [],
|
||||
resetCycleAnchor: currentEpochMs ?? Date.now(),
|
||||
freeTrial: null,
|
||||
now: currentEpochMs ?? Date.now(),
|
||||
billingVersion: BillingVersion.V2,
|
||||
},
|
||||
});
|
||||
|
||||
const patchParams: UpdateSubscriptionV1Params = {
|
||||
customer_id: fullCustomer.id ?? fullCustomer.internal_id,
|
||||
plan_id: params.plan_id,
|
||||
customize: params.customize,
|
||||
version: params.version,
|
||||
};
|
||||
|
||||
const patchContext = setupPatchContext({
|
||||
ctx,
|
||||
params: patchParams,
|
||||
customerProduct: baseCustomerProduct,
|
||||
fullProduct,
|
||||
});
|
||||
|
||||
if (!patchContext) return undefined;
|
||||
|
||||
return {
|
||||
fullProduct: patchContextToFullProduct({ ctx, patchContext }),
|
||||
customPrices: patchContext.customPrices,
|
||||
customEnts: patchContext.customEntitlements,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads the product being attached, handling version and custom items params.
|
||||
*/
|
||||
@@ -14,10 +102,14 @@ export const setupAttachProductContext = async ({
|
||||
ctx,
|
||||
params,
|
||||
contextOverride = {},
|
||||
fullCustomer,
|
||||
currentEpochMs,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
params: AttachParamsV1 | MultiAttachParamsV0["plans"][number];
|
||||
contextOverride?: BillingContextOverride;
|
||||
fullCustomer?: FullCustomer;
|
||||
currentEpochMs?: number;
|
||||
}) => {
|
||||
const { productContext } = contextOverride;
|
||||
if (productContext) return productContext;
|
||||
@@ -35,6 +127,20 @@ export const setupAttachProductContext = async ({
|
||||
logger: ctx.logger,
|
||||
});
|
||||
|
||||
if (fullCustomer) {
|
||||
const patchProductContext = setupAttachPatchProductContext({
|
||||
ctx,
|
||||
params,
|
||||
fullCustomer,
|
||||
fullProduct,
|
||||
currentEpochMs,
|
||||
});
|
||||
|
||||
if (patchProductContext) {
|
||||
return patchProductContext;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Handle custom items if provided
|
||||
const {
|
||||
fullProduct: customFullProduct,
|
||||
|
||||
@@ -14,8 +14,8 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { setupAttachProductContext } from "@/internal/billing/v2/actions/attach/setup/setupAttachProductContext";
|
||||
import { setupAttachTransitionContext } from "@/internal/billing/v2/actions/attach/setup/setupAttachTransitionContext";
|
||||
import { setupStripeBillingContext } from "@/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext";
|
||||
import { setupBillingCycleAnchor } from "@/internal/billing/v2/setup/setupBillingCycleAnchor";
|
||||
import { fetchStoredLineItemsForSubscriptionBilling } from "@/internal/billing/v2/setup/fetchStoredLineItemsForSubscriptionBilling";
|
||||
import { setupBillingCycleAnchor } from "@/internal/billing/v2/setup/setupBillingCycleAnchor";
|
||||
import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext";
|
||||
import { setupFullCustomerContext } from "@/internal/billing/v2/setup/setupFullCustomerContext";
|
||||
import { setupInvoiceModeContext } from "@/internal/billing/v2/setup/setupInvoiceModeContext";
|
||||
@@ -131,6 +131,7 @@ export const setupImmediateMultiProductBillingContext = async ({
|
||||
customize: plan.customize,
|
||||
version: plan.version,
|
||||
},
|
||||
fullCustomer,
|
||||
});
|
||||
|
||||
const { currentCustomerProduct, scheduledCustomerProduct } =
|
||||
|
||||
@@ -114,6 +114,8 @@ export const setupCreateScheduleBillingContext = async ({
|
||||
const scheduledPhaseContexts = await setupScheduledProductsContext({
|
||||
ctx,
|
||||
phases: futurePhases,
|
||||
fullCustomer: billingContext.fullCustomer,
|
||||
currentEpochMs: billingContext.currentEpochMs,
|
||||
});
|
||||
|
||||
const scheduledCustomPrices = scheduledPhaseContexts.flatMap((phase) =>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
CreateScheduleParamsV0,
|
||||
FullCustomer,
|
||||
ScheduledPhaseContext,
|
||||
} from "@autumn/shared";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
@@ -11,9 +12,13 @@ import { validateCreateSchedulePhasePlans } from "../errors/validateCreateSchedu
|
||||
export const setupScheduledProductsContext = async ({
|
||||
ctx,
|
||||
phases,
|
||||
fullCustomer,
|
||||
currentEpochMs,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
phases: CreateScheduleParamsV0["phases"][number][];
|
||||
fullCustomer: FullCustomer;
|
||||
currentEpochMs: number;
|
||||
}): Promise<ScheduledPhaseContext[]> =>
|
||||
Promise.all(
|
||||
phases.map(async (phase, index) => {
|
||||
@@ -28,6 +33,8 @@ export const setupScheduledProductsContext = async ({
|
||||
} = await setupAttachProductContext({
|
||||
ctx,
|
||||
params: plan,
|
||||
fullCustomer,
|
||||
currentEpochMs,
|
||||
});
|
||||
|
||||
const featureQuantities = setupFeatureQuantitiesContext({
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import {
|
||||
BillingMethod,
|
||||
CusProductStatus,
|
||||
customerEntitlements,
|
||||
customerProducts,
|
||||
ms,
|
||||
schedulePhases,
|
||||
} from "@autumn/shared";
|
||||
import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect";
|
||||
import { TestFeature } from "@tests/setup/v2Features";
|
||||
import { items } from "@tests/utils/fixtures/items";
|
||||
import { itemsV2 } from "@tests/utils/fixtures/itemsV2";
|
||||
@@ -16,10 +18,14 @@ import chalk from "chalk";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
getCustomerProductEntitlementBalances,
|
||||
getCustomerProductFeaturePriceAmounts,
|
||||
getCustomerProductPriceAmounts,
|
||||
getRequiredScheduleId,
|
||||
} from "../utils/createScheduleTestHelpers";
|
||||
|
||||
// Contract: V2.2 schedule customize accepts PATCH-style add_items/remove_items.
|
||||
// Contract: patched items/prices apply to immediate and future cusProducts, including Stripe.
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("create-schedule: preserves feature quantity options on created customer products")}`,
|
||||
async () => {
|
||||
@@ -76,6 +82,180 @@ test.concurrent(
|
||||
},
|
||||
);
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("create-schedule: patch customize applies to immediate customer products and Stripe")}`,
|
||||
async () => {
|
||||
const base = products.base({
|
||||
id: "create-schedule-patch-immediate",
|
||||
items: [
|
||||
items.monthlyPrice(),
|
||||
items.monthlyMessages({ includedUsage: 100 }),
|
||||
items.monthlyWords({ includedUsage: 50 }),
|
||||
],
|
||||
});
|
||||
|
||||
const { customerId, autumnV2_2, ctx } = await initScenario({
|
||||
customerId: "create-schedule-patch-immediate",
|
||||
setup: [
|
||||
s.customer({ paymentMethod: "success" }),
|
||||
s.products({ list: [base] }),
|
||||
],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const response = await autumnV2_2.billing.createSchedule({
|
||||
customer_id: customerId,
|
||||
phases: [
|
||||
{
|
||||
starts_at: Date.now(),
|
||||
plans: [
|
||||
{
|
||||
plan_id: base.id,
|
||||
customize: {
|
||||
price: itemsV2.monthlyPrice({ amount: 42 }),
|
||||
remove_items: [{ feature_id: TestFeature.Messages }],
|
||||
add_items: [itemsV2.dashboard()],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const customerProductId = response.phases[0]!.customer_product_ids[0]!;
|
||||
const customerProduct = await ctx.db.query.customerProducts.findFirst({
|
||||
where: eq(customerProducts.id, customerProductId),
|
||||
});
|
||||
|
||||
expect(customerProduct?.is_custom).toBe(true);
|
||||
expect(
|
||||
await getCustomerProductPriceAmounts({ ctx, customerProductId }),
|
||||
).toEqual([42]);
|
||||
expect(
|
||||
await getCustomerProductEntitlementBalances({
|
||||
ctx,
|
||||
customerProductId,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ feature_id: TestFeature.Words, balance: 50 },
|
||||
{ feature_id: TestFeature.Dashboard, balance: 0 },
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
await getCustomerProductEntitlementBalances({
|
||||
ctx,
|
||||
customerProductId,
|
||||
}),
|
||||
).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ feature_id: TestFeature.Messages }),
|
||||
]),
|
||||
);
|
||||
|
||||
await expectStripeSubscriptionCorrect({ ctx, customerId });
|
||||
},
|
||||
);
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("create-schedule: patch customize applies to future customer products and Stripe schedule")}`,
|
||||
async () => {
|
||||
const base = products.base({
|
||||
id: "create-schedule-patch-future",
|
||||
items: [
|
||||
items.monthlyPrice(),
|
||||
items.monthlyMessages({ includedUsage: 100 }),
|
||||
items.prepaid({
|
||||
featureId: TestFeature.Words,
|
||||
price: 10,
|
||||
billingUnits: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { customerId, autumnV2_2, ctx } = await initScenario({
|
||||
customerId: "create-schedule-patch-future",
|
||||
setup: [
|
||||
s.customer({ paymentMethod: "success" }),
|
||||
s.products({ list: [base] }),
|
||||
],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
const response = await autumnV2_2.billing.createSchedule({
|
||||
customer_id: customerId,
|
||||
phases: [
|
||||
{
|
||||
starts_at: now,
|
||||
plans: [{ plan_id: base.id }],
|
||||
},
|
||||
{
|
||||
starts_at: now + ms.days(30),
|
||||
plans: [
|
||||
{
|
||||
plan_id: base.id,
|
||||
customize: {
|
||||
remove_items: [
|
||||
{
|
||||
feature_id: TestFeature.Words,
|
||||
billing_method: BillingMethod.Prepaid,
|
||||
},
|
||||
],
|
||||
add_items: [
|
||||
itemsV2.prepaidWords({ amount: 7, billingUnits: 100 }),
|
||||
],
|
||||
},
|
||||
feature_quantities: [
|
||||
{
|
||||
feature_id: TestFeature.Words,
|
||||
quantity: 300,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const futureCustomerProductId =
|
||||
response.phases[1]!.customer_product_ids[0]!;
|
||||
const futureCustomerProduct = await ctx.db.query.customerProducts.findFirst(
|
||||
{
|
||||
where: eq(customerProducts.id, futureCustomerProductId),
|
||||
},
|
||||
);
|
||||
|
||||
expect(futureCustomerProduct?.is_custom).toBe(true);
|
||||
expect(
|
||||
await getCustomerProductPriceAmounts({
|
||||
ctx,
|
||||
customerProductId: futureCustomerProductId,
|
||||
}),
|
||||
).toEqual([20]);
|
||||
expect(
|
||||
await getCustomerProductFeaturePriceAmounts({
|
||||
ctx,
|
||||
customerProductId: futureCustomerProductId,
|
||||
featureId: TestFeature.Words,
|
||||
}),
|
||||
).toEqual([7]);
|
||||
expect(
|
||||
await getCustomerProductEntitlementBalances({
|
||||
ctx,
|
||||
customerProductId: futureCustomerProductId,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ feature_id: TestFeature.Messages, balance: 100 },
|
||||
{ feature_id: TestFeature.Words, balance: 300 },
|
||||
]),
|
||||
);
|
||||
|
||||
await expectStripeSubscriptionCorrect({ ctx, customerId });
|
||||
},
|
||||
);
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("create-schedule: preserves customize.items on created customer products")}`,
|
||||
async () => {
|
||||
|
||||
@@ -51,6 +51,40 @@ export const getCustomerProductPriceAmounts = async ({
|
||||
.filter((amount): amount is number => typeof amount === "number")
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
export const getCustomerProductFeaturePriceAmounts = async ({
|
||||
ctx,
|
||||
customerProductId,
|
||||
featureId,
|
||||
}: {
|
||||
ctx: Ctx;
|
||||
customerProductId: string;
|
||||
featureId: string;
|
||||
}) =>
|
||||
(
|
||||
await ctx.db
|
||||
.select({ config: prices.config })
|
||||
.from(customerPrices)
|
||||
.innerJoin(prices, eq(customerPrices.price_id, prices.id))
|
||||
.where(eq(customerPrices.customer_product_id, customerProductId))
|
||||
)
|
||||
.flatMap((row) => {
|
||||
const config = row.config;
|
||||
if (
|
||||
!config ||
|
||||
!("feature_id" in config) ||
|
||||
config.feature_id !== featureId ||
|
||||
!("usage_tiers" in config) ||
|
||||
!Array.isArray(config.usage_tiers)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return config.usage_tiers
|
||||
.map((tier) => tier.amount)
|
||||
.filter((amount): amount is number => typeof amount === "number");
|
||||
})
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
export const getCustomerProductEntitlementBalances = async ({
|
||||
ctx,
|
||||
customerProductId,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PlanItemFilterSchema } from "@api/products/items/filter/planItemFilter"
|
||||
import { ResetInterval } from "@models/productModels/intervals/resetInterval";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
/** Deprecated: use remove_items and add_items to replace plan items. */
|
||||
export const UpdatePlanItemParamsV1Schema = z
|
||||
.object({
|
||||
filter: PlanItemFilterSchema.meta({
|
||||
@@ -19,15 +20,17 @@ export const UpdatePlanItemParamsV1Schema = z
|
||||
description:
|
||||
"Override the matched item's reset interval. Use 'one_off' for non-resetting balances.",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
title: "UpdatePlanItem",
|
||||
description:
|
||||
"Deprecated. Use remove_items and add_items to replace plan items.",
|
||||
deprecated: true,
|
||||
});
|
||||
})
|
||||
.meta({
|
||||
title: "UpdatePlanItem",
|
||||
description:
|
||||
"Deprecated. Use remove_items and add_items to replace plan items.",
|
||||
deprecated: true,
|
||||
});
|
||||
|
||||
export type UpdatePlanItemParamsV1 = z.infer<typeof UpdatePlanItemParamsV1Schema>;
|
||||
export type UpdatePlanItemParamsV1 = z.infer<
|
||||
typeof UpdatePlanItemParamsV1Schema
|
||||
>;
|
||||
|
||||
export const CustomizePlanV1Schema = z
|
||||
.object({
|
||||
@@ -35,22 +38,22 @@ export const CustomizePlanV1Schema = z
|
||||
description:
|
||||
"Override the base price of the plan. Pass null to remove the base price.",
|
||||
}),
|
||||
items: z.array(CreatePlanItemParamsV1Schema).optional().meta({
|
||||
description:
|
||||
"Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items.",
|
||||
}),
|
||||
items: z.array(CreatePlanItemParamsV1Schema).optional().meta({
|
||||
description:
|
||||
"Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items.",
|
||||
}),
|
||||
add_items: z.array(CreatePlanItemParamsV1Schema).optional().meta({
|
||||
description: "Items to add to the plan.",
|
||||
}),
|
||||
remove_items: z.array(PlanItemFilterSchema).optional().meta({
|
||||
description: "Filters selecting items to remove from the plan.",
|
||||
}),
|
||||
update_items: z.array(UpdatePlanItemParamsV1Schema).optional().meta({
|
||||
description:
|
||||
"Deprecated. Use remove_items and add_items to replace matched plan items.",
|
||||
internal: true,
|
||||
deprecated: true,
|
||||
}),
|
||||
update_items: z.array(UpdatePlanItemParamsV1Schema).optional().meta({
|
||||
description:
|
||||
"Deprecated. Use remove_items and add_items to replace matched plan items.",
|
||||
internal: true,
|
||||
deprecated: true,
|
||||
}),
|
||||
free_trial: FreeTrialParamsV1Schema.nullable().optional().meta({
|
||||
description:
|
||||
"Override the plan's default free trial. Pass an object to set a custom trial, or null to remove the trial entirely.",
|
||||
@@ -64,10 +67,10 @@ export const CustomizePlanV1Schema = z
|
||||
data.add_items !== undefined ||
|
||||
data.remove_items !== undefined ||
|
||||
data.update_items !== undefined,
|
||||
{
|
||||
message:
|
||||
"When using customize, at least one of price, items, add_items, remove_items, deprecated update_items, or free_trial must be provided",
|
||||
},
|
||||
{
|
||||
message:
|
||||
"When using customize, at least one of price, items, add_items, remove_items, deprecated update_items, or free_trial must be provided",
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
@@ -77,10 +80,10 @@ export const CustomizePlanV1Schema = z
|
||||
data.remove_items !== undefined ||
|
||||
data.update_items !== undefined)
|
||||
),
|
||||
{
|
||||
message:
|
||||
"customize.items (PUT-style) cannot be combined with add_items / remove_items / deprecated update_items (PATCH-style); pick one approach",
|
||||
},
|
||||
{
|
||||
message:
|
||||
"customize.items (PUT-style) cannot be combined with add_items / remove_items / deprecated update_items (PATCH-style); pick one approach",
|
||||
},
|
||||
)
|
||||
.meta({
|
||||
title: "CustomizePlan",
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
import { FeatureQuantityParamsV0Schema } from "@api/billing/common/featureQuantity/featureQuantityParamsV0";
|
||||
import { InvoiceModeParamsSchema } from "@api/billing/common/invoiceModeParams";
|
||||
import { RedirectModeSchema } from "@api/billing/common/redirectMode";
|
||||
import { BasePriceParamsSchema } from "@api/products/components/basePrice/basePrice";
|
||||
import { CreatePlanItemParamsV1Schema } from "@api/products/items/crud/createPlanItemParamsV1";
|
||||
import { z } from "zod/v4";
|
||||
import { AttachDiscountSchema } from "../attachV2/attachDiscount";
|
||||
import { BillingBehaviorSchema } from "../common/billingBehavior";
|
||||
import { BillingCycleAnchorSchema } from "../common/billingCycleAnchor";
|
||||
import { CustomizePlanV1Schema } from "../common/customizePlan/customizePlanV1";
|
||||
|
||||
const CreateScheduleCustomizePlanSchema = z
|
||||
.object({
|
||||
price: BasePriceParamsSchema.nullable().optional().meta({
|
||||
description:
|
||||
"Override the base price of the plan. Pass null to remove the base price.",
|
||||
}),
|
||||
items: z.array(CreatePlanItemParamsV1Schema).optional().meta({
|
||||
description: "Override the items in the plan.",
|
||||
}),
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(customize) =>
|
||||
customize.items !== undefined || customize.price !== undefined,
|
||||
{
|
||||
message: "When using customize, either items or price must be provided",
|
||||
},
|
||||
);
|
||||
const CreateScheduleCustomizePlanSchema = CustomizePlanV1Schema.omit({
|
||||
free_trial: true,
|
||||
});
|
||||
|
||||
export const CreateSchedulePlanSchema = z.object({
|
||||
plan_id: z.string().meta({
|
||||
@@ -39,7 +23,7 @@ export const CreateSchedulePlanSchema = z.object({
|
||||
}),
|
||||
customize: CreateScheduleCustomizePlanSchema.optional().meta({
|
||||
description:
|
||||
"Customize the plan to schedule. Can override the price, items, or both.",
|
||||
"Customize the plan to schedule. Can override price, replace items, or patch items with add_items, remove_items, and update_items.",
|
||||
}),
|
||||
subscription_id: z.string().optional().meta({
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user