caching cusProduct
This commit is contained in:
@@ -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();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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,
|
||||
// };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user