diff --git a/scripts/testGroups/all.sh b/scripts/testGroups/all.sh index 0984ac5f4..53b5bf62a 100755 --- a/scripts/testGroups/all.sh +++ b/scripts/testGroups/all.sh @@ -4,8 +4,8 @@ source "$(dirname "$0")/config.sh" export TEST_FILE_CONCURRENCY=2 BUN_PARALLEL_V2 \ - 'integration/billing/update-subscription' \ 'integration/billing/attach' \ + # 'integration/billing/update-subscription' \ # 'integration/billing/migrations' \ # 'integration/crud/customers' \ # 'integration/billing/stripe-webhooks' \ diff --git a/shared/api/billing/attachV2/attachParamsV0.ts b/shared/api/billing/attachV2/attachParamsV0.ts index 7ee61df95..22a269c9c 100644 --- a/shared/api/billing/attachV2/attachParamsV0.ts +++ b/shared/api/billing/attachV2/attachParamsV0.ts @@ -29,8 +29,6 @@ export const ExtAttachParamsV0Schema = BillingParamsBaseV0Schema.extend({ // - 'prorate_immediately' (default): Invoice line items are charged immediately // - 'next_cycle_only': Do NOT create any charges due to the attach billing_behavior: BillingBehaviorSchema.optional(), - - adjustable_quantity: z.boolean().optional(), }); export const AttachParamsV0Schema = ExtAttachParamsV0Schema.extend({ diff --git a/shared/api/billing/attachV2/requestChanges/V1.2_AttachParamsChange.ts b/shared/api/billing/attachV2/requestChanges/V1.2_AttachParamsChange.ts index a233e3f0f..7f69af64d 100644 --- a/shared/api/billing/attachV2/requestChanges/V1.2_AttachParamsChange.ts +++ b/shared/api/billing/attachV2/requestChanges/V1.2_AttachParamsChange.ts @@ -1,3 +1,4 @@ +import { billingParamsV0ToInvoiceModeParams } from "@api/billing/common/mappers/billingParamsV0ToInvoiceModeParams.js"; import { freeTrialParamsV0ToV1 } from "@api/common/freeTrial/mappers/freeTrialParamsV0ToV1.js"; import { ApiVersion } from "@api/versionUtils/ApiVersion.js"; import { @@ -41,9 +42,16 @@ export const V1_2_AttachParamsChange = defineVersionChange({ freeTrialParamsV0: input.free_trial, }); + const newPlanId = input.product_id ?? undefined; + const featureQuantities = input.options; + + const invoiceMode = billingParamsV0ToInvoiceModeParams({ input }); + return { ...input, - plan_id: input.product_id, + plan_id: newPlanId, + feature_quantities: featureQuantities, + invoice_mode: invoiceMode, free_trial: freeTrialV1, customize: customizeV1, }; diff --git a/shared/api/billing/common/billingParamsBase/billingParamsBaseV0.ts b/shared/api/billing/common/billingParamsBase/billingParamsBaseV0.ts index 167199848..7c3cd90ce 100644 --- a/shared/api/billing/common/billingParamsBase/billingParamsBaseV0.ts +++ b/shared/api/billing/common/billingParamsBase/billingParamsBaseV0.ts @@ -1,4 +1,5 @@ import { FeatureQuantityParamsV0Schema } from "@api/billing/common/featureQuantity/featureQuantityParamsV0.js"; +import { TransitionRulesSchema } from "@api/billing/common/transitionRules.js"; import { FreeTrialParamsV0Schema } from "@api/common/freeTrial/freeTrialParamsV0.js"; import { ProductItemSchema } from "@models/productV2Models/productItemModels/productItemModels.js"; import { z } from "zod/v4"; @@ -16,6 +17,8 @@ export const BillingParamsBaseV0Schema = z.object({ version: z.number().optional(), free_trial: FreeTrialParamsV0Schema.nullable().optional(), items: z.array(ProductItemSchema).optional(), + + transition_rules: TransitionRulesSchema.optional(), }); export type BillingParamsBaseV0 = z.infer; diff --git a/shared/api/billing/common/invoiceModeParams.ts b/shared/api/billing/common/invoiceModeParams.ts index 68e9379c0..46ba58945 100644 --- a/shared/api/billing/common/invoiceModeParams.ts +++ b/shared/api/billing/common/invoiceModeParams.ts @@ -1,8 +1,8 @@ import { z } from "zod/v4"; export const InvoiceModeParamsSchema = z.object({ enabled: z.boolean(), - enable_product_immediately: z.boolean().default(false), - finalize_invoice: z.boolean().default(true), + enable_plan_immediately: z.boolean().default(false), + finalize: z.boolean().default(true), }); export type InvoiceModeParams = z.infer; diff --git a/shared/api/billing/common/mappers/billingParamsV0ToInvoiceModeParams.ts b/shared/api/billing/common/mappers/billingParamsV0ToInvoiceModeParams.ts new file mode 100644 index 000000000..0e9fc0018 --- /dev/null +++ b/shared/api/billing/common/mappers/billingParamsV0ToInvoiceModeParams.ts @@ -0,0 +1,22 @@ +import type { InvoiceModeParams } from "../invoiceModeParams.js"; + +/** + * Converts V0 billing params invoice fields to V1 InvoiceModeParams. + */ +export const billingParamsV0ToInvoiceModeParams = ({ + input, +}: { + input: { + invoice?: boolean; + enable_product_immediately?: boolean; + finalize_invoice?: boolean; + }; +}): InvoiceModeParams | undefined => { + if (!input.invoice) return undefined; + + return { + enabled: true, + enable_plan_immediately: input.enable_product_immediately ?? false, + finalize: input.finalize_invoice ?? true, + }; +}; diff --git a/shared/api/billing/updateSubscription/requestChanges/V1.2_UpdateSubscriptionParamsChange.ts b/shared/api/billing/updateSubscription/requestChanges/V1.2_UpdateSubscriptionParamsChange.ts index 37c4dde4c..3c8b34543 100644 --- a/shared/api/billing/updateSubscription/requestChanges/V1.2_UpdateSubscriptionParamsChange.ts +++ b/shared/api/billing/updateSubscription/requestChanges/V1.2_UpdateSubscriptionParamsChange.ts @@ -1,3 +1,4 @@ +import { billingParamsV0ToInvoiceModeParams } from "@api/billing/common/mappers/billingParamsV0ToInvoiceModeParams.js"; import { freeTrialParamsV0ToV1 } from "@api/common/freeTrial/mappers/freeTrialParamsV0ToV1.js"; import { ApiVersion } from "@api/versionUtils/ApiVersion.js"; import { @@ -41,8 +42,17 @@ export const V1_2_UpdateSubscriptionParamsChange = defineVersionChange({ freeTrialParamsV0: input.free_trial, }); + const newPlanId = input.product_id ?? undefined; + const featureQuantities = input.options; + + const invoiceMode = billingParamsV0ToInvoiceModeParams({ input }); + return { ...input, + plan_id: newPlanId, + invoice_mode: invoiceMode, + feature_quantities: featureQuantities, + free_trial: freeTrialV1, customize: customizeV1, }; diff --git a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts index c0db0b202..f476e7012 100644 --- a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts +++ b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts @@ -1,4 +1,3 @@ -import { RefundBehaviorSchema } from "@api/billing/common/refundBehavior"; import { nullish } from "@utils/utils"; import { z } from "zod/v4"; import { BillingBehaviorSchema } from "../common/billingBehavior"; @@ -23,20 +22,12 @@ export const ExtUpdateSubscriptionV0ParamsSchema = // - 'prorate_immediately' (default): Invoice line items are charged immediately // - 'next_cycle_only': Do NOT create any charges due to the update billing_behavior: BillingBehaviorSchema.optional(), - - // Refund behavior for negative invoice totals (downgrades): - // - 'grant_invoice_credits' (default): Apply credits to customer balance - // - 'refund_payment_method': Issue refund to payment method - // refund_behavior: RefundBehaviorSchema.optional(), - - // reset_billing_cycle_anchor: z.boolean().optional(), - // new_billing_subscription: z.boolean().optional(), }); export const UpdateSubscriptionV0ParamsSchema = ExtUpdateSubscriptionV0ParamsSchema.extend({ customer_product_id: z.string().optional(), - refund_behavior: RefundBehaviorSchema.optional(), + // refund_behavior: RefundBehaviorSchema.optional(), }) .refine( (data) => { diff --git a/shared/api/billing/updateSubscription/updateSubscriptionV1Params.ts b/shared/api/billing/updateSubscription/updateSubscriptionV1Params.ts index 974cef1f1..c03bddfd7 100644 --- a/shared/api/billing/updateSubscription/updateSubscriptionV1Params.ts +++ b/shared/api/billing/updateSubscription/updateSubscriptionV1Params.ts @@ -6,7 +6,7 @@ import { InvoiceModeParamsSchema } from "../common/invoiceModeParams.js"; export const UpdateSubscriptionV1ParamsSchema = BillingParamsBaseV1Schema.extend({ - plan_id: z.string().nullish(), + plan_id: z.string().optional(), invoice_mode: InvoiceModeParamsSchema.optional(), diff --git a/shared/api/customers/changes/V2.0_CustomerChange.ts b/shared/api/customers/changes/V2.0_CustomerChange.ts index 5c41b1bf1..1243abfe0 100644 --- a/shared/api/customers/changes/V2.0_CustomerChange.ts +++ b/shared/api/customers/changes/V2.0_CustomerChange.ts @@ -4,6 +4,7 @@ import { defineVersionChange, } from "@api/versionUtils/versionChangeUtils/VersionChange.js"; import type { z } from "zod/v4"; +import type { SharedContext } from "../../../types/sharedContext.js"; import { ApiCustomerSchema } from "../apiCustomer.js"; import { ApiCustomerV5Schema } from "../apiCustomerV5.js"; import type { ApiBalance } from "../cusFeatures/apiBalance.js"; @@ -23,8 +24,10 @@ export const V2_0_CustomerChange = defineVersionChange({ affectsResponse: true, transformResponse: ({ + ctx, input, }: { + ctx: SharedContext; input: z.infer; }): z.infer => { // Transform balances from V1 to V0 @@ -41,17 +44,17 @@ export const V2_0_CustomerChange = defineVersionChange({ const transformedSubscriptions: ApiSubscription[] = allSubscriptions .filter((sub) => sub.status !== "scheduled") - .map((sub) => apiSubscriptionV1ToV0({ input: sub })); + .map((sub) => apiSubscriptionV1ToV0({ ctx, input: sub })); const transformedScheduledSubscriptions: ApiSubscription[] = allSubscriptions .filter((sub) => sub.status === "scheduled") - .map((sub) => apiSubscriptionV1ToV0({ input: sub })); + .map((sub) => apiSubscriptionV1ToV0({ ctx, input: sub })); // Convert purchases to subscriptions and add to subscriptions array const purchasesAsSubscriptions: ApiSubscription[] = ( input.purchases ?? [] - ).map((purchase) => apiPurchaseV0ToSubscriptionV0({ input: purchase })); + ).map((purchase) => apiPurchaseV0ToSubscriptionV0({ ctx, input: purchase })); // Return V0 customer format (without purchases field) const { purchases: _purchases, ...rest } = input; diff --git a/shared/api/customers/components/customerExpand/customerExpand.ts b/shared/api/customers/components/customerExpand/customerExpand.ts new file mode 100644 index 000000000..445d6015a --- /dev/null +++ b/shared/api/customers/components/customerExpand/customerExpand.ts @@ -0,0 +1,16 @@ +import { z } from "zod/v4"; + +// Re-export enum values as constants for easy access (matches old enum usage pattern) +export enum CustomerExpand { + Invoices = "invoices", + TrialsUsed = "trials_used", + Rewards = "rewards", + Entities = "entities", + Referrals = "referrals", + PaymentMethod = "payment_method", + SubscriptionsPlan = "subscriptions.plan", + PurchasesPlan = "purchases.plan", + BalancesFeature = "balances.feature", +} + +export const CustomerExpandEnum = z.enum(CustomerExpand); diff --git a/shared/api/customers/cusPlans/mappers/apiPurchaseV0ToSubscriptionV0.ts b/shared/api/customers/cusPlans/mappers/apiPurchaseV0ToSubscriptionV0.ts index 29166b6a2..dee9ae625 100644 --- a/shared/api/customers/cusPlans/mappers/apiPurchaseV0ToSubscriptionV0.ts +++ b/shared/api/customers/cusPlans/mappers/apiPurchaseV0ToSubscriptionV0.ts @@ -1,4 +1,5 @@ import { planV1ToV0 } from "@api/products/mappers/planV1ToV0.js"; +import type { SharedContext } from "../../../../types/sharedContext.js"; import type { ApiSubscription } from "../apiSubscription.js"; import type { ApiPurchaseV0 } from "../apiSubscriptionV1.js"; @@ -7,12 +8,14 @@ import type { ApiPurchaseV0 } from "../apiSubscriptionV1.js"; * Purchases are represented as subscriptions with sensible defaults for missing fields. */ export function apiPurchaseV0ToSubscriptionV0({ + ctx, input, }: { + ctx: SharedContext; input: ApiPurchaseV0; }): ApiSubscription { return { - plan: input.plan ? planV1ToV0(input.plan) : undefined, + plan: input.plan ? planV1ToV0({ ctx, plan: input.plan }) : undefined, plan_id: input.plan_id, default: false, add_on: true, diff --git a/shared/api/customers/cusPlans/mappers/apiSubscriptionV1ToV0.ts b/shared/api/customers/cusPlans/mappers/apiSubscriptionV1ToV0.ts index 24e244c65..98df68d2a 100644 --- a/shared/api/customers/cusPlans/mappers/apiSubscriptionV1ToV0.ts +++ b/shared/api/customers/cusPlans/mappers/apiSubscriptionV1ToV0.ts @@ -1,14 +1,17 @@ import { planV1ToV0 } from "@api/products/mappers/planV1ToV0.js"; +import type { SharedContext } from "../../../../types/sharedContext.js"; import type { ApiSubscription } from "../apiSubscription.js"; import type { ApiSubscriptionV1 } from "../apiSubscriptionV1.js"; export function apiSubscriptionV1ToV0({ + ctx, input, }: { + ctx: SharedContext; input: ApiSubscriptionV1; }): ApiSubscription { return { - plan: input.plan ? planV1ToV0(input.plan) : undefined, + plan: input.plan ? planV1ToV0({ ctx, plan: input.plan }) : undefined, plan_id: input.plan_id, default: input.auto_enable, add_on: input.add_on, diff --git a/shared/api/entities/changes/V2.0_EntityChange.ts b/shared/api/entities/changes/V2.0_EntityChange.ts index 6789ffbc7..34281dfa8 100644 --- a/shared/api/entities/changes/V2.0_EntityChange.ts +++ b/shared/api/entities/changes/V2.0_EntityChange.ts @@ -6,6 +6,7 @@ import { defineVersionChange, } from "@api/versionUtils/versionChangeUtils/VersionChange.js"; import type { z } from "zod/v4"; +import type { SharedContext } from "../../../types/sharedContext.js"; import type { ApiSubscription } from "../../customers/cusPlans/apiSubscription.js"; import { apiPurchaseV0ToSubscriptionV0 } from "../../customers/cusPlans/mappers/apiPurchaseV0ToSubscriptionV0.js"; import { apiSubscriptionV1ToV0 } from "../../customers/cusPlans/mappers/apiSubscriptionV1ToV0.js"; @@ -39,8 +40,10 @@ export const V2_0_EntityChange = defineVersionChange({ affectsResponse: true, transformResponse: ({ + ctx, input, }: { + ctx: SharedContext; input: z.infer; }): z.infer => { // Transform subscriptions from V1 to V0 @@ -48,16 +51,16 @@ export const V2_0_EntityChange = defineVersionChange({ const activeSubscriptionsV0: ApiSubscription[] = allSubscriptions .filter((sub) => sub.status === "active") - .map((sub) => apiSubscriptionV1ToV0({ input: sub })); + .map((sub) => apiSubscriptionV1ToV0({ ctx, input: sub })); const scheduledSubscriptionsV0: ApiSubscription[] = allSubscriptions .filter((sub) => sub.status === "scheduled") - .map((sub) => apiSubscriptionV1ToV0({ input: sub })); + .map((sub) => apiSubscriptionV1ToV0({ ctx, input: sub })); // Convert purchases to subscriptions and add to active subscriptions const purchasesAsSubscriptions: ApiSubscription[] = ( input.purchases ?? [] - ).map((purchase) => apiPurchaseV0ToSubscriptionV0({ input: purchase })); + ).map((purchase) => apiPurchaseV0ToSubscriptionV0({ ctx, input: purchase })); const balancesV0: Record = {}; if (input.balances) { diff --git a/shared/api/products/changes/V2.0_PlanChanges.ts b/shared/api/products/changes/V2.0_PlanChanges.ts index 691bf01c2..8899a94c6 100644 --- a/shared/api/products/changes/V2.0_PlanChanges.ts +++ b/shared/api/products/changes/V2.0_PlanChanges.ts @@ -5,6 +5,7 @@ import { AffectedResource, defineVersionChange, } from "@api/versionUtils/versionChangeUtils/VersionChange.js"; +import type { SharedContext } from "../../../types/sharedContext.js"; import { PlanLegacyDataSchema } from "../planLegacyData.js"; import { type ApiPlan, @@ -28,7 +29,13 @@ export const V2_0_PlanChanges = defineVersionChange({ affectsRequest: false, affectsResponse: true, - transformResponse: ({ input }: { input: ApiPlanV1 }): ApiPlan => { - return planV1ToV0(input); + transformResponse: ({ + ctx, + input, + }: { + ctx: SharedContext; + input: ApiPlanV1; + }): ApiPlan => { + return planV1ToV0({ ctx, plan: input }); }, }); diff --git a/shared/api/products/items/apiPlanItemV1.ts b/shared/api/products/items/apiPlanItemV1.ts index 824209874..420c97958 100644 --- a/shared/api/products/items/apiPlanItemV1.ts +++ b/shared/api/products/items/apiPlanItemV1.ts @@ -56,6 +56,10 @@ export const ApiPlanItemV1Schema = z on_decrease: z.enum(OnDecrease).optional(), }) .optional(), + + entity_feature_id: z.string().optional().meta({ + internal: true, + }), }) .check((ctx) => { const resetInterval = ctx.value.reset?.interval; diff --git a/shared/api/products/items/crud/createPlanItemParamsV1.ts b/shared/api/products/items/crud/createPlanItemParamsV1.ts index 88143fe1a..5b0eb9c91 100644 --- a/shared/api/products/items/crud/createPlanItemParamsV1.ts +++ b/shared/api/products/items/crud/createPlanItemParamsV1.ts @@ -51,6 +51,10 @@ export const CreatePlanItemParamsV1Schema = z }) .optional(), + entity_feature_id: z.string().optional().meta({ + internal: true, + }), + // Internal entitlement_id: z.string().optional().meta({ internal: true, diff --git a/shared/api/products/items/mappers/planItemV0ToProductItem.ts b/shared/api/products/items/mappers/planItemV0ToProductItem.ts index 12c9ff153..16defaf20 100644 --- a/shared/api/products/items/mappers/planItemV0ToProductItem.ts +++ b/shared/api/products/items/mappers/planItemV0ToProductItem.ts @@ -14,7 +14,7 @@ import { } from "@models/productV2Models/productItemModels/productItemModels.js"; import { dbToApiFeatureV1 } from "@utils/featureUtils/apiFeatureToDbFeature.js"; import { featureToItemFeatureType } from "@utils/featureUtils/convertFeatureUtils.js"; - +import { featureUtils } from "@utils/featureUtils/index.js"; import { resetIntvToItemIntv } from "@utils/productV2Utils/productItemUtils/convertProductItem/planItemIntervals.js"; import { billingToItemInterval } from "@utils/productV2Utils/productItemUtils/itemIntervalUtils.js"; import type { SharedContext } from "../../../../types/sharedContext.js"; @@ -25,7 +25,6 @@ import { } from "../../../models.js"; import { ApiVersion } from "../../../versionUtils/ApiVersion.js"; import { ApiVersionClass } from "../../../versionUtils/ApiVersionClass.js"; - import { hasPrice, hasResetInterval } from "../utils/classifyPlanItemV0.js"; const planItemV0ToProductItemInterval = ({ @@ -123,6 +122,8 @@ export const planItemV0ToProductItem = ({ "entitlement_id" in planItem ? planItem.entitlement_id : undefined; const priceId = "price_id" in planItem ? planItem.price_id : undefined; + const resetUsageWhenEnabled = featureUtils.isConsumable(feature); + return ProductItemSchema.parse({ type, @@ -139,8 +140,6 @@ export const planItemV0ToProductItem = ({ interval, interval_count: planItem.reset?.interval_count, - entity_feature_id: null, - price: planItem.price?.amount, tiers: planItem.price?.tiers?.map((tier) => ({ @@ -154,7 +153,8 @@ export const planItemV0ToProductItem = ({ ? planItem.price.max_purchase + (planItem.granted_balance ?? 0) : undefined, - reset_usage_when_enabled: planItem.reset?.reset_when_enabled, + reset_usage_when_enabled: + planItem.reset?.reset_when_enabled ?? resetUsageWhenEnabled, config, @@ -162,5 +162,7 @@ export const planItemV0ToProductItem = ({ entitlement_id: entitlementId, price_id: priceId, + + entity_feature_id: planItem.entity_feature_id, } satisfies ProductItem); }; diff --git a/shared/api/products/items/mappers/planItemV1ToV0.ts b/shared/api/products/items/mappers/planItemV1ToV0.ts index bbfe0bc27..b5196561e 100644 --- a/shared/api/products/items/mappers/planItemV1ToV0.ts +++ b/shared/api/products/items/mappers/planItemV1ToV0.ts @@ -1,16 +1,27 @@ +import type { CreatePlanItemParamsV1 } from "@api/models.js"; import { billingMethodToUsageModel } from "@api/products/components/mappers/billingMethodTousageModel.js"; import type { ApiPlanItemV0 } from "@api/products/items/previousVersions/apiPlanItemV0.js"; -import type { ApiPlanItemV1 } from "../apiPlanItemV1.js"; -import type { CreatePlanItemParamsV1 } from "../crud/createPlanItemParamsV1.js"; +import { featureUtils } from "@utils/featureUtils/index.js"; +import type { SharedContext } from "../../../../types/sharedContext.js"; +import type { ApiPlanItemV1 } from "../apiPlanItemV1"; /** Transform ApiPlanItemV1 to ApiPlanItemV0 */ -export function planItemV1ToV0( - item: ApiPlanItemV1 | CreatePlanItemParamsV1, -): ApiPlanItemV0 { +export function planItemV1ToV0({ + ctx, + item, +}: { + ctx: SharedContext; + item: ApiPlanItemV1 | CreatePlanItemParamsV1; +}): ApiPlanItemV0 { const { included = 0, price, ...restItem } = item; const billingUnits = price?.billing_units ?? 1; + const feature = ctx.features.find((f) => f.id === item.feature_id); + const resetUsageWhenEnabled = feature + ? featureUtils.isConsumable(feature) + : true; + return { ...restItem, unlimited: item.unlimited ?? false, @@ -19,7 +30,7 @@ export function planItemV1ToV0( ? { interval: item.reset.interval, interval_count: item.reset.interval_count, - reset_when_enabled: false, + reset_when_enabled: resetUsageWhenEnabled, } : null, price: price diff --git a/shared/api/products/items/previousVersions/apiPlanItemV0.ts b/shared/api/products/items/previousVersions/apiPlanItemV0.ts index 53c36c94f..446b89106 100644 --- a/shared/api/products/items/previousVersions/apiPlanItemV0.ts +++ b/shared/api/products/items/previousVersions/apiPlanItemV0.ts @@ -58,6 +58,10 @@ export const ApiPlanItemV0Schema = z }) .optional(), + entity_feature_id: z.string().optional().meta({ + internal: true, + }), + entitlement_id: z.string().optional().meta({ internal: true, }), diff --git a/shared/api/products/mappers/planV1ToProductItems.ts b/shared/api/products/mappers/planV1ToProductItems.ts index 24ab3018b..5383a7029 100644 --- a/shared/api/products/mappers/planV1ToProductItems.ts +++ b/shared/api/products/mappers/planV1ToProductItems.ts @@ -24,7 +24,7 @@ export const planV1ToProductItems = ({ // Convert features to items const featureItems = plan.items?.map((planItem) => { - const planV0 = planItemV1ToV0(planItem); + const planV0 = planItemV1ToV0({ ctx, item: planItem }); return planItemV0ToProductItem({ ctx, planItem: planV0 }); }) ?? []; diff --git a/shared/api/products/mappers/planV1ToV0.ts b/shared/api/products/mappers/planV1ToV0.ts index 2abd6d7cf..a9d8100f1 100644 --- a/shared/api/products/mappers/planV1ToV0.ts +++ b/shared/api/products/mappers/planV1ToV0.ts @@ -1,6 +1,7 @@ import type { ApiPlanV1 } from "@api/products/apiPlanV1.js"; import { planItemV1ToV0 } from "@api/products/items/mappers/planItemV1ToV0.js"; import type { ApiPlan } from "@api/products/previousVersions/apiPlanV0.js"; +import type { SharedContext } from "../../../types/sharedContext"; /** * Transform ApiPlanV1 to ApiPlan (V0) @@ -9,10 +10,16 @@ import type { ApiPlan } from "@api/products/previousVersions/apiPlanV0.js"; * - auto_enable -> default * - features: ApiPlanItemV1[] -> ApiPlanItemV0[] */ -export function planV1ToV0(plan: ApiPlanV1): ApiPlan { +export function planV1ToV0({ + ctx, + plan, +}: { + ctx: SharedContext; + plan: ApiPlanV1; +}): ApiPlan { return { ...plan, default: plan.auto_enable, - features: plan.items.map(planItemV1ToV0), + features: plan.items.map((item) => planItemV1ToV0({ ctx, item })), }; } diff --git a/shared/models/billingModels/context/transitionConfig.ts b/shared/models/billingModels/context/transitionConfig.ts index 08abed5c3..80472520b 100644 --- a/shared/models/billingModels/context/transitionConfig.ts +++ b/shared/models/billingModels/context/transitionConfig.ts @@ -1,7 +1,7 @@ import { z } from "zod/v4"; export const TransitionConfigSchema = z.object({ - resetAfterTrialEndFeaturIds: z.array(z.string()).optional(), + resetAfterTrialEndFeatureIds: z.array(z.string()).optional(), }); export type TransitionConfig = z.infer; diff --git a/shared/models/billingModels/stripe/stripeDiscountWithCoupon.ts b/shared/models/billingModels/stripe/stripeDiscountWithCoupon.ts index 4324527ef..577b119a3 100644 --- a/shared/models/billingModels/stripe/stripeDiscountWithCoupon.ts +++ b/shared/models/billingModels/stripe/stripeDiscountWithCoupon.ts @@ -4,8 +4,12 @@ import type Stripe from "stripe"; * A discount source with a guaranteed expanded Stripe Coupon object. * When the discount originates from a promotion code, promotionCodeId * is included for proper attribution in checkout sessions. + * + * Note: `id` is only present for existing discounts from a subscription. + * When resolving coupons/promo codes for new subscriptions, `id` is undefined. */ export type StripeDiscountWithCoupon = { + id?: string; source: { coupon: Stripe.Coupon }; promotionCodeId?: string; }; diff --git a/shared/utils/productV2Utils/productItemUtils/convertProductItem/productItemToPlanItemParamsV1.ts b/shared/utils/productV2Utils/productItemUtils/convertProductItem/productItemToPlanItemParamsV1.ts index 4bac7c614..74e88531b 100644 --- a/shared/utils/productV2Utils/productItemUtils/convertProductItem/productItemToPlanItemParamsV1.ts +++ b/shared/utils/productV2Utils/productItemUtils/convertProductItem/productItemToPlanItemParamsV1.ts @@ -61,6 +61,7 @@ export const productItemToPlanItemParamsV1 = ({ : undefined, // Add internal fields + entity_feature_id: item.entity_feature_id ?? undefined, entitlement_id: item.entitlement_id ?? undefined, price_id: item.price_id ?? undefined, };