Product Performance (ABC Analysis)
product_performanceClassify products by revenue using ABC analysis (top 80%, next 15%, bottom 5%) and review trends, margins, and daily sales velocity.
Instructions
Product performance report with ABC analysis. Category A = top 80% revenue, B = next 15%, C = bottom 5%. Includes trends, margins, and daily sales velocity.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| store_id | Yes | UUID of a connected store (returned by store_connect with action="connect" or visible in store_connect with action="list" / the store_overview resource) | |
| period_days | No | Look-back window for ABC classification, 7–90 days. Defaults to 30. Shorter windows favour recent trends; longer windows smooth seasonality. |
Implementation Reference
- src/tools/products.ts:22-125 (handler)The main handler function `getProductPerformance` that executes the product performance (ABC analysis) tool logic. It validates the store, aggregates order data over two time periods, computes metrics (units sold, revenue, cost, profit, margin, trends), categorizes products into A/B/C by cumulative revenue share, and returns a `ProductPerformanceSummary`.
export async function getProductPerformance(storeId: string, periodDays = 30): Promise<ProductPerformanceSummary> { validateUUID(storeId, 'store'); const store = await storage.getStoreById(storeId); if (!store) throw new NotFoundError('Store', storeId); const products = await storage.getProducts(storeId); const orders = await storage.getOrders(storeId); const now = Date.now(); const cutoff = now - periodDays * MS_PER_DAY; const olderCutoff = now - 2 * periodDays * MS_PER_DAY; const recentOrders = orders.filter((o) => new Date(o.created_at).getTime() >= cutoff && o.status !== 'cancelled' && o.status !== 'refunded' ); const olderOrders = orders.filter((o) => { const ts = new Date(o.created_at).getTime(); return ts >= olderCutoff && ts < cutoff && o.status !== 'cancelled' && o.status !== 'refunded'; }); // Aggregate per-product metrics const productMetrics = new Map<string, { unitsSold: number; revenue: number; prevUnitsSold: number }>(); for (const order of recentOrders) { for (const item of order.items) { const existing = productMetrics.get(item.product_id) ?? { unitsSold: 0, revenue: 0, prevUnitsSold: 0 }; existing.unitsSold += item.quantity; existing.revenue += item.total; productMetrics.set(item.product_id, existing); } } for (const order of olderOrders) { for (const item of order.items) { const existing = productMetrics.get(item.product_id) ?? { unitsSold: 0, revenue: 0, prevUnitsSold: 0 }; existing.prevUnitsSold += item.quantity; productMetrics.set(item.product_id, existing); } } const totalRevenue = [...productMetrics.values()].reduce((sum, m) => sum + m.revenue, 0); // Build performance records sorted by revenue const perfRecords: ProductPerformance[] = []; for (const product of products) { const metrics = productMetrics.get(product.id); if (!metrics && product.status !== 'active') continue; const unitsSold = metrics?.unitsSold ?? 0; const revenue = metrics?.revenue ?? 0; const prevUnits = metrics?.prevUnitsSold ?? 0; const cost = product.cost_price !== null ? product.cost_price * unitsSold : null; const profit = cost !== null ? revenue - cost : null; const marginPercent = revenue > 0 && cost !== null ? Math.round(((revenue - cost) / revenue) * 10000) / 100 : null; // Trend: compare with previous period let trend: 'rising' | 'stable' | 'declining'; if (prevUnits === 0 && unitsSold > 0) trend = 'rising'; else if (prevUnits === 0 && unitsSold === 0) trend = 'stable'; else { const changeRate = (unitsSold - prevUnits) / Math.max(1, prevUnits); trend = changeRate > 0.15 ? 'rising' : changeRate < -0.15 ? 'declining' : 'stable'; } perfRecords.push({ product_id: product.id, product_title: product.title, sku: product.sku, units_sold: unitsSold, revenue: Math.round(revenue * 100) / 100, cost, profit: profit !== null ? Math.round(profit * 100) / 100 : null, margin_percent: marginPercent, abc_category: 'C', // placeholder, calculated below revenue_share_percent: totalRevenue > 0 ? Math.round((revenue / totalRevenue) * 10000) / 100 : 0, avg_daily_units: Math.round((unitsSold / periodDays) * 100) / 100, trend, }); } // Sort by revenue descending for ABC perfRecords.sort((a, b) => b.revenue - a.revenue); // ABC categorization let cumulativeShare = 0; for (const rec of perfRecords) { cumulativeShare += rec.revenue_share_percent; if (cumulativeShare <= 80) rec.abc_category = 'A'; else if (cumulativeShare <= 95) rec.abc_category = 'B'; else rec.abc_category = 'C'; } return { store_id: storeId, period_days: periodDays, total_products: perfRecords.length, total_revenue: Math.round(totalRevenue * 100) / 100, category_a: perfRecords.filter((p) => p.abc_category === 'A').length, category_b: perfRecords.filter((p) => p.abc_category === 'B').length, category_c: perfRecords.filter((p) => p.abc_category === 'C').length, products: perfRecords, }; } - src/models/store.ts:188-202 (schema)`ProductPerformanceSchema` — Zod schema defining the shape of each product performance record (product_id, product_title, sku, units_sold, revenue, cost, profit, margin_percent, abc_category, revenue_share_percent, avg_daily_units, trend). Also exports the `ProductPerformance` type.
export const ProductPerformanceSchema = z.object({ product_id: z.string(), product_title: z.string(), sku: z.string().nullable(), units_sold: z.number().int(), revenue: z.number(), cost: z.number().nullable(), profit: z.number().nullable(), margin_percent: z.number().nullable(), abc_category: ABCCategorySchema, revenue_share_percent: z.number(), avg_daily_units: z.number(), trend: z.enum(['rising', 'stable', 'declining']), }); export type ProductPerformance = z.infer<typeof ProductPerformanceSchema>; - src/index.ts:287-305 (registration)Registration of the 'product_performance' tool via `server.registerTool()`. Defines input schema (store_id UUID, period_days with default 30), annotations, and an async handler that calls `getProductPerformance()` and returns JSON-stringified results.
// ── Tool: product_performance ───────────────────────────────────── server.registerTool( 'product_performance', { title: 'Product Performance (ABC Analysis)', description: 'Product performance report with ABC analysis. Category A = top 80% revenue, B = next 15%, C = bottom 5%. Includes trends, margins, and daily sales velocity.', inputSchema: z.object({ store_id: z.string().uuid().describe('UUID of a connected store (returned by store_connect with action="connect" or visible in store_connect with action="list" / the store_overview resource)'), period_days: z.number().int().min(7).max(90).default(30).describe('Look-back window for ABC classification, 7–90 days. Defaults to 30. Shorter windows favour recent trends; longer windows smooth seasonality.'), }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async ({ store_id, period_days }) => { try { const result = await getProductPerformance(store_id, period_days); return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; } catch (e) { return handleToolError(e); } } ); - src/tools/products.ts:7-16 (helper)`ProductPerformanceSummary` TypeScript interface — structure of the overall tool response (store_id, period_days, total_products, total_revenue, category counts, and products array).
export interface ProductPerformanceSummary { store_id: string; period_days: number; total_products: number; total_revenue: number; category_a: number; category_b: number; category_c: number; products: ProductPerformance[]; }