From 1696ecf591e52623aec95f3756674688e4416c2c Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 26 Sep 2025 00:11:09 -0400 Subject: [PATCH] Redemption rate part 3 + update cogs options --- .../dashboard/acot-server/routes/discounts.js | 84 +++++-------------- .../discount-simulator/ConfigPanel.tsx | 79 +++++++++++------ inventory/src/pages/DiscountSimulator.tsx | 15 +++- inventory/src/types/discount-simulator.ts | 4 + 4 files changed, 95 insertions(+), 87 deletions(-) diff --git a/inventory-server/dashboard/acot-server/routes/discounts.js b/inventory-server/dashboard/acot-server/routes/discounts.js index e237b13..2ef7a3b 100644 --- a/inventory-server/dashboard/acot-server/routes/discounts.js +++ b/inventory-server/dashboard/acot-server/routes/discounts.js @@ -165,6 +165,7 @@ router.post('/simulate', async (req, res) => { shippingTiers = [], merchantFeePercent, fixedCostPerOrder, + cogsCalculationMode = 'actual', pointsConfig = {} } = req.body || {}; @@ -373,74 +374,25 @@ router.post('/simulate', async (req, res) => { const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE; - // Calculate redemption rate using aggregated award vs redemption pairing per customer + // 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 extendedEndDt = DateTime.min( - endDt.plus({ months: 12 }), - DateTime.now().endOf('day') - ); - - const redemptionStatsQuery = ` - SELECT - SUM(awards.points_awarded) AS total_awarded_points, - SUM( - LEAST( - awards.points_awarded, - COALESCE(redemptions.redemption_amount, 0) / ? - ) - ) AS matched_redeemed_points - FROM ( - SELECT - o.order_cid, - SUM(o.summary_points) AS points_awarded - 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} - GROUP BY o.order_cid - ) AS awards - LEFT JOIN ( - SELECT - o.order_cid, - SUM(od.discount_amount) AS redemption_amount - FROM order_discounts od - JOIN _order o ON od.order_id = o.order_id - WHERE od.discount_type = 20 AND od.discount_active = 1 - AND o.order_status NOT IN (15) - AND o.ship_country = ? - AND o.date_placed BETWEEN ? AND ? - GROUP BY o.order_cid - ) AS redemptions ON redemptions.order_cid = awards.order_cid - `; - - const redemptionStatsParams = [ - pointDollarValue, - ...filteredOrdersParams, - shipCountry, - formatDateForSql(startDt), - formatDateForSql(extendedEndDt) - ]; - - const [redemptionStatsRows] = await connection.execute(redemptionStatsQuery, redemptionStatsParams); - const redemptionStats = redemptionStatsRows[0] || {}; - const totalAwardedPoints = Number(redemptionStats.total_awarded_points || 0); - const matchedRedeemedPoints = Number(redemptionStats.matched_redeemed_points || 0); - - if (totalAwardedPoints > 0 && matchedRedeemedPoints > 0) { - calculatedRedemptionRate = Math.min(1, matchedRedeemedPoints / totalAwardedPoints); + 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; @@ -457,7 +409,16 @@ router.post('/simulate', async (req, res) => { 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; + + // 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) @@ -557,7 +518,8 @@ router.post('/simulate', async (req, res) => { redemptionRate, pointDollarValue, weightedProfitAmount, - weightedProfitPercent + weightedProfitPercent, + overallCogsPercentage: cogsCalculationMode === 'average' ? overallCogsPercentage : undefined }, buckets: bucketResults }); diff --git a/inventory/src/components/discount-simulator/ConfigPanel.tsx b/inventory/src/components/discount-simulator/ConfigPanel.tsx index 796038c..b62598f 100644 --- a/inventory/src/components/discount-simulator/ConfigPanel.tsx +++ b/inventory/src/components/discount-simulator/ConfigPanel.tsx @@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { DiscountPromoOption, DiscountPromoType, ShippingPromoType, ShippingTierConfig, DiscountSimulationResponse } from "@/types/discount-simulator"; +import { DiscountPromoOption, DiscountPromoType, ShippingPromoType, ShippingTierConfig, DiscountSimulationResponse, CogsCalculationMode } from "@/types/discount-simulator"; import { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; import { formatNumber } from "@/utils/productUtils"; @@ -41,6 +41,8 @@ interface ConfigPanelProps { onMerchantFeeChange: (value: number) => void; fixedCostPerOrder: number; onFixedCostChange: (value: number) => void; + cogsCalculationMode: CogsCalculationMode; + onCogsCalculationModeChange: (mode: CogsCalculationMode) => void; pointsPerDollar: number; redemptionRate: number; pointDollarValue: number; @@ -105,6 +107,8 @@ export function ConfigPanel({ onMerchantFeeChange, fixedCostPerOrder, onFixedCostChange, + cogsCalculationMode, + onCogsCalculationModeChange, pointsPerDollar, redemptionRate, pointDollarValue, @@ -288,6 +292,11 @@ export function ConfigPanel({ {formatPercent(result.totals.productDiscountRate)} avg discount + {result.totals.overallCogsPercentage != null && ( + + {formatPercent(result.totals.overallCogsPercentage)} avg COGS + + )} )} @@ -515,34 +524,54 @@ export function ConfigPanel({
Order costs -
+
- - { + +
-
- - { - onConfigInputChange(); - onFixedCostChange(parseNumber(event.target.value, fixedCostPerOrder)); - }} - onBlur={handleFieldBlur} - /> +
+
+ + { + onConfigInputChange(); + onMerchantFeeChange(parseNumber(event.target.value, merchantFeePercent)); + }} + onBlur={handleFieldBlur} + /> +
+
+ + { + onConfigInputChange(); + onFixedCostChange(parseNumber(event.target.value, fixedCostPerOrder)); + }} + onBlur={handleFieldBlur} + /> +
diff --git a/inventory/src/pages/DiscountSimulator.tsx b/inventory/src/pages/DiscountSimulator.tsx index 3125bc6..43b1c44 100644 --- a/inventory/src/pages/DiscountSimulator.tsx +++ b/inventory/src/pages/DiscountSimulator.tsx @@ -16,6 +16,7 @@ import { DiscountPromoType, ShippingPromoType, ShippingTierConfig, + CogsCalculationMode, } from "@/types/discount-simulator"; import { useToast } from "@/hooks/use-toast"; @@ -57,6 +58,7 @@ export function DiscountSimulator() { const [shippingTiers, setShippingTiers] = useState([]); const [merchantFeePercent, setMerchantFeePercent] = useState(DEFAULT_MERCHANT_FEE); const [fixedCostPerOrder, setFixedCostPerOrder] = useState(DEFAULT_FIXED_COST); + const [cogsCalculationMode, setCogsCalculationMode] = useState('actual'); const [pointDollarValue, setPointDollarValue] = useState(DEFAULT_POINT_VALUE); const [pointDollarTouched, setPointDollarTouched] = useState(false); const [simulationResult, setSimulationResult] = useState(undefined); @@ -155,6 +157,7 @@ export function DiscountSimulator() { }), merchantFeePercent, fixedCostPerOrder, + cogsCalculationMode, pointsConfig: payloadPointsConfig, }; }, [ @@ -166,6 +169,7 @@ export function DiscountSimulator() { shippingTiers, merchantFeePercent, fixedCostPerOrder, + cogsCalculationMode, pointDollarValue, ]); @@ -246,6 +250,7 @@ export function DiscountSimulator() { shippingTiers?: ShippingTierConfig[]; merchantFeePercent?: number; fixedCostPerOrder?: number; + cogsCalculationMode?: CogsCalculationMode; pointsConfig?: { pointsPerDollar?: number | null; redemptionRate?: number | null; @@ -292,6 +297,10 @@ export function DiscountSimulator() { setFixedCostPerOrder(parsed.fixedCostPerOrder); } + if (parsed.cogsCalculationMode === 'actual' || parsed.cogsCalculationMode === 'average') { + setCogsCalculationMode(parsed.cogsCalculationMode); + } + if (parsed.pointsConfig && typeof parsed.pointsConfig.pointDollarValue === 'number') { setPointDollarValue(parsed.pointsConfig.pointDollarValue); setPointDollarTouched(true); @@ -328,9 +337,10 @@ export function DiscountSimulator() { shippingTiers, merchantFeePercent, fixedCostPerOrder, + cogsCalculationMode, pointDollarValue, }); - }, [dateRange, selectedPromoId, productPromo, shippingPromo, shippingTiers, merchantFeePercent, fixedCostPerOrder, pointDollarValue]); + }, [dateRange, selectedPromoId, productPromo, shippingPromo, shippingTiers, merchantFeePercent, fixedCostPerOrder, cogsCalculationMode, pointDollarValue]); useEffect(() => { if (!hasLoadedConfig) { @@ -403,6 +413,7 @@ export function DiscountSimulator() { setShippingTiers([]); setMerchantFeePercent(DEFAULT_MERCHANT_FEE); setFixedCostPerOrder(DEFAULT_FIXED_COST); + setCogsCalculationMode('actual'); setPointDollarValue(DEFAULT_POINT_VALUE); setPointDollarTouched(false); setSimulationResult(undefined); @@ -449,6 +460,8 @@ export function DiscountSimulator() { onMerchantFeeChange={setMerchantFeePercent} fixedCostPerOrder={fixedCostPerOrder} onFixedCostChange={setFixedCostPerOrder} + cogsCalculationMode={cogsCalculationMode} + onCogsCalculationModeChange={setCogsCalculationMode} pointsPerDollar={currentPointsPerDollar} redemptionRate={currentRedemptionRate} pointDollarValue={pointDollarValue} diff --git a/inventory/src/types/discount-simulator.ts b/inventory/src/types/discount-simulator.ts index 76edc4e..a70811a 100644 --- a/inventory/src/types/discount-simulator.ts +++ b/inventory/src/types/discount-simulator.ts @@ -54,6 +54,7 @@ export interface DiscountSimulationTotals { pointDollarValue: number; weightedProfitAmount: number; weightedProfitPercent: number; + overallCogsPercentage?: number; } export interface DiscountSimulationResponse { @@ -65,6 +66,8 @@ export interface DiscountSimulationResponse { buckets: DiscountSimulationBucket[]; } +export type CogsCalculationMode = 'actual' | 'average'; + export interface DiscountSimulationRequest { dateRange: { start: string; @@ -89,6 +92,7 @@ export interface DiscountSimulationRequest { shippingTiers: ShippingTierConfig[]; merchantFeePercent: number; fixedCostPerOrder: number; + cogsCalculationMode: CogsCalculationMode; pointsConfig: { pointsPerDollar: number | null; redemptionRate: number | null;