const express = require('express'); const { DateTime } = require('luxon'); const router = express.Router(); const { getDbConnection, getPoolStatus } = require('../db/connection'); const { getTimeRangeConditions, formatBusinessDate, getBusinessDayBounds, _internal: timeHelpers } = require('../utils/timeUtils'); const TIMEZONE = 'America/New_York'; const BUSINESS_DAY_START_HOUR = timeHelpers?.BUSINESS_DAY_START_HOUR ?? 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 }; }; // Main stats endpoint - replaces /api/klaviyo/events/stats router.get('/stats', async (req, res) => { const startTime = Date.now(); console.log(`[STATS] Starting request for timeRange: ${req.query.timeRange}`); // Set a timeout for the entire operation const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Request timeout after 15 seconds')), 15000); }); try { const mainOperation = async () => { const { timeRange, startDate, endDate } = req.query; console.log(`[STATS] Getting DB connection...`); const { connection, release } = await getDbConnection(); console.log(`[STATS] DB connection obtained in ${Date.now() - startTime}ms`); const { whereClause, params, dateRange } = getTimeRangeConditions(timeRange, startDate, endDate); // Main order stats query 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, SUM(CASE WHEN order_status IN (100, 92) THEN 1 ELSE 0 END) as shippedCount, SUM(CASE WHEN order_status = 15 THEN 1 ELSE 0 END) as cancelledCount, SUM(CASE WHEN order_status = 15 THEN summary_total ELSE 0 END) as cancelledTotal FROM _order WHERE order_status > 15 AND ${whereClause} `; const [mainStats] = await connection.execute(mainStatsQuery, params); const stats = mainStats[0]; // Refunds query 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 ${whereClause.replace('date_placed', 'o.date_placed')} `; const [refundStats] = await connection.execute(refundsQuery, params); // Best revenue day query const bestDayQuery = ` SELECT DATE(date_placed) as date, SUM(summary_total) as revenue, COUNT(*) as orders FROM _order WHERE order_status > 15 AND ${whereClause} GROUP BY DATE(date_placed) ORDER BY revenue DESC LIMIT 1 `; const [bestDayResult] = await connection.execute(bestDayQuery, params); // Peak hour query (for single day periods) let peakHour = null; if (['today', 'yesterday'].includes(timeRange)) { const peakHourQuery = ` SELECT HOUR(date_placed) as hour, COUNT(*) as count FROM _order WHERE order_status > 15 AND ${whereClause} GROUP BY HOUR(date_placed) ORDER BY count DESC LIMIT 1 `; const [peakHourResult] = await connection.execute(peakHourQuery, params); if (peakHourResult.length > 0) { const hour = peakHourResult[0].hour; const date = new Date(); date.setHours(hour, 0, 0); peakHour = { hour, count: peakHourResult[0].count, displayHour: date.toLocaleString("en-US", { hour: "numeric", hour12: true }) }; } } // Brands and categories query - simplified for now since we don't have the category tables // We'll use a simple approach without company table for now const brandsQuery = ` SELECT 'Various Brands' 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 WHERE o.order_status > 15 AND ${whereClause.replace('date_placed', 'o.date_placed')} HAVING revenue > 0 `; const [brandsResult] = await connection.execute(brandsQuery, params); // For categories, we'll use a simplified approach const categoriesQuery = ` SELECT 'General' 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 WHERE o.order_status > 15 AND ${whereClause.replace('date_placed', 'o.date_placed')} HAVING revenue > 0 `; const [categoriesResult] = await connection.execute(categoriesQuery, params); // Shipping locations query const shippingQuery = ` SELECT ship_country, ship_state, ship_method_selected, COUNT(*) as count FROM _order WHERE order_status IN (100, 92) AND ${whereClause} GROUP BY ship_country, ship_state, ship_method_selected `; const [shippingResult] = await connection.execute(shippingQuery, params); // Process shipping data const shippingStats = processShippingData(shippingResult, stats.shippedCount); // Order value range query const orderRangeQuery = ` SELECT MIN(summary_total) as smallest, MAX(summary_total) as largest FROM _order WHERE order_status > 15 AND ${whereClause} `; const [orderRangeResult] = await connection.execute(orderRangeQuery, params); // Calculate period progress for incomplete periods let periodProgress = 100; if (['today', 'thisWeek', 'thisMonth'].includes(timeRange)) { periodProgress = calculatePeriodProgress(timeRange); } // Previous period comparison data const prevPeriodData = await getPreviousPeriodData(connection, timeRange, startDate, endDate); 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(stats.shippedCount || 0), locations: shippingStats.locations, methodStats: shippingStats.methods }, // Brands and categories brands: { total: brandsResult.length, list: brandsResult.slice(0, 50).map(brand => ({ name: brand.brandName, count: parseInt(brand.itemCount), revenue: parseFloat(brand.revenue) })) }, categories: { total: categoriesResult.length, list: categoriesResult.slice(0, 50).map(category => ({ 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(stats.cancelledTotal || 0), count: parseInt(stats.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, // 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, release }; }; // Race between the main operation and timeout let result; try { result = await Promise.race([mainOperation(), timeoutPromise]); } catch (error) { // If it's a timeout, we don't have a release function to call if (error.message.includes('timeout')) { console.log(`[STATS] Request timed out in ${Date.now() - startTime}ms`); throw error; } // For other errors, re-throw throw error; } const { response, release } = result; // Release connection back to pool if (release) release(); console.log(`[STATS] Request completed in ${Date.now() - startTime}ms`); res.json(response); } catch (error) { console.error('Error in /stats:', error); console.log(`[STATS] Request failed in ${Date.now() - startTime}ms`); 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 } = req.query; const { connection, release: releaseConn } = await getDbConnection(); release = releaseConn; const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate); // Daily breakdown query 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 ${whereClause} 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 const start = new Date(startDate); const end = new Date(endDate); const duration = end.getTime() - start.getTime(); const prevEnd = new Date(start.getTime() - 1); const prevStart = new Date(prevEnd.getTime() - duration); prevWhereClause = 'date_placed >= ? AND date_placed <= ?'; prevParams = [prevStart.toISOString(), prevEnd.toISOString()]; } // Get previous period daily data 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 ${prevWhereClause} GROUP BY DATE(date_placed) `; const [prevResults] = await connection.execute(prevQuery, prevParams); // Create a map for quick lookup of previous period data const prevMap = new Map(); prevResults.forEach(prev => { const key = new Date(prev.date).toISOString().split('T')[0]; prevMap.set(key, 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 } = req.query; 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 = DateTime.fromSQL(value, { zone: 'UTC-05:00' }); if (!parsed.isValid) { 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), params ); const totals = normalizeFinancialTotals(totalsRows[0]); console.log('[FINANCIALS] totals query result', { rows: totalsRows.length, totals, }); const [trendRows] = await connection.execute( buildFinancialTrendQuery(financialWhere), 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), 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 } = req.query; const { connection, release: releaseConn } = await getDbConnection(); release = releaseConn; const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate); 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 ${whereClause.replace('date_placed', '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(); } }); // Projection endpoint - replaces /api/klaviyo/events/projection router.get('/projection', async (req, res) => { let release; try { const { timeRange, startDate, endDate } = req.query; // Only provide projections for incomplete periods if (!['today', 'thisWeek', 'thisMonth'].includes(timeRange)) { return res.json({ projectedRevenue: 0, confidence: 0 }); } const { connection, release: releaseConn } = await getDbConnection(); release = releaseConn; // Get current period data const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate); const currentQuery = ` SELECT SUM(summary_total) as currentRevenue, COUNT(*) as currentOrders FROM _order WHERE order_status > 15 AND ${whereClause} `; const [currentResult] = await connection.execute(currentQuery, params); const current = currentResult[0]; // Get historical data for the same period type const historicalQuery = await getHistoricalProjectionData(connection, timeRange); // Calculate projection based on current progress and historical patterns const periodProgress = calculatePeriodProgress(timeRange); const projection = calculateSmartProjection( parseFloat(current.currentRevenue || 0), parseInt(current.currentOrders || 0), periodProgress, historicalQuery ); res.json(projection); } catch (error) { console.error('Error in /projection:', error); res.status(500).json({ error: error.message }); } finally { // Release connection back to pool if (release) release(); } }); // 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: totalShipped, 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) { 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) { const businessDayOffset = BUSINESS_DAY_START_HOUR; return ` SELECT DATE_FORMAT( DATE_SUB(date_change, INTERVAL ${businessDayOffset} HOUR), '%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; } const start = new Date(startDate); const end = new Date(endDate); if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { return null; } const duration = end.getTime() - start.getTime(); if (!Number.isFinite(duration) || duration <= 0) { return null; } const prevEnd = new Date(start.getTime() - 1); const prevStart = new Date(prevEnd.getTime() - duration); return getTimeRangeConditions('custom', prevStart.toISOString(), prevEnd.toISOString()); } async function getPreviousPeriodData(connection, timeRange, startDate, endDate) { // 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 const start = new Date(startDate); const end = new Date(endDate); const duration = end.getTime() - start.getTime(); const prevEnd = new Date(start.getTime() - 1); const prevStart = new Date(prevEnd.getTime() - duration); prevWhereClause = 'date_placed >= ? AND date_placed <= ?'; prevParams = [prevStart.toISOString(), prevEnd.toISOString()]; } const prevQuery = ` SELECT COUNT(*) as orderCount, SUM(summary_total) as revenue, AVG(summary_total) as averageOrderValue FROM _order WHERE order_status > 15 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; } async function getHistoricalProjectionData(connection, timeRange) { // Get historical data for projection calculations // This is a simplified version - you could make this more sophisticated const historicalQuery = ` SELECT SUM(summary_total) as revenue, COUNT(*) as orders FROM _order WHERE order_status > 15 AND date_placed >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND date_placed < DATE_SUB(NOW(), INTERVAL 1 DAY) `; const [result] = await connection.execute(historicalQuery); return result; } function calculateSmartProjection(currentRevenue, currentOrders, periodProgress, historicalData) { if (periodProgress >= 100) { return { projectedRevenue: currentRevenue, projectedOrders: currentOrders, confidence: 1.0 }; } // Simple linear projection with confidence based on how much of the period has elapsed const projectedRevenue = currentRevenue / (periodProgress / 100); const projectedOrders = Math.round(currentOrders / (periodProgress / 100)); // Confidence increases with more data (higher period progress) const confidence = Math.min(0.95, Math.max(0.1, periodProgress / 100)); return { projectedRevenue, projectedOrders, confidence }; } // 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() }); }); module.exports = router;