caching cusProduct

This commit is contained in:
John Yeo
2025-09-03 12:51:56 -07:00
parent 66cd5531fc
commit 06acf84956
6 changed files with 196 additions and 130 deletions

View File

@@ -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<any>[] = [];
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();
};
};

View File

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

View File

@@ -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({

View File

@@ -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 <LoadingScreen />;
@@ -224,17 +209,3 @@ export const CopyUrl = ({
</div>
);
};
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";
};

View File

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

View File

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