refactor: third iteration of update subscription cleanup

This commit is contained in:
Charlie Lamb
2025-12-29 20:17:38 +00:00
parent d642b560e1
commit d978e400d8
14 changed files with 235 additions and 134 deletions

View File

@@ -51,8 +51,8 @@ export const computeStripeInvoiceAction = ({
description: detail.stripeInvoiceItemDescription, description: detail.stripeInvoiceItemDescription,
amount: Math.round(detail.calculatedProrationAmountDollars * 100), amount: Math.round(detail.calculatedProrationAmountDollars * 100),
period: { period: {
start: msToSeconds(detail.subscriptionPeriodStartEpochMs), start: msToSeconds(detail.billingPeriod.start),
end: msToSeconds(detail.subscriptionPeriodEndEpochMs), end: msToSeconds(detail.billingPeriod.end),
}, },
})); }));

View File

@@ -1,18 +1,21 @@
import { import {
cusProductToProduct, cusProductToProduct,
extractBillingPeriod,
type FeatureOptions, type FeatureOptions,
findFeatureByInternalId, findFeatureByInternalId,
findFeatureOptionsByFeature,
InternalError, InternalError,
type LineItemContext, type LineItemContext,
orgToCurrency,
secondsToMs,
} from "@autumn/shared"; } from "@autumn/shared";
import { usagePriceToLineDescription } from "@autumn/shared/utils/billingUtils/invoicingUtils/descriptionUtils/usagePriceToLineDescription"; import { usagePriceToLineDescription } from "@autumn/shared/utils/billingUtils/invoicingUtils/descriptionUtils/usagePriceToLineDescription";
import { getLineItemBillingPeriod } from "@shared/utils/billingUtils/cycleUtils/getLineItemBillingPeriod";
import type Stripe from "stripe"; import type Stripe from "stripe";
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils"; import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils";
import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { AutumnContext } from "@/honoUtils/HonoEnv";
import type { QuantityUpdateDetails } from "@/internal/billing/v2/typesOld"; import type { QuantityUpdateDetails } from "@/internal/billing/v2/typesOld";
import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema"; import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema";
import { calculateEntitlementChange } from "./quantityUpdateUtils/calculateEntitlementChange"; import { calculateCustomerEntitlementChange } from "./quantityUpdateUtils/calculateCustomerEntitlementChange";
import { calculateProrationAmount } from "./quantityUpdateUtils/calculateProrationAmount"; import { calculateProrationAmount } from "./quantityUpdateUtils/calculateProrationAmount";
import { calculateQuantityDifferences } from "./quantityUpdateUtils/calculateQuantityDifferences"; import { calculateQuantityDifferences } from "./quantityUpdateUtils/calculateQuantityDifferences";
import { resolvePriceForQuantityUpdate } from "./quantityUpdateUtils/resolvePriceForQuantityUpdate"; import { resolvePriceForQuantityUpdate } from "./quantityUpdateUtils/resolvePriceForQuantityUpdate";
@@ -32,18 +35,16 @@ import { resolvePriceForQuantityUpdate } from "./quantityUpdateUtils/resolvePric
*/ */
export const computeQuantityUpdateDetails = ({ export const computeQuantityUpdateDetails = ({
ctx, ctx,
previousOptions,
updatedOptions, updatedOptions,
updateSubscriptionContext, updateSubscriptionContext,
}: { }: {
ctx: AutumnContext; ctx: AutumnContext;
previousOptions: FeatureOptions;
updatedOptions: FeatureOptions; updatedOptions: FeatureOptions;
updateSubscriptionContext: UpdateSubscriptionContext; updateSubscriptionContext: UpdateSubscriptionContext;
}): QuantityUpdateDetails => { }): QuantityUpdateDetails => {
const { customerProduct, stripeSubscription, currentEpochMs } = const { customerProduct, stripeSubscription, currentEpochMs } =
updateSubscriptionContext; updateSubscriptionContext;
const { features } = ctx; const { features, org } = ctx;
const internalFeatureId = updatedOptions.internal_feature_id; const internalFeatureId = updatedOptions.internal_feature_id;
const featureId = updatedOptions.feature_id; const featureId = updatedOptions.feature_id;
@@ -71,6 +72,11 @@ export const computeQuantityUpdateDetails = ({
}); });
} }
const previousOptions = findFeatureOptionsByFeature({
featureOptions: customerProduct.options,
feature,
});
const quantityDifferences = calculateQuantityDifferences({ const quantityDifferences = calculateQuantityDifferences({
previousOptions, previousOptions,
updatedOptions, updatedOptions,
@@ -82,13 +88,28 @@ export const computeQuantityUpdateDetails = ({
isUpgrade: quantityDifferences.isUpgrade, isUpgrade: quantityDifferences.isUpgrade,
}); });
const billingPeriod = extractBillingPeriod({ const billingCycleAnchorMs = secondsToMs(
stripeSubscription, stripeSubscription.billing_cycle_anchor,
interval: priceConfiguration.priceConfig.interval, );
intervalCount: priceConfiguration.priceConfig.interval_count,
currentEpochMs, 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({ const calculatedProrationAmountDollars = calculateProrationAmount({
updateSubscriptionContext, updateSubscriptionContext,
previousOptions, previousOptions,
@@ -100,11 +121,13 @@ export const computeQuantityUpdateDetails = ({
const product = cusProductToProduct({ cusProduct: customerProduct }); const product = cusProductToProduct({ cusProduct: customerProduct });
const currency = orgToCurrency({ org });
const lineItemContext: LineItemContext = { const lineItemContext: LineItemContext = {
price: priceConfiguration.price, price: priceConfiguration.price,
product, product,
feature, feature,
currency: "usd", currency,
direction: "charge", direction: "charge",
now: currentEpochMs, now: currentEpochMs,
billingTiming: "in_advance", billingTiming: "in_advance",
@@ -120,7 +143,7 @@ export const computeQuantityUpdateDetails = ({
stripeItems: stripeSubscription.items.data, stripeItems: stripeSubscription.items.data,
}) as Stripe.SubscriptionItem | undefined; }) as Stripe.SubscriptionItem | undefined;
const entitlementChange = calculateEntitlementChange({ const entitlementChange = calculateCustomerEntitlementChange({
quantityDifferenceForEntitlements: quantityDifferenceForEntitlements:
quantityDifferences.quantityDifferenceForEntitlements, quantityDifferences.quantityDifferenceForEntitlements,
billingUnitsPerQuantity: priceConfiguration.billingUnitsPerQuantity, billingUnitsPerQuantity: priceConfiguration.billingUnitsPerQuantity,
@@ -137,6 +160,7 @@ export const computeQuantityUpdateDetails = ({
return { return {
featureId, featureId,
internalFeatureId, internalFeatureId,
billingPeriod,
previousFeatureQuantity: previousOptions.quantity, previousFeatureQuantity: previousOptions.quantity,
updatedFeatureQuantity: updatedOptions.quantity, updatedFeatureQuantity: updatedOptions.quantity,
quantityDifferenceForEntitlements: quantityDifferenceForEntitlements:
@@ -148,9 +172,6 @@ export const computeQuantityUpdateDetails = ({
priceConfiguration.shouldFinalizeInvoiceImmediately, priceConfiguration.shouldFinalizeInvoiceImmediately,
billingUnitsPerQuantity: priceConfiguration.billingUnitsPerQuantity, billingUnitsPerQuantity: priceConfiguration.billingUnitsPerQuantity,
calculatedProrationAmountDollars, calculatedProrationAmountDollars,
subscriptionPeriodStartEpochMs:
billingPeriod.subscriptionPeriodStartEpochMs,
subscriptionPeriodEndEpochMs: billingPeriod.subscriptionPeriodEndEpochMs,
stripeInvoiceItemDescription, stripeInvoiceItemDescription,
customerPrice: priceConfiguration.customerPrice, customerPrice: priceConfiguration.customerPrice,
stripePriceId: priceConfiguration.price.config.stripe_price_id, stripePriceId: priceConfiguration.price.config.stripe_price_id,

View File

@@ -1,8 +1,4 @@
import { import { InternalError, type SubscriptionUpdateV0Params } from "@autumn/shared";
findFeatureOptionsByFeature,
InternalError,
type SubscriptionUpdateV0Params,
} from "@autumn/shared";
import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { AutumnContext } from "@/honoUtils/HonoEnv";
import type { BillingPlan } from "../../billingPlan"; import type { BillingPlan } from "../../billingPlan";
import { buildStripeSubscriptionAction } from "../../providers/stripe/actionBuilders/buildStripeSubscriptionAction"; import { buildStripeSubscriptionAction } from "../../providers/stripe/actionBuilders/buildStripeSubscriptionAction";
@@ -30,19 +26,13 @@ export const computeSubscriptionUpdateQuantityPlan = ({
const newOptions = params.options || []; const newOptions = params.options || [];
const quantityUpdateDetails = newOptions.map((updatedOption) => { const quantityUpdateDetails = newOptions.map((updatedOptions) =>
const previousOption = findFeatureOptionsByFeature({ computeQuantityUpdateDetails({
featureOptions: customerProduct.options,
featureId: updatedOption.feature_id,
});
return computeQuantityUpdateDetails({
ctx, ctx,
previousOptions: previousOption, updatedOptions,
updatedOptions: updatedOption,
updateSubscriptionContext, updateSubscriptionContext,
}); }),
}); );
const customerProductWithNewOptions = { const customerProductWithNewOptions = {
...customerProduct, ...customerProduct,

View File

@@ -17,7 +17,7 @@ import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cus
* @param customerEntitlements - Array of all entitlements for this customer product * @param customerEntitlements - Array of all entitlements for this customer product
* @returns Entitlement ID and balance change to apply * @returns Entitlement ID and balance change to apply
*/ */
export const calculateEntitlementChange = ({ export const calculateCustomerEntitlementChange = ({
quantityDifferenceForEntitlements, quantityDifferenceForEntitlements,
billingUnitsPerQuantity, billingUnitsPerQuantity,
customerPrice, customerPrice,

View File

@@ -1,6 +1,6 @@
import { import {
applyProration, applyProration,
type extractBillingPeriod, type BillingPeriod,
type FeatureOptions, type FeatureOptions,
priceToLineAmount, priceToLineAmount,
} from "@autumn/shared"; } from "@autumn/shared";
@@ -36,11 +36,10 @@ export const calculateProrationAmount = ({
updatedOptions: FeatureOptions; updatedOptions: FeatureOptions;
priceConfiguration: ReturnType<typeof resolvePriceForQuantityUpdate>; priceConfiguration: ReturnType<typeof resolvePriceForQuantityUpdate>;
quantityDifferences: ReturnType<typeof calculateQuantityDifferences>; quantityDifferences: ReturnType<typeof calculateQuantityDifferences>;
billingPeriod: ReturnType<typeof extractBillingPeriod>; billingPeriod: BillingPeriod;
}): number | undefined => { }): number | undefined => {
const { stripeSubscription, currentEpochMs } = updateSubscriptionContext; const { stripeSubscription, currentEpochMs } = updateSubscriptionContext;
const { price, billingUnitsPerQuantity, shouldApplyProration } = const { price, billingUnitsPerQuantity } = priceConfiguration;
priceConfiguration;
const { isUpgrade } = quantityDifferences; const { isUpgrade } = quantityDifferences;
if (!stripeSubscription) { if (!stripeSubscription) {
@@ -49,7 +48,7 @@ export const calculateProrationAmount = ({
const isTrialing = stripeSubscription.status === "trialing"; const isTrialing = stripeSubscription.status === "trialing";
if (!shouldApplyProration || isTrialing) { if (isTrialing) {
return undefined; return undefined;
} }
@@ -76,10 +75,7 @@ export const calculateProrationAmount = ({
const proratedAmountDollars = applyProration({ const proratedAmountDollars = applyProration({
now: currentEpochMs, now: currentEpochMs,
billingPeriod: { billingPeriod,
start: billingPeriod.subscriptionPeriodStartEpochMs,
end: billingPeriod.subscriptionPeriodEndEpochMs,
},
amount: amountDifferenceDollars.toNumber(), amount: amountDifferenceDollars.toNumber(),
}); });

View File

@@ -3,8 +3,8 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv";
import { fetchStripeCustomerForBilling } from "@/internal/billing/v2/providers/stripe/fetch/fetchStripeCustomerForBilling"; import { fetchStripeCustomerForBilling } from "@/internal/billing/v2/providers/stripe/fetch/fetchStripeCustomerForBilling";
import { fetchStripeSubscriptionForBilling } from "@/internal/billing/v2/providers/stripe/fetch/fetchStripeSubscriptionForBilling"; import { fetchStripeSubscriptionForBilling } from "@/internal/billing/v2/providers/stripe/fetch/fetchStripeSubscriptionForBilling";
import { fetchStripeSubscriptionScheduleForBilling } from "@/internal/billing/v2/providers/stripe/fetch/fetchStripeSubscriptionScheduleForBilling"; import { fetchStripeSubscriptionScheduleForBilling } from "@/internal/billing/v2/providers/stripe/fetch/fetchStripeSubscriptionScheduleForBilling";
import { mapOptionsList } from "@/internal/customers/attach/attachUtils/mapOptionsList";
import { CusService } from "../../../../customers/CusService"; import { CusService } from "../../../../customers/CusService";
import { parseFeatureQuantitiesParams } from "../../utils/parseFeatureQuantitiesParams";
import { fetchTargetCusProductForUpdate } from "./fetchTargetCusProductForUpdate"; import { fetchTargetCusProductForUpdate } from "./fetchTargetCusProductForUpdate";
import type { UpdateSubscriptionContext } from "./updateSubscriptionContextSchema"; import type { UpdateSubscriptionContext } from "./updateSubscriptionContextSchema";
@@ -74,11 +74,11 @@ export const fetchApiSubscriptionUpdateContext = async ({
}); });
if (params.options) { if (params.options) {
params.options = mapOptionsList({ params.options = parseFeatureQuantitiesParams({
optionsInput: params.options, optionsInput: params.options,
features, features,
prices: targetCustomerProduct.customer_prices.map((cp) => cp.price), prices: targetCustomerProduct.customer_prices.map((cp) => cp.price),
curCusProduct: targetCustomerProduct, currentCustomerProduct: targetCustomerProduct,
}); });
} }

View File

@@ -1,5 +1,6 @@
import { import {
type AttachBodyV1, type AttachBodyV1,
BillingPeriodSchema,
type FeatureOptions, type FeatureOptions,
type FreeTrial, type FreeTrial,
type FullCusProduct, type FullCusProduct,
@@ -96,8 +97,7 @@ export const QuantityUpdateDetailsSchema = z.object({
billingUnitsPerQuantity: z.number(), billingUnitsPerQuantity: z.number(),
calculatedProrationAmountDollars: z.number().optional(), calculatedProrationAmountDollars: z.number().optional(),
subscriptionPeriodStartEpochMs: z.number(), billingPeriod: BillingPeriodSchema,
subscriptionPeriodEndEpochMs: z.number(),
stripeInvoiceItemDescription: z.string(), stripeInvoiceItemDescription: z.string(),

View File

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

View File

@@ -7,10 +7,14 @@ import {
type UsagePriceConfig, type UsagePriceConfig,
} from "@autumn/shared"; } from "@autumn/shared";
import { Decimal } from "decimal.js"; import { Decimal } from "decimal.js";
import { parseFeatureQuantitiesParams } from "@/internal/billing/v2/utils/parseFeatureQuantitiesParams.js";
import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import RecaseError from "@/utils/errorUtils.js"; import RecaseError from "@/utils/errorUtils.js";
/**
* @deprecated Can now use {@link parseFeatureQuantitiesParams} instead
*/
export const mapOptionsList = ({ export const mapOptionsList = ({
optionsInput, optionsInput,
features, features,

View File

@@ -8,7 +8,6 @@ export * from "./intervalUtils/intervalArithmetic";
// Invoicing utils // Invoicing utils
export * from "./invoicingUtils/cusProductToArrearLineItems"; export * from "./invoicingUtils/cusProductToArrearLineItems";
export * from "./invoicingUtils/cusProductToLineItems"; export * from "./invoicingUtils/cusProductToLineItems";
export * from "./invoicingUtils/extractBillingPeriod";
export * from "./invoicingUtils/lineItemBuilders/fixedPriceToLineItem"; export * from "./invoicingUtils/lineItemBuilders/fixedPriceToLineItem";
export * from "./invoicingUtils/lineItemBuilders/usagePriceToLineItem"; export * from "./invoicingUtils/lineItemBuilders/usagePriceToLineItem";
export * from "./invoicingUtils/lineItemUtils/priceToLineAmount"; export * from "./invoicingUtils/lineItemUtils/priceToLineAmount";

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ export * from "./cusProductUtils/cusProductConstants.js";
export * from "./cusProductUtils/cusProductUtils.js"; export * from "./cusProductUtils/cusProductUtils.js";
export * from "./cusProductUtils/filterCusProductUtils.js"; export * from "./cusProductUtils/filterCusProductUtils.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/getCusProductFromCustomer.js";
export * from "./cusProductUtils/productIdToCusProduct.js"; export * from "./cusProductUtils/productIdToCusProduct.js";
// Cus utils // Cus utils