diff --git a/server/benchmarks/attach-benchmarks.ts b/server/benchmarks/attach-benchmarks.ts index a3afa0d2a..6d9eda1f9 100644 --- a/server/benchmarks/attach-benchmarks.ts +++ b/server/benchmarks/attach-benchmarks.ts @@ -1,127 +1,133 @@ -import chalk from 'chalk'; -import { - BenchmarkRunner, - DryRunHelper, - createMockCustomer, - createMockProduct -} from './benchmark-utils.js'; +import chalk from "chalk"; +import { + BenchmarkRunner, + DryRunHelper, + createMockCustomer, + createMockProduct, +} from "./benchmark-utils.js"; // Mock product attachment operations const mockAttachProduct = async (params: any) => { const { customer_id, product_id, force_checkout } = params; - + // Simulate the attach workflow from existing tests - DryRunHelper.mockDbOperation('getCustomer', { customerId: customer_id }); - DryRunHelper.mockDbOperation('getProduct', { productId: product_id }); - + DryRunHelper.mockDbOperation("getCustomer", { customerId: customer_id }); + DryRunHelper.mockDbOperation("getProduct", { productId: product_id }); + // Simulate pricing calculations DryRunHelper.mockComplexCalculation(300); // Price calculation logic - + if (force_checkout) { // Simulate Stripe checkout creation - DryRunHelper.mockStripeOperation('createCheckout', { + DryRunHelper.mockStripeOperation("createCheckout", { customer_id, product_id, }); } - + // Simulate entitlement updates - DryRunHelper.mockDbOperation('updateEntitlements', { + DryRunHelper.mockDbOperation("updateEntitlements", { customer_id, product_id, - entitlements: ['premium_feature', 'advanced_api'], + entitlements: ["premium_feature", "advanced_api"], }); - + return { success: true, attached: true }; }; const mockUpgradeProduct = async (params: any) => { const { customer_id, from_product_id, to_product_id } = params; - + // Simulate upgrade workflow - DryRunHelper.mockDbOperation('getCurrentProduct', { customer_id, from_product_id }); - DryRunHelper.mockDbOperation('getTargetProduct', { to_product_id }); - + DryRunHelper.mockDbOperation("getCurrentProduct", { + customer_id, + from_product_id, + }); + DryRunHelper.mockDbOperation("getTargetProduct", { to_product_id }); + // Simulate prorated billing calculation (CPU intensive) DryRunHelper.mockComplexCalculation(800); - + // Simulate Stripe subscription update - DryRunHelper.mockStripeOperation('updateSubscription', { + DryRunHelper.mockStripeOperation("updateSubscription", { customer_id, from_product_id, to_product_id, }); - + // Update entitlements - DryRunHelper.mockDbOperation('migrateEntitlements', { + DryRunHelper.mockDbOperation("migrateEntitlements", { customer_id, from_product_id, to_product_id, }); - + return { success: true, upgraded: true }; }; const mockDowngradeProduct = async (params: any) => { const { customer_id, from_product_id, to_product_id } = params; - + // Similar to upgrade but with different calculations - DryRunHelper.mockDbOperation('getCurrentProduct', { customer_id, from_product_id }); - DryRunHelper.mockDbOperation('getTargetProduct', { to_product_id }); - + DryRunHelper.mockDbOperation("getCurrentProduct", { + customer_id, + from_product_id, + }); + DryRunHelper.mockDbOperation("getTargetProduct", { to_product_id }); + // Downgrade calculations (typically simpler) DryRunHelper.mockComplexCalculation(400); - + // Stripe operations - DryRunHelper.mockStripeOperation('updateSubscription', { + DryRunHelper.mockStripeOperation("updateSubscription", { customer_id, from_product_id, to_product_id, }); - + // Handle feature restrictions - DryRunHelper.mockDbOperation('restrictEntitlements', { + DryRunHelper.mockDbOperation("restrictEntitlements", { customer_id, - restricted_features: ['premium_feature'], + restricted_features: ["premium_feature"], }); - + return { success: true, downgraded: true }; }; const mockCalculatePricing = async (params: any) => { const { product_id, customer_id, usage_data } = params; - + // Simulate complex pricing calculation - DryRunHelper.mockDbOperation('getProductPricing', { product_id }); - DryRunHelper.mockDbOperation('getCustomerUsage', { customer_id }); - + DryRunHelper.mockDbOperation("getProductPricing", { product_id }); + DryRunHelper.mockDbOperation("getCustomerUsage", { customer_id }); + // CPU-intensive pricing calculations DryRunHelper.mockComplexCalculation(600); - + // Simulate tier-based pricing logic const tiers = usage_data?.tiers || [100, 1000, 10000]; let totalCost = 0; for (const tier of tiers) { totalCost += tier * 0.01; // Mock pricing calculation } - + return { totalCost, breakdown: tiers }; }; const mockEntityAttachment = async (params: any) => { const { customer_id, product_id, entity_id } = params; - + // Simulate entity-specific attachment - DryRunHelper.mockDbOperation('getEntity', { entity_id }); - DryRunHelper.mockDbOperation('attachToEntity', { + DryRunHelper.mockDbOperation("getEntity", { entity_id }); + DryRunHelper.mockDbOperation("attachToEntity", { customer_id, product_id, entity_id, }); - + // Entity-specific calculations DryRunHelper.mockComplexCalculation(200); - + return { success: true, entity_attached: true }; }; @@ -131,73 +137,75 @@ export const runAttachBenchmarks = async () => { warmupIterations: 5, }); - console.log(chalk.cyan('🔗 Product & Billing Operations')); - console.log(chalk.gray('Measuring subscription and pricing workflows\n')); - + console.log(chalk.cyan("🔗 Product & Billing Operations")); + console.log(chalk.gray("Measuring subscription and pricing workflows\n")); + // Real-world product operations - await runner.run('Free Plan Signup', async () => { + await runner.run("Free Plan Signup", async () => { await mockAttachProduct({ - customer_id: 'new_customer_123', - product_id: 'starter_free', + customer_id: "new_customer_123", + product_id: "starter_free", force_checkout: false, }); }); - await runner.run('Paid Plan Subscription', async () => { + await runner.run("Paid Plan Subscription", async () => { await mockAttachProduct({ - customer_id: 'converting_customer_456', - product_id: 'pro_monthly', + customer_id: "converting_customer_456", + product_id: "pro_monthly", force_checkout: true, }); }); - await runner.run('Plan Upgrade (Basic → Pro)', async () => { + await runner.run("Plan Upgrade (Basic → Pro)", async () => { await mockUpgradeProduct({ - customer_id: 'existing_customer_789', - from_product_id: 'basic_monthly', - to_product_id: 'pro_monthly', + customer_id: "existing_customer_789", + from_product_id: "basic_monthly", + to_product_id: "pro_monthly", }); }); - await runner.run('Plan Downgrade (Pro → Basic)', async () => { + await runner.run("Plan Downgrade (Pro → Basic)", async () => { await mockDowngradeProduct({ - customer_id: 'downgrading_customer_321', - from_product_id: 'pro_monthly', - to_product_id: 'basic_monthly', + customer_id: "downgrading_customer_321", + from_product_id: "pro_monthly", + to_product_id: "basic_monthly", }); }); - await runner.run('Usage-Based Pricing Calc', async () => { + await runner.run("Usage-Based Pricing Calc", async () => { await mockCalculatePricing({ - product_id: 'usage_tier_product', - customer_id: 'heavy_user_654', + product_id: "usage_tier_product", + customer_id: "heavy_user_654", usage_data: { tiers: [1000, 5000, 25000, 100000], - features: ['api_requests', 'storage_gb', 'compute_hours'], + features: ["api_requests", "storage_gb", "compute_hours"], }, }); }); - await runner.run('Team Plan Setup', async () => { + await runner.run("Team Plan Setup", async () => { await mockEntityAttachment({ - customer_id: 'team_lead_987', - product_id: 'team_plan', - entity_id: 'team_acme_corp', + customer_id: "team_lead_987", + product_id: "team_plan", + entity_id: "team_acme_corp", }); }); - await runner.run('Bulk Plan Changes (5 customers)', async () => { + await runner.run("Bulk Plan Changes (5 customers)", async () => { const promises: Promise[] = []; for (let i = 0; i < 5; i++) { - promises.push(mockAttachProduct({ - customer_id: `bulk_customer_${i}`, - product_id: 'standard_plan', - force_checkout: false, - })); + promises.push( + mockAttachProduct({ + customer_id: `bulk_customer_${i}`, + product_id: "standard_plan", + force_checkout: false, + }) + ); } await Promise.all(promises); }); runner.printSummary(); return runner.getResults(); -}; \ No newline at end of file +}; diff --git a/server/src/internal/customers/cusProducts/cusProductUtils.ts b/server/src/internal/customers/cusProducts/cusProductUtils.ts index c5986d93c..042c8cd4e 100644 --- a/server/src/internal/customers/cusProducts/cusProductUtils.ts +++ b/server/src/internal/customers/cusProducts/cusProductUtils.ts @@ -4,7 +4,6 @@ import { AttachScenario, CusProductResponseSchema, CusProductStatus, - Customer, Entity, FixedPriceConfig, FullCusProduct, @@ -22,13 +21,8 @@ import { import { ProductService } from "@/internal/products/ProductService.js"; import { CusProductService, RELEVANT_STATUSES } from "./CusProductService.js"; import { createStripeCli } from "@/external/stripe/utils.js"; -import { createFullCusProduct } from "../add-product/createFullCusProduct.js"; -import Stripe from "stripe"; -import { - deleteScheduledIds, - getStripeSubs, - subIsPrematurelyCanceled, -} from "@/external/stripe/stripeSubUtils.js"; + +import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; import { getRelatedCusEnt } from "./cusPrices/cusPriceUtils.js"; import { notNullish, nullish } from "@/utils/genUtils.js"; import { BREAK_API_VERSION } from "@/utils/constants.js"; diff --git a/server/src/internal/customers/internalCusRouter.ts b/server/src/internal/customers/internalCusRouter.ts index f934ba87a..cf989e7aa 100644 --- a/server/src/internal/customers/internalCusRouter.ts +++ b/server/src/internal/customers/internalCusRouter.ts @@ -135,14 +135,12 @@ cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => { const { customer_id } = req.params; const orgId = req.orgId; - console.time("get_customer"); let internalCustomer = await CusService.get({ db, orgId, env, idOrInternalId: customer_id, }); - console.timeEnd("get_customer"); if (!internalCustomer) { throw new RecaseError({ diff --git a/vite/src/views/customers/customer/product/CustomerProductView.tsx b/vite/src/views/customers/customer/product/CustomerProductView.tsx index 1a5f893e6..0bac8a4bc 100644 --- a/vite/src/views/customers/customer/product/CustomerProductView.tsx +++ b/vite/src/views/customers/customer/product/CustomerProductView.tsx @@ -71,15 +71,6 @@ export default function CustomerProductView() { const version = searchParams.get("version"); const customer_product_id = searchParams.get("id"); - // const { data, isLoading, error } = useAxiosSWR({ - // url: `/customers/${customer_id}/product/${product_id}${getProductUrlParams({ - // version, - // customer_product_id, - // entity_id: entityId, - // })}`, - // env, - // }); - const { product: originalProduct, cusProduct, @@ -142,12 +133,6 @@ export default function CustomerProductView() { ); } - // console.log("Product:", product); - // console.log("Is loading:", isLoading); - // console.log("Is org loading:", orgLoading); - // console.log("Is cus loading:", cusLoading); - // console.log("Is features loading:", featuresLoading); - if (isLoading || !product || cusLoading || orgLoading || featuresLoading) return ; @@ -224,17 +209,3 @@ export const CopyUrl = ({ ); }; - -export type NewSubscription = { - id: "new"; - code: "checkout_session" | "combine_subscription" | "renew"; -}; - -export type UpdateSubscription = { - id: "update"; - code: "upgrade" | "downgrade"; -}; - -export type Product = { - scenario: "new:checkout_session" | "new:combine_subscription"; -}; diff --git a/vite/src/views/customers/customer/product/hooks/useCusProductCache.tsx b/vite/src/views/customers/customer/product/hooks/useCusProductCache.tsx new file mode 100644 index 000000000..1bcf69c4a --- /dev/null +++ b/vite/src/views/customers/customer/product/hooks/useCusProductCache.tsx @@ -0,0 +1,93 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { + ACTIVE_STATUSES, + FullCusProduct, + FullCustomer, + productToCusProduct, +} from "@autumn/shared"; + +export const useCusProductCache = ({ + customerId, + productId, + queryStates, +}: { + customerId: string | undefined; + productId: string | undefined; + queryStates: { + customerProductId: string | undefined; + version: number | undefined; + entityId: string | undefined; + }; +}) => { + const queryClient = useQueryClient(); + + const getCachedCusProduct = (): FullCusProduct | null => { + if (!customerId || !productId) return null; + + // Check all cached full customers queries + const queryCache = queryClient.getQueryCache(); + const customerQuery = queryCache.findAll({ + queryKey: ["customer", customerId], + }); + + // Sort by most recently updated first to get the freshest data + const sortedQueries = customerQuery.sort((a, b) => { + const aTime = a.state.dataUpdatedAt || 0; + const bTime = b.state.dataUpdatedAt || 0; + return bTime - aTime; + }); + + for (const query of sortedQueries) { + // Only use data that's not stale and has been successfully fetched + if (query.state.status === "success" && query.state.data) { + const cachedData = query.state.data as + | { customer: FullCustomer } + | undefined; + + if (cachedData?.customer) { + const cusProducts = cachedData.customer.customer_products; + + const internalEntityId = queryStates.entityId + ? cachedData.customer.entities.find( + (entity) => entity.id === queryStates.entityId + )?.internal_id + : cachedData.customer.entity?.internal_id; + + const cusProduct = productToCusProduct({ + productId: productId!, + cusProducts, + internalEntityId, + version: queryStates.version, + cusProductId: queryStates.customerProductId, + inStatuses: ACTIVE_STATUSES, + // version: undefined, + // cusProductId: undefined, + // inStatuses: ACTIVE_STATUSES, + }); + + console.log("Cached cus product:", cusProduct); + } + + // if (cachedData?.fullCustomers) { + // const cachedCustomer = cachedData.fullCustomers.find( + // (cusProduct) => + // cusProduct.id === customerId || + // cusProduct.internal_customer_id === customerId + // ); + + // if (cachedCustomer) { + // return cachedCustomer; + // } + // } + } + } + + // const productQueries = queryCache.findAll({ + // queryKey: ["products"], + // }); + + return null; + }; + + return { getCachedCusProduct }; +}; diff --git a/vite/src/views/customers/customer/product/hooks/useCusProductQuery.tsx b/vite/src/views/customers/customer/product/hooks/useCusProductQuery.tsx index 54dc59bcf..de41f84ce 100644 --- a/vite/src/views/customers/customer/product/hooks/useCusProductQuery.tsx +++ b/vite/src/views/customers/customer/product/hooks/useCusProductQuery.tsx @@ -3,17 +3,30 @@ import { FullCusProduct, ProductV2 } from "@autumn/shared"; import { useQuery } from "@tanstack/react-query"; import { parseAsInteger, parseAsString, useQueryStates } from "nuqs"; import { useParams } from "react-router"; +import { useCusProductCache } from "./useCusProductCache"; +import { useMemo } from "react"; export const useCusProductQuery = () => { const axiosInstance = useAxiosInstance(); const { customer_id, product_id } = useParams(); - const [queryStates] = useQueryStates({ version: parseAsInteger, customer_product_id: parseAsString, entity_id: parseAsString, }); + const { getCachedCusProduct } = useCusProductCache({ + customerId: customer_id, + productId: product_id, + queryStates: { + version: queryStates.version ?? undefined, + customerProductId: queryStates.customer_product_id ?? undefined, + entityId: queryStates.entity_id ?? undefined, + }, + }); + + const cachedCusProduct = useMemo(getCachedCusProduct, [getCachedCusProduct]); + const fetcher = async () => { const queryParams = { version: queryStates.version, @@ -47,15 +60,4 @@ export const useCusProductQuery = () => { error, refetch, }; - - // const { data, isLoading, error, refetch } = useQuery({ - // queryKey: ["customer_product", customer_id, product_id], - // queryFn: fetcher, - // }); - // return { - // cusProduct, - // isLoading, - // error, - // refetch, - // }; };