diff --git a/server/src/internal/billing/v2/execute/executeAutumnActions/applyOngoingCusProductAction.ts b/server/src/internal/billing/v2/execute/executeAutumnActions/applyOngoingCusProductAction.ts index 0823cc72d..f9c2d1109 100644 --- a/server/src/internal/billing/v2/execute/executeAutumnActions/applyOngoingCusProductAction.ts +++ b/server/src/internal/billing/v2/execute/executeAutumnActions/applyOngoingCusProductAction.ts @@ -10,6 +10,11 @@ export const applyOngoingCusProductAction = async ({ ongoingCusProductAction: OngoingCusProductAction; }) => { const { action, cusProduct } = ongoingCusProductAction; + + if (action === "update") { + return; + } + if (action === "expire") { return await CusProductService.update({ db: ctx.db, diff --git a/server/src/internal/billing/v2/execute/executeAutumnActions/executeCusProductActions.ts b/server/src/internal/billing/v2/execute/executeAutumnActions/executeCusProductActions.ts index b7a481fef..050ddba07 100644 --- a/server/src/internal/billing/v2/execute/executeAutumnActions/executeCusProductActions.ts +++ b/server/src/internal/billing/v2/execute/executeAutumnActions/executeCusProductActions.ts @@ -1,30 +1,62 @@ -import type { FullCusProduct, OngoingCusProductAction } from "@autumn/shared"; +import type { + FeatureOptions, + FullCusProduct, + OngoingCusProductAction, +} from "@autumn/shared"; import type { AutumnContext } from "../../../../../honoUtils/HonoEnv"; +import type { QuantityUpdateDetails } from "../../types"; import { applyOngoingCusProductAction } from "./applyOngoingCusProductAction"; import { insertNewCusProducts } from "./insertNewCusProducts"; +import { updateCustomerEntitlements } from "./updateCustomerEntitlements"; +import { updateCustomerProductOptions } from "./updateCustomerProductOptions"; export const executeCusProductActions = async ({ ctx, - // cusProductActions, ongoingCusProductAction, newCusProducts, + quantityUpdateDetails, + updatedFeatureOptions, }: { ctx: AutumnContext; - // cusProductActions: CusProductActions; ongoingCusProductAction?: OngoingCusProductAction; newCusProducts: FullCusProduct[]; + quantityUpdateDetails?: QuantityUpdateDetails[]; + updatedFeatureOptions?: FeatureOptions[]; }) => { - // 1. Insert new cus products + const { logger } = ctx; + + logger.info("Inserting new customer products"); await insertNewCusProducts({ ctx, newCusProducts, }); - // 2. Apply ongoing cus product action if (ongoingCusProductAction) { + logger.info( + `Applying ongoing customer product action: ${ongoingCusProductAction.action}`, + ); await applyOngoingCusProductAction({ ctx, ongoingCusProductAction, }); } + + if (updatedFeatureOptions && ongoingCusProductAction?.cusProduct) { + logger.info("Updating customer product options"); + await updateCustomerProductOptions({ + ctx, + customerProductId: ongoingCusProductAction.cusProduct.id, + updatedFeatureOptions, + }); + } + + if (quantityUpdateDetails && quantityUpdateDetails.length > 0) { + logger.info("Updating customer entitlements"); + await updateCustomerEntitlements({ + ctx, + quantityUpdateDetails, + }); + } + + logger.info("Successfully executed all customer product actions"); }; diff --git a/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts b/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts new file mode 100644 index 000000000..7d8a55a20 --- /dev/null +++ b/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts @@ -0,0 +1,65 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService"; +import type { QuantityUpdateDetails } from "../../types"; + +/** + * Update customer entitlement balances based on quantity changes. + * + * Extracted from: + * - handleQuantityUpgrade.ts:191-206 + * - handleQuantityDowngrade.ts:182-198 + */ +export const updateCustomerEntitlements = async ({ + ctx, + quantityUpdateDetails, +}: { + ctx: AutumnContext; + quantityUpdateDetails: QuantityUpdateDetails[]; +}) => { + const { db, logger } = ctx; + + for (const updateDetail of quantityUpdateDetails) { + if (!updateDetail.customerEntitlementId) { + logger.info( + `No entitlement found for feature ${updateDetail.featureId}, skipping entitlement update`, + ); + continue; + } + + const { + customerEntitlementBalanceChange, + customerEntitlementId, + featureId, + } = updateDetail; + + if (customerEntitlementBalanceChange > 0) { + logger.info( + `Incrementing entitlement for feature ${featureId} by ${customerEntitlementBalanceChange} units`, + ); + + await CusEntService.increment({ + db, + id: customerEntitlementId, + amount: customerEntitlementBalanceChange, + }); + } else if (customerEntitlementBalanceChange < 0) { + const absoluteDecrement = Math.abs(customerEntitlementBalanceChange); + + logger.info( + `Decrementing entitlement for feature ${featureId} by ${absoluteDecrement} units`, + ); + + await CusEntService.decrement({ + db, + id: customerEntitlementId, + amount: absoluteDecrement, + }); + } else { + logger.info( + `No entitlement balance change required for feature ${featureId}`, + ); + } + } + + logger.info("Successfully updated all customer entitlements"); +}; diff --git a/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerProductOptions.ts b/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerProductOptions.ts new file mode 100644 index 000000000..8dee20da1 --- /dev/null +++ b/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerProductOptions.ts @@ -0,0 +1,33 @@ +import type { FeatureOptions } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService"; + +/** + * Update customer product options with new feature quantities. + * + * Extracted from: + * - updateQuantityFlow.ts:55-59 + */ +export const updateCustomerProductOptions = async ({ + ctx, + customerProductId, + updatedFeatureOptions, +}: { + ctx: AutumnContext; + customerProductId: string; + updatedFeatureOptions: FeatureOptions[]; +}) => { + const { db, logger } = ctx; + + logger.info( + `Updating customer product ${customerProductId} with ${updatedFeatureOptions.length} feature options`, + ); + + await CusProductService.update({ + db, + cusProductId: customerProductId, + updates: { options: updatedFeatureOptions }, + }); + + logger.info("Successfully updated customer product options"); +}; diff --git a/server/src/internal/billing/v2/execute/executeInvoiceAction.ts b/server/src/internal/billing/v2/execute/executeInvoiceAction.ts new file mode 100644 index 000000000..becffe53e --- /dev/null +++ b/server/src/internal/billing/v2/execute/executeInvoiceAction.ts @@ -0,0 +1,103 @@ +import type { FullCusProduct } from "@autumn/shared"; +import { msToSeconds, orgToCurrency } from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { InvoiceService } from "@/internal/invoices/InvoiceService"; +import { getInvoiceItems } from "@/internal/invoices/invoiceUtils"; +import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice"; +import type { SubscriptionUpdateInvoiceAction } from "../types"; + +/** + * Execute invoice creation and finalization for subscription updates. + * + * Extracted from: + * - handleQuantityUpgrade.ts:130-164 + * - handleQuantityDowngrade.ts:130-165 + */ +export const executeInvoiceAction = async ({ + ctx, + invoiceAction, + stripeCustomerId, + stripeSubscriptionId, + customerProduct, +}: { + ctx: AutumnContext; + invoiceAction: SubscriptionUpdateInvoiceAction; + stripeCustomerId: string; + stripeSubscriptionId: string; + customerProduct: FullCusProduct; +}) => { + if (!invoiceAction.shouldCreateInvoice) { + ctx.logger.info("No invoice creation required"); + return null; + } + + const { db, org, logger, env } = ctx; + const stripeClient = createStripeCli({ org, env }); + + logger.info(`Creating ${invoiceAction.invoiceItems.length} invoice items`); + + for (const invoiceItem of invoiceAction.invoiceItems) { + const amountCents = Math.round(invoiceItem.amountDollars * 100); + + logger.info( + `Creating invoice item: ${invoiceItem.description} - $${invoiceItem.amountDollars} (${amountCents} cents)`, + ); + + await stripeClient.invoiceItems.create({ + customer: stripeCustomerId, + amount: amountCents, + currency: orgToCurrency({ org }), + description: invoiceItem.description, + subscription: stripeSubscriptionId, + period: { + start: msToSeconds(invoiceItem.periodStartEpochMs), + end: msToSeconds(invoiceItem.periodEndEpochMs), + }, + }); + } + + if (invoiceAction.shouldChargeImmediately) { + logger.info("Finalizing and charging invoice immediately"); + + const { invoice: finalizedStripeInvoice } = await createAndFinalizeInvoice({ + stripeCli: stripeClient, + stripeCusId: stripeCustomerId, + stripeSubId: stripeSubscriptionId, + paymentMethod: invoiceAction.paymentMethod || null, + chargeAutomatically: true, + logger, + }); + + try { + const parsedInvoiceItems = await getInvoiceItems({ + stripeInvoice: finalizedStripeInvoice, + prices: invoiceAction.customerPrices.map( + (customerPrice) => customerPrice.price, + ), + logger, + }); + + await InvoiceService.createInvoiceFromStripe({ + db, + stripeInvoice: finalizedStripeInvoice, + internalCustomerId: customerProduct.internal_customer_id!, + internalEntityId: customerProduct.internal_entity_id, + productIds: [customerProduct.product_id], + internalProductIds: [customerProduct.internal_product_id], + org, + sendRevenueEvent: true, + items: parsedInvoiceItems, + }); + + logger.info("Successfully created internal invoice record"); + } catch (error) { + logger.error(`Failed to create internal invoice record: ${error}`); + } + + return finalizedStripeInvoice; + } + + logger.info("Invoice items created, finalization skipped"); + return null; +}; diff --git a/server/src/internal/billing/v2/execute/executeStripeSubAction.ts b/server/src/internal/billing/v2/execute/executeStripeSubAction.ts index db9921043..1dfea419e 100644 --- a/server/src/internal/billing/v2/execute/executeStripeSubAction.ts +++ b/server/src/internal/billing/v2/execute/executeStripeSubAction.ts @@ -1,5 +1,6 @@ import type { AutumnContext } from "../../../../honoUtils/HonoEnv"; import type { StripeSubAction } from "../types"; +import { executeStripeSubscriptionUpdate } from "./executeStripeSubscriptionActions/executeStripeSubscriptionUpdate"; export const executeStripeSubAction = async ({ ctx, @@ -8,19 +9,39 @@ export const executeStripeSubAction = async ({ ctx: AutumnContext; stripeSubAction: StripeSubAction; }) => { - switch ( - stripeSubAction.type - // case "create": - // return await executeStripeSubCreate({ ctx, stripeSubAction }); + const { logger } = ctx; - // case "update": - // return await executeStripeSubUpdate({ ctx, stripeSubAction }); - // return await executeStripeSubUpdate({ ctx, stripeSubAction }); - // case "cancel_immediately": - // return await executeStripeSubCancelImmediately({ ctx, stripeSubAction }); - // case "cancel_at_period_end": - // return await executeStripeSubCancelAtPeriodEnd({ ctx, stripeSubAction }); - // case "none": - ) { + switch (stripeSubAction.type) { + case "update": + logger.info("Executing Stripe subscription update"); + return await executeStripeSubscriptionUpdate({ + ctx, + stripeSubscriptionAction: stripeSubAction, + }); + + case "create": + logger.info("Executing Stripe subscription create"); + throw new Error("Stripe subscription create not yet implemented"); + + case "cancel_immediately": + logger.info("Executing Stripe subscription cancel immediately"); + throw new Error( + "Stripe subscription cancel immediately not yet implemented", + ); + + case "cancel_at_period_end": + logger.info("Executing Stripe subscription cancel at period end"); + throw new Error( + "Stripe subscription cancel at period end not yet implemented", + ); + + case "none": + logger.info("No Stripe subscription action required"); + return; + + default: + throw new Error( + `Unknown Stripe subscription action type: ${stripeSubAction.type}`, + ); } }; diff --git a/server/src/internal/billing/v2/execute/executeStripeSubscriptionActions/executeStripeSubscriptionUpdate.ts b/server/src/internal/billing/v2/execute/executeStripeSubscriptionActions/executeStripeSubscriptionUpdate.ts new file mode 100644 index 000000000..395a17f88 --- /dev/null +++ b/server/src/internal/billing/v2/execute/executeStripeSubscriptionActions/executeStripeSubscriptionUpdate.ts @@ -0,0 +1,60 @@ +import { createStripeCli } from "@/external/connect/createStripeCli"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import type { StripeSubAction } from "../../types"; + +/** + * Execute Stripe subscription item updates. + * Handles creating, updating, and deleting subscription items. + * + * Extracted from: + * - handleQuantityUpgrade.ts:168-187 + * - handleQuantityDowngrade.ts:168-172 + */ +export const executeStripeSubscriptionUpdate = async ({ + ctx, + stripeSubscriptionAction, +}: { + ctx: AutumnContext; + stripeSubscriptionAction: StripeSubAction; +}) => { + const { org, env, logger } = ctx; + const stripeClient = createStripeCli({ org, env }); + if ( + !stripeSubscriptionAction.items || + stripeSubscriptionAction.items.length === 0 + ) { + logger.info("No subscription items to update"); + return; + } + + logger.info( + `Updating ${stripeSubscriptionAction.items.length} subscription items`, + ); + + for (const subscriptionItem of stripeSubscriptionAction.items) { + if (subscriptionItem.deleted) { + logger.info(`Deleting subscription item ${subscriptionItem.id}`); + await stripeClient.subscriptionItems.del(subscriptionItem.id!); + } else if (subscriptionItem.id) { + logger.info( + `Updating subscription item ${subscriptionItem.id} to quantity ${subscriptionItem.quantity}`, + ); + await stripeClient.subscriptionItems.update(subscriptionItem.id, { + quantity: subscriptionItem.quantity, + proration_behavior: "none", + }); + } else { + logger.info( + `Creating new subscription item for price ${subscriptionItem.price} with quantity ${subscriptionItem.quantity}`, + ); + await stripeClient.subscriptionItems.create({ + subscription: stripeSubscriptionAction.subId!, + price: subscriptionItem.price!, + quantity: subscriptionItem.quantity, + proration_behavior: "none", + }); + } + } + + logger.info("Successfully updated all subscription items"); +}; diff --git a/server/src/internal/billing/v2/handlers/handleApiSubscriptionUpdate.ts b/server/src/internal/billing/v2/handlers/handleApiSubscriptionUpdate.ts index c376fb5b7..9e3993945 100644 --- a/server/src/internal/billing/v2/handlers/handleApiSubscriptionUpdate.ts +++ b/server/src/internal/billing/v2/handlers/handleApiSubscriptionUpdate.ts @@ -10,17 +10,19 @@ export const handleApiSubscriptionUpdate = createRoute({ const ctx = c.get("ctx"); const body = c.req.valid("json"); - const updateSubscriptionContext = await fetchApiSubscriptionUpdateContext( + const updateSubscriptionContext = await fetchApiSubscriptionUpdateContext({ ctx, - body, - ); + params: body, + }); - const subscriptionUpdatePlan = computeSubscriptionUpdatePlan(ctx, { + const subscriptionUpdatePlan = computeSubscriptionUpdatePlan({ + ctx, updateSubscriptionContext, params: body, }); - await executeSubscriptionUpdate(ctx, { + await executeSubscriptionUpdate({ + ctx, params: body, updateSubscriptionContext, subscriptionUpdatePlan, diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeInvoiceAction.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeInvoiceAction.ts new file mode 100644 index 000000000..acc70d3c6 --- /dev/null +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeInvoiceAction.ts @@ -0,0 +1,71 @@ +import type Stripe from "stripe"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import type { + QuantityUpdateDetails, + SubscriptionUpdateInvoiceAction, +} from "../../types"; + +/** + * Aggregate invoice items and determine invoice creation strategy. + * PURE FUNCTION - no side effects, only calculations. + * + * Extracted from: + * - handleQuantityUpgrade.ts:79-164 + * - handleQuantityDowngrade.ts:78-165 + */ +export const computeInvoiceAction = ({ + ctx, + quantityUpdateDetails, + stripeSubscription, + stripeCustomerId, + paymentMethod, + shouldGenerateInvoiceOnly, +}: { + ctx: AutumnContext; + quantityUpdateDetails: QuantityUpdateDetails[]; + stripeSubscription: Stripe.Subscription; + stripeCustomerId: string; + paymentMethod?: Stripe.PaymentMethod; + shouldGenerateInvoiceOnly?: boolean; +}): SubscriptionUpdateInvoiceAction | undefined => { + if (stripeSubscription.status === "trialing") { + return undefined; + } + + const detailsRequiringInvoiceItems = quantityUpdateDetails.filter( + ( + detail, + ): detail is typeof detail & { calculatedProrationAmountDollars: number } => + detail.shouldApplyProration && + detail.calculatedProrationAmountDollars !== undefined, + ); + + if (detailsRequiringInvoiceItems.length === 0) { + return undefined; + } + + const invoiceItems = detailsRequiringInvoiceItems.map((detail) => ({ + description: detail.stripeInvoiceItemDescription, + amountDollars: detail.calculatedProrationAmountDollars, + stripePriceId: detail.stripePriceId, + periodStartEpochMs: detail.subscriptionPeriodStartEpochMs, + periodEndEpochMs: detail.subscriptionPeriodEndEpochMs, + })); + + const shouldChargeImmediately = quantityUpdateDetails.some( + (detail) => detail.shouldFinalizeInvoiceImmediately, + ); + + const customerPrices = quantityUpdateDetails.map( + (detail) => detail.customerPrice, + ); + + return { + shouldCreateInvoice: true, + invoiceItems, + shouldChargeImmediately: + shouldChargeImmediately && !shouldGenerateInvoiceOnly, + paymentMethod, + customerPrices, + }; +}; diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeQuantityUpdateDetails.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeQuantityUpdateDetails.ts new file mode 100644 index 000000000..7e09edf1f --- /dev/null +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeQuantityUpdateDetails.ts @@ -0,0 +1,228 @@ +import { + cusProductToProduct, + type Feature, + type FeatureOptions, + type FullCusProduct, + findCusPriceByFeature, + getFeatureInvoiceDescription, + InternalError, + OnDecrease, + OnIncrease, + priceToInvoiceAmount, + secondsToMs, + type UsagePriceConfig, +} from "@autumn/shared"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils"; +import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils"; +import { + shouldBillNow, + shouldProrate, +} from "@/internal/products/prices/priceUtils/prorationConfigUtils"; +import { notNullish } from "@/utils/genUtils"; +import type { QuantityUpdateDetails } from "../../types"; + +/** + * Compute all details for a single feature quantity update. + * PURE FUNCTION - no side effects, only calculations. + * + * Extracted from: + * - handleQuantityUpgrade.ts:58-206 + * - handleQuantityDowngrade.ts:53-198 + */ +export const computeQuantityUpdateDetails = ({ + ctx, + previousOptions, + updatedOptions, + customerProduct, + stripeSubscription, + currentEpochMs, +}: { + ctx: AutumnContext; + previousOptions: FeatureOptions; + updatedOptions: FeatureOptions; + customerProduct: FullCusProduct; + stripeSubscription: Stripe.Subscription; + currentEpochMs: number; +}): QuantityUpdateDetails => { + const { features } = ctx; + + if (!updatedOptions.internal_feature_id) { + throw new InternalError({ + message: `[Quantity Update] Missing internal_feature_id for feature: ${updatedOptions.feature_id}`, + }); + } + + const customerPrice = findCusPriceByFeature({ + internalFeatureId: updatedOptions.internal_feature_id, + cusPrices: customerProduct.customer_prices, + }); + + if (!customerPrice) { + throw new InternalError({ + message: `[Quantity Update] Customer price not found for internal_feature_id: ${updatedOptions.internal_feature_id}`, + }); + } + + const price = customerPrice.price; + const priceConfig = price.config as UsagePriceConfig; + const billingUnitsPerQuantity = priceConfig.billing_units || 1; + + const isUpgrade = updatedOptions.quantity > previousOptions.quantity; + + const prorationBehaviorConfig = isUpgrade + ? price.proration_config?.on_increase || OnIncrease.ProrateImmediately + : price.proration_config?.on_decrease || OnDecrease.ProrateImmediately; + + const shouldApplyProration = shouldProrate(prorationBehaviorConfig); + const shouldFinalizeInvoiceImmediately = shouldBillNow( + prorationBehaviorConfig, + ); + + const quantityDifferenceForEntitlements = new Decimal(updatedOptions.quantity) + .minus(previousOptions.quantity) + .toNumber(); + + const upcomingQuantityToConsider = notNullish( + previousOptions.upcoming_quantity, + ) + ? previousOptions.upcoming_quantity + : previousOptions.quantity; + + const stripeSubscriptionItemQuantityDifference = new Decimal( + updatedOptions.quantity, + ) + .minus(upcomingQuantityToConsider) + .toNumber(); + + const { start: periodStartSeconds, end: periodEndSeconds } = + subToPeriodStartEnd({ + sub: stripeSubscription, + }); + + const periodStartMs = secondsToMs(periodStartSeconds); + const periodEndMs = secondsToMs(periodEndSeconds); + + if (!periodStartMs || !periodEndMs) { + throw new InternalError({ + message: `[Quantity Update] Invalid subscription period: start=${periodStartSeconds}, end=${periodEndSeconds}`, + }); + } + + const subscriptionPeriodStartEpochMs: number = periodStartMs; + const subscriptionPeriodEndEpochMs: number = periodEndMs; + + let calculatedProrationAmountDollars: number | undefined; + if (shouldApplyProration && stripeSubscription.status !== "trialing") { + const previousQuantityActual = new Decimal(previousOptions.quantity) + .mul(billingUnitsPerQuantity) + .toNumber(); + const updatedQuantityActual = new Decimal(updatedOptions.quantity) + .mul(billingUnitsPerQuantity) + .toNumber(); + + const previousAmountDollars = priceToInvoiceAmount({ + price, + quantity: previousQuantityActual, + }); + + const updatedAmountDollars = priceToInvoiceAmount({ + price, + quantity: updatedQuantityActual, + }); + + const amountDifferenceDollars = new Decimal(updatedAmountDollars).minus( + previousAmountDollars, + ); + + const timeRemainingMs = new Decimal(subscriptionPeriodEndEpochMs).minus( + currentEpochMs, + ); + const totalPeriodMs = new Decimal(subscriptionPeriodEndEpochMs).minus( + subscriptionPeriodStartEpochMs, + ); + + const proratedAmountDollars = timeRemainingMs + .div(totalPeriodMs) + .mul(amountDifferenceDollars); + + if (proratedAmountDollars.lte(0) && isUpgrade) { + calculatedProrationAmountDollars = 0; + } else { + calculatedProrationAmountDollars = proratedAmountDollars.toNumber(); + } + } + + const feature = features.find( + (f: Feature) => f.internal_id === updatedOptions.internal_feature_id, + ); + + if (!feature) { + throw new InternalError({ + message: `[Quantity Update] Feature not found for internal_id: ${updatedOptions.internal_feature_id}`, + }); + } + + const product = cusProductToProduct({ cusProduct: customerProduct }); + + const stripeInvoiceItemDescription = getFeatureInvoiceDescription({ + feature, + usage: updatedOptions.quantity, + billingUnits: billingUnitsPerQuantity, + prodName: product.name, + isPrepaid: true, + fromUnix: currentEpochMs, + }); + + const existingStripeSubscriptionItem = findStripeItemForPrice({ + price, + stripeItems: stripeSubscription.items.data, + }) as Stripe.SubscriptionItem | undefined; + + const customerEntitlement = getRelatedCusEnt({ + cusPrice: customerPrice, + cusEnts: customerProduct.customer_entitlements, + }); + + const customerEntitlementBalanceChange = new Decimal( + quantityDifferenceForEntitlements, + ) + .mul(billingUnitsPerQuantity) + .toNumber(); + + if (!price.config.stripe_price_id) { + throw new InternalError({ + message: `[Quantity Update] Stripe price ID not found for price: ${price.id}`, + }); + } + + return { + featureId: updatedOptions.feature_id, + internalFeatureId: updatedOptions.internal_feature_id, + + previousFeatureQuantity: previousOptions.quantity, + updatedFeatureQuantity: updatedOptions.quantity, + quantityDifferenceForEntitlements, + stripeSubscriptionItemQuantityDifference, + + shouldApplyProration, + shouldFinalizeInvoiceImmediately, + billingUnitsPerQuantity, + + calculatedProrationAmountDollars, + subscriptionPeriodStartEpochMs, + subscriptionPeriodEndEpochMs, + + stripeInvoiceItemDescription, + + customerPrice, + stripePriceId: price.config.stripe_price_id, + existingStripeSubscriptionItem, + + customerEntitlementId: customerEntitlement?.id, + customerEntitlementBalanceChange, + }; +}; diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdatePlan.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdatePlan.ts index dd5ddc25b..420bb0e6b 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdatePlan.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdatePlan.ts @@ -3,7 +3,7 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { SubscriptionUpdatePlan } from "../../types"; import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema"; import { computeSubscriptionUpdateIntent } from "./computeSubscriptionUpdateIntent"; -import { getComputeSubscriptionUpdatePlanIntentMap } from "./computeSubscriptionUpdatePlanIntentMap"; +import { getComputeSubscriptionUpdatePlanFunction } from "./computeSubscriptionUpdatePlanIntentMap"; /** * Compute the subscription update plan @@ -11,18 +11,17 @@ import { getComputeSubscriptionUpdatePlanIntentMap } from "./computeSubscription * @param params - The parameters for the subscription update * @returns The subscription update plan */ -export const computeSubscriptionUpdatePlan = ( - ctx: AutumnContext, - { - updateSubscriptionContext, - params, - }: { - updateSubscriptionContext: UpdateSubscriptionContext; - params: SubscriptionUpdateV0Params; - }, -): SubscriptionUpdatePlan => { +export const computeSubscriptionUpdatePlan = ({ + ctx, + updateSubscriptionContext, + params, +}: { + ctx: AutumnContext; + updateSubscriptionContext: UpdateSubscriptionContext; + params: SubscriptionUpdateV0Params; +}): SubscriptionUpdatePlan => { const intent = computeSubscriptionUpdateIntent(params); - const computePlan = getComputeSubscriptionUpdatePlanIntentMap(intent); + const computePlan = getComputeSubscriptionUpdatePlanFunction(intent); - return computePlan(ctx, { updateSubscriptionContext, params }); + return computePlan({ ctx, updateSubscriptionContext, params }); }; diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdatePlanIntentMap.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdatePlanIntentMap.ts index 6f7d65a74..45a01f6d8 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdatePlanIntentMap.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdatePlanIntentMap.ts @@ -9,16 +9,15 @@ import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionConte import { computeSubscriptionUpdateQuantityPlan } from "./computeSubscriptionUpdateQuantityPlan"; import { SubscriptionUpdateIntentEnum } from "./computeSubscriptionUpdateSchema"; -export type ComputeSubscriptionUpdatePlan = ( - ctx: AutumnContext, - { - updateSubscriptionContext, - params, - }: { - updateSubscriptionContext: UpdateSubscriptionContext; - params: SubscriptionUpdateV0Params; - }, -) => SubscriptionUpdatePlan; +export type ComputeSubscriptionUpdatePlan = ({ + ctx, + updateSubscriptionContext, + params, +}: { + ctx: AutumnContext; + updateSubscriptionContext: UpdateSubscriptionContext; + params: SubscriptionUpdateV0Params; +}) => SubscriptionUpdatePlan; export type ComputeSubscriptionUpdatePlanIntentMap = Partial< Record @@ -33,7 +32,7 @@ const computeSubscriptionUpdatePlanIntentMap: ComputeSubscriptionUpdatePlanInten computeSubscriptionUpdateQuantityPlan, }; -export const getComputeSubscriptionUpdatePlanIntentMap = ( +export const getComputeSubscriptionUpdatePlanFunction = ( intent: SubscriptionUpdateIntentEnum, ): ComputeSubscriptionUpdatePlan => { const plan = computeSubscriptionUpdatePlanIntentMap[intent]; diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateQuantityPlan.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateQuantityPlan.ts index 4f3e0dc01..587f74924 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateQuantityPlan.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateQuantityPlan.ts @@ -4,35 +4,29 @@ import { secondsToMs, } from "@shared/index"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import { cusProductToExistingUsages } from "@/internal/billing/billingUtils/handleExistingUsages/cusProductToExistingUsages"; -import { initFullCusProduct } from "@/internal/billing/billingUtils/initFullCusProduct/initFullCusProduct"; import { buildAutumnLineItems } from "../../compute/computeAutumnUtils/buildAutumnLineItems"; -import { buildStripeSubAction } from "../../compute/computeStripeUtils/buildStripeSubAction"; -import { - SubscriptionUpdateQuantityAction, - type SubscriptionUpdateQuantityPlan, -} from "../../types"; +import type { SubscriptionUpdateQuantityPlan } from "../../types"; import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema"; +import { computeInvoiceAction } from "./computeInvoiceAction"; +import { computeQuantityUpdateDetails } from "./computeQuantityUpdateDetails"; import { SubscriptionUpdateIntentEnum } from "./computeSubscriptionUpdateSchema"; -export const computeSubscriptionUpdateQuantityPlan = ( - ctx: AutumnContext, - { - updateSubscriptionContext, - params, - }: { - updateSubscriptionContext: UpdateSubscriptionContext; - params: SubscriptionUpdateV0Params; - }, -): SubscriptionUpdateQuantityPlan => { +export const computeSubscriptionUpdateQuantityPlan = ({ + ctx, + updateSubscriptionContext, + params, +}: { + ctx: AutumnContext; + updateSubscriptionContext: UpdateSubscriptionContext; + params: SubscriptionUpdateV0Params; +}): SubscriptionUpdateQuantityPlan => { const { options } = params; const { customerProduct, - fullCustomer, stripeSubscription, testClockFrozenTime, - product, paymentMethod, + stripeCustomer, } = updateSubscriptionContext; const featureQuantities = { @@ -40,19 +34,39 @@ export const computeSubscriptionUpdateQuantityPlan = ( new: options || [], }; - const isUpgrade = - featureQuantities.new[0].quantity > featureQuantities.old[0].quantity; + const currentEpochMs = testClockFrozenTime || Date.now(); - const action = isUpgrade - ? SubscriptionUpdateQuantityAction.Upgrade - : SubscriptionUpdateQuantityAction.Downgrade; + const quantityUpdateDetails = featureQuantities.new.map( + (updatedOption, index) => + computeQuantityUpdateDetails({ + ctx, + previousOptions: featureQuantities.old[index], + updatedOptions: updatedOption, + customerProduct, + stripeSubscription, + currentEpochMs, + }), + ); + + const isSubscriptionTrialing = stripeSubscription.status === "trialing"; + + const invoiceAction = !isSubscriptionTrialing + ? computeInvoiceAction({ + ctx, + quantityUpdateDetails, + stripeSubscription, + stripeCustomerId: stripeCustomer.id, + paymentMethod, + shouldGenerateInvoiceOnly: !(params.finalize_invoice ?? true), + }) + : undefined; const billingCycleAnchor = secondsToMs( - stripeSubscription?.billing_cycle_anchor, + stripeSubscription.billing_cycle_anchor, ); const ongoingCusProductAction = { - action: OngoingCusProductActionEnum.Expire, + action: OngoingCusProductActionEnum.Update, cusProduct: customerProduct, }; @@ -64,35 +78,32 @@ export const computeSubscriptionUpdateQuantityPlan = ( testClockFrozenTime, }); - const newCustomerProduct = initFullCusProduct({ - ctx, - fullCus: fullCustomer, - initContext: { - fullCus: fullCustomer, - product, - featureQuantities: [], - replaceables: [], - existingUsages: cusProductToExistingUsages({ - cusProduct: customerProduct, - }), - }, - }); + const stripeSubscriptionAction = { + type: "update" as const, + subId: stripeSubscription.id, + items: quantityUpdateDetails.map((detail) => { + if (detail.existingStripeSubscriptionItem) { + return { + id: detail.existingStripeSubscriptionItem.id, + quantity: detail.updatedFeatureQuantity, + }; + } - const stripeSubscriptionAction = buildStripeSubAction({ - ctx, - stripeSub: stripeSubscription!, - fullCus: fullCustomer, - paymentMethod, - ongoingCusProductAction, - newCusProducts: [newCustomerProduct], - }); + return { + price: detail.stripePriceId, + quantity: detail.updatedFeatureQuantity, + }; + }), + }; return { intent: SubscriptionUpdateIntentEnum.UpdateQuantity, customEntitlements: [], customPrices: [], featureQuantities, - action, + quantityUpdateDetails, + isSubscriptionTrialing, + invoiceAction, autumnLineItems, stripeSubscriptionAction, ongoingCusProductAction, diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateSchema.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateSchema.ts index 6e9ca9f64..83bccce71 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateSchema.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateSchema.ts @@ -1,5 +1,8 @@ import { z } from "zod/v4"; +/** + * The intent for a subscription update + */ export enum SubscriptionUpdateIntentEnum { UpdateQuantity = "update_quantity", UpdatePlan = "update_plan", diff --git a/server/src/internal/billing/v2/subscriptionUpdate/execute/executeSubscriptionUpdate.ts b/server/src/internal/billing/v2/subscriptionUpdate/execute/executeSubscriptionUpdate.ts index 16c8b235c..914640d33 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/execute/executeSubscriptionUpdate.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/execute/executeSubscriptionUpdate.ts @@ -1,53 +1,95 @@ import type { SubscriptionUpdateV0Params } from "@shared/index"; +import { createStripeCli } from "@/external/connect/createStripeCli"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService"; import { EntitlementService } from "@/internal/products/entitlements/EntitlementService"; import { PriceService } from "@/internal/products/prices/PriceService"; import { executeCusProductActions } from "../../execute/executeAutumnActions/executeCusProductActions"; +import { executeInvoiceAction } from "../../execute/executeInvoiceAction"; import { executeStripeSubAction } from "../../execute/executeStripeSubAction"; import type { SubscriptionUpdatePlan } from "../../types"; import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema"; -export const executeSubscriptionUpdate = async ( - ctx: AutumnContext, - { - params, - updateSubscriptionContext, - subscriptionUpdatePlan, - }: { - params: SubscriptionUpdateV0Params; - updateSubscriptionContext: UpdateSubscriptionContext; - subscriptionUpdatePlan: SubscriptionUpdatePlan; - }, -) => { - const { db, logger } = ctx; - const { customerProduct } = updateSubscriptionContext; +export const executeSubscriptionUpdate = async ({ + ctx, + params, + updateSubscriptionContext, + subscriptionUpdatePlan, +}: { + ctx: AutumnContext; + params: SubscriptionUpdateV0Params; + updateSubscriptionContext: UpdateSubscriptionContext; + subscriptionUpdatePlan: SubscriptionUpdatePlan; +}) => { + const { db, logger, org, env } = ctx; + const { customerProduct, stripeCustomer, stripeSubscription } = + updateSubscriptionContext; const { customEntitlements, customPrices, ongoingCusProductAction, stripeSubscriptionAction, + quantityUpdateDetails, + invoiceAction, } = subscriptionUpdatePlan; - await EntitlementService.insert({ - db, - data: customEntitlements, - }); + if (customEntitlements.length > 0) { + logger.info("Inserting custom entitlements"); + await EntitlementService.insert({ db, data: customEntitlements }); + } - await PriceService.insert({ - db, - data: customPrices, - }); + if (customPrices.length > 0) { + logger.info("Inserting custom prices"); + await PriceService.insert({ db, data: customPrices }); + } - logger.info("Executing stripe sub action"); + const isProductCanceled = customerProduct.canceled === true; + if (isProductCanceled) { + logger.info("Uncanceling subscription in Stripe"); + const stripeClient = createStripeCli({ org, env }); + await stripeClient.subscriptions.update(stripeSubscription.id, { + cancel_at_period_end: false, + }); + + logger.info("Uncanceling customer product in Autumn"); + await CusProductService.update({ + db, + cusProductId: customerProduct.id, + updates: { + canceled: false, + canceled_at: null, + ended_at: null, + }, + }); + } + + logger.info("Executing Stripe subscription action"); await executeStripeSubAction({ ctx, stripeSubAction: stripeSubscriptionAction, }); - logger.info("Executing cus product actions"); + if (invoiceAction) { + logger.info("Executing invoice action"); + await executeInvoiceAction({ + ctx, + invoiceAction, + stripeCustomerId: stripeCustomer.id, + stripeSubscriptionId: stripeSubscription.id, + customerProduct, + }); + } else { + logger.info("No invoice action required"); + } + + logger.info("Executing customer product actions"); await executeCusProductActions({ ctx, ongoingCusProductAction, - newCusProducts: [customerProduct], + newCusProducts: [], + quantityUpdateDetails, + updatedFeatureOptions: params.options || [], }); + + logger.info("Successfully completed subscription update"); }; diff --git a/server/src/internal/billing/v2/subscriptionUpdate/fetch/fetchApiSubscriptionUpdateContext.ts b/server/src/internal/billing/v2/subscriptionUpdate/fetch/fetchApiSubscriptionUpdateContext.ts index a485bf452..850d963b6 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/fetch/fetchApiSubscriptionUpdateContext.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/fetch/fetchApiSubscriptionUpdateContext.ts @@ -4,6 +4,7 @@ import { type SubscriptionUpdateV0Params, } from "@shared/index"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { mapOptionsList } from "@/internal/customers/attach/attachUtils/mapOptionsList"; import { CusService } from "../../../../customers/CusService"; import { fetchStripeCustomerForBilling } from "../../fetch/fetchStripeUtils/fetchStripeCustomerForBilling"; import { fetchStripeSubscriptionForBilling } from "../../fetch/fetchStripeUtils/fetchStripeSubscriptionForBilling"; @@ -14,23 +15,15 @@ import type { UpdateSubscriptionContext } from "./updateSubscriptionContextSchem * @param ctx - The context * @param body - The body of the request * @returns The update subscription context - * @example - * const context = await fetchApiSubscriptionUpdateContext(ctx, params); - * - * Returns: - * 1. Full customer - * 2. Target customer product - * 3. Stripe subscription (if applicable) - * 4. Stripe schedule (if applicable) - * 5. Stripe customer - * 6. Payment method (if applicable) - * 7. Test clock frozen time (if applicable) */ -export const fetchApiSubscriptionUpdateContext = async ( - ctx: AutumnContext, - params: SubscriptionUpdateV0Params, -): Promise => { - const { db, org, env } = ctx; +export const fetchApiSubscriptionUpdateContext = async ({ + ctx, + params, +}: { + ctx: AutumnContext; + params: SubscriptionUpdateV0Params; +}): Promise => { + const { db, org, env, features } = ctx; const { customer_id: customerId, product_id: productId } = params; const fullCustomer = await CusService.getFull({ @@ -63,6 +56,12 @@ export const fetchApiSubscriptionUpdateContext = async ( targetCusProductId: targetCustomerProduct.id, }); + if (!stripeSubscription) { + throw new InternalError({ + message: `[API Subscription Update] No active subscription found for customer product: ${productId}`, + }); + } + const { stripeCus: stripeCustomer, paymentMethod, @@ -72,6 +71,15 @@ export const fetchApiSubscriptionUpdateContext = async ( fullCus: fullCustomer, }); + if (params.options) { + params.options = mapOptionsList({ + optionsInput: params.options, + features, + prices: targetCustomerProduct.customer_prices.map((cp) => cp.price), + curCusProduct: targetCustomerProduct, + }); + } + return { fullCustomer, product: targetProduct, diff --git a/server/src/internal/billing/v2/subscriptionUpdate/fetch/updateSubscriptionContextSchema.ts b/server/src/internal/billing/v2/subscriptionUpdate/fetch/updateSubscriptionContextSchema.ts index c595f63a5..1d5f02916 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/fetch/updateSubscriptionContextSchema.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/fetch/updateSubscriptionContextSchema.ts @@ -5,7 +5,7 @@ export type UpdateSubscriptionContext = { fullCustomer: FullCustomer; product: FullProduct; customerProduct: FullCusProduct; - stripeSubscription?: Stripe.Subscription; + stripeSubscription: Stripe.Subscription; stripeSubscriptionSchedule?: Stripe.SubscriptionSchedule; stripeCustomer: Stripe.Customer; paymentMethod?: Stripe.PaymentMethod; diff --git a/server/src/internal/billing/v2/types.ts b/server/src/internal/billing/v2/types.ts index 55e44b66a..62a127637 100644 --- a/server/src/internal/billing/v2/types.ts +++ b/server/src/internal/billing/v2/types.ts @@ -5,6 +5,7 @@ import type { FreeTrial, FullCusProduct, FullCustomer, + FullCustomerPrice, FullProduct, LineItem, OngoingCusProductAction, @@ -84,17 +85,55 @@ export type BaseSubscriptionUpdatePlan = BillingPlan & { ongoingCusProductAction: OngoingCusProductAction; }; -export enum SubscriptionUpdateQuantityAction { - Upgrade = "upgrade", - Downgrade = "downgrade", -} +export type QuantityUpdateDetails = { + featureId: string; + internalFeatureId: string; + + previousFeatureQuantity: number; + updatedFeatureQuantity: number; + quantityDifferenceForEntitlements: number; + stripeSubscriptionItemQuantityDifference: number; + + shouldApplyProration: boolean; + shouldFinalizeInvoiceImmediately: boolean; + billingUnitsPerQuantity: number; + + calculatedProrationAmountDollars?: number; + subscriptionPeriodStartEpochMs: number; + subscriptionPeriodEndEpochMs: number; + + stripeInvoiceItemDescription: string; + + customerPrice: FullCustomerPrice; + stripePriceId: string; + existingStripeSubscriptionItem?: Stripe.SubscriptionItem; + + customerEntitlementId?: string; + customerEntitlementBalanceChange: number; +}; + +export type SubscriptionUpdateInvoiceAction = { + shouldCreateInvoice: boolean; + invoiceItems: { + description: string; + amountDollars: number; + stripePriceId: string; + periodStartEpochMs: number; + periodEndEpochMs: number; + }[]; + shouldChargeImmediately: boolean; + paymentMethod?: Stripe.PaymentMethod; + customerPrices: FullCustomerPrice[]; +}; export type SubscriptionUpdateQuantityPlan = BaseSubscriptionUpdatePlan & { featureQuantities: { old: FeatureOptions[]; new: FeatureOptions[]; }; - action: SubscriptionUpdateQuantityAction; + quantityUpdateDetails: QuantityUpdateDetails[]; + isSubscriptionTrialing: boolean; + invoiceAction?: SubscriptionUpdateInvoiceAction; }; export type SubscriptionUpdatePlan = SubscriptionUpdateQuantityPlan; diff --git a/server/tests/_temp/temp.test.ts b/server/tests/_temp/temp.test.ts index 116791909..ffc8272be 100644 --- a/server/tests/_temp/temp.test.ts +++ b/server/tests/_temp/temp.test.ts @@ -3,6 +3,7 @@ import { ApiVersion, CouponDurationType, type CreateReward, + ProductItemFeatureType, RewardType, } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; @@ -40,7 +41,7 @@ const free = constructProduct({ items: [ constructFeatureItem({ featureId: TestFeature.Messages, - includedUsage: 100, + includedUsage: 12, }), ], }); @@ -51,7 +52,7 @@ const pro = constructProduct({ items: [ constructFeatureItem({ featureId: TestFeature.Messages, - includedUsage: 100, + includedUsage: 12, }), // constructArrearProratedItem({ // featureId: TestFeature.Users, @@ -185,6 +186,17 @@ const reward: CreateReward = { }, }; +const superProd = constructRawProduct({ + id: "super", + items: [ + constructPrepaidItem({ + featureId: TestFeature.Messages, + billingUnits: 12, + price: 8, + }), + ], +}); + describe(`${chalk.yellowBright("temp: temporary script for testing")}`, () => { const customerId = "temp"; const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); @@ -206,6 +218,7 @@ describe(`${chalk.yellowBright("temp: temporary script for testing")}`, () => { freeAddOn, monthlyAddOn, oneOffCredits, + superProd, ], prefix: customerId, }); @@ -221,7 +234,13 @@ describe(`${chalk.yellowBright("temp: temporary script for testing")}`, () => { await autumnV1.attach({ customer_id: customerId, - product_id: pro.id, + product_id: superProd.id, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 10, + }, + ], }); // await autumnV1.entities.create(customerId, entities); diff --git a/server/tsconfig.json b/server/tsconfig.json index 8a0d0d6a7..833b5afa3 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -35,6 +35,6 @@ "@utils/*": ["../shared/utils/*"], } }, - "include": ["src", "tests", "scripts", "emails", "experiments", "src/internal/billing/v2/subscriptionUpdate", "../shared/api/billing/subscriptionUpdate/compute", "../shared/api/billing/subscriptionUpdate/fetch"], + "include": ["src", "tests", "scripts", "emails", "experiments"], "exclude": ["node_modules", "dist", "tests/archives"] } diff --git a/shared/models/billingModels/ongoingCusProductAction.ts b/shared/models/billingModels/ongoingCusProductAction.ts index 8c02eeb6c..740b4d922 100644 --- a/shared/models/billingModels/ongoingCusProductAction.ts +++ b/shared/models/billingModels/ongoingCusProductAction.ts @@ -5,6 +5,7 @@ export enum OngoingCusProductActionEnum { Expire = "expire", Cancel = "cancel", Uncancel = "uncancel", + Update = "update", } // What happens to the CURRENT active cus product diff --git a/shared/tsconfig.json b/shared/tsconfig.json index e3c2fa833..727da44c8 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -17,10 +17,7 @@ "@utils/*": ["./utils/*"] } }, - "include": [ - "./**/*", - "../server/src/internal/billing/v2/update-subscription" - ], + "include": ["./**/*"], "types": ["node"], "exclude": ["node_modules", "dist"] }