From d978e400d8449bdbe38454af4863bc9af2dd8397 Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Mon, 29 Dec 2025 20:17:38 +0000 Subject: [PATCH] refactor: third iteration of update subscription cleanup --- .../compute/computeInvoiceAction.ts | 4 +- .../compute/computeQuantityUpdateDetails.ts | 51 ++++-- .../computeSubscriptionUpdateQuantityPlan.ts | 22 +-- ... => calculateCustomerEntitlementChange.ts} | 2 +- .../calculateProrationAmount.ts | 14 +- .../fetchApiSubscriptionUpdateContext.ts | 6 +- server/src/internal/billing/v2/typesOld.ts | 4 +- .../v2/utils/parseFeatureQuantitiesParams.ts | 145 ++++++++++++++++++ .../attach/attachUtils/mapOptionsList.ts | 4 + shared/utils/billingUtils/index.ts | 1 - .../invoicingUtils/extractBillingPeriod.ts | 63 -------- .../featureOptionUtils/findFeatureOptions.ts | 30 ++++ .../findFeatureOptionsByFeature.ts | 21 --- shared/utils/index.ts | 2 +- 14 files changed, 235 insertions(+), 134 deletions(-) rename server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/{calculateEntitlementChange.ts => calculateCustomerEntitlementChange.ts} (96%) create mode 100644 server/src/internal/billing/v2/utils/parseFeatureQuantitiesParams.ts delete mode 100644 shared/utils/billingUtils/invoicingUtils/extractBillingPeriod.ts create mode 100644 shared/utils/cusProductUtils/featureOptionUtils/findFeatureOptions.ts delete mode 100644 shared/utils/cusProductUtils/findFeatureOptionsByFeature.ts diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeInvoiceAction.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeInvoiceAction.ts index f6b0415ff..96939f68d 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeInvoiceAction.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeInvoiceAction.ts @@ -51,8 +51,8 @@ export const computeStripeInvoiceAction = ({ description: detail.stripeInvoiceItemDescription, amount: Math.round(detail.calculatedProrationAmountDollars * 100), period: { - start: msToSeconds(detail.subscriptionPeriodStartEpochMs), - end: msToSeconds(detail.subscriptionPeriodEndEpochMs), + start: msToSeconds(detail.billingPeriod.start), + end: msToSeconds(detail.billingPeriod.end), }, })); diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeQuantityUpdateDetails.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeQuantityUpdateDetails.ts index 67459d6f5..85bbbb156 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeQuantityUpdateDetails.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeQuantityUpdateDetails.ts @@ -1,18 +1,21 @@ import { cusProductToProduct, - extractBillingPeriod, type FeatureOptions, findFeatureByInternalId, + findFeatureOptionsByFeature, InternalError, type LineItemContext, + orgToCurrency, + secondsToMs, } from "@autumn/shared"; import { usagePriceToLineDescription } from "@autumn/shared/utils/billingUtils/invoicingUtils/descriptionUtils/usagePriceToLineDescription"; +import { getLineItemBillingPeriod } from "@shared/utils/billingUtils/cycleUtils/getLineItemBillingPeriod"; import type Stripe from "stripe"; import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { QuantityUpdateDetails } from "@/internal/billing/v2/typesOld"; import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema"; -import { calculateEntitlementChange } from "./quantityUpdateUtils/calculateEntitlementChange"; +import { calculateCustomerEntitlementChange } from "./quantityUpdateUtils/calculateCustomerEntitlementChange"; import { calculateProrationAmount } from "./quantityUpdateUtils/calculateProrationAmount"; import { calculateQuantityDifferences } from "./quantityUpdateUtils/calculateQuantityDifferences"; import { resolvePriceForQuantityUpdate } from "./quantityUpdateUtils/resolvePriceForQuantityUpdate"; @@ -32,18 +35,16 @@ import { resolvePriceForQuantityUpdate } from "./quantityUpdateUtils/resolvePric */ export const computeQuantityUpdateDetails = ({ ctx, - previousOptions, updatedOptions, updateSubscriptionContext, }: { ctx: AutumnContext; - previousOptions: FeatureOptions; updatedOptions: FeatureOptions; updateSubscriptionContext: UpdateSubscriptionContext; }): QuantityUpdateDetails => { const { customerProduct, stripeSubscription, currentEpochMs } = updateSubscriptionContext; - const { features } = ctx; + const { features, org } = ctx; const internalFeatureId = updatedOptions.internal_feature_id; const featureId = updatedOptions.feature_id; @@ -71,6 +72,11 @@ export const computeQuantityUpdateDetails = ({ }); } + const previousOptions = findFeatureOptionsByFeature({ + featureOptions: customerProduct.options, + feature, + }); + const quantityDifferences = calculateQuantityDifferences({ previousOptions, updatedOptions, @@ -82,13 +88,28 @@ export const computeQuantityUpdateDetails = ({ isUpgrade: quantityDifferences.isUpgrade, }); - const billingPeriod = extractBillingPeriod({ - stripeSubscription, - interval: priceConfiguration.priceConfig.interval, - intervalCount: priceConfiguration.priceConfig.interval_count, - currentEpochMs, + const billingCycleAnchorMs = secondsToMs( + stripeSubscription.billing_cycle_anchor, + ); + + if (!billingCycleAnchorMs) { + throw new InternalError({ + message: `[Quantity Update] Invalid billing_cycle_anchor: ${stripeSubscription.billing_cycle_anchor}`, + }); + } + + const billingPeriod = getLineItemBillingPeriod({ + anchor: billingCycleAnchorMs, + price: priceConfiguration.price, + now: currentEpochMs, }); + if (!billingPeriod) { + throw new InternalError({ + message: `[Quantity Update] Billing period not found for price: ${priceConfiguration.price.id}`, + }); + } + const calculatedProrationAmountDollars = calculateProrationAmount({ updateSubscriptionContext, previousOptions, @@ -100,11 +121,13 @@ export const computeQuantityUpdateDetails = ({ const product = cusProductToProduct({ cusProduct: customerProduct }); + const currency = orgToCurrency({ org }); + const lineItemContext: LineItemContext = { price: priceConfiguration.price, product, feature, - currency: "usd", + currency, direction: "charge", now: currentEpochMs, billingTiming: "in_advance", @@ -120,7 +143,7 @@ export const computeQuantityUpdateDetails = ({ stripeItems: stripeSubscription.items.data, }) as Stripe.SubscriptionItem | undefined; - const entitlementChange = calculateEntitlementChange({ + const entitlementChange = calculateCustomerEntitlementChange({ quantityDifferenceForEntitlements: quantityDifferences.quantityDifferenceForEntitlements, billingUnitsPerQuantity: priceConfiguration.billingUnitsPerQuantity, @@ -137,6 +160,7 @@ export const computeQuantityUpdateDetails = ({ return { featureId, internalFeatureId, + billingPeriod, previousFeatureQuantity: previousOptions.quantity, updatedFeatureQuantity: updatedOptions.quantity, quantityDifferenceForEntitlements: @@ -148,9 +172,6 @@ export const computeQuantityUpdateDetails = ({ priceConfiguration.shouldFinalizeInvoiceImmediately, billingUnitsPerQuantity: priceConfiguration.billingUnitsPerQuantity, calculatedProrationAmountDollars, - subscriptionPeriodStartEpochMs: - billingPeriod.subscriptionPeriodStartEpochMs, - subscriptionPeriodEndEpochMs: billingPeriod.subscriptionPeriodEndEpochMs, stripeInvoiceItemDescription, customerPrice: priceConfiguration.customerPrice, stripePriceId: priceConfiguration.price.config.stripe_price_id, diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateQuantityPlan.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateQuantityPlan.ts index a6e16cc0c..da5ba6e56 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateQuantityPlan.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/computeSubscriptionUpdateQuantityPlan.ts @@ -1,8 +1,4 @@ -import { - findFeatureOptionsByFeature, - InternalError, - type SubscriptionUpdateV0Params, -} from "@autumn/shared"; +import { InternalError, type SubscriptionUpdateV0Params } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { BillingPlan } from "../../billingPlan"; import { buildStripeSubscriptionAction } from "../../providers/stripe/actionBuilders/buildStripeSubscriptionAction"; @@ -30,19 +26,13 @@ export const computeSubscriptionUpdateQuantityPlan = ({ const newOptions = params.options || []; - const quantityUpdateDetails = newOptions.map((updatedOption) => { - const previousOption = findFeatureOptionsByFeature({ - featureOptions: customerProduct.options, - featureId: updatedOption.feature_id, - }); - - return computeQuantityUpdateDetails({ + const quantityUpdateDetails = newOptions.map((updatedOptions) => + computeQuantityUpdateDetails({ ctx, - previousOptions: previousOption, - updatedOptions: updatedOption, + updatedOptions, updateSubscriptionContext, - }); - }); + }), + ); const customerProductWithNewOptions = { ...customerProduct, diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateEntitlementChange.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateCustomerEntitlementChange.ts similarity index 96% rename from server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateEntitlementChange.ts rename to server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateCustomerEntitlementChange.ts index 28ca581c0..20ac1b71e 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateEntitlementChange.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateCustomerEntitlementChange.ts @@ -17,7 +17,7 @@ import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cus * @param customerEntitlements - Array of all entitlements for this customer product * @returns Entitlement ID and balance change to apply */ -export const calculateEntitlementChange = ({ +export const calculateCustomerEntitlementChange = ({ quantityDifferenceForEntitlements, billingUnitsPerQuantity, customerPrice, diff --git a/server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateProrationAmount.ts b/server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateProrationAmount.ts index d0fdf55ce..b818552b8 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateProrationAmount.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/compute/quantityUpdateUtils/calculateProrationAmount.ts @@ -1,6 +1,6 @@ import { applyProration, - type extractBillingPeriod, + type BillingPeriod, type FeatureOptions, priceToLineAmount, } from "@autumn/shared"; @@ -36,11 +36,10 @@ export const calculateProrationAmount = ({ updatedOptions: FeatureOptions; priceConfiguration: ReturnType; quantityDifferences: ReturnType; - billingPeriod: ReturnType; + billingPeriod: BillingPeriod; }): number | undefined => { const { stripeSubscription, currentEpochMs } = updateSubscriptionContext; - const { price, billingUnitsPerQuantity, shouldApplyProration } = - priceConfiguration; + const { price, billingUnitsPerQuantity } = priceConfiguration; const { isUpgrade } = quantityDifferences; if (!stripeSubscription) { @@ -49,7 +48,7 @@ export const calculateProrationAmount = ({ const isTrialing = stripeSubscription.status === "trialing"; - if (!shouldApplyProration || isTrialing) { + if (isTrialing) { return undefined; } @@ -76,10 +75,7 @@ export const calculateProrationAmount = ({ const proratedAmountDollars = applyProration({ now: currentEpochMs, - billingPeriod: { - start: billingPeriod.subscriptionPeriodStartEpochMs, - end: billingPeriod.subscriptionPeriodEndEpochMs, - }, + billingPeriod, amount: amountDifferenceDollars.toNumber(), }); diff --git a/server/src/internal/billing/v2/subscriptionUpdate/fetch/fetchApiSubscriptionUpdateContext.ts b/server/src/internal/billing/v2/subscriptionUpdate/fetch/fetchApiSubscriptionUpdateContext.ts index eafa90f02..970e4c06f 100644 --- a/server/src/internal/billing/v2/subscriptionUpdate/fetch/fetchApiSubscriptionUpdateContext.ts +++ b/server/src/internal/billing/v2/subscriptionUpdate/fetch/fetchApiSubscriptionUpdateContext.ts @@ -3,8 +3,8 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { fetchStripeCustomerForBilling } from "@/internal/billing/v2/providers/stripe/fetch/fetchStripeCustomerForBilling"; import { fetchStripeSubscriptionForBilling } from "@/internal/billing/v2/providers/stripe/fetch/fetchStripeSubscriptionForBilling"; import { fetchStripeSubscriptionScheduleForBilling } from "@/internal/billing/v2/providers/stripe/fetch/fetchStripeSubscriptionScheduleForBilling"; -import { mapOptionsList } from "@/internal/customers/attach/attachUtils/mapOptionsList"; import { CusService } from "../../../../customers/CusService"; +import { parseFeatureQuantitiesParams } from "../../utils/parseFeatureQuantitiesParams"; import { fetchTargetCusProductForUpdate } from "./fetchTargetCusProductForUpdate"; import type { UpdateSubscriptionContext } from "./updateSubscriptionContextSchema"; @@ -74,11 +74,11 @@ export const fetchApiSubscriptionUpdateContext = async ({ }); if (params.options) { - params.options = mapOptionsList({ + params.options = parseFeatureQuantitiesParams({ optionsInput: params.options, features, prices: targetCustomerProduct.customer_prices.map((cp) => cp.price), - curCusProduct: targetCustomerProduct, + currentCustomerProduct: targetCustomerProduct, }); } diff --git a/server/src/internal/billing/v2/typesOld.ts b/server/src/internal/billing/v2/typesOld.ts index 01e429900..56beb8dfb 100644 --- a/server/src/internal/billing/v2/typesOld.ts +++ b/server/src/internal/billing/v2/typesOld.ts @@ -1,5 +1,6 @@ import { type AttachBodyV1, + BillingPeriodSchema, type FeatureOptions, type FreeTrial, type FullCusProduct, @@ -96,8 +97,7 @@ export const QuantityUpdateDetailsSchema = z.object({ billingUnitsPerQuantity: z.number(), calculatedProrationAmountDollars: z.number().optional(), - subscriptionPeriodStartEpochMs: z.number(), - subscriptionPeriodEndEpochMs: z.number(), + billingPeriod: BillingPeriodSchema, stripeInvoiceItemDescription: z.string(), diff --git a/server/src/internal/billing/v2/utils/parseFeatureQuantitiesParams.ts b/server/src/internal/billing/v2/utils/parseFeatureQuantitiesParams.ts new file mode 100644 index 000000000..396039cbd --- /dev/null +++ b/server/src/internal/billing/v2/utils/parseFeatureQuantitiesParams.ts @@ -0,0 +1,145 @@ +import { + ErrCode, + type Feature, + type FeatureOptions, + type FullCusProduct, + nullish, + type Price, + type UsagePriceConfig, +} from "@autumn/shared"; +import { Decimal } from "decimal.js"; +import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; +import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; + +/** + * Parses and normalizes feature quantity options for billing. + * + * Converts raw quantity input to billing units and merges with existing options + * from the current customer product (for recurring products only). + * + * @param optionsInput - Raw feature options with quantities + * @param features - Available features to validate against + * @param prices - Product prices to find prepaid price config + * @param currentCustomerProduct - Existing customer product for merging options + * @returns Normalized feature options with internal_feature_id and adjusted quantities + */ +export const parseFeatureQuantitiesParams = ({ + optionsInput, + features, + prices, + currentCustomerProduct, +}: { + optionsInput?: FeatureOptions[]; + features: Feature[]; + prices: Price[]; + currentCustomerProduct?: FullCusProduct; +}): FeatureOptions[] => { + const parsedOptions = parseAndNormalizeOptions({ + optionsInput, + features, + prices, + }); + + if (isOneOff(prices) || isFreeProduct(prices)) { + return parsedOptions; + } + + return mergeWithExistingOptions({ + parsedOptions, + currentCustomerProduct, + prices, + }); +}; + +const parseAndNormalizeOptions = ({ + optionsInput, + features, + prices, +}: { + optionsInput?: FeatureOptions[]; + features: Feature[]; + prices: Price[]; +}): FeatureOptions[] => { + const result: FeatureOptions[] = []; + + for (const options of optionsInput || []) { + const feature = features.find( + (feature) => feature.id === options.feature_id, + ); + + if (!feature) { + throw new RecaseError({ + message: `Feature ${options.feature_id} passed into options but not found`, + code: ErrCode.FeatureNotFound, + }); + } + + const prepaidPrice = findPrepaidPrice({ + prices, + internalFeatureId: feature.internal_id, + }); + + if (!prepaidPrice) { + throw new RecaseError({ + message: `No prepaid price found for feature ${feature.id}`, + code: ErrCode.PriceNotFound, + }); + } + + const config = prepaidPrice.config as UsagePriceConfig; + const billingUnits = config.billing_units || 1; + + if (nullish(options.quantity)) { + throw new RecaseError({ + message: `Quantity is required for feature ${feature.id}`, + code: ErrCode.InvalidOptions, + }); + } + + const normalizedQuantity = new Decimal(options.quantity) + .div(billingUnits) + .ceil() + .toNumber(); + + result.push({ + ...options, + internal_feature_id: feature.internal_id, + quantity: normalizedQuantity, + }); + } + + return result; +}; + +const mergeWithExistingOptions = ({ + parsedOptions, + currentCustomerProduct, + prices, +}: { + parsedOptions: FeatureOptions[]; + currentCustomerProduct?: FullCusProduct; + prices: Price[]; +}): FeatureOptions[] => { + const existingOptions = currentCustomerProduct?.options || []; + const mergedOptions = [...parsedOptions]; + + for (const existingOption of existingOptions) { + const alreadyIncluded = parsedOptions.some( + (option) => option.feature_id === existingOption.feature_id, + ); + + if (alreadyIncluded) continue; + + const hasPrepaidPrice = findPrepaidPrice({ + prices, + internalFeatureId: existingOption.internal_feature_id!, + }); + + if (hasPrepaidPrice) { + mergedOptions.push(existingOption); + } + } + + return mergedOptions; +}; diff --git a/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts b/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts index 00e2162c7..4f6f9252b 100644 --- a/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts +++ b/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts @@ -7,10 +7,14 @@ import { type UsagePriceConfig, } from "@autumn/shared"; import { Decimal } from "decimal.js"; +import { parseFeatureQuantitiesParams } from "@/internal/billing/v2/utils/parseFeatureQuantitiesParams.js"; import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; import RecaseError from "@/utils/errorUtils.js"; +/** + * @deprecated Can now use {@link parseFeatureQuantitiesParams} instead + */ export const mapOptionsList = ({ optionsInput, features, diff --git a/shared/utils/billingUtils/index.ts b/shared/utils/billingUtils/index.ts index 66599202d..6fa48838b 100644 --- a/shared/utils/billingUtils/index.ts +++ b/shared/utils/billingUtils/index.ts @@ -8,7 +8,6 @@ export * from "./intervalUtils/intervalArithmetic"; // Invoicing utils export * from "./invoicingUtils/cusProductToArrearLineItems"; export * from "./invoicingUtils/cusProductToLineItems"; -export * from "./invoicingUtils/extractBillingPeriod"; export * from "./invoicingUtils/lineItemBuilders/fixedPriceToLineItem"; export * from "./invoicingUtils/lineItemBuilders/usagePriceToLineItem"; export * from "./invoicingUtils/lineItemUtils/priceToLineAmount"; diff --git a/shared/utils/billingUtils/invoicingUtils/extractBillingPeriod.ts b/shared/utils/billingUtils/invoicingUtils/extractBillingPeriod.ts deleted file mode 100644 index 0f18ba8bf..000000000 --- a/shared/utils/billingUtils/invoicingUtils/extractBillingPeriod.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { InternalError } from "@api/errors/base/InternalError"; -import type { BillingInterval } from "@models/productModels/intervals/billingInterval"; -import { secondsToMs } from "@utils/common/unixUtils"; -import type Stripe from "stripe"; -import { getCycleEnd } from "../cycleUtils/getCycleEnd"; -import { getCycleStart } from "../cycleUtils/getCycleStart"; - -/** - * Calculates the current billing period for a subscription. - * - * Uses the subscription's billing_cycle_anchor and the price's interval configuration - * to calculate the billing period in-house, rather than relying on Stripe subscription - * item periods. - * - * @param stripeSubscription - Stripe subscription object (for billing_cycle_anchor) - * @param interval - The billing interval from the price config - * @param intervalCount - Number of intervals per cycle (default: 1) - * @param currentEpochMs - Current timestamp in milliseconds - * @returns Start and end timestamps in milliseconds - */ -export const extractBillingPeriod = ({ - stripeSubscription, - interval, - intervalCount = 1, - currentEpochMs, -}: { - stripeSubscription: Stripe.Subscription; - interval: BillingInterval; - intervalCount?: number; - currentEpochMs: number; -}): { - subscriptionPeriodStartEpochMs: number; - subscriptionPeriodEndEpochMs: number; -} => { - const billingCycleAnchorMs = secondsToMs( - stripeSubscription.billing_cycle_anchor, - ); - - if (!billingCycleAnchorMs) { - throw new InternalError({ - message: `[Billing] Invalid billing_cycle_anchor: ${stripeSubscription.billing_cycle_anchor}`, - }); - } - - const subscriptionPeriodStartEpochMs = getCycleStart({ - anchor: billingCycleAnchorMs, - interval, - intervalCount, - now: currentEpochMs, - }); - - const subscriptionPeriodEndEpochMs = getCycleEnd({ - anchor: billingCycleAnchorMs, - interval, - intervalCount, - now: currentEpochMs, - }); - - return { - subscriptionPeriodStartEpochMs, - subscriptionPeriodEndEpochMs, - }; -}; diff --git a/shared/utils/cusProductUtils/featureOptionUtils/findFeatureOptions.ts b/shared/utils/cusProductUtils/featureOptionUtils/findFeatureOptions.ts new file mode 100644 index 000000000..76b250c55 --- /dev/null +++ b/shared/utils/cusProductUtils/featureOptionUtils/findFeatureOptions.ts @@ -0,0 +1,30 @@ +import { InternalError } from "@api/errors"; +import type { FeatureOptions } from "@models/cusProductModels/cusProductModels"; +import type { Feature } from "@models/featureModels/featureModels"; + +/** + * Find the feature options for a feature + * @param featureOptions - The feature options to search through + * @param feature - The feature to find the options for + * @returns The feature options, or undefined if not found + */ +export const findFeatureOptionsByFeature = ({ + featureOptions, + feature, +}: { + featureOptions: FeatureOptions[]; + feature: Feature; +}) => { + const options = featureOptions.find( + (oldOption) => + oldOption.internal_feature_id === feature.internal_id || + oldOption.feature_id === feature.id, + ); + + if (!options) { + throw new InternalError({ + message: `[Find Feature Options By Feature] Cannot find feature options for feature: ${feature.id}.`, + }); + } + return options; +}; diff --git a/shared/utils/cusProductUtils/findFeatureOptionsByFeature.ts b/shared/utils/cusProductUtils/findFeatureOptionsByFeature.ts deleted file mode 100644 index 33b962a73..000000000 --- a/shared/utils/cusProductUtils/findFeatureOptionsByFeature.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { InternalError } from "@api/errors"; -import type { FeatureOptions } from "@models/cusProductModels/cusProductModels"; - -export const findFeatureOptionsByFeature = ({ - featureOptions, - featureId, -}: { - featureOptions: FeatureOptions[]; - featureId: string; -}) => { - const previousOption = featureOptions.find( - (oldOption) => oldOption.feature_id === featureId, - ); - - if (!previousOption) { - throw new InternalError({ - message: `[Find Feature Options By Feature] Cannot find feature options for feature: ${featureId}.`, - }); - } - return previousOption; -}; diff --git a/shared/utils/index.ts b/shared/utils/index.ts index c77af11a8..54b94ab52 100644 --- a/shared/utils/index.ts +++ b/shared/utils/index.ts @@ -40,7 +40,7 @@ export * from "./cusProductUtils/cusProductConstants.js"; export * from "./cusProductUtils/cusProductUtils.js"; export * from "./cusProductUtils/filterCusProductUtils.js"; export * from "./cusProductUtils/filterCusProductUtils.js"; -export * from "./cusProductUtils/findFeatureOptionsByFeature.js"; +export * from "./cusProductUtils/featureOptionUtils/findFeatureOptions.js"; export * from "./cusProductUtils/getCusProductFromCustomer.js"; export * from "./cusProductUtils/productIdToCusProduct.js"; // Cus utils