const express = require('express'); const { DateTime } = require('luxon'); const { getDbConnection } = require('../db/connection'); const router = express.Router(); // Bucket boundaries by summary_subtotal (post-item-sale, pre-order-promo). // The final entry is open-ended: all orders >= the last bound land there. 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 ]; const FINAL_BUCKET_KEY = '99999'; function buildRangeDefinitions() { const ranges = []; let previous = 0; for (const bound of RANGE_BOUNDS) { const key = bound.toString().padStart(5, '0'); ranges.push({ min: previous, max: bound, label: `$${previous.toLocaleString()} - $${bound.toLocaleString()}`, key, }); previous = bound; } const lastBound = RANGE_BOUNDS[RANGE_BOUNDS.length - 1]; ranges.push({ min: lastBound, max: null, label: `$${lastBound.toLocaleString()}+`, key: FINAL_BUCKET_KEY, }); return ranges; } const RANGE_DEFINITIONS = buildRangeDefinitions(); function bucketKeyFor(subtotal) { for (const range of RANGE_DEFINITIONS) { if (range.max == null) return range.key; if (subtotal <= range.max) return range.key; } return FINAL_BUCKET_KEY; } const DEFAULT_POINT_DOLLAR_VALUE = 0.005; const DEFAULTS = { merchantFeePercent: 2.9, fixedCostPerOrder: 1.25, 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'); } 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' }); } }); function emptyBucketAccumulator(range) { return { key: range.key, label: range.label, min: range.min, max: range.max, orderCount: 0, sumOrderValue: 0, sumProductDiscountAmount: 0, sumPromoProductDiscount: 0, sumCustomerItemCost: 0, sumShippingChargeBase: 0, sumShippingAfterAuto: 0, sumShipPromoDiscount: 0, sumShippingSurcharge: 0, sumOrderSurcharge: 0, sumCustomerShipCost: 0, sumActualShippingCost: 0, sumTotalRevenue: 0, sumProductCogs: 0, sumMerchantFees: 0, sumPointsCost: 0, sumFixedCosts: 0, sumTotalCosts: 0, sumProfit: 0, }; } function simulateOrder(order, config, derived) { const orderValue = Number(order.summary_subtotal) || 0; const retail = Number(order.summary_subtotal_retail) || orderValue; const productDiscountAmount = Number(order.summary_discount_subtotal) || 0; const pointsRedeemedDollars = Number(order.points_redeemed) || 0; // summary_discount_subtotal is a kitchen-sink rollup that includes points // redemptions (type 20). pointsCost already accrues for points awarded, so // the points portion of historical discount must be excluded here to avoid // double-counting it on orders that redeemed points. const historicalProductDiscountExPoints = Math.max(0, productDiscountAmount - pointsRedeemedDollars); const shippingChargeBase = (Number(order.summary_shipping) || 0) + (Number(order.summary_shipping_rush) || 0); const actualShippingCost = Number(order.ship_method_cost) || 0; const cogs = Number(order.total_cogs) || 0; let promoProductDiscount = 0; if (config.productPromo.type === 'percentage_subtotal' && orderValue >= config.productPromo.minSubtotal) { promoProductDiscount = orderValue * (config.productPromo.value / 100); } else if (config.productPromo.type === 'percentage_regular' && orderValue >= config.productPromo.minSubtotal) { const targetRate = config.productPromo.value / 100; const targetCustomerPrice = retail * (1 - targetRate); promoProductDiscount = Math.max(0, orderValue - targetCustomerPrice); } else if (config.productPromo.type === 'fixed_amount' && orderValue >= config.productPromo.minSubtotal) { promoProductDiscount = config.productPromo.value; } else if (config.productPromo.type === 'none' && config.applyHistoricalProductPromo) { promoProductDiscount = historicalProductDiscountExPoints; } promoProductDiscount = Math.max(0, Math.min(promoProductDiscount, orderValue)); 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); } let shippingSurcharge = 0; let orderSurcharge = 0; for (const surcharge of config.surcharges) { const meetsMin = orderValue >= surcharge.threshold; const meetsMax = surcharge.maxThreshold == null || orderValue < surcharge.maxThreshold; if (meetsMin && meetsMax) { if (surcharge.target === 'shipping') shippingSurcharge += surcharge.amount; else if (surcharge.target === 'order') orderSurcharge += surcharge.amount; } } const customerShipCost = Math.max(0, shippingAfterAuto - shipPromoDiscount + shippingSurcharge); const customerItemCost = Math.max(0, orderValue - promoProductDiscount + orderSurcharge); const totalRevenue = customerItemCost + customerShipCost; const productCogs = config.cogsCalculationMode === 'average' ? orderValue * derived.overallCogsPercentage : cogs; const merchantFees = totalRevenue * (config.merchantFeePercent / 100); const pointsCost = orderValue * derived.pointsPerDollar * derived.redemptionRate * derived.pointDollarValue; const fixedCosts = config.fixedCostPerOrder; const totalCosts = productCogs + actualShippingCost + merchantFees + pointsCost + fixedCosts; const profit = totalRevenue - totalCosts; return { orderValue, productDiscountAmount, promoProductDiscount, customerItemCost, shippingChargeBase, shippingAfterAuto, shipPromoDiscount, shippingSurcharge, orderSurcharge, customerShipCost, actualShippingCost, totalRevenue, productCogs, merchantFees, pointsCost, fixedCosts, totalCosts, profit, }; } function accumulate(bucket, sim) { bucket.orderCount += 1; bucket.sumOrderValue += sim.orderValue; bucket.sumProductDiscountAmount += sim.productDiscountAmount; bucket.sumPromoProductDiscount += sim.promoProductDiscount; bucket.sumCustomerItemCost += sim.customerItemCost; bucket.sumShippingChargeBase += sim.shippingChargeBase; bucket.sumShippingAfterAuto += sim.shippingAfterAuto; bucket.sumShipPromoDiscount += sim.shipPromoDiscount; bucket.sumShippingSurcharge += sim.shippingSurcharge; bucket.sumOrderSurcharge += sim.orderSurcharge; bucket.sumCustomerShipCost += sim.customerShipCost; bucket.sumActualShippingCost += sim.actualShippingCost; bucket.sumTotalRevenue += sim.totalRevenue; bucket.sumProductCogs += sim.productCogs; bucket.sumMerchantFees += sim.merchantFees; bucket.sumPointsCost += sim.pointsCost; bucket.sumFixedCosts += sim.fixedCosts; bucket.sumTotalCosts += sim.totalCosts; bucket.sumProfit += sim.profit; } function finalizeBucket(b, totalOrders) { const n = b.orderCount; const avg = (sum) => (n > 0 ? sum / n : 0); return { key: b.key, label: b.label, min: b.min, max: b.max, orderCount: n, weight: totalOrders > 0 ? n / totalOrders : 0, orderValue: avg(b.sumOrderValue), productDiscountAmount: avg(b.sumProductDiscountAmount), promoProductDiscount: avg(b.sumPromoProductDiscount), customerItemCost: avg(b.sumCustomerItemCost), shippingChargeBase: avg(b.sumShippingChargeBase), shippingAfterAuto: avg(b.sumShippingAfterAuto), shipPromoDiscount: avg(b.sumShipPromoDiscount), shippingSurcharge: avg(b.sumShippingSurcharge), orderSurcharge: avg(b.sumOrderSurcharge), customerShipCost: avg(b.sumCustomerShipCost), actualShippingCost: avg(b.sumActualShippingCost), totalRevenue: avg(b.sumTotalRevenue), productCogs: avg(b.sumProductCogs), merchantFees: avg(b.sumMerchantFees), pointsCost: avg(b.sumPointsCost), fixedCosts: avg(b.sumFixedCosts), totalCosts: avg(b.sumTotalCosts), profit: avg(b.sumProfit), profitPercent: b.sumTotalRevenue > 0 ? b.sumProfit / b.sumTotalRevenue : 0, }; } router.post('/simulate', async (req, res) => { const { dateRange = {}, filters = {}, productPromo = {}, shippingPromo = {}, shippingTiers = [], surcharges = [], merchantFeePercent, fixedCostPerOrder, cogsCalculationMode = 'actual', applyHistoricalProductPromo = false, 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 promoIds = Array.from( new Set( [ ...(Array.isArray(filters.promoIds) ? filters.promoIds : []), ...(Array.isArray(filters.promoCodes) ? filters.promoCodes : []), ] .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, cogsCalculationMode, applyHistoricalProductPromo: applyHistoricalProductPromo === true, 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) : [], surcharges: Array.isArray(surcharges) ? surcharges .map(s => ({ threshold: Number(s.threshold || 0), maxThreshold: typeof s.maxThreshold === 'number' && s.maxThreshold > 0 ? s.maxThreshold : null, target: s.target === 'shipping' || s.target === 'order' ? s.target : 'shipping', amount: Number(s.amount || 0) })) .filter(s => s.threshold >= 0 && s.amount >= 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)]; let promoExistsClause = ''; if (promoIds.length > 0) { const placeholders = promoIds.map(() => '?').join(','); promoExistsClause = ` AND EXISTS ( SELECT 1 FROM order_discounts od WHERE od.order_id = o.order_id AND od.discount_active = 1 AND od.discount_type = 10 AND od.discount_code IN (${placeholders}) ) `; params.push(...promoIds); } const ordersQuery = ` SELECT o.order_id, o.summary_subtotal, COALESCE(o.summary_subtotal_retail, o.summary_subtotal) AS summary_subtotal_retail, COALESCE(o.summary_discount_subtotal, 0) AS summary_discount_subtotal, COALESCE(o.summary_shipping, 0) AS summary_shipping, COALESCE(o.summary_shipping_rush, 0) AS summary_shipping_rush, COALESCE(o.ship_method_cost, 0) AS ship_method_cost, COALESCE(o.summary_points, 0) AS summary_points, COALESCE(c.total_cogs, 0) AS total_cogs, COALESCE(p.points_redeemed, 0) AS points_redeemed FROM _order o LEFT JOIN ( SELECT order_id, SUM(cogs_amount) AS total_cogs FROM report_sales_data WHERE action IN (1,2,3) GROUP BY order_id ) c ON c.order_id = o.order_id LEFT JOIN ( SELECT order_id, SUM(discount_amount_subtotal) AS points_redeemed FROM order_discounts WHERE discount_type = 20 AND discount_active = 1 GROUP BY order_id ) p ON p.order_id = o.order_id WHERE o.summary_total > 0 AND o.order_status >= 20 AND o.ship_method_selected <> 'holdit' AND o.ship_country = ? AND o.date_placed BETWEEN ? AND ? ${promoExistsClause} `; const [orders] = await connection.execute(ordersQuery, params); if (release) { release(); release = null; } let totalSubtotal = 0; let totalProductDiscount = 0; let totalCogs = 0; let totalPointsAwarded = 0; let totalPointsRedeemedDollars = 0; for (const o of orders) { totalSubtotal += Number(o.summary_subtotal) || 0; totalProductDiscount += Number(o.summary_discount_subtotal) || 0; totalCogs += Number(o.total_cogs) || 0; totalPointsAwarded += Number(o.summary_points) || 0; totalPointsRedeemedDollars += Number(o.points_redeemed) || 0; } const productDiscountRate = totalSubtotal > 0 ? totalProductDiscount / totalSubtotal : 0; const overallCogsPercentage = totalSubtotal > 0 ? totalCogs / totalSubtotal : 0; const pointsPerDollar = config.points.pointsPerDollar != null ? config.points.pointsPerDollar : (totalSubtotal > 0 ? totalPointsAwarded / totalSubtotal : 0); const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE; let redemptionRate; if (config.points.redemptionRate != null) { redemptionRate = config.points.redemptionRate; } else if (totalPointsAwarded > 0 && pointDollarValue > 0) { const totalRedeemedPoints = totalPointsRedeemedDollars / pointDollarValue; redemptionRate = Math.min(1, totalRedeemedPoints / totalPointsAwarded); } else { redemptionRate = 0; } const derived = { overallCogsPercentage, pointsPerDollar, redemptionRate, pointDollarValue, }; const buckets = new Map(); for (const range of RANGE_DEFINITIONS) { buckets.set(range.key, emptyBucketAccumulator(range)); } let grandTotalProfit = 0; let grandTotalRevenue = 0; for (const order of orders) { const sim = simulateOrder(order, config, derived); const bucketKey = bucketKeyFor(sim.orderValue); const bucket = buckets.get(bucketKey); accumulate(bucket, sim); grandTotalProfit += sim.profit; grandTotalRevenue += sim.totalRevenue; } const totalOrders = orders.length; const bucketResults = RANGE_DEFINITIONS.map((range) => finalizeBucket(buckets.get(range.key), totalOrders) ); const weightedProfitAmount = totalOrders > 0 ? grandTotalProfit / totalOrders : 0; const weightedProfitPercent = grandTotalRevenue > 0 ? grandTotalProfit / grandTotalRevenue : 0; res.json({ dateRange: { start: startDt.toISO(), end: endDt.toISO() }, totals: { orders: totalOrders, subtotal: totalSubtotal, 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;