import express from 'express'; import { DateTime } from 'luxon'; import { getDbConnection, getPoolStatus } from '../db/connection.js'; import { getTimeRangeConditions, formatBusinessDate, getBusinessDayBounds, customRange, previousRange, parseMySql, toMySqlBound, _internal as timeHelpers, } from '../../../shared/business-time/index.js'; const router = express.Router(); const TIMEZONE = 'America/New_York'; const BUSINESS_DAY_START_HOUR = timeHelpers?.BUSINESS_DAY_START_HOUR ?? 1; // Cherry Box order types to exclude when excludeCherryBox=true is passed // 3 = cherrybox_subscription, 4 = cherrybox_sending, 5 = cherrybox_subscription_renew, 7 = cherrybox_refund const EXCLUDED_ORDER_TYPES = [3, 4, 5, 7]; const getCherryBoxClause = (exclude) => exclude ? `order_type NOT IN (${EXCLUDED_ORDER_TYPES.join(', ')})` : '1=1'; const getCherryBoxClauseAliased = (alias, exclude) => exclude ? `${alias}.order_type NOT IN (${EXCLUDED_ORDER_TYPES.join(', ')})` : '1=1'; const parseBoolParam = (value) => value === 'true' || value === '1'; // Image URL generation utility const getImageUrls = (pid, iid = 1) => { const imageUrlBase = 'https://sbing.com/i/products/0000/'; const paddedPid = pid.toString().padStart(6, '0'); const prefix = paddedPid.slice(0, 3); const basePath = `${imageUrlBase}${prefix}/${pid}`; return { image: `${basePath}-t-${iid}.jpg`, image_175: `${basePath}-175x175-${iid}.jpg`, image_full: `${basePath}-o-${iid}.jpg`, ImgThumb: `${basePath}-175x175-${iid}.jpg` // For ProductGrid component }; }; // The DB sits across an SSH tunnel (~190ms per round trip; the queries // themselves are ~5ms on the server), so /stats response time is dominated by // round-trip count, not query cost. Two mitigations: // 1. computeStats runs its queries over several pooled connections (mysql2 // serializes per connection), capped so concurrent refreshes can't starve // the 20-slot pool. // 2. Responses are cached per param-set and served stale-while-revalidate: // the dashboards poll every 30-60s, so they get the cached payload // instantly while a single-flight background refresh keeps it current. const STATS_QUERY_WIDTH = 4; const STATS_CACHE_TTL = 60 * 1000; // younger than this: serve as-is const STATS_CACHE_MAX_STALE = 15 * 60 * 1000; // older than this: block on refresh const statsCache = new Map(); // cacheKey -> { data, timestamp } const statsInflight = new Map(); // cacheKey -> Promise const withTimeout = (promise, ms, label) => { let timer; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${label} timeout after ${ms / 1000} seconds`)), ms); }); return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); }; // Drain [name, fn] tasks with up to `width` workers, each holding its own // pooled connection, so wall time is ~ceil(tasks / width) round trips. If a // task rejects, remaining queued tasks are abandoned and every worker still // releases its connection (mirrors the old single-connection failure mode). async function runStatsTasks(tasks, width) { const results = {}; const queue = [...tasks]; const worker = async () => { const { connection, release } = await getDbConnection(); try { let task; while ((task = queue.shift()) !== undefined) { results[task[0]] = await task[1](connection); } } finally { release(); } }; await Promise.all(Array.from({ length: Math.min(width, tasks.length) }, worker)); return results; } async function computeStats(timeRange, startDate, endDate, excludeCB) { const { whereClause, params, dateRange } = getTimeRangeConditions(timeRange, startDate, endDate); const isSingleDay = ['today', 'yesterday'].includes(timeRange); // Main order stats query (optionally excludes Cherry Box orders) // Note: order_status > 15 excludes cancelled (15), so cancelled stats are queried separately const mainStatsQuery = ` SELECT COUNT(*) as orderCount, SUM(summary_total) as revenue, SUM(stats_prod_pieces) as itemCount, AVG(summary_total) as averageOrderValue, AVG(stats_prod_pieces) as averageItemsPerOrder, SUM(CASE WHEN stats_waiting_preorder > 0 THEN 1 ELSE 0 END) as preOrderCount, SUM(CASE WHEN ship_method_selected = 'localpickup' THEN 1 ELSE 0 END) as localPickupCount, SUM(CASE WHEN ship_method_selected = 'holdit' THEN 1 ELSE 0 END) as onHoldCount FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause} `; // Cancelled orders query - uses date_cancelled instead of date_placed // Shows orders cancelled during the selected period, regardless of when they were placed const cancelledQuery = ` SELECT COUNT(*) as cancelledCount, SUM(summary_total) as cancelledTotal FROM _order WHERE order_status = 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause.replace(/date_placed/g, 'date_cancelled')} `; // Refunds query (optionally excludes Cherry Box orders) const refundsQuery = ` SELECT COUNT(*) as refundCount, ABS(SUM(payment_amount)) as refundTotal FROM order_payment op JOIN _order o ON op.order_id = o.order_id WHERE payment_amount < 0 AND o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace(/date_placed/g, 'o.date_placed')} `; // Shipped orders query - uses date_shipped instead of date_placed // This counts orders that were SHIPPED during the selected period, regardless of when they were placed const shippedQuery = ` SELECT COUNT(*) as shippedCount FROM _order WHERE order_status IN (92, 95, 100) AND ${getCherryBoxClause(excludeCB)} AND ${whereClause.replace(/date_placed/g, 'date_shipped')} `; // Best revenue day query (optionally excludes Cherry Box orders) const bestDayQuery = ` SELECT DATE(date_placed) as date, SUM(summary_total) as revenue, COUNT(*) as orders FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause} GROUP BY DATE(date_placed) ORDER BY revenue DESC LIMIT 1 `; // Peak hour query - uses selected time range for the card value. // date_placed stores Central wall-clock; the office reads the axis in // Eastern, and ET = CT + 1h year-round, so shift before taking HOUR(). // Only run for single-day ranges (see tasks below). const peakHourQuery = ` SELECT HOUR(ADDTIME(date_placed, '01:00:00')) as hour, COUNT(*) as count FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause} GROUP BY HOUR(ADDTIME(date_placed, '01:00:00')) ORDER BY count DESC LIMIT 1 `; // Hourly breakdown for detail chart - always rolling 24 hours (like revenue/orders use 30 days) // Hourly counts in EASTERN display hours (column stores Central // wall-clock; ET = CT + 1h year-round). currentHour comes from the same // expression so the rolling window stays aligned. const hourlyQuery = ` SELECT HOUR(ADDTIME(date_placed, '01:00:00')) as hour, COUNT(*) as count, HOUR(ADDTIME(NOW(), '01:00:00')) as currentHour FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND date_placed >= NOW() - INTERVAL 24 HOUR GROUP BY HOUR(ADDTIME(date_placed, '01:00:00')) `; // Brands query - products.company links to product_categories.cat_id for brand name // Only include products that have a brand assigned (INNER JOIN) const brandsQuery = ` SELECT pc.cat_id as catId, pc.name as brandName, COUNT(DISTINCT oi.order_id) as orderCount, SUM(oi.qty_ordered) as itemCount, SUM(oi.qty_ordered * oi.prod_price) as revenue FROM order_items oi JOIN _order o ON oi.order_id = o.order_id JOIN products p ON oi.prod_pid = p.pid JOIN product_categories pc ON p.company = pc.cat_id WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace(/date_placed/g, 'o.date_placed')} GROUP BY pc.cat_id, pc.name HAVING revenue > 0 ORDER BY revenue DESC LIMIT 100 `; // Categories query - uses product_category_index to get category assignments // Only include categories with valid types (no NULL/uncategorized) const categoriesQuery = ` SELECT pc.cat_id as catId, pc.name as categoryName, COUNT(DISTINCT oi.order_id) as orderCount, SUM(oi.qty_ordered) as itemCount, SUM(oi.qty_ordered * oi.prod_price) as revenue FROM order_items oi JOIN _order o ON oi.order_id = o.order_id JOIN products p ON oi.prod_pid = p.pid JOIN product_category_index pci ON p.pid = pci.pid JOIN product_categories pc ON pci.cat_id = pc.cat_id WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace(/date_placed/g, 'o.date_placed')} AND pc.type IN (10, 20, 11, 21, 12, 13) GROUP BY pc.cat_id, pc.name HAVING revenue > 0 ORDER BY revenue DESC LIMIT 100 `; // Shipping locations query - uses date_shipped to match shippedCount const shippingQuery = ` SELECT ship_country, ship_state, ship_method_selected, COUNT(*) as count FROM _order WHERE order_status IN (92, 95, 100) AND ${getCherryBoxClause(excludeCB)} AND ${whereClause.replace(/date_placed/g, 'date_shipped')} GROUP BY ship_country, ship_state, ship_method_selected `; // Order value range query (optionally excludes Cherry Box orders) // Excludes $0 orders from min calculation const orderRangeQuery = ` SELECT MIN(CASE WHEN summary_total > 0 THEN summary_total END) as smallest, MAX(summary_total) as largest FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause} `; // Run everything in parallel; heavier joins first so they don't tail-end // the worker queue. const rows = (sql, p = params) => (conn) => conn.execute(sql, p).then(([result]) => result); const tasks = [ ['brands', rows(brandsQuery)], ['categories', rows(categoriesQuery)], ['mainStats', rows(mainStatsQuery)], ['prevPeriod', (conn) => getPreviousPeriodData(conn, timeRange, startDate, endDate, excludeCB)], ['cancelled', rows(cancelledQuery)], ['refunds', rows(refundsQuery)], ['shipped', rows(shippedQuery)], ['bestDay', rows(bestDayQuery)], ['shipping', rows(shippingQuery)], ['orderRange', rows(orderRangeQuery)], ]; if (isSingleDay) { tasks.push(['peakHour', rows(peakHourQuery)]); tasks.push(['hourly', rows(hourlyQuery, [])]); } const r = await runStatsTasks(tasks, STATS_QUERY_WIDTH); const stats = r.mainStats[0]; const cancelledStats = r.cancelled[0] || { cancelledCount: 0, cancelledTotal: 0 }; const refundStats = r.refunds; const shippedCount = parseInt(r.shipped[0]?.shippedCount || 0); const bestDayResult = r.bestDay; const brandsResult = r.brands; const categoriesResult = r.categories; const orderRangeResult = r.orderRange; const prevPeriodData = r.prevPeriod; const shippingStats = processShippingData(r.shipping, shippedCount); let peakHour = null; if (isSingleDay && r.peakHour.length > 0) { const hour = parseInt(r.peakHour[0].hour); peakHour = { hour, count: parseInt(r.peakHour[0].count), displayHour: DateTime.fromObject({ hour }).toFormat('h a') }; } // Returns data ordered chronologically: [24hrs ago, ..., 1hr ago, current hour] let hourlyOrders = null; if (isSingleDay) { // Current hour in Eastern (fallback computes it directly) const currentHour = r.hourly.length > 0 ? parseInt(r.hourly[0].currentHour) : DateTime.now().setZone(TIMEZONE).hour; const hourCounts = {}; r.hourly.forEach(row => { hourCounts[parseInt(row.hour)] = parseInt(row.count); }); hourlyOrders = []; for (let i = 0; i < 24; i++) { const hour = (currentHour + 1 + i) % 24; // Start from 24hrs ago, end at current hour hourlyOrders.push({ hour, count: hourCounts[hour] || 0 }); } } // Calculate period progress for incomplete periods let periodProgress = 100; if (['today', 'thisWeek', 'thisMonth'].includes(timeRange)) { periodProgress = calculatePeriodProgress(timeRange); } const response = { timeRange: dateRange, stats: { revenue: parseFloat(stats.revenue || 0), orderCount: parseInt(stats.orderCount || 0), itemCount: parseInt(stats.itemCount || 0), averageOrderValue: parseFloat(stats.averageOrderValue || 0), averageItemsPerOrder: parseFloat(stats.averageItemsPerOrder || 0), // Order types orderTypes: { preOrders: { count: parseInt(stats.preOrderCount || 0), percentage: stats.orderCount > 0 ? (stats.preOrderCount / stats.orderCount) * 100 : 0 }, localPickup: { count: parseInt(stats.localPickupCount || 0), percentage: stats.orderCount > 0 ? (stats.localPickupCount / stats.orderCount) * 100 : 0 }, heldItems: { count: parseInt(stats.onHoldCount || 0), percentage: stats.orderCount > 0 ? (stats.onHoldCount / stats.orderCount) * 100 : 0 } }, // Shipping shipping: { shippedCount: parseInt(shippedCount || 0), locations: shippingStats.locations, methodStats: shippingStats.methods }, // Brands and categories brands: { total: brandsResult.length, list: brandsResult.slice(0, 50).map(brand => ({ id: brand.catId, name: brand.brandName, count: parseInt(brand.itemCount), revenue: parseFloat(brand.revenue) })) }, categories: { total: categoriesResult.length, list: categoriesResult.slice(0, 50).map(category => ({ id: category.catId, name: category.categoryName, count: parseInt(category.itemCount), revenue: parseFloat(category.revenue) })) }, // Refunds and cancellations refunds: { total: parseFloat(refundStats[0]?.refundTotal || 0), count: parseInt(refundStats[0]?.refundCount || 0) }, canceledOrders: { total: parseFloat(cancelledStats.cancelledTotal || 0), count: parseInt(cancelledStats.cancelledCount || 0) }, // Best day bestRevenueDay: bestDayResult.length > 0 ? { amount: parseFloat(bestDayResult[0].revenue), displayDate: bestDayResult[0].date, orders: parseInt(bestDayResult[0].orders) } : null, // Peak hour (for single days) peakOrderHour: peakHour, hourlyOrders: hourlyOrders, // Array of 24 hourly order counts for the detail chart // Order value range orderValueRange: orderRangeResult.length > 0 ? { smallest: parseFloat(orderRangeResult[0].smallest || 0), largest: parseFloat(orderRangeResult[0].largest || 0) } : { smallest: 0, largest: 0 }, // Period progress and projections periodProgress, projectedRevenue: periodProgress < 100 ? (stats.revenue / (periodProgress / 100)) : stats.revenue, // Previous period comparison prevPeriodRevenue: prevPeriodData.revenue, prevPeriodOrders: prevPeriodData.orderCount, prevPeriodAOV: prevPeriodData.averageOrderValue } }; return response; } // Main stats endpoint - replaces /api/klaviyo/events/stats router.get('/stats', async (req, res) => { const startTime = Date.now(); const { timeRange, startDate, endDate, excludeCherryBox } = req.query; const excludeCB = parseBoolParam(excludeCherryBox); const cacheKey = `${timeRange || ''}|${startDate || ''}|${endDate || ''}|${excludeCB}`; // Single-flight refresh: concurrent misses for the same key share one // computation instead of racing multi-connection batches against the pool. const refresh = () => { let inflight = statsInflight.get(cacheKey); if (inflight) return inflight; inflight = withTimeout(computeStats(timeRange, startDate, endDate, excludeCB), 15000, '[STATS]') .then((data) => { statsCache.set(cacheKey, { data, timestamp: Date.now() }); // Arbitrary custom date ranges would otherwise grow the map forever if (statsCache.size > 100) { const oldest = [...statsCache.entries()] .reduce((a, b) => (a[1].timestamp <= b[1].timestamp ? a : b)); statsCache.delete(oldest[0]); } return data; }) .finally(() => statsInflight.delete(cacheKey)); statsInflight.set(cacheKey, inflight); return inflight; }; try { const cached = statsCache.get(cacheKey); const age = cached ? Date.now() - cached.timestamp : Infinity; if (cached && age >= STATS_CACHE_TTL && age < STATS_CACHE_MAX_STALE) { refresh().catch((err) => console.error('[STATS] Background refresh failed:', err.message)); } if (cached && age < STATS_CACHE_MAX_STALE) { console.log(`[STATS] Cache hit (${Math.round(age / 1000)}s old) for ${cacheKey}`); return res.json(cached.data); } const data = await refresh(); console.log(`[STATS] Computed ${cacheKey} in ${Date.now() - startTime}ms`); res.json(data); } catch (error) { if (error.message.includes('timeout')) { console.log(`[STATS] Request timed out in ${Date.now() - startTime}ms`); } else { console.error('Error in /stats:', error); } res.status(500).json({ error: error.message }); } }); // Daily details endpoint - replaces /api/klaviyo/events/stats/details router.get('/stats/details', async (req, res) => { let release; try { const { timeRange, startDate, endDate, metric, daily, excludeCherryBox, orderType, eventType } = req.query; const excludeCB = parseBoolParam(excludeCherryBox); const { connection, release: releaseConn } = await getDbConnection(); release = releaseConn; const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate); // Handle special event types (refunds, cancellations) if (eventType === 'PAYMENT_REFUNDED') { // Refunds query - from order_payment table const refundsQuery = ` SELECT DATE(op.payment_date) as date, COUNT(*) as count, ABS(SUM(op.payment_amount)) as total FROM order_payment op JOIN _order o ON op.order_id = o.order_id WHERE op.payment_amount < 0 AND o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace(/date_placed/g, 'op.payment_date')} GROUP BY DATE(op.payment_date) ORDER BY DATE(op.payment_date) `; const [refundResults] = await connection.execute(refundsQuery, params); // Format matches what frontend expects: day.refunds.total, day.refunds.count const stats = refundResults.map(day => ({ timestamp: day.date, date: day.date, refunds: { total: parseFloat(day.total || 0), count: parseInt(day.count || 0), reasons: {} } })); if (release) release(); return res.json({ stats }); } if (eventType === 'CANCELED_ORDER') { // Cancellations query - uses date_cancelled to show when orders were actually cancelled const cancelQuery = ` SELECT DATE(date_cancelled) as date, COUNT(*) as count, SUM(summary_total) as total FROM _order WHERE order_status = 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause.replace(/date_placed/g, 'date_cancelled')} GROUP BY DATE(date_cancelled) ORDER BY DATE(date_cancelled) `; const [cancelResults] = await connection.execute(cancelQuery, params); // Format matches what frontend expects: day.canceledOrders.total, day.canceledOrders.count const stats = cancelResults.map(day => ({ timestamp: day.date, date: day.date, canceledOrders: { total: parseFloat(day.total || 0), count: parseInt(day.count || 0), reasons: {} } })); if (release) release(); return res.json({ stats }); } if (eventType === 'PLACED_ORDER') { // Order range query - daily min/max/average order values const orderRangeQuery = ` SELECT DATE(date_placed) as date, COUNT(*) as orders, MIN(CASE WHEN summary_total > 0 THEN summary_total END) as smallest, MAX(summary_total) as largest, AVG(summary_total) as averageOrderValue FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause} GROUP BY DATE(date_placed) ORDER BY DATE(date_placed) `; const [orderRangeResults] = await connection.execute(orderRangeQuery, params); // Format matches what frontend OrderRangeDetails expects const stats = orderRangeResults.map(day => ({ timestamp: day.date, date: day.date, orders: parseInt(day.orders || 0), orderValueRange: { smallest: parseFloat(day.smallest || 0), largest: parseFloat(day.largest || 0) }, averageOrderValue: parseFloat(day.averageOrderValue || 0) })); if (release) release(); return res.json({ stats }); } // Build order type filter based on orderType parameter let orderTypeFilter = ''; if (orderType === 'pre_orders') { orderTypeFilter = 'AND stats_waiting_preorder > 0'; } else if (orderType === 'local_pickup') { orderTypeFilter = "AND ship_method_selected = 'localpickup'"; } else if (orderType === 'on_hold') { orderTypeFilter = "AND ship_method_selected = 'holdit'"; } // Daily breakdown query (optionally excludes Cherry Box orders) const dailyQuery = ` SELECT DATE(date_placed) as date, COUNT(*) as orders, SUM(summary_total) as revenue, AVG(summary_total) as averageOrderValue, SUM(stats_prod_pieces) as itemCount FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause} ${orderTypeFilter} GROUP BY DATE(date_placed) ORDER BY DATE(date_placed) `; const [dailyResults] = await connection.execute(dailyQuery, params); // Get previous period data using the same logic as main stats endpoint let prevWhereClause, prevParams; if (timeRange && timeRange !== 'custom') { const prevTimeRange = getPreviousTimeRange(timeRange); const result = getTimeRangeConditions(prevTimeRange); prevWhereClause = result.whereClause; prevParams = result.params; } else { // Custom date range - go back by the same duration (business-day aware) const prev = previousRange(customRange(startDate, endDate)); prevWhereClause = 'date_placed >= ? AND date_placed < ?'; prevParams = [toMySqlBound(prev.start), toMySqlBound(prev.end)]; } // Get previous period daily data (optionally excludes Cherry Box orders) const prevQuery = ` SELECT DATE(date_placed) as date, COUNT(*) as prevOrders, SUM(summary_total) as prevRevenue, AVG(summary_total) as prevAvgOrderValue FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${prevWhereClause} ${orderTypeFilter} GROUP BY DATE(date_placed) `; const [prevResults] = await connection.execute(prevQuery, prevParams); // Create a map for quick lookup of previous period data // (dateStrings: true — DATE columns arrive as 'YYYY-MM-DD' strings) const prevMap = new Map(); prevResults.forEach(prev => { prevMap.set(String(prev.date), prev); }); // For period-to-period comparison, we need to map days by relative position // since dates won't match exactly (e.g., current week vs previous week) const dailyArray = dailyResults.map(day => ({ timestamp: day.date, date: day.date, orders: parseInt(day.orders), revenue: parseFloat(day.revenue), averageOrderValue: parseFloat(day.averageOrderValue || 0), itemCount: parseInt(day.itemCount) })); const prevArray = prevResults.map(day => ({ orders: parseInt(day.prevOrders), revenue: parseFloat(day.prevRevenue), averageOrderValue: parseFloat(day.prevAvgOrderValue || 0) })); // Combine current and previous period data by matching relative positions const statsWithComparison = dailyArray.map((day, index) => { const prev = prevArray[index] || { orders: 0, revenue: 0, averageOrderValue: 0 }; return { ...day, prevOrders: prev.orders, prevRevenue: prev.revenue, prevAvgOrderValue: prev.averageOrderValue }; }); res.json({ stats: statsWithComparison }); } catch (error) { console.error('Error in /stats/details:', error); res.status(500).json({ error: error.message }); } finally { // Release connection back to pool if (release) release(); } }); // Financial performance endpoint router.get('/financials', async (req, res) => { let release; try { const { timeRange, startDate, endDate, excludeCherryBox } = req.query; const excludeCB = parseBoolParam(excludeCherryBox); const { connection, release: releaseConn } = await getDbConnection(); release = releaseConn; const { whereClause, params, dateRange } = getTimeRangeConditions(timeRange, startDate, endDate); const financialWhere = whereClause.replace(/date_placed/g, 'date_change'); const formatDebugBound = (value) => { if (!value) return 'n/a'; const parsed = parseMySql(value); if (!parsed) { return `invalid(${value})`; } return parsed.setZone(TIMEZONE).toISO(); }; console.log('[FINANCIALS] request params', { timeRange: timeRange || 'default', startDate, endDate, whereClause: financialWhere, params, boundsEastern: Array.isArray(params) ? params.map(formatDebugBound) : [], }); const [totalsRows] = await connection.execute( buildFinancialTotalsQuery(financialWhere, excludeCB), params ); const totals = normalizeFinancialTotals(totalsRows[0]); console.log('[FINANCIALS] totals query result', { rows: totalsRows.length, totals, }); const [trendRows] = await connection.execute( buildFinancialTrendQuery(financialWhere, excludeCB), params ); const trend = trendRows.map(normalizeFinancialTrendRow); console.log('[FINANCIALS] trend query result', { rows: trendRows.length, first: trend[0] || null, last: trend[trend.length - 1] || null, }); let previousTotals = null; let comparison = null; const previousRange = getPreviousPeriodRange(timeRange, startDate, endDate); if (previousRange) { console.log('[FINANCIALS] previous range params', { timeRange: timeRange || 'default', prevWhere: previousRange.whereClause.replace(/date_placed/g, 'date_change'), params: previousRange.params, boundsEastern: Array.isArray(previousRange.params) ? previousRange.params.map(formatDebugBound) : [], }); const prevWhere = previousRange.whereClause.replace(/date_placed/g, 'date_change'); const [previousRows] = await connection.execute( buildFinancialTotalsQuery(prevWhere, excludeCB), previousRange.params ); previousTotals = normalizeFinancialTotals(previousRows[0]); comparison = { grossSales: calculateComparison(totals.grossSales, previousTotals.grossSales), refunds: calculateComparison(totals.refunds, previousTotals.refunds), taxCollected: calculateComparison(totals.taxCollected, previousTotals.taxCollected), discounts: calculateComparison(totals.discounts, previousTotals.discounts), cogs: calculateComparison(totals.cogs, previousTotals.cogs), income: calculateComparison(totals.income, previousTotals.income), profit: calculateComparison(totals.profit, previousTotals.profit), margin: calculateComparison(totals.margin, previousTotals.margin), }; } const trendDebugSample = trend.slice(-3).map((item) => ({ date: item.date, timestamp: item.timestamp, income: item.income, grossSales: item.grossSales, })); const debugInfo = { serverTimeUtc: new Date().toISOString(), timeRange: timeRange || 'default', params, boundsEastern: Array.isArray(params) ? params.map(formatDebugBound) : [], trendCount: trend.length, trendSample: trendDebugSample, previousRange: previousRange ? { params: previousRange.params, boundsEastern: Array.isArray(previousRange.params) ? previousRange.params.map(formatDebugBound) : [], } : null, }; res.json({ dateRange, totals, previousTotals, comparison, trend, debug: debugInfo, }); } catch (error) { console.error('Error in /financials:', error); res.status(500).json({ error: error.message }); } finally { if (release) release(); } }); // Products endpoint - replaces /api/klaviyo/events/products router.get('/products', async (req, res) => { let release; try { const { timeRange, startDate, endDate, excludeCherryBox } = req.query; const excludeCB = parseBoolParam(excludeCherryBox); const { connection, release: releaseConn } = await getDbConnection(); release = releaseConn; const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate); // Products query (optionally excludes Cherry Box orders) const productsQuery = ` SELECT p.pid, p.description as name, SUM(oi.qty_ordered) as totalQuantity, SUM(oi.qty_ordered * oi.prod_price) as totalRevenue, COUNT(DISTINCT oi.order_id) as orderCount, (SELECT pi.iid FROM product_images pi WHERE pi.pid = p.pid AND pi.order = 255 LIMIT 1) as primary_iid FROM order_items oi JOIN _order o ON oi.order_id = o.order_id JOIN products p ON oi.prod_pid = p.pid WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace(/date_placed/g, 'o.date_placed')} GROUP BY p.pid, p.description ORDER BY totalRevenue DESC LIMIT 500 `; const [productsResult] = await connection.execute(productsQuery, params); // Add image URLs to each product const productsWithImages = productsResult.map(product => { const imageUrls = getImageUrls(product.pid, product.primary_iid || 1); return { id: product.pid, name: product.name, totalQuantity: parseInt(product.totalQuantity), totalRevenue: parseFloat(product.totalRevenue), orderCount: parseInt(product.orderCount), ...imageUrls }; }); res.json({ stats: { products: { total: productsWithImages.length, list: productsWithImages } } }); } catch (error) { console.error('Error in /products:', error); res.status(500).json({ error: error.message }); } finally { // Release connection back to pool if (release) release(); } }); // Same stale-while-revalidate treatment as /stats: several tunnel round trips // per computation (~800ms), polled every 60s by every dashboard. const projectionCache = new Map(); // cacheKey -> { data, timestamp } const projectionInflight = new Map(); // cacheKey -> Promise async function computeProjection(timeRange, startDate, endDate, excludeCB) { const startTime = Date.now(); const { connection, release } = await getDbConnection(); try { const now = DateTime.now().setZone(TIMEZONE); // Get current period data const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate); // Current period query (optionally excludes Cherry Box orders) const currentQuery = ` SELECT SUM(summary_total) as currentRevenue, COUNT(*) as currentOrders FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause} `; const [currentResult] = await connection.execute(currentQuery, params); const current = currentResult[0]; console.log(`[PROJECTION] Current period data fetched in ${Date.now() - startTime}ms`); // Fetch pattern data in parallel for performance const patternStart = Date.now(); const [hourlyPattern, dayOfWeekPattern, dailyStats] = await Promise.all([ getHourlyRevenuePattern(connection, excludeCB), getDayOfWeekRevenuePattern(connection, excludeCB), getAverageDailyRevenue(connection, excludeCB) ]); console.log(`[PROJECTION] Pattern data fetched in ${Date.now() - patternStart}ms`); // Calculate period progress (for logging/debugging) const periodProgress = calculatePeriodProgress(timeRange); // Calculate pattern-based projection const projection = calculateSmartProjection( timeRange, parseFloat(current.currentRevenue || 0), parseInt(current.currentOrders || 0), periodProgress, hourlyPattern, dayOfWeekPattern, dailyStats, now ); // Add some useful debug info projection.periodProgress = periodProgress; projection.currentRevenue = parseFloat(current.currentRevenue || 0); projection.currentOrders = parseInt(current.currentOrders || 0); console.log(`[PROJECTION] Computed in ${Date.now() - startTime}ms - method: ${projection.method}, projected: $${projection.projectedRevenue?.toFixed(2)}`); return projection; } finally { release(); } } // Projection endpoint - replaces /api/klaviyo/events/projection router.get('/projection', async (req, res) => { const { timeRange, startDate, endDate, excludeCherryBox } = req.query; const excludeCB = parseBoolParam(excludeCherryBox); // Only provide projections for incomplete periods if (!['today', 'thisWeek', 'thisMonth'].includes(timeRange)) { return res.json({ projectedRevenue: 0, confidence: 0, method: 'none' }); } const cacheKey = `${timeRange}|${excludeCB}`; const refresh = () => { let inflight = projectionInflight.get(cacheKey); if (inflight) return inflight; inflight = computeProjection(timeRange, startDate, endDate, excludeCB) .then((data) => { projectionCache.set(cacheKey, { data, timestamp: Date.now() }); return data; }) .finally(() => projectionInflight.delete(cacheKey)); projectionInflight.set(cacheKey, inflight); return inflight; }; try { const cached = projectionCache.get(cacheKey); const age = cached ? Date.now() - cached.timestamp : Infinity; if (cached && age >= STATS_CACHE_TTL && age < STATS_CACHE_MAX_STALE) { refresh().catch((err) => console.error('[PROJECTION] Background refresh failed:', err.message)); } if (cached && age < STATS_CACHE_MAX_STALE) { return res.json(cached.data); } res.json(await refresh()); } catch (error) { console.error('[PROJECTION] Error:', error); res.status(500).json({ error: error.message }); } }); // Debug endpoint to check connection pool status router.get('/debug/pool', (req, res) => { res.json(getPoolStatus()); }); // Health check endpoint router.get('/health', async (req, res) => { try { const { connection, release } = await getDbConnection(); // Simple query to test connection const [result] = await connection.execute('SELECT 1 as test'); release(); res.json({ status: 'healthy', timestamp: new Date().toISOString(), pool: getPoolStatus(), dbTest: result[0] }); } catch (error) { res.status(500).json({ status: 'unhealthy', error: error.message, timestamp: new Date().toISOString(), pool: getPoolStatus() }); } }); // Helper functions function processShippingData(shippingResult, totalShipped) { const countries = {}; const states = {}; const methods = {}; shippingResult.forEach(row => { // Countries if (row.ship_country) { countries[row.ship_country] = (countries[row.ship_country] || 0) + row.count; } // States if (row.ship_state) { states[row.ship_state] = (states[row.ship_state] || 0) + row.count; } // Methods if (row.ship_method_selected) { methods[row.ship_method_selected] = (methods[row.ship_method_selected] || 0) + row.count; } }); return { locations: { total: Object.keys(states).length, // Count of unique states/regions shipped to byCountry: Object.entries(countries) .map(([country, count]) => ({ country, count, percentage: (count / totalShipped) * 100 })) .sort((a, b) => b.count - a.count), byState: Object.entries(states) .map(([state, count]) => ({ state, count, percentage: (count / totalShipped) * 100 })) .sort((a, b) => b.count - a.count) }, methods: Object.entries(methods) .map(([name, value]) => ({ name, value })) .sort((a, b) => b.value - a.value) }; } function calculatePeriodProgress(timeRange) { if (!['today', 'thisWeek', 'thisMonth'].includes(timeRange)) { return 100; } const now = DateTime.now().setZone(TIMEZONE); let range; try { range = timeHelpers.getRangeForTimeRange(timeRange, now); } catch (error) { console.error(`[STATS] Failed to derive range for ${timeRange}:`, error); return 100; } if (!range?.start || !range?.end) { return 100; } const total = range.end.toMillis() - range.start.toMillis(); if (total <= 0) { return 100; } const elapsed = Math.min( Math.max(now.toMillis() - range.start.toMillis(), 0), total ); return Math.min(100, Math.max(0, (elapsed / total) * 100)); } function buildFinancialTotalsQuery(whereClause, excludeCherryBox = false) { // Optionally join to _order to exclude Cherry Box orders if (excludeCherryBox) { return ` SELECT COALESCE(SUM(r.sale_amount), 0) as grossSales, COALESCE(SUM(r.refund_amount), 0) as refunds, COALESCE(SUM(r.shipping_collected_amount + r.small_order_fee_amount + r.rush_fee_amount), 0) as shippingFees, COALESCE(SUM(r.tax_collected_amount), 0) as taxCollected, COALESCE(SUM(r.discount_total_amount), 0) as discounts, COALESCE(SUM(r.cogs_amount), 0) as cogs FROM report_sales_data r JOIN _order o ON r.order_id = o.order_id WHERE ${whereClause.replace(/date_change/g, 'r.date_change')} AND r.action IN (1, 2, 3) AND ${getCherryBoxClauseAliased('o', true)} `; } return ` SELECT COALESCE(SUM(sale_amount), 0) as grossSales, COALESCE(SUM(refund_amount), 0) as refunds, COALESCE(SUM(shipping_collected_amount + small_order_fee_amount + rush_fee_amount), 0) as shippingFees, COALESCE(SUM(tax_collected_amount), 0) as taxCollected, COALESCE(SUM(discount_total_amount), 0) as discounts, COALESCE(SUM(cogs_amount), 0) as cogs FROM report_sales_data WHERE ${whereClause} AND action IN (1, 2, 3) `; } function buildFinancialTrendQuery(whereClause, excludeCherryBox = false) { // date_change stores Central wall-clock literals; business date = the // Central calendar date, so DATE_FORMAT(col) needs no hour shift. // Optionally join to _order to exclude Cherry Box orders if (excludeCherryBox) { return ` SELECT DATE_FORMAT(r.date_change, '%Y-%m-%d') as businessDate, SUM(r.sale_amount) as grossSales, SUM(r.refund_amount) as refunds, SUM(r.shipping_collected_amount + r.small_order_fee_amount + r.rush_fee_amount) as shippingFees, SUM(r.tax_collected_amount) as taxCollected, SUM(r.discount_total_amount) as discounts, SUM(r.cogs_amount) as cogs FROM report_sales_data r JOIN _order o ON r.order_id = o.order_id WHERE ${whereClause.replace(/date_change/g, 'r.date_change')} AND r.action IN (1, 2, 3) AND ${getCherryBoxClauseAliased('o', true)} GROUP BY businessDate ORDER BY businessDate ASC `; } return ` SELECT DATE_FORMAT(date_change, '%Y-%m-%d') as businessDate, SUM(sale_amount) as grossSales, SUM(refund_amount) as refunds, SUM(shipping_collected_amount + small_order_fee_amount + rush_fee_amount) as shippingFees, SUM(tax_collected_amount) as taxCollected, SUM(discount_total_amount) as discounts, SUM(cogs_amount) as cogs FROM report_sales_data WHERE ${whereClause} AND action IN (1, 2, 3) GROUP BY businessDate ORDER BY businessDate ASC `; } function normalizeFinancialTotals(row = {}) { const grossSales = parseFloat(row.grossSales || 0); const refunds = parseFloat(row.refunds || 0); const shippingFees = parseFloat(row.shippingFees || 0); const taxCollected = parseFloat(row.taxCollected || 0); const discounts = parseFloat(row.discounts || 0); const cogs = parseFloat(row.cogs || 0); const productNet = grossSales - refunds - discounts; const income = productNet + shippingFees; const profit = income - cogs; const margin = income !== 0 ? (profit / income) * 100 : 0; return { grossSales, refunds, shippingFees, taxCollected, discounts, cogs, income, profit, margin, }; } function normalizeFinancialTrendRow(row = {}) { const grossSales = parseFloat(row.grossSales || 0); const refunds = parseFloat(row.refunds || 0); const shippingFees = parseFloat(row.shippingFees || 0); const taxCollected = parseFloat(row.taxCollected || 0); const discounts = parseFloat(row.discounts || 0); const cogs = parseFloat(row.cogs || 0); const productNet = grossSales - refunds - discounts; const income = productNet + shippingFees; const profit = income - cogs; const margin = income !== 0 ? (profit / income) * 100 : 0; let timestamp = null; let dateValue = row.businessDate || row.date || null; const resolveBusinessDayStart = (value) => { if (!value) { return null; } let dt; if (value instanceof Date) { dt = DateTime.fromJSDate(value, { zone: TIMEZONE }); } else if (typeof value === 'string') { dt = DateTime.fromISO(value, { zone: TIMEZONE }); if (!dt.isValid) { dt = DateTime.fromSQL(value, { zone: TIMEZONE }); } } if (!dt || !dt.isValid) { return null; } const hour = BUSINESS_DAY_START_HOUR; return dt.set({ hour, minute: 0, second: 0, millisecond: 0, }); }; const businessDayStart = resolveBusinessDayStart(dateValue); if (businessDayStart) { timestamp = businessDayStart.toUTC().toISO(); dateValue = businessDayStart.toISO(); } else if (row.date instanceof Date) { timestamp = new Date(row.date.getTime()).toISOString(); } else if (typeof row.date === 'string') { timestamp = new Date(`${row.date}T00:00:00Z`).toISOString(); } return { date: dateValue, grossSales, refunds, shippingFees, taxCollected, discounts, cogs, income, profit, margin, timestamp, }; } function calculateComparison(currentValue, previousValue) { if (typeof previousValue !== 'number') { return { absolute: null, percentage: null }; } const absolute = typeof currentValue === 'number' ? currentValue - previousValue : null; const percentage = absolute !== null && previousValue !== 0 ? (absolute / Math.abs(previousValue)) * 100 : null; return { absolute, percentage }; } function getPreviousPeriodRange(timeRange, startDate, endDate) { if (timeRange && timeRange !== 'custom') { const prevTimeRange = getPreviousTimeRange(timeRange); if (!prevTimeRange || prevTimeRange === timeRange) { return null; } return getTimeRangeConditions(prevTimeRange); } const hasCustomDates = (timeRange === 'custom' || !timeRange) && startDate && endDate; if (!hasCustomDates) { return null; } try { const prev = previousRange(customRange(startDate, endDate)); if (!prev) return null; return getTimeRangeConditions('custom', prev.start.toISO(), prev.end.toISO()); } catch { return null; } } async function getPreviousPeriodData(connection, timeRange, startDate, endDate, excludeCherryBox = false) { // Calculate previous period dates let prevWhereClause, prevParams; if (timeRange && timeRange !== 'custom') { const prevTimeRange = getPreviousTimeRange(timeRange); const result = getTimeRangeConditions(prevTimeRange); prevWhereClause = result.whereClause; prevParams = result.params; } else { // Custom date range - go back by the same duration (business-day aware) const prev = previousRange(customRange(startDate, endDate)); prevWhereClause = 'date_placed >= ? AND date_placed < ?'; prevParams = [toMySqlBound(prev.start), toMySqlBound(prev.end)]; } // Previous period query (optionally excludes Cherry Box orders) const prevQuery = ` SELECT COUNT(*) as orderCount, SUM(summary_total) as revenue, AVG(summary_total) as averageOrderValue FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND ${prevWhereClause} `; const [prevResult] = await connection.execute(prevQuery, prevParams); const prev = prevResult[0] || { orderCount: 0, revenue: 0, averageOrderValue: 0 }; return { orderCount: parseInt(prev.orderCount || 0), revenue: parseFloat(prev.revenue || 0), averageOrderValue: parseFloat(prev.averageOrderValue || 0) }; } function getPreviousTimeRange(timeRange) { const map = { today: 'yesterday', thisWeek: 'lastWeek', thisMonth: 'lastMonth', last7days: 'previous7days', last30days: 'previous30days', last90days: 'previous90days', yesterday: 'twoDaysAgo' }; return map[timeRange] || timeRange; } /** * Get hourly revenue distribution pattern from last 8 weeks (same day of week) * Returns array of 24 objects with hour and avgShare (0-1 representing % of daily revenue) * Optimized: Uses JOIN instead of correlated subquery for O(n) instead of O(n²) */ async function getHourlyRevenuePattern(connection, excludeCherryBox = false) { const now = DateTime.now().setZone(TIMEZONE); const dayOfWeek = now.weekday; // 1=Monday, 7=Sunday (Luxon) const mysqlDayOfWeek = dayOfWeek === 7 ? 1 : dayOfWeek + 1; // Step 1: Get daily totals and hourly breakdowns in one efficient query const query = ` SELECT hourly.hour_of_day, AVG(hourly.hour_revenue / daily.daily_revenue) as avgShare FROM ( SELECT DATE(date_placed) as order_date, HOUR(date_placed) as hour_of_day, SUM(summary_total) as hour_revenue FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND date_placed >= DATE_SUB(NOW(), INTERVAL 8 WEEK) AND date_placed < DATE(NOW()) AND DAYOFWEEK(date_placed) = ? GROUP BY DATE(date_placed), HOUR(date_placed) ) hourly JOIN ( SELECT DATE(date_placed) as order_date, SUM(summary_total) as daily_revenue FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND date_placed >= DATE_SUB(NOW(), INTERVAL 8 WEEK) AND date_placed < DATE(NOW()) AND DAYOFWEEK(date_placed) = ? GROUP BY DATE(date_placed) HAVING daily_revenue > 0 ) daily ON hourly.order_date = daily.order_date GROUP BY hourly.hour_of_day ORDER BY hourly.hour_of_day `; const [result] = await connection.execute(query, [mysqlDayOfWeek, mysqlDayOfWeek]); // Convert to a full 24-hour array, filling gaps with 0 const hourlyPattern = Array(24).fill(0).map((_, i) => ({ hour: i, avgShare: 0 })); result.forEach(row => { hourlyPattern[row.hour_of_day] = { hour: row.hour_of_day, avgShare: parseFloat(row.avgShare) || 0 }; }); // Normalize so shares sum to 1.0 const totalShare = hourlyPattern.reduce((sum, h) => sum + h.avgShare, 0); if (totalShare > 0) { hourlyPattern.forEach(h => h.avgShare = h.avgShare / totalShare); } return hourlyPattern; } /** * Get day-of-week revenue distribution pattern from last 8 weeks * Returns array of 7 objects with dayOfWeek (1-7, Sunday=1) and avgShare * Optimized: Uses JOIN instead of correlated subquery */ async function getDayOfWeekRevenuePattern(connection, excludeCherryBox = false) { const query = ` SELECT daily.day_of_week, AVG(daily.day_revenue / weekly.weekly_revenue) as avgShare FROM ( SELECT YEARWEEK(date_placed, 0) as year_week, DAYOFWEEK(date_placed) as day_of_week, SUM(summary_total) as day_revenue FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND date_placed >= DATE_SUB(NOW(), INTERVAL 8 WEEK) AND date_placed < DATE(NOW()) GROUP BY YEARWEEK(date_placed, 0), DAYOFWEEK(date_placed) ) daily JOIN ( SELECT YEARWEEK(date_placed, 0) as year_week, SUM(summary_total) as weekly_revenue FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND date_placed >= DATE_SUB(NOW(), INTERVAL 8 WEEK) AND date_placed < DATE(NOW()) GROUP BY YEARWEEK(date_placed, 0) HAVING weekly_revenue > 0 ) weekly ON daily.year_week = weekly.year_week GROUP BY daily.day_of_week ORDER BY daily.day_of_week `; const [result] = await connection.execute(query); // Convert to array indexed by MySQL day of week (1=Sunday, 2=Monday, etc.) const weekPattern = Array(8).fill(0).map((_, i) => ({ dayOfWeek: i, avgShare: 0 })); result.forEach(row => { weekPattern[row.day_of_week] = { dayOfWeek: row.day_of_week, avgShare: parseFloat(row.avgShare) || 0 }; }); // Normalize (indices 1-7 are used, 0 is unused) const totalShare = weekPattern.slice(1).reduce((sum, d) => sum + d.avgShare, 0); if (totalShare > 0) { weekPattern.forEach(d => { if (d.dayOfWeek > 0) d.avgShare = d.avgShare / totalShare; }); } return weekPattern; } /** * Get average daily revenue for projection (last 30 days, excluding today) * Also gets same-day-of-week stats for more accurate confidence calculation */ async function getAverageDailyRevenue(connection, excludeCherryBox = false) { const now = DateTime.now().setZone(TIMEZONE); const mysqlDayOfWeek = now.weekday === 7 ? 1 : now.weekday + 1; // Get both overall 30-day stats AND same-day-of-week stats const query = ` SELECT AVG(daily_revenue) as avgDailyRevenue, STDDEV(daily_revenue) as stdDev, COUNT(*) as dayCount, ( SELECT AVG(day_rev) FROM ( SELECT SUM(summary_total) as day_rev FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND date_placed >= DATE_SUB(NOW(), INTERVAL 8 WEEK) AND date_placed < DATE(NOW()) AND DAYOFWEEK(date_placed) = ${mysqlDayOfWeek} GROUP BY DATE(date_placed) ) same_day ) as sameDayAvg, ( SELECT STDDEV(day_rev) FROM ( SELECT SUM(summary_total) as day_rev FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND date_placed >= DATE_SUB(NOW(), INTERVAL 8 WEEK) AND date_placed < DATE(NOW()) AND DAYOFWEEK(date_placed) = ${mysqlDayOfWeek} GROUP BY DATE(date_placed) ) same_day_std ) as sameDayStdDev, ( SELECT COUNT(*) FROM ( SELECT DATE(date_placed) as d FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND date_placed >= DATE_SUB(NOW(), INTERVAL 8 WEEK) AND date_placed < DATE(NOW()) AND DAYOFWEEK(date_placed) = ${mysqlDayOfWeek} GROUP BY DATE(date_placed) ) same_day_count ) as sameDayCount FROM ( SELECT DATE(date_placed) as order_date, SUM(summary_total) as daily_revenue FROM _order WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND date_placed >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND date_placed < DATE(NOW()) GROUP BY DATE(date_placed) ) daily_totals `; const [result] = await connection.execute(query); const row = result[0] || {}; return { avgDailyRevenue: parseFloat(row.avgDailyRevenue) || 0, stdDev: parseFloat(row.stdDev) || 0, dayCount: parseInt(row.dayCount) || 0, sameDayAvg: parseFloat(row.sameDayAvg) || 0, sameDayStdDev: parseFloat(row.sameDayStdDev) || 0, sameDayCount: parseInt(row.sameDayCount) || 0 }; } /** * Calculate meaningful confidence score based on multiple factors * Returns score between 0-1 and breakdown of contributing factors */ function calculateConfidence({ expectedProgress, currentRevenue, patternProjection, historicalDailyAvg, sameDayStdDev, sameDayCount, stdDev, dayCount }) { const factors = {}; // Factor 1: Time Progress (0-0.3) // More time elapsed = more data = higher confidence // Scales from 0 at 0% to 0.3 at 100% factors.timeProgress = Math.min(0.3, expectedProgress * 0.35); // Factor 2: Historical Predictability via Coefficient of Variation (0-0.35) // CV = stdDev / mean - lower is more predictable // Use same-day-of-week stats if available (more relevant) const relevantStdDev = sameDayStdDev || stdDev || 0; const relevantAvg = historicalDailyAvg || 1; const cv = relevantStdDev / relevantAvg; // CV of 0.1 (10% variation) = very predictable = full points // CV of 0.5 (50% variation) = unpredictable = minimal points // Scale: CV 0.1 -> 0.35, CV 0.3 -> 0.15, CV 0.5+ -> 0.05 if (cv <= 0.1) { factors.predictability = 0.35; } else if (cv <= 0.5) { factors.predictability = Math.max(0.05, 0.35 - (cv - 0.1) * 0.75); } else { factors.predictability = 0.05; } // Factor 3: Tracking Accuracy (0-0.25) // How well is today tracking the expected pattern? // If we're at 40% progress with 38-42% of expected revenue, that's good if (expectedProgress > 0.05 && historicalDailyAvg > 0) { const expectedRevenueSoFar = historicalDailyAvg * expectedProgress; const trackingRatio = currentRevenue / expectedRevenueSoFar; // Perfect tracking (ratio = 1.0) = full points // 20% off (ratio 0.8 or 1.2) = partial points // 50%+ off = minimal points const deviation = Math.abs(1 - trackingRatio); if (deviation <= 0.1) { factors.tracking = 0.25; } else if (deviation <= 0.3) { factors.tracking = 0.25 - (deviation - 0.1) * 0.5; } else if (deviation <= 0.5) { factors.tracking = 0.15 - (deviation - 0.3) * 0.4; } else { factors.tracking = 0.05; } } else { // Not enough progress to judge tracking factors.tracking = 0.1; } // Factor 4: Data Quality (0-0.1) // More historical data points = more reliable pattern const dataPoints = sameDayCount || Math.floor(dayCount / 7) || 0; // 8 weeks of same-day data = full points, less = proportionally less factors.dataQuality = Math.min(0.1, (dataPoints / 8) * 0.1); // Calculate total confidence score const score = Math.min(0.95, Math.max(0.1, factors.timeProgress + factors.predictability + factors.tracking + factors.dataQuality )); return { score, factors }; } /** * Calculate pattern-based projection for different time ranges */ function calculateSmartProjection( timeRange, currentRevenue, currentOrders, periodProgress, hourlyPattern, dayOfWeekPattern, dailyStats, now ) { if (periodProgress >= 100) { return { projectedRevenue: currentRevenue, projectedOrders: currentOrders, confidence: 1.0 }; } const currentHour = now.hour; const currentDayOfWeek = now.weekday === 7 ? 1 : now.weekday + 1; // Convert to MySQL day (1=Sunday) if (timeRange === 'today') { // Calculate expected progress based on hourly pattern // Sum up shares for all hours up to and including current hour let expectedProgress = 0; for (let h = 0; h <= currentHour; h++) { expectedProgress += hourlyPattern[h]?.avgShare || 0; } // Adjust for partial hour (how far through current hour we are) const minuteProgress = now.minute / 60; const currentHourShare = hourlyPattern[currentHour]?.avgShare || 0; expectedProgress = expectedProgress - currentHourShare + (currentHourShare * minuteProgress); // Avoid division by zero and handle edge cases if (expectedProgress <= 0.01) { // Very early in day, use linear projection with low confidence const linearProjection = currentRevenue / Math.max(periodProgress / 100, 0.01); return { projectedRevenue: linearProjection, projectedOrders: Math.round(currentOrders / Math.max(periodProgress / 100, 0.01)), confidence: 0.1, method: 'linear_fallback' }; } const patternProjection = currentRevenue / expectedProgress; // Blend with historical average for stability early in day // Use same-day-of-week average if available, otherwise fall back to overall average const historicalDailyAvg = dailyStats.sameDayAvg || dailyStats.avgDailyRevenue || patternProjection; const actualWeight = Math.pow(expectedProgress, 0.8); // More weight to actual as day progresses const projectedRevenue = (patternProjection * actualWeight) + (historicalDailyAvg * (1 - actualWeight)); // Calculate meaningful confidence based on multiple factors const confidence = calculateConfidence({ expectedProgress, currentRevenue, patternProjection, historicalDailyAvg, sameDayStdDev: dailyStats.sameDayStdDev, sameDayCount: dailyStats.sameDayCount, stdDev: dailyStats.stdDev, dayCount: dailyStats.dayCount }); return { projectedRevenue, projectedOrders: Math.round(currentOrders / expectedProgress), confidence: confidence.score, confidenceFactors: confidence.factors, method: 'hourly_pattern', debug: { expectedProgress, actualWeight, patternProjection, historicalDailyAvg } }; } if (timeRange === 'thisWeek') { // Calculate revenue expected so far this week based on day-of-week pattern // And project remaining days // Days completed so far (Sunday = day 1 in MySQL) // If today is Tuesday (MySQL day 3), completed days are Sunday(1) and Monday(2) let expectedProgressSoFar = 0; for (let d = 1; d < currentDayOfWeek; d++) { expectedProgressSoFar += dayOfWeekPattern[d]?.avgShare || 0; } // Add partial progress through today using hourly pattern let todayExpectedProgress = 0; for (let h = 0; h <= currentHour; h++) { todayExpectedProgress += hourlyPattern[h]?.avgShare || 0; } const minuteProgress = now.minute / 60; const currentHourShare = hourlyPattern[currentHour]?.avgShare || 0; todayExpectedProgress = todayExpectedProgress - currentHourShare + (currentHourShare * minuteProgress); // Add today's partial contribution const todayFullShare = dayOfWeekPattern[currentDayOfWeek]?.avgShare || (1/7); expectedProgressSoFar += todayFullShare * todayExpectedProgress; // Avoid division by zero if (expectedProgressSoFar <= 0.01) { return { projectedRevenue: currentRevenue / Math.max(periodProgress / 100, 0.01), projectedOrders: Math.round(currentOrders / Math.max(periodProgress / 100, 0.01)), confidence: 0.1, method: 'linear_fallback' }; } const projectedWeekRevenue = currentRevenue / expectedProgressSoFar; const projectedWeekOrders = Math.round(currentOrders / expectedProgressSoFar); // Calculate meaningful confidence const historicalWeeklyAvg = dailyStats.avgDailyRevenue * 7; const confidence = calculateConfidence({ expectedProgress: expectedProgressSoFar, currentRevenue, patternProjection: projectedWeekRevenue, historicalDailyAvg: historicalWeeklyAvg, sameDayStdDev: dailyStats.stdDev * Math.sqrt(7), // Approximate weekly stdDev sameDayCount: Math.floor(dailyStats.dayCount / 7), stdDev: dailyStats.stdDev * Math.sqrt(7), dayCount: dailyStats.dayCount }); return { projectedRevenue: projectedWeekRevenue, projectedOrders: projectedWeekOrders, confidence: confidence.score, confidenceFactors: confidence.factors, method: 'weekly_pattern', debug: { expectedProgressSoFar, currentDayOfWeek, todayExpectedProgress } }; } if (timeRange === 'thisMonth') { // For month projection, use days elapsed and average daily revenue const currentDay = now.day; const daysInMonth = now.daysInMonth; // Calculate average daily revenue so far this month const daysElapsed = currentDay - 1; // Full days completed // Add partial progress through today let todayExpectedProgress = 0; for (let h = 0; h <= currentHour; h++) { todayExpectedProgress += hourlyPattern[h]?.avgShare || 0; } const minuteProgress = now.minute / 60; const currentHourShare = hourlyPattern[currentHour]?.avgShare || 0; todayExpectedProgress = todayExpectedProgress - currentHourShare + (currentHourShare * minuteProgress); const effectiveDaysElapsed = daysElapsed + todayExpectedProgress; if (effectiveDaysElapsed <= 0.1) { // Very early in month, use historical average const projectedRevenue = dailyStats.avgDailyRevenue * daysInMonth; return { projectedRevenue, projectedOrders: Math.round(currentOrders / Math.max(periodProgress / 100, 0.01)), confidence: 0.15, method: 'historical_monthly' }; } // Calculate implied daily rate from current data const impliedDailyRate = currentRevenue / effectiveDaysElapsed; // Blend with historical average (more weight to actual data as month progresses) const actualWeight = Math.min(0.9, effectiveDaysElapsed / 10); // Full weight after ~10 days const blendedDailyRate = (impliedDailyRate * actualWeight) + (dailyStats.avgDailyRevenue * (1 - actualWeight)); const projectedMonthRevenue = blendedDailyRate * daysInMonth; const projectedMonthOrders = Math.round((currentOrders / effectiveDaysElapsed) * daysInMonth); // Calculate meaningful confidence const historicalMonthlyAvg = dailyStats.avgDailyRevenue * daysInMonth; const confidence = calculateConfidence({ expectedProgress: effectiveDaysElapsed / daysInMonth, currentRevenue, patternProjection: projectedMonthRevenue, historicalDailyAvg: historicalMonthlyAvg, sameDayStdDev: dailyStats.stdDev * Math.sqrt(daysInMonth), sameDayCount: 1, // Only ~1 month of same-month data typically stdDev: dailyStats.stdDev * Math.sqrt(daysInMonth), dayCount: dailyStats.dayCount }); return { projectedRevenue: projectedMonthRevenue, projectedOrders: projectedMonthOrders, confidence: confidence.score, confidenceFactors: confidence.factors, method: 'monthly_blend', debug: { effectiveDaysElapsed, daysInMonth, impliedDailyRate, blendedDailyRate } }; } // Fallback for any other case const linearProjection = currentRevenue / (periodProgress / 100); return { projectedRevenue: linearProjection, projectedOrders: Math.round(currentOrders / (periodProgress / 100)), confidence: Math.min(0.95, Math.max(0.1, periodProgress / 100)), method: 'linear_fallback' }; } // Health check endpoint router.get('/health', async (req, res) => { try { const poolStatus = getPoolStatus(); // Test database connectivity const { connection, release } = await getDbConnection(); await connection.execute('SELECT 1 as test'); release(); res.json({ status: 'healthy', timestamp: new Date().toISOString(), pool: poolStatus, database: 'connected' }); } catch (error) { console.error('Health check failed:', error); res.status(500).json({ status: 'unhealthy', timestamp: new Date().toISOString(), error: error.message, pool: getPoolStatus() }); } }); // Debug endpoint for pool status router.get('/debug/pool', (req, res) => { res.json({ timestamp: new Date().toISOString(), pool: getPoolStatus() }); }); export default router;