diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts index f1173065a..15b93da08 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts @@ -131,7 +131,7 @@ export const newCusToAttachParams = ({ cusProducts: [], entities: [], features: [], - invoiceOnly: isDefaultTrial, + invoiceOnly: isDefaultTrial || undefined, }; return attachParams; }; diff --git a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts index 0f58d78e8..68737a748 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts @@ -1,41 +1,47 @@ -import { ExtendedRequest } from "@/utils/models/Request.js"; - -import { AttachBody } from "@autumn/shared"; -import { AttachParams } from "../../cusProducts/AttachParams.js"; -import { notNullish } from "@/utils/genUtils.js"; -import { AttachBranch, AttachErrCode, BillingInterval } from "@autumn/shared"; -import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js"; -import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js"; +import { + type AttachBody, + AttachBranch, + AttachErrCode, + BillingInterval, + cusProductToPrices, + cusProductToProduct, + type FeatureOptions, + type FullCusProduct, + productsAreSame, +} from "@autumn/shared"; import { ErrCode } from "@/errors/errCodes.js"; -import RecaseError from "@/utils/errorUtils.js"; +import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; +import { hasPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; +import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js"; + import { isFreeProduct, isProductUpgrade, } from "@/internal/products/productUtils.js"; -import { cusProductToPrices, cusProductToProduct } from "@autumn/shared"; -import { FeatureOptions, FullCusProduct } from "@autumn/shared"; -import { productsAreSame } from "@/internal/products/productUtils/compareProductUtils.js"; -import { hasPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AttachParams } from "../../cusProducts/AttachParams.js"; +import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js"; +import { isMainTrialBranch } from "./attachUtils.js"; import { attachParamToCusProducts, getCustomerSub, } from "./convertAttachParams.js"; -import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; -import { isMainTrialBranch } from "./attachUtils.js"; const handleMultiProductErrors = async ({ attachParams, }: { attachParams: AttachParams; }) => { - let { products } = attachParams; + const { products } = attachParams; if (pricesOnlyOneOff(attachParams.prices)) { return true; } for (const product of products) { - let { curMainProduct, curSameProduct, curScheduledProduct } = + const { curMainProduct, curSameProduct, curScheduledProduct } = getExistingCusProducts({ product, cusProducts: attachParams.cusProducts!, @@ -55,7 +61,7 @@ const handleMultiProductErrors = async ({ }); } - let curPaidProduct = + const curPaidProduct = curMainProduct && !isFreeProduct(cusProductToPrices({ cusProduct: curMainProduct })); @@ -85,16 +91,16 @@ const getOptionsToUpdate = ({ newOptionsList: FeatureOptions[]; curSameProduct: FullCusProduct; }) => { - let optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[] = []; + const optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[] = []; const prices = cusProductToPrices({ cusProduct: curSameProduct }); for (const newOptions of newOptionsList) { - let internalFeatureId = newOptions.internal_feature_id; - let existingOptions = oldOptionsList.find( + const internalFeatureId = newOptions.internal_feature_id; + const existingOptions = oldOptionsList.find( (o) => o.internal_feature_id === internalFeatureId, ); - let price = findPrepaidPrice({ + const price = findPrepaidPrice({ prices, internalFeatureId: internalFeatureId!, }); @@ -123,9 +129,9 @@ export const checkSameCustom = async ({ fromPreview?: boolean; optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[]; }) => { - let product = attachParams.products[0]; + const product = attachParams.products[0]; - let { itemsSame, freeTrialsSame, onlyEntsChanged } = productsAreSame({ + const { itemsSame, freeTrialsSame, onlyEntsChanged } = productsAreSame({ newProductV1: { ...product, prices: attachParams.prices, @@ -176,7 +182,7 @@ const getSameProductBranch = async ({ attachParams: AttachParams; fromPreview?: boolean; }) => { - let product = attachParams.products[0]; + const product = attachParams.products[0]; let { curSameProduct, curScheduledProduct } = attachParamToCusProducts({ attachParams, @@ -190,7 +196,7 @@ const getSameProductBranch = async ({ return AttachBranch.NewVersion; } - let optionsToUpdate = getOptionsToUpdate({ + const optionsToUpdate = getOptionsToUpdate({ oldOptionsList: curSameProduct.options, newOptionsList: attachParams.optionsList, curSameProduct, @@ -257,16 +263,19 @@ const getChangeProductBranch = async ({ // 1. If main product is free, it's the same as adding a new product - let mainProduct = cusProductToProduct({ cusProduct: curMainProduct! }); + const mainProduct = cusProductToProduct({ cusProduct: curMainProduct! }); if (isFreeProduct(mainProduct.prices)) { return AttachBranch.MainIsFree; } // 2. If main product is paid, check if upgrade or downgrade - let curPrices = cusProductToPrices({ cusProduct: curMainProduct! }); - let newPrices = attachParams.prices; + const curPrices = cusProductToPrices({ cusProduct: curMainProduct! }); + const newPrices = attachParams.prices; - let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices }); + const isUpgrade = isProductUpgrade({ + prices1: curPrices, + prices2: newPrices, + }); if (isUpgrade) { if (isMainTrialBranch({ attachParams })) { @@ -309,7 +318,7 @@ export const getAttachBranch = async ({ return AttachBranch.MultiProduct; } - let { curSameProduct, curMainProduct } = attachParamToCusProducts({ + const { curSameProduct, curMainProduct } = attachParamToCusProducts({ attachParams, }); @@ -318,7 +327,7 @@ export const getAttachBranch = async ({ return await getSameProductBranch({ attachParams, fromPreview }); } - let product = attachParams.products[0]; + const product = attachParams.products[0]; if (product.is_add_on) { return AttachBranch.AddOn; } diff --git a/server/src/internal/customers/attach/handleAttachPreview/getUpdateEntsPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getUpdateEntsPreview.ts deleted file mode 100644 index 6ff274065..000000000 --- a/server/src/internal/customers/attach/handleAttachPreview/getUpdateEntsPreview.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { productsAreSame } from "@/internal/products/productUtils/compareProductUtils.js"; -import { - attachParamsToProduct, - attachParamToCusProducts, -} from "../attachUtils/convertAttachParams.js"; -import { cusProductToProduct } from "@autumn/shared"; - -export const getUpdateEntsPreview = async ({ - req, - attachParams, - now, -}: { - req: any; - attachParams: any; - now: number; -}) => { - const { curMainProduct } = attachParamToCusProducts({ attachParams }); - const curProduct = cusProductToProduct({ cusProduct: curMainProduct! }); - const newProduct = attachParamsToProduct({ attachParams }); - const features = attachParams.features; - - const res = productsAreSame({ - newProductV1: newProduct, - curProductV1: curProduct, - features, - }); - - return { - new_items: res.newItems, - }; -}; diff --git a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts index 787ee42e2..54a7518fd 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts @@ -1,4 +1,9 @@ -import { ErrCode, type FullProduct, UpdateProductSchema } from "@autumn/shared"; +import { + ErrCode, + mapToProductItems, + productsAreSame, + UpdateProductSchema, +} from "@autumn/shared"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; @@ -10,19 +15,15 @@ import { addTaskToQueue } from "@/queue/queueUtils.js"; import RecaseError from "@/utils/errorUtils.js"; import { notNullish } from "@/utils/genUtils.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { getEntsWithFeature } from "../../entitlements/entitlementUtils.js"; import { validateOneOffTrial } from "../../free-trials/freeTrialUtils.js"; import { ProductService } from "../../ProductService.js"; -import { productsAreSame } from "../../productUtils/compareProductUtils.js"; import { initProductInStripe } from "../../productUtils.js"; -import { mapToProductItems } from "../../productV2Utils.js"; import { disableCurrentDefault, handleCreateProduct, } from "../handleCreateProduct.js"; import { handleVersionProductV2 } from "../handleVersionProduct.js"; import { handleUpdateProductDetails } from "./updateProductDetails.js"; -import { formatPrice } from "../../prices/priceUtils.js"; export const handleUpdateProductV2 = async (req: any, res: any) => routeHandler({ diff --git a/server/src/internal/products/productRouter.ts b/server/src/internal/products/productRouter.ts index 4f71b5183..26bede0bd 100644 --- a/server/src/internal/products/productRouter.ts +++ b/server/src/internal/products/productRouter.ts @@ -1,4 +1,4 @@ -import { ErrCode } from "@autumn/shared"; +import { ErrCode, productsAreSame } from "@autumn/shared"; import { Router } from "express"; import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js"; @@ -13,10 +13,9 @@ import { handleCopyProduct } from "./handlers/handleCopyProduct.js"; import { handleCreateProduct } from "./handlers/handleCreateProduct.js"; import { handleDeleteProduct } from "./handlers/handleDeleteProduct.js"; import { handleGetProduct } from "./handlers/handleGetProduct.js"; +import { handleGetProductDeleteInfo } from "./handlers/handleGetProductDeleteInfo.js"; import { handleListProductsBeta } from "./handlers/handleListProductsBeta.js"; import { handleUpdateProductV2 } from "./handlers/handleUpdateProduct/handleUpdateProduct.js"; -import { productsAreSame } from "./productUtils/compareProductUtils.js"; -import { handleGetProductDeleteInfo } from "./handlers/handleGetProductDeleteInfo.js"; export const productBetaRouter: Router = Router(); productBetaRouter.get("", handleListProductsBeta); diff --git a/shared/index.ts b/shared/index.ts index 4ca0e47e0..d86c11d44 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -127,10 +127,10 @@ export * from "./models/subModels/subTable.js"; export * from "./utils/displayUtils.js"; export * from "./utils/index.js"; export * from "./utils/intervalUtils.js"; -export * from "./utils/productDisplayUtils/getProductItemRes.js"; export * from "./utils/productDisplayUtils/sortProductItems.js"; export * from "./utils/productDisplayUtils.js"; export * from "./utils/productUtils/priceToInvoiceAmount.js"; export * from "./utils/productUtils.js"; +export * from "./utils/productV2Utils/productItemUtils/getProductItemRes.js"; export * from "./utils/productV3Utils/mapToProductV3.js"; export * from "./utils/rewardUtils/rewardMigrationUtils.js"; diff --git a/shared/models/cusModels/entityModels/entityResModels.ts b/shared/models/cusModels/entityModels/entityResModels.ts index 8bdc17311..36a4fc4c4 100644 --- a/shared/models/cusModels/entityModels/entityResModels.ts +++ b/shared/models/cusModels/entityModels/entityResModels.ts @@ -1,9 +1,8 @@ -import { AppEnv } from "../../genModels/genEnums.js"; - import { z } from "zod"; -import { InvoiceResponseSchema } from "../invoiceModels/invoiceResponseModels.js"; -import { CusProductResponseSchema } from "../cusResModels/cusProductResponse.js"; +import { AppEnv } from "../../genModels/genEnums.js"; import { CusEntResponseV2Schema } from "../cusResModels/cusFeatureResponse.js"; +import { CusProductResponseSchema } from "../cusResModels/cusProductResponse.js"; +import { InvoiceResponseSchema } from "../invoiceModels/invoiceResponseModels.js"; export const EntityResponseSchema = z.object({ id: z.string().nullable(), diff --git a/shared/utils/index.ts b/shared/utils/index.ts index 5153d093e..7fa44066d 100644 --- a/shared/utils/index.ts +++ b/shared/utils/index.ts @@ -1,23 +1,30 @@ // Cus ent utils +export * from "./cusEntUtils/balanceUtils.js"; export * from "./cusEntUtils/sortCusEntsForDeduction.js"; // Cus product utils export * from "./cusProductUtils/classifyCusProduct.js"; export * from "./cusProductUtils/convertCusProduct.js"; -export * from "./cusProductUtils/productIdToCusProduct.js"; export * from "./cusProductUtils/cusProductConstants.js"; export * from "./cusProductUtils/cusProductUtils.js"; export * from "./cusProductUtils/formatCusProductUtils.js"; -export * from "./utils.js"; - -// Cus ent utils -export * from "./cusEntUtils/balanceUtils.js"; - -// Item utils -export * from "./productV2Utils/productItemUtils/mapToItem.js"; -export * from "./productV2Utils/productItemUtils/productItemUtils.js"; -export * from "./productV2Utils/mapToProductV2.js"; +export * from "./cusProductUtils/productIdToCusProduct.js"; +export * from "./productDisplayUtils.js"; // Product utils export * from "./productUtils/convertUtils.js"; export * from "./productUtils/priceUtils.js"; +export * from "./productV2Utils/compareProductUtils.ts/compareItemUtils.js"; +export * from "./productV2Utils/compareProductUtils.ts/compareProductUtils.js"; +export * from "./productV2Utils/mapToProductV2.js"; +export * from "./productV2Utils/productItemUtils/convertItemUtils.js"; +export * from "./productV2Utils/productItemUtils/getItemType.js"; + +// Item utils +export * from "./productV2Utils/productItemUtils/mapToItem.js"; +export * from "./productV2Utils/productItemUtils/productItemUtils.js"; + +// Product V3 utils +export * from "./productV3Utils/mapToProductV3.js"; +export * from "./productV3Utils/productItemUtils/productItemUtils.js"; +export * from "./utils.js"; diff --git a/shared/utils/productDisplayUtils.ts b/shared/utils/productDisplayUtils.ts index 475f7fb23..fecd9312e 100644 --- a/shared/utils/productDisplayUtils.ts +++ b/shared/utils/productDisplayUtils.ts @@ -1,20 +1,20 @@ -import { Feature } from "../models/featureModels/featureModels.js"; +import type { Feature } from "../models/featureModels/featureModels.js"; import { Infinite } from "../models/productModels/productEnums.js"; import { - ProductItem, + type ProductItem, ProductItemFeatureType, - ProductItemInterval, + type ProductItemInterval, } from "../models/productV2Models/productItemModels/productItemModels.js"; import { + formatAmount, getFeatureName, numberWithCommas, - formatAmount, } from "./displayUtils.js"; import { isFeatureItem, isFeaturePriceItem, isPriceItem, -} from "./productDisplayUtils/getItemType.js"; +} from "./productV2Utils/productItemUtils/getItemType.js"; import { notNullish, nullish } from "./utils.js"; export const formatTiers = ({ @@ -24,17 +24,17 @@ export const formatTiers = ({ item: ProductItem; currency?: string | null; }) => { - let tiers = item.tiers; + const tiers = item.tiers; if (tiers) { - if (tiers.length == 1) { + if (tiers.length === 1) { return formatAmount({ currency, amount: tiers[0].amount, }); } - let firstPrice = tiers[0].amount; - let lastPrice = tiers[tiers.length - 1].amount; + const firstPrice = tiers[0].amount; + const lastPrice = tiers[tiers.length - 1].amount; return `${formatAmount({ currency, @@ -54,7 +54,7 @@ export const getIntervalString = ({ intervalCount?: number | null; }) => { if (!interval) return ""; - if (intervalCount == 1) { + if (intervalCount === 1) { return `per ${interval}`; } return `per ${intervalCount} ${interval}s`; @@ -71,7 +71,7 @@ export const getFeatureItemDisplay = ({ throw new Error(`Feature ${item.feature_id} not found`); } // 1. If feature - if (item.feature_type == ProductItemFeatureType.Static) { + if (item.feature_type === ProductItemFeatureType.Static) { return { primary_text: getFeatureName({ feature, @@ -81,13 +81,13 @@ export const getFeatureItemDisplay = ({ }; } - let featureName = getFeatureName({ + const featureName = getFeatureName({ feature, units: item.included_usage, }); - let includedUsageTxt = - item.included_usage == Infinite + const includedUsageTxt = + item.included_usage === Infinite ? "Unlimited " : nullish(item.included_usage) || item.included_usage == 0 ? "" @@ -105,7 +105,7 @@ export const getPriceItemDisplay = ({ item: ProductItem; currency?: string | null; }) => { - let primaryText = formatAmount({ + const primaryText = formatAmount({ currency, amount: item.price as number, }); @@ -115,7 +115,7 @@ export const getPriceItemDisplay = ({ intervalCount: item.interval_count, }); - let secondaryText = intervalStr || undefined; + const secondaryText = intervalStr || undefined; return { primary_text: primaryText, @@ -141,12 +141,12 @@ export const getFeaturePriceItemDisplay = ({ } // 1. Get included usage - let includedFeatureName = getFeatureName({ + const includedFeatureName = getFeatureName({ feature, units: item.included_usage, }); - let includedUsage = item.included_usage as number | null; + const includedUsage = item.included_usage as number | null; let includedUsageStr = ""; if (notNullish(includedUsage) && includedUsage! > 0) { if (minifyIncluded) { @@ -156,8 +156,8 @@ export const getFeaturePriceItemDisplay = ({ } } - let priceStr = formatTiers({ item, currency }); - let billingFeatureName = getFeatureName({ + const priceStr = formatTiers({ item, currency }); + const billingFeatureName = getFeatureName({ feature, units: item.billing_units, }); @@ -170,7 +170,7 @@ export const getFeaturePriceItemDisplay = ({ } // let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : ""; - let intervalStr = isMainPrice + const intervalStr = isMainPrice ? getIntervalString({ interval: item.interval!, intervalCount: item.interval_count, diff --git a/shared/utils/productDisplayUtils/sortProductItems.ts b/shared/utils/productDisplayUtils/sortProductItems.ts index ece0cd198..5de5addb7 100644 --- a/shared/utils/productDisplayUtils/sortProductItems.ts +++ b/shared/utils/productDisplayUtils/sortProductItems.ts @@ -1,11 +1,14 @@ -import { Feature } from "../../models/featureModels/featureModels.js"; -import { ProductItem } from "../../models/productV2Models/productItemModels/productItemModels.js"; -import { isFeaturePriceItem, isPriceItem } from "./getItemType.js"; +import type { Feature } from "../../models/featureModels/featureModels.js"; +import type { ProductItem } from "../../models/productV2Models/productItemModels/productItemModels.js"; +import { + isFeaturePriceItem, + isPriceItem, +} from "../productV2Utils/productItemUtils/getItemType.js"; export const sortProductItems = (items: ProductItem[], features: Feature[]) => { items.sort((a, b) => { - let aIsPriceItem = isPriceItem(a); - let bIsPriceItem = isPriceItem(b); + const aIsPriceItem = isPriceItem(a); + const bIsPriceItem = isPriceItem(b); if (aIsPriceItem && bIsPriceItem) { return 0; @@ -20,8 +23,8 @@ export const sortProductItems = (items: ProductItem[], features: Feature[]) => { } // 2. Put feature price next - let aIsFeatureItem = isFeaturePriceItem(a); - let bIsFeatureItem = isFeaturePriceItem(b); + const aIsFeatureItem = isFeaturePriceItem(a); + const bIsFeatureItem = isFeaturePriceItem(b); if (aIsFeatureItem && !bIsFeatureItem) { return -1; @@ -32,9 +35,9 @@ export const sortProductItems = (items: ProductItem[], features: Feature[]) => { } // 3. Put feature price items in alphabetical order - let feature = features.find((f) => f.id == a.feature_id); - let aFeatureName = feature?.name; - let bFeatureName = features.find((f) => f.id == b.feature_id)?.name; + const feature = features.find((f) => f.id === a.feature_id); + const aFeatureName = feature?.name; + const bFeatureName = features.find((f) => f.id === b.feature_id)?.name; if (!aFeatureName || !bFeatureName) { return 0; diff --git a/shared/utils/productUtils.ts b/shared/utils/productUtils.ts index aa0aeb1a2..54e2d3314 100644 --- a/shared/utils/productUtils.ts +++ b/shared/utils/productUtils.ts @@ -1,17 +1,10 @@ +import { Decimal } from "decimal.js"; +import { type ProductItem, type ProductV2 } from "../index.js"; import { - BillingType, - Price, - ProductItem, - ProductResponse, - ProductV2, -} from "../index.js"; -import { nullish } from "./utils.js"; -import { - isFeatureItem, isFeaturePriceItem, isPriceItem, -} from "./productDisplayUtils/getItemType.js"; -import { Decimal } from "decimal.js"; +} from "./productV2Utils/productItemUtils/getItemType.js"; +import { nullish } from "./utils.js"; export const isFreeProductV2 = ({ items }: { items: ProductItem[] }) => { return items.every((item) => nullish(item.price) && nullish(item.tiers)); @@ -44,7 +37,7 @@ export const isProductUpgradeV2 = ({ for (const item of items) { if (item.price) totalPrice = totalPrice.plus(item.price); if (item.tiers) { - let tierTotal = item.tiers.reduce( + const tierTotal = item.tiers.reduce( (acc, tier) => acc.plus(tier.amount), new Decimal(0), ); @@ -75,7 +68,7 @@ export const sortProductsV2 = ({ products }: { products: ProductV2[] }) => { } // Primary sort: by price (using upgrade logic) - let isUpgrade = isProductUpgradeV2({ + const isUpgrade = isProductUpgradeV2({ items1: a.items, items2: b.items, }); diff --git a/shared/utils/productUtils/freeTrialUtils.ts b/shared/utils/productUtils/freeTrialUtils.ts new file mode 100644 index 000000000..ceecfe341 --- /dev/null +++ b/shared/utils/productUtils/freeTrialUtils.ts @@ -0,0 +1,21 @@ +import type { + CreateFreeTrial, + FreeTrial, +} from "../../models/productModels/freeTrialModels/freeTrialModels.js"; + +export const freeTrialsAreSame = ({ + ft1, + ft2, +}: { + ft1?: FreeTrial | CreateFreeTrial | null; + ft2?: FreeTrial | CreateFreeTrial | null; +}) => { + if (!ft1 && !ft2) return true; + if (!ft1 || !ft2) return false; + return ( + ft1.length === ft2.length && + ft1.unique_fingerprint === ft2.unique_fingerprint && + ft1.duration === ft2.duration && + ft1.card_required === ft2.card_required + ); +}; diff --git a/shared/utils/productUtils/priceToInvoiceAmount.ts b/shared/utils/productUtils/priceToInvoiceAmount.ts index a10213beb..1b6256757 100644 --- a/shared/utils/productUtils/priceToInvoiceAmount.ts +++ b/shared/utils/productUtils/priceToInvoiceAmount.ts @@ -1,20 +1,20 @@ -import { FixedPriceConfig } from "../../models/productModels/priceModels/priceConfig/fixedPriceConfig.js"; -import { UsagePriceConfig } from "../../models/productModels/priceModels/priceConfig/usagePriceConfig.js"; +import { Decimal } from "decimal.js"; +import type { FixedPriceConfig } from "../../models/productModels/priceModels/priceConfig/fixedPriceConfig.js"; +import type { UsagePriceConfig } from "../../models/productModels/priceModels/priceConfig/usagePriceConfig.js"; import { BillingType } from "../../models/productModels/priceModels/priceEnums.js"; -import { Price } from "../../models/productModels/priceModels/priceModels.js"; +import type { Price } from "../../models/productModels/priceModels/priceModels.js"; import { Infinite } from "../../models/productModels/productEnums.js"; import { - ProductItem, + type ProductItem, UsageModel, } from "../../models/productV2Models/productItemModels/productItemModels.js"; -import { isPriceItem } from "../productDisplayUtils/getItemType.js"; +import { isPriceItem } from "../productV2Utils/productItemUtils/getItemType.js"; import { calculateProrationAmount, - Proration, -} from "../productDisplayUtils/getProductItemRes.js"; + type Proration, +} from "../productV2Utils/productItemUtils/getProductItemRes.js"; import { nullish } from "../utils.js"; import { getBillingType, isFixedPrice } from "./priceUtils.js"; -import { Decimal } from "decimal.js"; export const getAmountForQuantity = ({ price, @@ -25,7 +25,7 @@ export const getAmountForQuantity = ({ }) => { const config = price.config as UsagePriceConfig; - let billingUnits = config.billing_units || 1; + const billingUnits = config.billing_units || 1; const roundedQuantity = new Decimal(quantity) .div(billingUnits) @@ -42,19 +42,19 @@ export const getAmountForQuantity = ({ // console.log("Usage tiers:", config.usage_tiers); for (let i = 0; i < config.usage_tiers.length; i++) { - let tier = config.usage_tiers[i]; + const tier = config.usage_tiers[i]; let usageWithinTier = new Decimal(0); - if (tier.to == Infinite || tier.to == -1) { + if (tier.to === Infinite || tier.to === -1) { usageWithinTier = remainingUsage; } else { - let tierUsage = new Decimal(tier.to).minus(lastTierTo); + const tierUsage = new Decimal(tier.to).minus(lastTierTo); usageWithinTier = Decimal.min(remainingUsage, tierUsage); lastTierTo = tier.to; } - let amountPerUnit = new Decimal(tier.amount).div(billingUnits); - let amountWithinTier = amountPerUnit.mul(usageWithinTier); + const amountPerUnit = new Decimal(tier.amount).div(billingUnits); + const amountWithinTier = amountPerUnit.mul(usageWithinTier); amount = amount.plus(amountWithinTier); remainingUsage = remainingUsage.minus(usageWithinTier); @@ -86,7 +86,7 @@ export const itemToInvoiceAmount = ({ ); } - let price = { + const price = { config: { usage_tiers: item.tiers || [ { @@ -98,7 +98,7 @@ export const itemToInvoiceAmount = ({ }, } as unknown as Price; - if (item.usage_model == UsageModel.Prepaid) { + if (item.usage_model === UsageModel.Prepaid) { amount = getAmountForQuantity({ price, quantity: quantity! }); } else { amount = getAmountForQuantity({ price, quantity: overage! }); @@ -131,7 +131,7 @@ export const priceToInvoiceAmount = ({ amount = (price.config as FixedPriceConfig).amount; } else { const config = price.config as UsagePriceConfig; - let billingType = getBillingType(config); + const billingType = getBillingType(config); if (!nullish(quantity) && !nullish(overage)) { throw new Error( @@ -139,7 +139,7 @@ export const priceToInvoiceAmount = ({ ); } - if (billingType == BillingType.UsageInAdvance) { + if (billingType === BillingType.UsageInAdvance) { amount = getAmountForQuantity({ price, quantity: quantity! }); } else { amount = getAmountForQuantity({ price, quantity: overage! }); diff --git a/server/src/internal/products/product-items/compareItemUtils.ts b/shared/utils/productV2Utils/compareProductUtils.ts/compareItemUtils.ts similarity index 91% rename from server/src/internal/products/product-items/compareItemUtils.ts rename to shared/utils/productV2Utils/compareProductUtils.ts/compareItemUtils.ts index 1eb89331e..902f3ece5 100644 --- a/server/src/internal/products/product-items/compareItemUtils.ts +++ b/shared/utils/productV2Utils/compareProductUtils.ts/compareItemUtils.ts @@ -1,21 +1,22 @@ +/** biome-ignore-all lint/suspicious/noDoubleEquals: need to compare null / undefined for different fields */ import { - Feature, - FeatureItem, + type Feature, + type FeatureItem, FeatureItemSchema, - FeaturePriceItem, + type FeaturePriceItem, FeaturePriceItemSchema, FeatureUsageType, - PriceItem, + type PriceItem, PriceItemSchema, - ProductItem, + type ProductItem, + type UsageTier, } from "@autumn/shared"; +import { itemToFeature } from "../productItemUtils/convertItemUtils.js"; import { isFeatureItem, isFeaturePriceItem, isPriceItem, -} from "./productItemUtils/getItemType.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { itemToFeature } from "./productItemUtils/convertItem.js"; +} from "../productItemUtils/getItemType.js"; export const findSimilarItem = ({ item, @@ -44,7 +45,10 @@ export const findSimilarItem = ({ return null; }; -const tiersAreSame = (tiers1: any, tiers2: any) => { +const tiersAreSame = ( + tiers1: UsageTier[] | null, + tiers2: UsageTier[] | null, +) => { if (!tiers1 && !tiers2) { return true; } @@ -58,7 +62,7 @@ const tiersAreSame = (tiers1: any, tiers2: any) => { } return tiers1.every( - (tier: any, index: number) => + (tier: UsageTier, index: number) => tier.amount === tiers2[index].amount && tier.to === tiers2[index].to, ); }; @@ -154,7 +158,7 @@ export const featurePriceItemsAreSame = ({ message: `Price different: ${item1.price} != ${item2.price}`, }, tiers: { - condition: tiersAreSame(item1.tiers, item2.tiers), + condition: tiersAreSame(item1.tiers || null, item2.tiers || null), message: `Tiers different`, }, billing_units: { @@ -237,7 +241,7 @@ export const itemsAreSame = ({ same = same_; - let feature = itemToFeature({ + const feature = itemToFeature({ item: item1, features: features || [], }); diff --git a/server/src/internal/products/productUtils/compareProductUtils.ts b/shared/utils/productV2Utils/compareProductUtils.ts/compareProductUtils.ts similarity index 62% rename from server/src/internal/products/productUtils/compareProductUtils.ts rename to shared/utils/productV2Utils/compareProductUtils.ts/compareProductUtils.ts index 773e9e9c4..ae8dcf7c3 100644 --- a/server/src/internal/products/productUtils/compareProductUtils.ts +++ b/shared/utils/productV2Utils/compareProductUtils.ts/compareProductUtils.ts @@ -1,27 +1,20 @@ import { - ErrCode, - Feature, - FullProduct, + type Feature, + type FullProduct, OnDecrease, OnIncrease, - ProductItem, - ProductV2, + type ProductItem, + type ProductV2, } from "@autumn/shared"; -import { mapToProductItems } from "../productV2Utils.js"; -import { - findSimilarItem, - itemsAreSame, -} from "../product-items/compareItemUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { freeTrialsAreSame } from "../free-trials/freeTrialUtils.js"; +import { freeTrialsAreSame } from "../../productUtils/freeTrialUtils.js"; +import { mapToProductItems } from "../mapToProductV2.js"; import { isFeaturePriceItem, isPriceItem, -} from "../product-items/productItemUtils/getItemType.js"; -import { StatusCodes } from "http-status-codes"; -import { itemToPriceOrTiers } from "../product-items/productItemUtils.js"; -import { nullish } from "@/utils/genUtils.js"; -import { getResetUsage } from "../product-items/productItemUtils/itemToPriceAndEnt.js"; +} from "../productItemUtils/getItemType.js"; +import { itemToPriceOrTiers } from "../productItemUtils/getProductItemRes.js"; +import { getResetUsage } from "../productItemUtils/productItemUtils.js"; +import { findSimilarItem, itemsAreSame } from "./compareItemUtils.js"; const sanitizeItems = ({ items, @@ -31,7 +24,7 @@ const sanitizeItems = ({ features: Feature[]; }) => { return items.map((item) => { - let priceData = itemToPriceOrTiers({ item }); + const priceData = itemToPriceOrTiers({ item }); const newItem = { ...item, reset_usage_when_enabled: getResetUsage({ @@ -51,6 +44,7 @@ const sanitizeItems = ({ return newItem; }); }; + export const productsAreSame = ({ newProductV1, newProductV2, @@ -65,17 +59,11 @@ export const productsAreSame = ({ features: Feature[]; }) => { if (!newProductV1 && !newProductV2) { - throw new RecaseError({ - message: "productsAreSame error: product1 not provided", - code: ErrCode.InvalidRequest, - }); + throw new Error("productsAreSame error: product1 not provided"); } if (!curProductV1 && !curProductV2) { - throw new RecaseError({ - message: "productsAreSame error: product2 not provided", - code: ErrCode.InvalidRequest, - }); + throw new Error("productsAreSame error: product2 not provided"); } let items1 = @@ -97,6 +85,9 @@ export const productsAreSame = ({ items1 = sanitizeItems({ items: items1, features }); items2 = sanitizeItems({ items: items2, features }); + console.log("Items 1:", items1); + console.log("Items 2:", items2); + let itemsSame = true; let pricesChanged = false; const newItems: ProductItem[] = []; @@ -106,29 +97,31 @@ export const productsAreSame = ({ itemsSame = false; } - // Check if any feature's usage limits have changed - items1.some((item1) => { - const matchingItem2 = items2?.find( - (item2) => item2.feature_id === item1.feature_id, - ); - if (!matchingItem2) return false; + // // Check if any feature's usage limits have changed + // items1.some((item1: ProductItem) => { + // const matchingItem2 = items2?.find( + // (item2: ProductItem) => item2.feature_id === item1.feature_id, + // ); + // if (!matchingItem2) return false; - const feature = features.find((f) => f.id === item1.feature_id); - if (!feature) return false; + // const feature = features.find((f) => f.id === item1.feature_id); + // if (!feature) return false; - return false; - }); + // return false; + // }); - if (items1.length !== items2.length) { - itemsSame = false; - } + if (items1.length !== items2.length) itemsSame = false; for (const item of items1) { - let similarItem = findSimilarItem({ + console.log("Item:", item); + + const similarItem = findSimilarItem({ item, items: items2, }); + console.log("Similar item:", similarItem); + if (!similarItem) { if (isFeaturePriceItem(item) || isPriceItem(item)) { pricesChanged = true; @@ -142,7 +135,7 @@ export const productsAreSame = ({ const { same, pricesChanged: pricesChanged_ } = itemsAreSame({ item1: item, - item2: similarItem!, + item2: similarItem, features, }); @@ -157,7 +150,7 @@ export const productsAreSame = ({ } for (const item of items2) { - let similarItem = findSimilarItem({ + const similarItem = findSimilarItem({ item, items: items1, }); @@ -173,10 +166,10 @@ export const productsAreSame = ({ } // Compare free trial - let freeTrial1 = curProductV1?.free_trial || curProductV2?.free_trial; - let freeTrial2 = newProductV1?.free_trial || newProductV2?.free_trial; + const freeTrial1 = curProductV1?.free_trial || curProductV2?.free_trial; + const freeTrial2 = newProductV1?.free_trial || newProductV2?.free_trial; - let freeTrialsSame = freeTrialsAreSame({ + const freeTrialsSame = freeTrialsAreSame({ ft1: freeTrial1, ft2: freeTrial2, }); diff --git a/shared/utils/productV2Utils/mapToProductV2.ts b/shared/utils/productV2Utils/mapToProductV2.ts index 97a28cd9c..90c4a3f6c 100644 --- a/shared/utils/productV2Utils/mapToProductV2.ts +++ b/shared/utils/productV2Utils/mapToProductV2.ts @@ -1,11 +1,49 @@ -import { Feature } from "../../models/featureModels/featureModels.js"; -import { FullProduct } from "../../models/productModels/productModels.js"; -import { ProductItem } from "../../models/productV2Models/productItemModels/productItemModels.js"; -import { ProductV2 } from "../../models/productV2Models/productV2Models.js"; +import type { Feature } from "../../models/featureModels/featureModels.js"; +import type { EntitlementWithFeature } from "../../models/productModels/entModels/entModels.js"; +import type { Price } from "../../models/productModels/priceModels/priceModels.js"; +import type { FullProduct } from "../../models/productModels/productModels.js"; +import type { ProductItem } from "../../models/productV2Models/productItemModels/productItemModels.js"; +import type { ProductV2 } from "../../models/productV2Models/productV2Models.js"; import { entToPrice, priceToEnt } from "../productUtils/convertUtils.js"; import { toProductItem } from "./productItemUtils/mapToItem.js"; import { getItemFeatureType } from "./productItemUtils/productItemUtils.js"; +export const mapToProductItems = ({ + prices, + entitlements, + features, +}: { + prices: Price[]; + entitlements: EntitlementWithFeature[]; + features: Feature[]; +}): ProductItem[] => { + const items: ProductItem[] = []; + + for (const ent of entitlements) { + // const relatedPrice = getEntRelatedPrice(ent, prices, allowFeatureMatch); + const relatedPrice = entToPrice({ ent, prices }); + const item = toProductItem({ ent, price: relatedPrice }); + items.push(item); + } + + for (const price of prices) { + const relatedEnt = priceToEnt({ price, entitlements }); + + if (!relatedEnt) { + items.push(toProductItem({ price })); + } + } + + for (const item of items) { + const feature = features.find((f) => f.id === item.feature_id); + if (feature) { + item.feature_type = getItemFeatureType({ item, features }); + } + } + + return items; +}; + export const mapToProductV2 = ({ product, features, @@ -13,15 +51,18 @@ export const mapToProductV2 = ({ product: FullProduct; features?: Feature[]; }): ProductV2 => { - let items: ProductItem[] = []; + const items: ProductItem[] = []; for (const ent of product.entitlements) { - let relatedPrice = entToPrice({ ent, prices: product.prices }); + const relatedPrice = entToPrice({ ent, prices: product.prices }); items.push(toProductItem({ ent, price: relatedPrice })); } for (const price of product.prices) { - let relatedEnt = priceToEnt({ price, entitlements: product.entitlements }); + const relatedEnt = priceToEnt({ + price, + entitlements: product.entitlements, + }); if (!relatedEnt) { items.push(toProductItem({ price })); @@ -36,7 +77,7 @@ export const mapToProductV2 = ({ item.feature_type = getItemFeatureType({ item, features }); } - let productV2: ProductV2 = { + const productV2: ProductV2 = { internal_id: product.internal_id, id: product.id, diff --git a/shared/utils/productV2Utils/productItemUtils/convertItemUtils.ts b/shared/utils/productV2Utils/productItemUtils/convertItemUtils.ts new file mode 100644 index 000000000..9930a25a8 --- /dev/null +++ b/shared/utils/productV2Utils/productItemUtils/convertItemUtils.ts @@ -0,0 +1,39 @@ +import { FeatureType } from "../../../models/featureModels/featureEnums.js"; +import type { Feature } from "../../../models/featureModels/featureModels.js"; +import { + type ProductItem, + ProductItemFeatureType, +} from "../../../models/productV2Models/productItemModels/productItemModels.js"; + +export const itemToFeature = ({ + item, + features, +}: { + item: ProductItem; + features: Feature[]; +}) => { + const feature = features.find((f) => f.id === item.feature_id); + + return feature; +}; + +export const itemToUsageType = ({ + item, + features, +}: { + item: ProductItem; + features: Feature[]; +}) => { + const feature = itemToFeature({ item, features }); + if (!feature || !feature.config) return null; + + if (feature.type === FeatureType.Boolean) { + return ProductItemFeatureType.Static; + } + + if (feature.type === FeatureType.CreditSystem) { + return ProductItemFeatureType.SingleUse; + } + + return feature.config.usage_type as ProductItemFeatureType; +}; diff --git a/shared/utils/productDisplayUtils/getItemType.ts b/shared/utils/productV2Utils/productItemUtils/getItemType.ts similarity index 77% rename from shared/utils/productDisplayUtils/getItemType.ts rename to shared/utils/productV2Utils/productItemUtils/getItemType.ts index 1e04d3b66..336aa301e 100644 --- a/shared/utils/productDisplayUtils/getItemType.ts +++ b/shared/utils/productV2Utils/productItemUtils/getItemType.ts @@ -1,13 +1,13 @@ import { - ProductItem, + type ProductItem, ProductItemType, -} from "../../models/productV2Models/productItemModels/productItemModels.js"; -import { notNullish, nullish } from "../utils.js"; +} from "../../../models/productV2Models/productItemModels/productItemModels.js"; +import { notNullish, nullish } from "../../utils.js"; export const isBooleanFeatureItem = (item: ProductItem) => { return ( notNullish(item.feature_id) && - (nullish(item.price) || item.price == 0) && + (nullish(item.price) || item.price === 0) && nullish(item.tiers) && nullish(item.interval) && nullish(item.included_usage) @@ -17,7 +17,7 @@ export const isBooleanFeatureItem = (item: ProductItem) => { export const isFeatureItem = (item: ProductItem) => { return ( notNullish(item.feature_id) && - (nullish(item.price) || item.price == 0) && + (nullish(item.price) || item.price === 0) && nullish(item.tiers) ); }; diff --git a/shared/utils/productDisplayUtils/getProductItemRes.ts b/shared/utils/productV2Utils/productItemUtils/getProductItemRes.ts similarity index 71% rename from shared/utils/productDisplayUtils/getProductItemRes.ts rename to shared/utils/productV2Utils/productItemUtils/getProductItemRes.ts index 77edb6f5e..7d9e5e92c 100644 --- a/shared/utils/productDisplayUtils/getProductItemRes.ts +++ b/shared/utils/productV2Utils/productItemUtils/getProductItemRes.ts @@ -1,15 +1,15 @@ -import { FeatureOptions } from "../../models/cusProductModels/cusProductModels.js"; -import { Feature } from "../../models/featureModels/featureModels.js"; -import { ProductItemResponseSchema } from "../../models/productV2Models/productItemModels/prodItemResponseModels.js"; -import { - ProductItem, - UsageModel, -} from "../../models/productV2Models/productItemModels/productItemModels.js"; -import { toAPIFeature } from "../featureUtils.js"; -import { getProductItemDisplay } from "../productDisplayUtils.js"; -import { notNullish } from "../utils.js"; -import { getItemType } from "./getItemType.js"; import { Decimal } from "decimal.js"; +import type { FeatureOptions } from "../../../models/cusProductModels/cusProductModels.js"; +import type { Feature } from "../../../models/featureModels/featureModels.js"; +import { ProductItemResponseSchema } from "../../../models/productV2Models/productItemModels/prodItemResponseModels.js"; +import { + type ProductItem, + UsageModel, +} from "../../../models/productV2Models/productItemModels/productItemModels.js"; +import { toAPIFeature } from "../../featureUtils.js"; +import { getProductItemDisplay } from "../../productDisplayUtils.js"; +import { notNullish } from "../../utils.js"; +import { getItemType } from "./getItemType.js"; export const calculateProrationAmount = ({ periodEnd, @@ -109,22 +109,22 @@ export const getProductItemResponse = ({ options?: FeatureOptions[]; }) => { // 1. Get item type - let type = getItemType(item); + const type = getItemType(item); // 2. Get display - let display = getProductItemDisplay({ + const display = getProductItemDisplay({ item, features, currency, }); - let priceData = itemToPriceOrTiers({ item }); + const priceData = itemToPriceOrTiers({ item }); - let quantity = undefined; - let upcomingQuantity = undefined; + let quantity: number | undefined; + let upcomingQuantity: number | undefined; - if (item.usage_model == UsageModel.Prepaid && notNullish(options)) { - let option = options!.find((o) => o.feature_id == item.feature_id); + if (item.usage_model === UsageModel.Prepaid && notNullish(options)) { + const option = options!.find((o) => o.feature_id === item.feature_id); quantity = option?.quantity ? option?.quantity * (item.billing_units ?? 1) : undefined; @@ -134,7 +134,7 @@ export const getProductItemResponse = ({ : undefined; } - let feature = features.find((f) => f.id == item.feature_id); + const feature = features.find((f) => f.id === item.feature_id); return ProductItemResponseSchema.parse({ type, ...item, diff --git a/shared/utils/productV2Utils/productItemUtils/productItemUtils.ts b/shared/utils/productV2Utils/productItemUtils/productItemUtils.ts index 054d23e47..94ff471e9 100644 --- a/shared/utils/productV2Utils/productItemUtils/productItemUtils.ts +++ b/shared/utils/productV2Utils/productItemUtils/productItemUtils.ts @@ -1,22 +1,27 @@ -import { FeatureType } from "../../../models/featureModels/featureEnums.js"; -import { Feature } from "../../../models/featureModels/featureModels.js"; +import { + FeatureType, + FeatureUsageType, +} from "../../../models/featureModels/featureEnums.js"; +import type { Feature } from "../../../models/featureModels/featureModels.js"; import { EntInterval } from "../../../models/productModels/entModels/entEnums.js"; import { BillingInterval } from "../../../models/productModels/priceModels/priceEnums.js"; import { - ProductItem, + type ProductItem, ProductItemFeatureType, - ProductItemInterval, + type ProductItemInterval, } from "../../../models/productV2Models/productItemModels/productItemModels.js"; +import { nullish } from "../../utils.js"; +import { isFeatureItem, isFeaturePriceItem } from "./getItemType.js"; export const entToItemInterval = (entInterval: EntInterval) => { - if (entInterval == EntInterval.Lifetime) { + if (entInterval === EntInterval.Lifetime) { return null; } return entInterval as unknown as ProductItemInterval; }; export const billingToItemInterval = (billingInterval: BillingInterval) => { - if (billingInterval == BillingInterval.OneOff) { + if (billingInterval === BillingInterval.OneOff) { return null; } @@ -30,12 +35,12 @@ export const getItemFeatureType = ({ item: ProductItem; features: Feature[]; }) => { - let feature = features.find((f) => f.id == item.feature_id); + const feature = features.find((f) => f.id === item.feature_id); if (feature) { - if (feature.type == FeatureType.Boolean) { + if (feature.type === FeatureType.Boolean) { return ProductItemFeatureType.Static; - } else if (feature.type == FeatureType.CreditSystem) { + } else if (feature.type === FeatureType.CreditSystem) { return ProductItemFeatureType.SingleUse; } else { return feature.config?.usage_type; @@ -44,3 +49,23 @@ export const getItemFeatureType = ({ return undefined; }; + +export const getResetUsage = ({ + item, + feature, +}: { + item: ProductItem; + feature?: Feature; +}) => { + if (!item.feature_id) { + return undefined; + } + if ( + nullish(item.reset_usage_when_enabled) && + (isFeatureItem(item) || isFeaturePriceItem(item)) && + feature + ) { + return feature?.config?.usage_type === FeatureUsageType.Single; + } + return item.reset_usage_when_enabled; +}; diff --git a/shared/utils/productV3Utils/productItemUtils/productItemUtils.ts b/shared/utils/productV3Utils/productItemUtils/productItemUtils.ts index d2c22a38c..cea04d405 100644 --- a/shared/utils/productV3Utils/productItemUtils/productItemUtils.ts +++ b/shared/utils/productV3Utils/productItemUtils/productItemUtils.ts @@ -1,34 +1,20 @@ -import { ProductItemInterval } from "../../../models/productV2Models/productItemModels/productItemModels.js"; +import type { ProductItemInterval } from "../../../models/productV2Models/productItemModels/productItemModels.js"; import type { ProductV2 } from "../../../models/productV2Models/productV2Models.js"; -import { isPriceItem } from "../../productDisplayUtils/getItemType.js"; +import { isPriceItem } from "../../productV2Utils/productItemUtils/getItemType.js"; export function productV2ToBasePrice({ product, }: { product: ProductV2; }): { amount: number; interval: ProductItemInterval } | null { - try { - const item = product.items.find( - (x) => isPriceItem(x) - ); - - if(item) { - // Prefer price_config if available, otherwise use direct price - if (item.price_config?.type === "fixed" && item.price_config?.amount !== null) { - return { - amount: item.price_config.amount, - interval: item.price_config.interval, - }; - } else if (item.price !== null && item.price !== undefined) { - return { - amount: item.price, - interval: ProductItemInterval.Month, // Default interval if not specified - }; - } - } - } catch (error: unknown) { - console.error("Error extracting price:", error); - return null; + const item = product.items.find((x) => isPriceItem(x)); + + if (item) { + return { + amount: item.price ?? 0, + interval: (item.interval as unknown as ProductItemInterval) || null, + }; } + return null; } diff --git a/shared/utils/utils.ts b/shared/utils/utils.ts index 5f8b96b3a..ca2999f16 100644 --- a/shared/utils/utils.ts +++ b/shared/utils/utils.ts @@ -1,2 +1,4 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: one off */ + export const notNullish = (value: any) => value !== null && value !== undefined; export const nullish = (value: any) => value === null || value === undefined; diff --git a/vite/src/components/v2/breadcrumb.tsx b/vite/src/components/v2/breadcrumb.tsx index 018952079..abbfc84a5 100644 --- a/vite/src/components/v2/breadcrumb.tsx +++ b/vite/src/components/v2/breadcrumb.tsx @@ -5,6 +5,7 @@ import { BreadcrumbList, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; +import { cn } from "@/lib/utils"; import { useEnv } from "@/utils/envUtils"; import { navigateTo } from "@/utils/genUtils"; @@ -15,14 +16,18 @@ interface BreadcrumbItemType { export default function V2Breadcrumb({ items, + className, }: { items: BreadcrumbItemType[]; + className?: string; }) { const env = useEnv(); const navigate = useNavigate(); return ( - + {items.map((item, index) => ( <> diff --git a/vite/src/components/v2/button.tsx b/vite/src/components/v2/button.tsx index fc14ea013..93083267f 100644 --- a/vite/src/components/v2/button.tsx +++ b/vite/src/components/v2/button.tsx @@ -1,160 +1,957 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +; diff --git a/vite/src/components/v2/buttons/Button.tsx b/vite/src/components/v2/buttons/Button.tsx new file mode 100644 index 000000000..8715eb29b --- /dev/null +++ b/vite/src/components/v2/buttons/Button.tsx @@ -0,0 +1,67 @@ +/* eslint-disable react-refresh/only-export-components */ +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + `inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-none + + h-6 rounded-lg + `, + { + variants: { + variant: { + // Custom + primary: `btn-primary-shadow !text-white bg-primary border border-transparent hover:bg-primary-btn-hover + focus:bg-primary-btn-active focus:border-primary-btn-border`, + secondary: + "bg-white border border-input hover:border-primary hover:bg-hover-primary focus:bg-active-primary focus:border-primary btn-secondary-shadow", + + // destructive: + // "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + // outline: + // "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + // secondary: + // "bg-secondary text-secondary-foreground hover:bg-secondary/80", + // ghost: + // "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + // link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "py-1 !px-[7px] text-body has-[>svg]:px-3", + // sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + // lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + // icon: "size-9", + }, + }, + defaultVariants: { + variant: "primary", + size: "default", + }, + }, +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/vite/src/components/v2/checkboxes/LongCheckbox.tsx b/vite/src/components/v2/checkboxes/LongCheckbox.tsx index 26336b76f..a40ab3135 100644 --- a/vite/src/components/v2/checkboxes/LongCheckbox.tsx +++ b/vite/src/components/v2/checkboxes/LongCheckbox.tsx @@ -2,7 +2,7 @@ import React from "react"; import { cn } from "@/lib/utils"; -import { Checkbox } from "./checkbox"; +import { Checkbox } from "./Checkbox"; interface LongCheckboxProps { className?: string; @@ -26,12 +26,12 @@ function LongCheckbox({ return (