From acaf05d92a7ce1061aa4b31fa6f25ae344154322 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 6 Feb 2026 10:16:27 -0800 Subject: [PATCH] added utilities --- .../customerProductToPrice.ts | 85 +++++++++++++++++++ .../findActiveCustomerProduct.ts | 26 ++++++ .../priceUtils/classifyPriceUtils.ts | 14 +++ 3 files changed, 125 insertions(+) create mode 100644 shared/utils/cusProductUtils/convertCusProduct/customerProductToPrice.ts diff --git a/shared/utils/cusProductUtils/convertCusProduct/customerProductToPrice.ts b/shared/utils/cusProductUtils/convertCusProduct/customerProductToPrice.ts new file mode 100644 index 000000000..386da1ee7 --- /dev/null +++ b/shared/utils/cusProductUtils/convertCusProduct/customerProductToPrice.ts @@ -0,0 +1,85 @@ +import { InternalError } from "@api/errors/base/InternalError.js"; +import type { FullCusProduct } from "../../../models/cusProductModels/cusProductModels"; +import type { Feature } from "../../../models/featureModels/featureModels"; +import type { Price } from "../../../models/productModels/priceModels/priceModels"; + +import { + isFixedPrice, + isPrepaidPrice, + priceOnFeature, +} from "../../productUtils/priceUtils/classifyPriceUtils"; +import { cusProductToPrices } from "../convertCusProduct"; + +// Overload: errorOnNotFound = true → guaranteed Price +export function customerProductToBasePrice(params: { + customerProduct: FullCusProduct; + errorOnNotFound: true; +}): Price; + +// Overload: errorOnNotFound = false/undefined → Price | undefined +export function customerProductToBasePrice(params: { + customerProduct: FullCusProduct; + errorOnNotFound?: false; +}): Price | undefined; + +// Implementation +export function customerProductToBasePrice({ + customerProduct, + errorOnNotFound, +}: { + customerProduct: FullCusProduct; + errorOnNotFound?: boolean; +}): Price | undefined { + const prices = cusProductToPrices({ cusProduct: customerProduct }); + const result = prices.find((p) => isFixedPrice(p)); + + if (errorOnNotFound && !result) { + throw new InternalError({ + message: `Base price not found for customer product ${customerProduct.id}`, + }); + } + + return result; +} + +// Overload: errorOnNotFound = true → guaranteed Price +export function customerProductToPrepaidPrice(params: { + customerProduct: FullCusProduct; + feature?: Feature; + errorOnNotFound: true; +}): Price; + +// Overload: errorOnNotFound = false/undefined → Price | undefined +export function customerProductToPrepaidPrice(params: { + customerProduct: FullCusProduct; + feature?: Feature; + errorOnNotFound?: false; +}): Price | undefined; + +// Implementation +export function customerProductToPrepaidPrice({ + customerProduct, + feature, + errorOnNotFound, +}: { + customerProduct: FullCusProduct; + feature?: Feature; + errorOnNotFound?: boolean; +}): Price | undefined { + const prices = cusProductToPrices({ cusProduct: customerProduct }); + const result = prices.find((p) => { + const featureMatch = feature + ? priceOnFeature({ price: p, feature }) + : true; + return isPrepaidPrice(p) && featureMatch; + }); + + if (errorOnNotFound && !result) { + const featureMsg = feature ? ` for feature ${feature.id}` : ""; + throw new InternalError({ + message: `Prepaid price not found${featureMsg} for customer product ${customerProduct.id}`, + }); + } + + return result; +} diff --git a/shared/utils/cusProductUtils/findCustomerProduct/findActiveCustomerProduct.ts b/shared/utils/cusProductUtils/findCustomerProduct/findActiveCustomerProduct.ts index a9f070a65..33a3033f3 100644 --- a/shared/utils/cusProductUtils/findCustomerProduct/findActiveCustomerProduct.ts +++ b/shared/utils/cusProductUtils/findCustomerProduct/findActiveCustomerProduct.ts @@ -32,3 +32,29 @@ export const findMainActiveCustomerProductByGroup = ({ return activeMainCusProduct; }; + +export const findActiveCustomerProductById = ({ + fullCus, + productId, + internalEntityId, +}: { + fullCus: FullCustomer; + productId: string; + internalEntityId?: string; +}) => { + if (!internalEntityId) { + internalEntityId = fullCus.entity?.internal_id; + } + + const cusProducts = fullCus.customer_products; + + const activeMainCusProduct = cusProducts.find((customerProduct) => { + const { valid } = cp(customerProduct) + .hasProductId({ productId }) + .onEntity({ internalEntityId }); + + return valid; + }); + + return activeMainCusProduct; +}; diff --git a/shared/utils/productUtils/priceUtils/classifyPriceUtils.ts b/shared/utils/productUtils/priceUtils/classifyPriceUtils.ts index c8b3b2c96..bb5276b84 100644 --- a/shared/utils/productUtils/priceUtils/classifyPriceUtils.ts +++ b/shared/utils/productUtils/priceUtils/classifyPriceUtils.ts @@ -1,3 +1,4 @@ +import type { Feature } from "../../../models/featureModels/featureModels"; import { BillingInterval } from "../../../models/productModels/intervals/billingInterval"; import type { FixedPriceConfig } from "../../../models/productModels/priceModels/priceConfig/fixedPriceConfig"; import type { UsagePriceConfig } from "../../../models/productModels/priceModels/priceConfig/usagePriceConfig"; @@ -73,3 +74,16 @@ export const isPrepaidPrice = ( const billingType = getBillingType(price.config); return billingType === BillingType.UsageInAdvance; }; + +export const priceOnFeature = ({ + price, + feature, +}: { + price: Price; + feature: Feature; +}) => { + return ( + price.config.internal_feature_id === feature.internal_id || + price.config.feature_id === feature.id + ); +};