Put back files

This commit is contained in:
2025-10-04 16:14:09 -04:00
parent 4953355b91
commit e84c7e568f
166 changed files with 61620 additions and 259 deletions

View File

@@ -0,0 +1,546 @@
const express = require('express');
const { DateTime } = require('luxon');
const { getDbConnection } = require('../db/connection');
const router = express.Router();
const RANGE_BOUNDS = [
10, 20, 30, 40, 50, 60, 70, 80, 90,
100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200,
300, 400, 500, 1000, 1500, 2000
];
const FINAL_BUCKET_KEY = 'PLUS';
function buildRangeDefinitions() {
const ranges = [];
let previous = 0;
for (const bound of RANGE_BOUNDS) {
const label = `$${previous.toLocaleString()} - $${bound.toLocaleString()}`;
const key = bound.toString().padStart(5, '0');
ranges.push({
min: previous,
max: bound,
label,
key,
sort: bound
});
previous = bound;
}
// Remove the 2000+ category - all orders >2000 will go into the 2000 bucket
return ranges;
}
const RANGE_DEFINITIONS = buildRangeDefinitions();
const BUCKET_CASE = (() => {
const parts = [];
for (let i = 0; i < RANGE_BOUNDS.length; i++) {
const bound = RANGE_BOUNDS[i];
const key = bound.toString().padStart(5, '0');
if (i === RANGE_BOUNDS.length - 1) {
// For the last bucket (2000), include all orders >= 1500 (previous bound)
parts.push(`ELSE '${key}'`);
} else {
parts.push(`WHEN o.summary_subtotal <= ${bound} THEN '${key}'`);
}
}
return `CASE\n ${parts.join('\n ')}\n END`;
})();
const DEFAULT_POINT_DOLLAR_VALUE = 0.005; // 1000 points = $5, so 200 points = $1
const DEFAULTS = {
merchantFeePercent: 2.9,
fixedCostPerOrder: 1.5,
pointsPerDollar: 0,
pointsRedemptionRate: 0, // Will be calculated from actual data
pointDollarValue: DEFAULT_POINT_DOLLAR_VALUE,
};
function parseDate(value, fallback) {
if (!value) {
return fallback;
}
const parsed = DateTime.fromISO(value);
if (!parsed.isValid) {
return fallback;
}
return parsed;
}
function formatDateForSql(dt) {
return dt.toFormat('yyyy-LL-dd HH:mm:ss');
}
function getMidpoint(range) {
if (range.max == null) {
return range.min + 200; // Rough estimate for 2000+
}
return (range.min + range.max) / 2;
}
router.get('/promos', async (req, res) => {
let connection;
try {
const { connection: conn, release } = await getDbConnection();
connection = conn;
const releaseConnection = release;
const { startDate, endDate } = req.query || {};
const now = DateTime.now().endOf('day');
const defaultStart = now.minus({ years: 3 }).startOf('day');
const parsedStart = startDate ? parseDate(startDate, defaultStart).startOf('day') : defaultStart;
const parsedEnd = endDate ? parseDate(endDate, now).endOf('day') : now;
const rangeStart = parsedStart <= parsedEnd ? parsedStart : parsedEnd;
const rangeEnd = parsedEnd >= parsedStart ? parsedEnd : parsedStart;
const rangeStartSql = formatDateForSql(rangeStart);
const rangeEndSql = formatDateForSql(rangeEnd);
const sql = `
SELECT
p.promo_id AS id,
p.promo_code AS code,
p.promo_description_online AS description_online,
p.promo_description_private AS description_private,
p.date_start,
p.date_end,
COALESCE(u.usage_count, 0) AS usage_count
FROM promos p
LEFT JOIN (
SELECT
discount_code,
COUNT(DISTINCT order_id) AS usage_count
FROM order_discounts
WHERE discount_type = 10 AND discount_active = 1
GROUP BY discount_code
) u ON u.discount_code = p.promo_id
WHERE p.date_start IS NOT NULL
AND p.date_end IS NOT NULL
AND NOT (p.date_end < ? OR p.date_start > ?)
AND p.store = 1
AND p.date_start >= '2010-01-01'
ORDER BY p.promo_id DESC
LIMIT 200
`;
const [rows] = await connection.execute(sql, [rangeStartSql, rangeEndSql]);
releaseConnection();
const promos = rows.map(row => ({
id: Number(row.id),
code: row.code,
description: row.description_online || row.description_private || '',
privateDescription: row.description_private || '',
promo_description_online: row.description_online || '',
promo_description_private: row.description_private || '',
dateStart: row.date_start,
dateEnd: row.date_end,
usageCount: Number(row.usage_count || 0)
}));
res.json({ promos });
} catch (error) {
if (connection) {
try {
connection.destroy();
} catch (destroyError) {
console.error('Failed to destroy connection after error:', destroyError);
}
}
console.error('Error fetching promos:', error);
res.status(500).json({ error: 'Failed to fetch promos' });
}
});
router.post('/simulate', async (req, res) => {
const {
dateRange = {},
filters = {},
productPromo = {},
shippingPromo = {},
shippingTiers = [],
merchantFeePercent,
fixedCostPerOrder,
cogsCalculationMode = 'actual',
pointsConfig = {}
} = req.body || {};
const endDefault = DateTime.now();
const startDefault = endDefault.minus({ months: 6 });
const startDt = parseDate(dateRange.start, startDefault).startOf('day');
const endDt = parseDate(dateRange.end, endDefault).endOf('day');
const shipCountry = filters.shipCountry || 'US';
const rawPromoFilters = [
...(Array.isArray(filters.promoIds) ? filters.promoIds : []),
...(Array.isArray(filters.promoCodes) ? filters.promoCodes : []),
];
const promoCodes = Array.from(
new Set(
rawPromoFilters
.map((value) => {
if (typeof value === 'string') {
return value.trim();
}
if (typeof value === 'number') {
return String(value);
}
return '';
})
.filter((value) => value.length > 0)
)
);
const config = {
merchantFeePercent: typeof merchantFeePercent === 'number' ? merchantFeePercent : DEFAULTS.merchantFeePercent,
fixedCostPerOrder: typeof fixedCostPerOrder === 'number' ? fixedCostPerOrder : DEFAULTS.fixedCostPerOrder,
productPromo: {
type: productPromo.type || 'none',
value: Number(productPromo.value || 0),
minSubtotal: Number(productPromo.minSubtotal || 0)
},
shippingPromo: {
type: shippingPromo.type || 'none',
value: Number(shippingPromo.value || 0),
minSubtotal: Number(shippingPromo.minSubtotal || 0),
maxDiscount: Number(shippingPromo.maxDiscount || 0)
},
shippingTiers: Array.isArray(shippingTiers)
? shippingTiers
.map(tier => ({
threshold: Number(tier.threshold || 0),
mode: tier.mode === 'percentage' || tier.mode === 'flat' ? tier.mode : 'percentage',
value: Number(tier.value || 0)
}))
.filter(tier => tier.threshold >= 0 && tier.value >= 0)
.sort((a, b) => a.threshold - b.threshold)
: [],
points: {
pointsPerDollar: typeof pointsConfig.pointsPerDollar === 'number' ? pointsConfig.pointsPerDollar : null,
redemptionRate: typeof pointsConfig.redemptionRate === 'number' ? pointsConfig.redemptionRate : null,
pointDollarValue: typeof pointsConfig.pointDollarValue === 'number'
? pointsConfig.pointDollarValue
: DEFAULT_POINT_DOLLAR_VALUE
}
};
let connection;
let release;
try {
const dbConn = await getDbConnection();
connection = dbConn.connection;
release = dbConn.release;
const filteredOrdersParams = [
shipCountry,
formatDateForSql(startDt),
formatDateForSql(endDt)
];
const promoJoin = promoCodes.length > 0
? 'JOIN order_discounts od ON od.order_id = o.order_id AND od.discount_active = 1 AND od.discount_type = 10'
: '';
let promoFilterClause = '';
if (promoCodes.length > 0) {
const placeholders = promoCodes.map(() => '?').join(',');
promoFilterClause = `AND od.discount_code IN (${placeholders})`;
filteredOrdersParams.push(...promoCodes);
}
const filteredOrdersQuery = `
SELECT
o.order_id,
o.order_cid,
o.summary_subtotal,
o.summary_discount_subtotal,
o.summary_shipping,
o.ship_method_rate,
o.ship_method_cost,
o.summary_points,
${BUCKET_CASE} AS bucket_key
FROM _order o
${promoJoin}
WHERE o.summary_shipping > 0
AND o.summary_total > 0
AND o.order_status NOT IN (15)
AND o.ship_method_selected <> 'holdit'
AND o.ship_country = ?
AND o.date_placed BETWEEN ? AND ?
${promoFilterClause}
`;
const bucketParams = [
...filteredOrdersParams,
formatDateForSql(startDt),
formatDateForSql(endDt)
];
const bucketQuery = `
SELECT
f.bucket_key,
COUNT(*) AS order_count,
SUM(f.summary_subtotal) AS subtotal_sum,
SUM(f.summary_discount_subtotal) AS product_discount_sum,
SUM(f.summary_subtotal + f.summary_discount_subtotal) AS regular_subtotal_sum,
SUM(f.ship_method_rate) AS ship_rate_sum,
SUM(f.ship_method_cost) AS ship_cost_sum,
SUM(f.summary_points) AS points_awarded_sum,
SUM(COALESCE(p.points_redeemed, 0)) AS points_redeemed_sum,
SUM(COALESCE(c.total_cogs, 0)) AS cogs_sum,
AVG(f.summary_subtotal) AS avg_subtotal,
AVG(f.summary_discount_subtotal) AS avg_product_discount,
AVG(f.ship_method_rate) AS avg_ship_rate,
AVG(f.ship_method_cost) AS avg_ship_cost,
AVG(COALESCE(c.total_cogs, 0)) AS avg_cogs
FROM (
${filteredOrdersQuery}
) AS f
LEFT JOIN (
SELECT order_id, SUM(cogs_amount) AS total_cogs
FROM report_sales_data
WHERE action IN (1,2,3)
AND date_change BETWEEN ? AND ?
GROUP BY order_id
) AS c ON c.order_id = f.order_id
LEFT JOIN (
SELECT order_id, SUM(discount_amount) AS points_redeemed
FROM order_discounts
WHERE discount_type = 20 AND discount_active = 1
GROUP BY order_id
) AS p ON p.order_id = f.order_id
GROUP BY f.bucket_key
`;
const [rows] = await connection.execute(bucketQuery, bucketParams);
const totals = {
orders: 0,
subtotal: 0,
productDiscount: 0,
regularSubtotal: 0,
shipRate: 0,
shipCost: 0,
cogs: 0,
pointsAwarded: 0,
pointsRedeemed: 0
};
const rowMap = new Map();
for (const row of rows) {
const key = row.bucket_key || FINAL_BUCKET_KEY;
const parsed = {
orderCount: Number(row.order_count || 0),
subtotalSum: Number(row.subtotal_sum || 0),
productDiscountSum: Number(row.product_discount_sum || 0),
regularSubtotalSum: Number(row.regular_subtotal_sum || 0),
shipRateSum: Number(row.ship_rate_sum || 0),
shipCostSum: Number(row.ship_cost_sum || 0),
pointsAwardedSum: Number(row.points_awarded_sum || 0),
pointsRedeemedSum: Number(row.points_redeemed_sum || 0),
cogsSum: Number(row.cogs_sum || 0),
avgSubtotal: Number(row.avg_subtotal || 0),
avgProductDiscount: Number(row.avg_product_discount || 0),
avgShipRate: Number(row.avg_ship_rate || 0),
avgShipCost: Number(row.avg_ship_cost || 0),
avgCogs: Number(row.avg_cogs || 0)
};
rowMap.set(key, parsed);
totals.orders += parsed.orderCount;
totals.subtotal += parsed.subtotalSum;
totals.productDiscount += parsed.productDiscountSum;
totals.regularSubtotal += parsed.regularSubtotalSum;
totals.shipRate += parsed.shipRateSum;
totals.shipCost += parsed.shipCostSum;
totals.cogs += parsed.cogsSum;
totals.pointsAwarded += parsed.pointsAwardedSum;
totals.pointsRedeemed += parsed.pointsRedeemedSum;
}
const productDiscountRate = totals.regularSubtotal > 0
? totals.productDiscount / totals.regularSubtotal
: 0;
const pointsPerDollar = config.points.pointsPerDollar != null
? config.points.pointsPerDollar
: totals.subtotal > 0
? totals.pointsAwarded / totals.subtotal
: 0;
const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE;
// Calculate redemption rate using dollars redeemed from the matched order set
let calculatedRedemptionRate = 0;
if (config.points.redemptionRate != null) {
calculatedRedemptionRate = config.points.redemptionRate;
} else if (totals.pointsAwarded > 0 && pointDollarValue > 0) {
const totalRedeemedPoints = totals.pointsRedeemed / pointDollarValue;
if (totalRedeemedPoints > 0) {
calculatedRedemptionRate = Math.min(1, totalRedeemedPoints / totals.pointsAwarded);
}
}
const redemptionRate = calculatedRedemptionRate;
// Calculate overall average COGS percentage for 'average' mode
let overallCogsPercentage = 0;
if (cogsCalculationMode === 'average' && totals.subtotal > 0) {
overallCogsPercentage = totals.cogs / totals.subtotal;
}
const bucketResults = [];
let weightedProfitAmount = 0;
let weightedProfitPercent = 0;
for (const range of RANGE_DEFINITIONS) {
const data = rowMap.get(range.key) || {
orderCount: 0,
avgSubtotal: 0,
avgShipRate: 0,
avgShipCost: 0,
avgCogs: 0
};
const orderValue = data.avgSubtotal > 0 ? data.avgSubtotal : getMidpoint(range);
const shippingChargeBase = data.avgShipRate > 0 ? data.avgShipRate : 0;
const actualShippingCost = data.avgShipCost > 0 ? data.avgShipCost : 0;
// Calculate COGS based on the selected mode
let productCogs;
if (cogsCalculationMode === 'average') {
// Use overall average COGS percentage applied to this bucket's order value
productCogs = orderValue * overallCogsPercentage;
} else {
// Use actual COGS data from this bucket (existing behavior)
productCogs = data.avgCogs > 0 ? data.avgCogs : 0;
}
const productDiscountAmount = orderValue * productDiscountRate;
const effectiveRegularPrice = productDiscountRate < 0.99
? orderValue / (1 - productDiscountRate)
: orderValue;
let promoProductDiscount = 0;
if (config.productPromo.type === 'percentage_subtotal' && orderValue >= config.productPromo.minSubtotal) {
promoProductDiscount = Math.min(orderValue, (config.productPromo.value / 100) * orderValue);
} else if (config.productPromo.type === 'percentage_regular' && orderValue >= config.productPromo.minSubtotal) {
const targetRate = config.productPromo.value / 100;
const additionalRate = Math.max(0, targetRate - productDiscountRate);
promoProductDiscount = Math.min(orderValue, additionalRate * effectiveRegularPrice);
} else if (config.productPromo.type === 'fixed_amount' && orderValue >= config.productPromo.minSubtotal) {
promoProductDiscount = Math.min(orderValue, config.productPromo.value);
}
let shippingAfterAuto = shippingChargeBase;
for (const tier of config.shippingTiers) {
if (orderValue >= tier.threshold) {
if (tier.mode === 'percentage') {
shippingAfterAuto = shippingChargeBase * Math.max(0, 1 - tier.value / 100);
} else if (tier.mode === 'flat') {
shippingAfterAuto = tier.value;
}
}
}
let shipPromoDiscount = 0;
if (config.shippingPromo.type !== 'none' && orderValue >= config.shippingPromo.minSubtotal) {
if (config.shippingPromo.type === 'percentage') {
shipPromoDiscount = shippingAfterAuto * (config.shippingPromo.value / 100);
} else if (config.shippingPromo.type === 'fixed') {
shipPromoDiscount = config.shippingPromo.value;
}
if (config.shippingPromo.maxDiscount > 0) {
shipPromoDiscount = Math.min(shipPromoDiscount, config.shippingPromo.maxDiscount);
}
shipPromoDiscount = Math.min(shipPromoDiscount, shippingAfterAuto);
}
const customerShipCost = Math.max(0, shippingAfterAuto - shipPromoDiscount);
const customerItemCost = Math.max(0, orderValue - promoProductDiscount);
const totalRevenue = customerItemCost + customerShipCost;
const merchantFees = totalRevenue * (config.merchantFeePercent / 100);
const pointsCost = customerItemCost * pointsPerDollar * redemptionRate * pointDollarValue;
const fixedCosts = config.fixedCostPerOrder;
const totalCosts = productCogs + actualShippingCost + merchantFees + pointsCost + fixedCosts;
const profit = totalRevenue - totalCosts;
const profitPercent = totalRevenue > 0 ? (profit / totalRevenue) : 0;
const weight = totals.orders > 0 ? (data.orderCount || 0) / totals.orders : 0;
weightedProfitAmount += profit * weight;
weightedProfitPercent += profitPercent * weight;
bucketResults.push({
key: range.key,
label: range.label,
min: range.min,
max: range.max,
orderCount: data.orderCount || 0,
weight,
orderValue,
productDiscountAmount,
promoProductDiscount,
customerItemCost,
shippingChargeBase,
shippingAfterAuto,
shipPromoDiscount,
customerShipCost,
actualShippingCost,
totalRevenue,
productCogs,
merchantFees,
pointsCost,
fixedCosts,
totalCosts,
profit,
profitPercent
});
}
if (release) {
release();
}
res.json({
dateRange: {
start: startDt.toISO(),
end: endDt.toISO()
},
totals: {
orders: totals.orders,
subtotal: totals.subtotal,
productDiscountRate,
pointsPerDollar,
redemptionRate,
pointDollarValue,
weightedProfitAmount,
weightedProfitPercent,
overallCogsPercentage: cogsCalculationMode === 'average' ? overallCogsPercentage : undefined
},
buckets: bucketResults
});
} catch (error) {
if (release) {
try {
release();
} catch (releaseError) {
console.error('Failed to release connection after error:', releaseError);
}
} else if (connection) {
try {
connection.destroy();
} catch (destroyError) {
console.error('Failed to destroy connection after error:', destroyError);
}
}
console.error('Error running discount simulation:', error);
res.status(500).json({ error: 'Failed to run discount simulation' });
}
});
module.exports = router;

