Weekly Report
report_weeklyCompare weekly revenue and order changes, analyze customer segments, identify trending products, and receive AI-generated insights.
Instructions
Weekly trend report: revenue/order changes vs previous week, customer segment distribution, trending products, and AI-generated insights.
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) |
Implementation Reference
- src/index.ts:327-346 (registration)Tool 'report_weekly' registered with MCP server. Accepts store_id (UUID) input. Pro license-gated. Delegates to generateWeeklyReport().
// ── Tool: report_weekly ─────────────────────────────────────────── server.registerTool( 'report_weekly', { title: 'Weekly Report', description: 'Weekly trend report: revenue/order changes vs previous week, customer segment distribution, trending products, and AI-generated insights.', 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)'), }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async ({ store_id }) => { try { const reject = await ensureProOrReject(LICENSE_CONFIG, 'report_weekly'); if (reject) return reject; const result = await generateWeeklyReport(store_id); return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; } catch (e) { return handleToolError(e); } } ); - src/tools/reports.ts:103-201 (handler)Core business logic: computes weekly report comparing this week vs previous week, including revenue/order changes, RFM customer segment distribution, trending products (rising/stable/declining), and AI-generated text insights.
export async function generateWeeklyReport(storeId: string): Promise<WeeklyReport> { validateUUID(storeId, 'store'); const store = await storage.getStoreById(storeId); if (!store) throw new NotFoundError('Store', storeId); const now = Date.now(); const weekEnd = new Date(now).toISOString().slice(0, 10); const weekStart = new Date(now - 7 * MS_PER_DAY).toISOString().slice(0, 10); const allOrders = await storage.getOrders(storeId); const thisWeek = allOrders.filter((o) => { const ts = new Date(o.created_at).getTime(); return ts >= now - 7 * MS_PER_DAY && o.status !== 'cancelled' && o.status !== 'refunded'; }); const prevWeek = allOrders.filter((o) => { const ts = new Date(o.created_at).getTime(); return ts >= now - 14 * MS_PER_DAY && ts < now - 7 * MS_PER_DAY && o.status !== 'cancelled' && o.status !== 'refunded'; }); const thisRevenue = thisWeek.reduce((sum, o) => sum + o.total, 0); const prevRevenue = prevWeek.reduce((sum, o) => sum + o.total, 0); const avgOrderValue = thisWeek.length > 0 ? thisRevenue / thisWeek.length : 0; const revenueChange = prevRevenue > 0 ? Math.round(((thisRevenue - prevRevenue) / prevRevenue) * 10000) / 100 : 0; const orderChange = prevWeek.length > 0 ? Math.round(((thisWeek.length - prevWeek.length) / prevWeek.length) * 10000) / 100 : 0; // Segment summary const customers = await storage.getCustomers(storeId); const rfmResults = segmentCustomers(customers, allOrders); const segmentMap = new Map<string, { count: number; revenue: number }>(); for (const r of rfmResults) { const existing = segmentMap.get(r.segment) ?? { count: 0, revenue: 0 }; existing.count++; existing.revenue += r.total_spent; segmentMap.set(r.segment, existing); } const topSegments = [...segmentMap.entries()] .sort((a, b) => b[1].revenue - a[1].revenue) .slice(0, 5) .map(([segment, data]) => ({ segment: segment as RFMSegment, count: data.count, revenue: Math.round(data.revenue * 100) / 100, })); // Product trends const thisWeekProducts = new Map<string, { title: string; units: number }>(); const prevWeekProducts = new Map<string, { title: string; units: number }>(); for (const order of thisWeek) { for (const item of order.items) { const ex = thisWeekProducts.get(item.product_id) ?? { title: item.title, units: 0 }; ex.units += item.quantity; thisWeekProducts.set(item.product_id, ex); } } for (const order of prevWeek) { for (const item of order.items) { const ex = prevWeekProducts.get(item.product_id) ?? { title: item.title, units: 0 }; ex.units += item.quantity; prevWeekProducts.set(item.product_id, ex); } } const trendingProducts = [...thisWeekProducts.entries()] .map(([id, data]) => { const prev = prevWeekProducts.get(id)?.units ?? 0; const change = data.units - prev; const trend = change > 2 ? 'rising' as const : change < -2 ? 'declining' as const : 'stable' as const; return { title: data.title, units_change: change, trend }; }) .sort((a, b) => Math.abs(b.units_change) - Math.abs(a.units_change)) .slice(0, 5); // Insights const insights: string[] = []; if (revenueChange > 10) insights.push(`Revenue up ${revenueChange}% vs last week — strong growth`); else if (revenueChange < -10) insights.push(`Revenue down ${Math.abs(revenueChange)}% vs last week — needs attention`); else insights.push('Revenue is stable compared to last week'); const risingProducts = trendingProducts.filter((p) => p.trend === 'rising'); if (risingProducts.length > 0) insights.push(`${risingProducts.length} product(s) trending upward: ${risingProducts.map((p) => p.title).join(', ')}`); const atRisk = rfmResults.filter((r) => r.segment === 'at_risk' || r.segment === 'lost'); if (atRisk.length > 0) insights.push(`${atRisk.length} customer(s) at risk of churning — consider win-back campaigns`); return { store_id: storeId, week_start: weekStart, week_end: weekEnd, total_orders: thisWeek.length, total_revenue: Math.round(thisRevenue * 100) / 100, avg_order_value: Math.round(avgOrderValue * 100) / 100, revenue_change_percent: revenueChange, order_change_percent: orderChange, top_segments: topSegments, trending_products: trendingProducts, insights, summary: insights.join('. ') + '.', }; } - src/models/store.ts:228-250 (schema)Zod schema defining the WeeklyReport return type with fields: store_id, week_start/end, total_orders, total_revenue, avg_order_value, revenue/order change percent, top_segments, trending_products, insights, and summary.
export const WeeklyReportSchema = z.object({ store_id: z.string().uuid(), week_start: z.string(), week_end: z.string(), total_orders: z.number().int(), total_revenue: z.number(), avg_order_value: z.number(), revenue_change_percent: z.number(), order_change_percent: z.number(), top_segments: z.array(z.object({ segment: RFMSegmentSchema, count: z.number().int(), revenue: z.number(), })), trending_products: z.array(z.object({ title: z.string(), units_change: z.number().int(), trend: z.enum(['rising', 'stable', 'declining']), })), insights: z.array(z.string()), summary: z.string(), }); export type WeeklyReport = z.infer<typeof WeeklyReportSchema>;