Restore removed files
This commit is contained in:
@@ -1,7 +1,24 @@
|
||||
const express = require('express');
|
||||
const { DateTime } = require('luxon');
|
||||
|
||||
const router = express.Router();
|
||||
const { getDbConnection, getPoolStatus } = require('../db/connection');
|
||||
const { getTimeRangeConditions, formatBusinessDate, getBusinessDayBounds } = require('../utils/timeUtils');
|
||||
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;
|
||||
|
||||
// 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) => {
|
||||
@@ -29,14 +46,15 @@ router.get('/stats', async (req, res) => {
|
||||
|
||||
try {
|
||||
const mainOperation = async () => {
|
||||
const { timeRange, startDate, endDate } = req.query;
|
||||
console.log(`[STATS] Getting DB connection...`);
|
||||
const { timeRange, startDate, endDate, excludeCherryBox } = req.query;
|
||||
const excludeCB = parseBoolParam(excludeCherryBox);
|
||||
console.log(`[STATS] Getting DB connection... (excludeCherryBox: ${excludeCB})`);
|
||||
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
|
||||
// Main order stats query (optionally excludes Cherry Box orders)
|
||||
const mainStatsQuery = `
|
||||
SELECT
|
||||
COUNT(*) as orderCount,
|
||||
@@ -51,32 +69,32 @@ router.get('/stats', async (req, res) => {
|
||||
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}
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
|
||||
`;
|
||||
|
||||
const [mainStats] = await connection.execute(mainStatsQuery, params);
|
||||
const stats = mainStats[0];
|
||||
|
||||
// Refunds query
|
||||
// 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 ${whereClause.replace('date_placed', 'o.date_placed')}
|
||||
WHERE payment_amount < 0 AND o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
|
||||
`;
|
||||
|
||||
const [refundStats] = await connection.execute(refundsQuery, params);
|
||||
|
||||
// Best revenue day query
|
||||
// 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 ${whereClause}
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
|
||||
GROUP BY DATE(date_placed)
|
||||
ORDER BY revenue DESC
|
||||
LIMIT 1
|
||||
@@ -84,7 +102,7 @@ router.get('/stats', async (req, res) => {
|
||||
|
||||
const [bestDayResult] = await connection.execute(bestDayQuery, params);
|
||||
|
||||
// Peak hour query (for single day periods)
|
||||
// Peak hour query (for single day periods, optionally excludes Cherry Box orders)
|
||||
let peakHour = null;
|
||||
if (['today', 'yesterday'].includes(timeRange)) {
|
||||
const peakHourQuery = `
|
||||
@@ -92,7 +110,7 @@ router.get('/stats', async (req, res) => {
|
||||
HOUR(date_placed) as hour,
|
||||
COUNT(*) as count
|
||||
FROM _order
|
||||
WHERE order_status > 15 AND ${whereClause}
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
|
||||
GROUP BY HOUR(date_placed)
|
||||
ORDER BY count DESC
|
||||
LIMIT 1
|
||||
@@ -112,7 +130,7 @@ router.get('/stats', async (req, res) => {
|
||||
}
|
||||
|
||||
// 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
|
||||
// We'll use a simple approach without company table for now (optionally excludes Cherry Box orders)
|
||||
const brandsQuery = `
|
||||
SELECT
|
||||
'Various Brands' as brandName,
|
||||
@@ -122,13 +140,13 @@ router.get('/stats', async (req, res) => {
|
||||
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')}
|
||||
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} 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
|
||||
// For categories, we'll use a simplified approach (optionally excludes Cherry Box orders)
|
||||
const categoriesQuery = `
|
||||
SELECT
|
||||
'General' as categoryName,
|
||||
@@ -138,13 +156,13 @@ router.get('/stats', async (req, res) => {
|
||||
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')}
|
||||
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
|
||||
HAVING revenue > 0
|
||||
`;
|
||||
|
||||
const [categoriesResult] = await connection.execute(categoriesQuery, params);
|
||||
|
||||
// Shipping locations query
|
||||
// Shipping locations query (optionally excludes Cherry Box orders)
|
||||
const shippingQuery = `
|
||||
SELECT
|
||||
ship_country,
|
||||
@@ -152,7 +170,7 @@ router.get('/stats', async (req, res) => {
|
||||
ship_method_selected,
|
||||
COUNT(*) as count
|
||||
FROM _order
|
||||
WHERE order_status IN (100, 92) AND ${whereClause}
|
||||
WHERE order_status IN (100, 92) AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
|
||||
GROUP BY ship_country, ship_state, ship_method_selected
|
||||
`;
|
||||
|
||||
@@ -161,13 +179,13 @@ router.get('/stats', async (req, res) => {
|
||||
// Process shipping data
|
||||
const shippingStats = processShippingData(shippingResult, stats.shippedCount);
|
||||
|
||||
// Order value range query
|
||||
// Order value range query (optionally excludes Cherry Box orders)
|
||||
const orderRangeQuery = `
|
||||
SELECT
|
||||
MIN(summary_total) as smallest,
|
||||
MAX(summary_total) as largest
|
||||
FROM _order
|
||||
WHERE order_status > 15 AND ${whereClause}
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
|
||||
`;
|
||||
|
||||
const [orderRangeResult] = await connection.execute(orderRangeQuery, params);
|
||||
@@ -179,7 +197,7 @@ router.get('/stats', async (req, res) => {
|
||||
}
|
||||
|
||||
// Previous period comparison data
|
||||
const prevPeriodData = await getPreviousPeriodData(connection, timeRange, startDate, endDate);
|
||||
const prevPeriodData = await getPreviousPeriodData(connection, timeRange, startDate, endDate, excludeCB);
|
||||
|
||||
const response = {
|
||||
timeRange: dateRange,
|
||||
@@ -306,13 +324,14 @@ router.get('/stats', async (req, res) => {
|
||||
router.get('/stats/details', async (req, res) => {
|
||||
let release;
|
||||
try {
|
||||
const { timeRange, startDate, endDate, metric, daily } = req.query;
|
||||
const { timeRange, startDate, endDate, metric, daily, excludeCherryBox } = req.query;
|
||||
const excludeCB = parseBoolParam(excludeCherryBox);
|
||||
const { connection, release: releaseConn } = await getDbConnection();
|
||||
release = releaseConn;
|
||||
|
||||
const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate);
|
||||
|
||||
// Daily breakdown query
|
||||
// Daily breakdown query (optionally excludes Cherry Box orders)
|
||||
const dailyQuery = `
|
||||
SELECT
|
||||
DATE(date_placed) as date,
|
||||
@@ -321,7 +340,7 @@ router.get('/stats/details', async (req, res) => {
|
||||
AVG(summary_total) as averageOrderValue,
|
||||
SUM(stats_prod_pieces) as itemCount
|
||||
FROM _order
|
||||
WHERE order_status > 15 AND ${whereClause}
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
|
||||
GROUP BY DATE(date_placed)
|
||||
ORDER BY DATE(date_placed)
|
||||
`;
|
||||
@@ -349,7 +368,7 @@ router.get('/stats/details', async (req, res) => {
|
||||
prevParams = [prevStart.toISOString(), prevEnd.toISOString()];
|
||||
}
|
||||
|
||||
// Get previous period daily data
|
||||
// Get previous period daily data (optionally excludes Cherry Box orders)
|
||||
const prevQuery = `
|
||||
SELECT
|
||||
DATE(date_placed) as date,
|
||||
@@ -357,7 +376,7 @@ router.get('/stats/details', async (req, res) => {
|
||||
SUM(summary_total) as prevRevenue,
|
||||
AVG(summary_total) as prevAvgOrderValue
|
||||
FROM _order
|
||||
WHERE order_status > 15 AND ${prevWhereClause}
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${prevWhereClause}
|
||||
GROUP BY DATE(date_placed)
|
||||
`;
|
||||
|
||||
@@ -414,35 +433,73 @@ router.get('/stats/details', async (req, res) => {
|
||||
router.get('/financials', async (req, res) => {
|
||||
let release;
|
||||
try {
|
||||
const { timeRange, startDate, endDate } = req.query;
|
||||
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_SUB(date_change, INTERVAL 1 HOUR)');
|
||||
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),
|
||||
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),
|
||||
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) {
|
||||
const prevWhere = previousRange.whereClause.replace(/date_placed/g, 'DATE_SUB(date_change, INTERVAL 1 HOUR)');
|
||||
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),
|
||||
buildFinancialTotalsQuery(prevWhere, excludeCB),
|
||||
previousRange.params
|
||||
);
|
||||
previousTotals = normalizeFinancialTotals(previousRows[0]);
|
||||
@@ -458,12 +515,37 @@ router.get('/financials', async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -477,12 +559,14 @@ router.get('/financials', async (req, res) => {
|
||||
router.get('/products', async (req, res) => {
|
||||
let release;
|
||||
try {
|
||||
const { timeRange, startDate, endDate } = req.query;
|
||||
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,
|
||||
@@ -494,7 +578,7 @@ router.get('/products', async (req, res) => {
|
||||
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')}
|
||||
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
|
||||
GROUP BY p.pid, p.description
|
||||
ORDER BY totalRevenue DESC
|
||||
LIMIT 500
|
||||
@@ -537,7 +621,8 @@ router.get('/products', async (req, res) => {
|
||||
router.get('/projection', async (req, res) => {
|
||||
let release;
|
||||
try {
|
||||
const { timeRange, startDate, endDate } = req.query;
|
||||
const { timeRange, startDate, endDate, excludeCherryBox } = req.query;
|
||||
const excludeCB = parseBoolParam(excludeCherryBox);
|
||||
|
||||
// Only provide projections for incomplete periods
|
||||
if (!['today', 'thisWeek', 'thisMonth'].includes(timeRange)) {
|
||||
@@ -550,19 +635,20 @@ router.get('/projection', async (req, res) => {
|
||||
// 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 ${whereClause}
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} 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);
|
||||
const historicalQuery = await getHistoricalProjectionData(connection, timeRange, excludeCB);
|
||||
|
||||
// Calculate projection based on current progress and historical patterns
|
||||
const periodProgress = calculatePeriodProgress(timeRange);
|
||||
@@ -662,47 +748,55 @@ function processShippingData(shippingResult, totalShipped) {
|
||||
}
|
||||
|
||||
function calculatePeriodProgress(timeRange) {
|
||||
const now = new Date();
|
||||
const easternTime = new Date(now.getTime() - (5 * 60 * 60 * 1000)); // UTC-5
|
||||
|
||||
switch (timeRange) {
|
||||
case 'today': {
|
||||
const { start } = getBusinessDayBounds('today');
|
||||
const businessStart = new Date(start);
|
||||
const businessEnd = new Date(businessStart);
|
||||
businessEnd.setDate(businessEnd.getDate() + 1);
|
||||
businessEnd.setHours(0, 59, 59, 999); // 12:59 AM next day
|
||||
|
||||
const elapsed = easternTime.getTime() - businessStart.getTime();
|
||||
const total = businessEnd.getTime() - businessStart.getTime();
|
||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
||||
}
|
||||
case 'thisWeek': {
|
||||
const startOfWeek = new Date(easternTime);
|
||||
startOfWeek.setDate(easternTime.getDate() - easternTime.getDay()); // Sunday
|
||||
startOfWeek.setHours(1, 0, 0, 0); // 1 AM business day start
|
||||
|
||||
const endOfWeek = new Date(startOfWeek);
|
||||
endOfWeek.setDate(endOfWeek.getDate() + 7);
|
||||
|
||||
const elapsed = easternTime.getTime() - startOfWeek.getTime();
|
||||
const total = endOfWeek.getTime() - startOfWeek.getTime();
|
||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
||||
}
|
||||
case 'thisMonth': {
|
||||
const startOfMonth = new Date(easternTime.getFullYear(), easternTime.getMonth(), 1, 1, 0, 0, 0);
|
||||
const endOfMonth = new Date(easternTime.getFullYear(), easternTime.getMonth() + 1, 1, 0, 59, 59, 999);
|
||||
|
||||
const elapsed = easternTime.getTime() - startOfMonth.getTime();
|
||||
const total = endOfMonth.getTime() - startOfMonth.getTime();
|
||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
||||
}
|
||||
default:
|
||||
return 100;
|
||||
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) {
|
||||
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,
|
||||
@@ -717,10 +811,37 @@ function buildFinancialTotalsQuery(whereClause) {
|
||||
`;
|
||||
}
|
||||
|
||||
function buildFinancialTrendQuery(whereClause) {
|
||||
function buildFinancialTrendQuery(whereClause, excludeCherryBox = false) {
|
||||
const businessDayOffset = BUSINESS_DAY_START_HOUR;
|
||||
// Optionally join to _order to exclude Cherry Box orders
|
||||
if (excludeCherryBox) {
|
||||
return `
|
||||
SELECT
|
||||
DATE_FORMAT(
|
||||
DATE_SUB(r.date_change, INTERVAL ${businessDayOffset} HOUR),
|
||||
'%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(DATE_SUB(date_change, INTERVAL 1 HOUR)) as date,
|
||||
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,
|
||||
@@ -730,8 +851,8 @@ function buildFinancialTrendQuery(whereClause) {
|
||||
FROM report_sales_data
|
||||
WHERE ${whereClause}
|
||||
AND action IN (1, 2, 3)
|
||||
GROUP BY DATE(DATE_SUB(date_change, INTERVAL 1 HOUR))
|
||||
ORDER BY date ASC
|
||||
GROUP BY businessDate
|
||||
ORDER BY businessDate ASC
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -772,16 +893,44 @@ function normalizeFinancialTrendRow(row = {}) {
|
||||
const profit = income - cogs;
|
||||
const margin = income !== 0 ? (profit / income) * 100 : 0;
|
||||
let timestamp = null;
|
||||
let dateValue = null;
|
||||
let dateValue = row.businessDate || row.date || null;
|
||||
|
||||
if (row.date instanceof Date) {
|
||||
dateValue = row.date.toISOString().slice(0, 10);
|
||||
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') {
|
||||
dateValue = row.date;
|
||||
}
|
||||
|
||||
if (typeof dateValue === 'string') {
|
||||
timestamp = new Date(`${dateValue}T06:00:00.000Z`).toISOString();
|
||||
timestamp = new Date(`${row.date}T00:00:00Z`).toISOString();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -845,7 +994,7 @@ function getPreviousPeriodRange(timeRange, startDate, endDate) {
|
||||
return getTimeRangeConditions('custom', prevStart.toISOString(), prevEnd.toISOString());
|
||||
}
|
||||
|
||||
async function getPreviousPeriodData(connection, timeRange, startDate, endDate) {
|
||||
async function getPreviousPeriodData(connection, timeRange, startDate, endDate, excludeCherryBox = false) {
|
||||
// Calculate previous period dates
|
||||
let prevWhereClause, prevParams;
|
||||
|
||||
@@ -867,13 +1016,14 @@ async function getPreviousPeriodData(connection, timeRange, startDate, endDate)
|
||||
prevParams = [prevStart.toISOString(), prevEnd.toISOString()];
|
||||
}
|
||||
|
||||
// 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 ${prevWhereClause}
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND ${prevWhereClause}
|
||||
`;
|
||||
|
||||
const [prevResult] = await connection.execute(prevQuery, prevParams);
|
||||
@@ -899,8 +1049,8 @@ function getPreviousTimeRange(timeRange) {
|
||||
return map[timeRange] || timeRange;
|
||||
}
|
||||
|
||||
async function getHistoricalProjectionData(connection, timeRange) {
|
||||
// Get historical data for projection calculations
|
||||
async function getHistoricalProjectionData(connection, timeRange, excludeCherryBox = false) {
|
||||
// Get historical data for projection calculations (optionally excludes Cherry Box orders)
|
||||
// This is a simplified version - you could make this more sophisticated
|
||||
const historicalQuery = `
|
||||
SELECT
|
||||
@@ -908,6 +1058,7 @@ async function getHistoricalProjectionData(connection, timeRange) {
|
||||
COUNT(*) as orders
|
||||
FROM _order
|
||||
WHERE order_status > 15
|
||||
AND ${getCherryBoxClause(excludeCherryBox)}
|
||||
AND date_placed >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
AND date_placed < DATE_SUB(NOW(), INTERVAL 1 DAY)
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user