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 const DEFAULTS = { merchantFeePercent: 2.9, fixedCostPerOrder: 1.5, pointsPerDollar: 0, pointsRedemptionRate: 0, 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, 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 params = [ 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})`; params.push(...promoCodes); } params.push(formatDateForSql(startDt), formatDateForSql(endDt)); const filteredOrdersQuery = ` SELECT o.order_id, 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_total > 0 AND o.summary_subtotal > 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 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_points) 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, params); 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 redemptionRate = config.points.redemptionRate != null ? config.points.redemptionRate : totals.pointsAwarded > 0 ? Math.min(1, totals.pointsRedeemed / totals.pointsAwarded) : 0; const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE; 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; const 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 }, 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;