View File

@@ -1,7 +1,17 @@
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;
// Image URL generation utility
const getImageUrls = (pid, iid = 1) => {
@@ -419,7 +429,25 @@ router.get('/financials', async (req, res) => {
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),
@@ -428,6 +456,11 @@ router.get('/financials', async (req, res) => {
const totals = normalizeFinancialTotals(totalsRows[0]);
console.log('[FINANCIALS] totals query result', {
rows: totalsRows.length,
totals,
});
const [trendRows] = await connection.execute(
buildFinancialTrendQuery(financialWhere),
params
@@ -435,12 +468,26 @@ router.get('/financials', async (req, res) => {
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),
previousRange.params
@@ -458,12 +505,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);
@@ -662,44 +734,35 @@ 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) {
@@ -718,9 +781,13 @@ function buildFinancialTotalsQuery(whereClause) {
}
function buildFinancialTrendQuery(whereClause) {
const businessDayOffset = BUSINESS_DAY_START_HOUR;
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 +797,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 +839,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 {

View File

@@ -0,0 +1,57 @@
const express = require('express');
const router = express.Router();
const { getDbConnection, getCachedQuery } = require('../db/connection');
// Test endpoint to count orders
router.get('/order-count', async (req, res) => {
try {
const { connection } = await getDbConnection();
// Simple query to count orders from _order table
const queryFn = async () => {
const [rows] = await connection.execute('SELECT COUNT(*) as count FROM _order');
return rows[0].count;
};
const cacheKey = 'order-count';
const count = await getCachedQuery(cacheKey, 'default', queryFn);
res.json({
success: true,
data: {
orderCount: count,
timestamp: new Date().toISOString()
}
});
} catch (error) {
console.error('Error fetching order count:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Test connection endpoint
router.get('/test-connection', async (req, res) => {
try {
const { connection } = await getDbConnection();
// Test the connection with a simple query
const [rows] = await connection.execute('SELECT 1 as test');
res.json({
success: true,
message: 'Database connection successful',
data: rows[0]
});
} catch (error) {
console.error('Error testing connection:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
module.exports = router;