From d3e3cba087386ddee572c5f9d507a3ed0a9aca90 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Sep 2025 21:27:28 -0400 Subject: [PATCH] Start fixing points --- .../dashboard/acot-server/routes/discounts.js | 63 ++++++++-- .../discount-simulator/ConfigPanel.tsx | 90 ++++--------- inventory/src/pages/DiscountSimulator.tsx | 118 ++++++++---------- inventory/src/types/discount-simulator.ts | 6 +- 4 files changed, 134 insertions(+), 143 deletions(-) diff --git a/inventory-server/dashboard/acot-server/routes/discounts.js b/inventory-server/dashboard/acot-server/routes/discounts.js index 02c008b..50c4395 100644 --- a/inventory-server/dashboard/acot-server/routes/discounts.js +++ b/inventory-server/dashboard/acot-server/routes/discounts.js @@ -48,13 +48,13 @@ const BUCKET_CASE = (() => { return `CASE\n ${parts.join('\n ')}\n END`; })(); -const DEFAULT_POINT_DOLLAR_VALUE = 0.005; // 1000 points = $5 +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, + pointsRedemptionRate: 0, // Will be calculated from actual data pointDollarValue: DEFAULT_POINT_DOLLAR_VALUE, }; @@ -265,8 +265,8 @@ router.post('/simulate', async (req, res) => { ${BUCKET_CASE} AS bucket_key FROM _order o ${promoJoin} - WHERE o.summary_total > 0 - AND o.summary_subtotal > 0 + 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 = ? @@ -302,7 +302,7 @@ router.post('/simulate', async (req, res) => { 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 + SELECT order_id, SUM(discount_amount) AS points_redeemed FROM order_discounts WHERE discount_type = 20 AND discount_active = 1 GROUP BY order_id @@ -366,11 +366,54 @@ router.post('/simulate', async (req, res) => { ? 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; + // Calculate redemption rate with extended lookback to account for redemption lag + let calculatedRedemptionRate = 0; + if (config.points.redemptionRate != null) { + calculatedRedemptionRate = config.points.redemptionRate; + } else if (totals.pointsAwarded > 0) { + // Use a 12-month lookback to capture more realistic redemption patterns + const extendedStartDt = startDt.minus({ months: 12 }); + const extendedRedemptionQuery = ` + SELECT SUM(od.discount_amount) as extended_redemptions + 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 ? + AND o.order_cid IN ( + SELECT DISTINCT order_cid + FROM _order + WHERE date_placed BETWEEN ? AND ? + AND order_status NOT IN (15) + AND summary_points > 0 + ) + `; + + try { + const [extendedRows] = await connection.execute(extendedRedemptionQuery, [ + shipCountry, + formatDateForSql(extendedStartDt), + formatDateForSql(endDt), + formatDateForSql(startDt), + formatDateForSql(endDt) + ]); + + const extendedRedemptions = Number(extendedRows[0]?.extended_redemptions || 0); + // Convert dollar redemptions to points using the correct conversion rate (200 points = $1) + const extendedRedemptionsInPoints = extendedRedemptions * 200; + if (extendedRedemptionsInPoints > 0) { + calculatedRedemptionRate = Math.min(1, extendedRedemptionsInPoints / totals.pointsAwarded); + } else { + throw new Error('Unable to calculate redemption rate: no redemption data found in extended lookback period'); + } + } catch (error) { + console.error('Failed to calculate redemption rate:', error); + throw error; // Let it fail instead of using fallback + } + } + + const redemptionRate = calculatedRedemptionRate; const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE; diff --git a/inventory/src/components/discount-simulator/ConfigPanel.tsx b/inventory/src/components/discount-simulator/ConfigPanel.tsx index 69249fe..796038c 100644 --- a/inventory/src/components/discount-simulator/ConfigPanel.tsx +++ b/inventory/src/components/discount-simulator/ConfigPanel.tsx @@ -11,7 +11,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { DiscountPromoOption, DiscountPromoType, ShippingPromoType, ShippingTierConfig, DiscountSimulationResponse } from "@/types/discount-simulator"; import { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; -import { formatCurrency, formatNumber } from "@/utils/productUtils"; +import { formatNumber } from "@/utils/productUtils"; import { PlusIcon, X } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; @@ -44,21 +44,13 @@ interface ConfigPanelProps { pointsPerDollar: number; redemptionRate: number; pointDollarValue: number; - onPointsChange: (update: { - pointsPerDollar?: number; - redemptionRate?: number; - pointDollarValue?: number; - }) => void; + onPointDollarValueChange: (value: number) => void; onConfigInputChange: () => void; onResetConfig: () => void; onRunSimulation: () => void; isRunning: boolean; - recommendedPoints?: { - pointsPerDollar: number; - redemptionRate: number; - pointDollarValue: number; - }; - onApplyRecommendedPoints?: () => void; + recommendedPointDollarValue?: number; + onApplyRecommendedPointDollarValue?: () => void; result?: DiscountSimulationResponse; } @@ -116,14 +108,14 @@ export function ConfigPanel({ pointsPerDollar, redemptionRate, pointDollarValue, - onPointsChange, + onPointDollarValueChange, onConfigInputChange, onResetConfig, promoLoading = false, onRunSimulation, isRunning, - recommendedPoints, - onApplyRecommendedPoints, + recommendedPointDollarValue, + onApplyRecommendedPointDollarValue, result }: ConfigPanelProps) { const promoOptions = useMemo(() => { @@ -213,10 +205,8 @@ export function ConfigPanel({ onShippingTiersChange(tiers); }; - const recommendedDiffers = recommendedPoints - ? recommendedPoints.pointsPerDollar !== pointsPerDollar || - recommendedPoints.redemptionRate !== redemptionRate || - recommendedPoints.pointDollarValue !== pointDollarValue + const recommendedDiffers = typeof recommendedPointDollarValue === 'number' + ? recommendedPointDollarValue !== pointDollarValue : false; const fieldClass = "flex flex-col gap-1.5"; @@ -230,6 +220,7 @@ export function ConfigPanel({ const compactWideNumberClass = "h-8 px-2 text-sm"; const showProductAdjustments = productPromo.type !== "none"; const showShippingAdjustments = shippingPromo.type !== "none"; + const promoSelectValue = selectedPromoId != null ? selectedPromoId.toString() : "__all__"; const handleFieldBlur = useCallback(() => { setTimeout(onRunSimulation, 0); @@ -252,14 +243,14 @@ export function ConfigPanel({
{ - onConfigInputChange(); - onPointsChange({ pointsPerDollar: parseNumber(event.target.value, pointsPerDollar) }); - }} - onBlur={handleFieldBlur} - /> +
+
+ Points per $ + {Number.isFinite(pointsPerDollar) ? pointsPerDollar.toFixed(4) : '—'}
-
- - { - onConfigInputChange(); - onPointsChange({ redemptionRate: parseNumber(event.target.value, redemptionRate * 100) / 100 }); - }} - onBlur={handleFieldBlur} - /> +
+ Redemption rate (%) + {formatPercent(redemptionRate)}
@@ -607,15 +569,15 @@ export function ConfigPanel({ value={pointDollarValue} onChange={(event) => { onConfigInputChange(); - onPointsChange({ pointDollarValue: parseNumber(event.target.value, pointDollarValue) }); + onPointDollarValueChange(parseNumber(event.target.value, pointDollarValue)); }} onBlur={handleFieldBlur} />
- {recommendedPoints && ( + {typeof recommendedPointDollarValue === 'number' && (
- {recommendedDiffers && onApplyRecommendedPoints && ( - )} diff --git a/inventory/src/pages/DiscountSimulator.tsx b/inventory/src/pages/DiscountSimulator.tsx index c899a8a..3125bc6 100644 --- a/inventory/src/pages/DiscountSimulator.tsx +++ b/inventory/src/pages/DiscountSimulator.tsx @@ -57,12 +57,8 @@ export function DiscountSimulator() { const [shippingTiers, setShippingTiers] = useState([]); const [merchantFeePercent, setMerchantFeePercent] = useState(DEFAULT_MERCHANT_FEE); const [fixedCostPerOrder, setFixedCostPerOrder] = useState(DEFAULT_FIXED_COST); - const [pointsConfig, setPointsConfig] = useState({ - pointsPerDollar: 0, - redemptionRate: 0, - pointDollarValue: DEFAULT_POINT_VALUE, - }); - const [pointsTouched, setPointsTouched] = useState(false); + const [pointDollarValue, setPointDollarValue] = useState(DEFAULT_POINT_VALUE); + const [pointDollarTouched, setPointDollarTouched] = useState(false); const [simulationResult, setSimulationResult] = useState(undefined); const [isSimulating, setIsSimulating] = useState(false); const [hasLoadedConfig, setHasLoadedConfig] = useState(false); @@ -134,6 +130,12 @@ export function DiscountSimulator() { const createPayload = useCallback((): DiscountSimulationRequest => { const { from, to } = ensureDateRange(dateRange); + const payloadPointsConfig = { + pointsPerDollar: null, + redemptionRate: null, + pointDollarValue, + }; + return { dateRange: { start: startOfDay(from).toISOString(), @@ -153,11 +155,7 @@ export function DiscountSimulator() { }), merchantFeePercent, fixedCostPerOrder, - pointsConfig: { - pointsPerDollar: pointsConfig.pointsPerDollar, - redemptionRate: pointsConfig.redemptionRate, - pointDollarValue: pointsConfig.pointDollarValue, - }, + pointsConfig: payloadPointsConfig, }; }, [ dateRange, @@ -168,7 +166,7 @@ export function DiscountSimulator() { shippingTiers, merchantFeePercent, fixedCostPerOrder, - pointsConfig, + pointDollarValue, ]); const simulationMutation = useMutation< @@ -193,25 +191,14 @@ export function DiscountSimulator() { setSimulationResult(data); - if (!pointsTouched) { - skipAutoRunRef.current = true; - setPointsConfig((prev) => { - if ( - prev.pointsPerDollar === data.totals.pointsPerDollar && - prev.redemptionRate === data.totals.redemptionRate && - prev.pointDollarValue === data.totals.pointDollarValue - ) { - skipAutoRunRef.current = false; - return prev; - } - - return { - ...prev, - pointsPerDollar: data.totals.pointsPerDollar, - redemptionRate: data.totals.redemptionRate, - pointDollarValue: data.totals.pointDollarValue, - }; - }); + if (!pointDollarTouched && typeof data.totals.pointDollarValue === 'number') { + const incomingValue = data.totals.pointDollarValue; + if (pointDollarValue !== incomingValue) { + skipAutoRunRef.current = true; + setPointDollarValue(incomingValue); + } else { + skipAutoRunRef.current = false; + } } }, onError: (error) => { @@ -259,7 +246,12 @@ export function DiscountSimulator() { shippingTiers?: ShippingTierConfig[]; merchantFeePercent?: number; fixedCostPerOrder?: number; - pointsConfig?: typeof pointsConfig; + pointsConfig?: { + pointsPerDollar?: number | null; + redemptionRate?: number | null; + pointDollarValue?: number | null; + }; + pointDollarValue?: number; }; skipAutoRunRef.current = true; @@ -300,9 +292,14 @@ export function DiscountSimulator() { setFixedCostPerOrder(parsed.fixedCostPerOrder); } - if (parsed.pointsConfig) { - setPointsConfig(parsed.pointsConfig); - setPointsTouched(true); + if (parsed.pointsConfig && typeof parsed.pointsConfig.pointDollarValue === 'number') { + setPointDollarValue(parsed.pointsConfig.pointDollarValue); + setPointDollarTouched(true); + } + + if (typeof parsed.pointDollarValue === 'number') { + setPointDollarValue(parsed.pointDollarValue); + setPointDollarTouched(true); } setLoadedFromStorage(true); @@ -331,9 +328,9 @@ export function DiscountSimulator() { shippingTiers, merchantFeePercent, fixedCostPerOrder, - pointsConfig, + pointDollarValue, }); - }, [dateRange, selectedPromoId, productPromo, shippingPromo, shippingTiers, merchantFeePercent, fixedCostPerOrder, pointsConfig]); + }, [dateRange, selectedPromoId, productPromo, shippingPromo, shippingTiers, merchantFeePercent, fixedCostPerOrder, pointDollarValue]); useEffect(() => { if (!hasLoadedConfig) { @@ -379,26 +376,19 @@ export function DiscountSimulator() { return () => window.clearTimeout(timeoutId); }, [loadedFromStorage, runSimulation]); - const recommendedPoints = simulationResult?.totals - ? { - pointsPerDollar: simulationResult.totals.pointsPerDollar, - redemptionRate: simulationResult.totals.redemptionRate, - pointDollarValue: simulationResult.totals.pointDollarValue, - } - : undefined; + const currentPointsPerDollar = simulationResult?.totals?.pointsPerDollar ?? 0; + const currentRedemptionRate = simulationResult?.totals?.redemptionRate ?? 0; + const recommendedPointDollarValue = simulationResult?.totals?.pointDollarValue; - const handlePointsChange = (update: Partial) => { - setPointsTouched(true); - setPointsConfig((prev) => ({ - ...prev, - ...update, - })); + const handlePointDollarValueChange = (value: number) => { + setPointDollarTouched(true); + setPointDollarValue(value); }; - const handleApplyRecommendedPoints = () => { - if (recommendedPoints) { - setPointsConfig(recommendedPoints); - setPointsTouched(true); + const handleApplyRecommendedPointValue = () => { + if (typeof recommendedPointDollarValue === 'number') { + setPointDollarValue(recommendedPointDollarValue); + setPointDollarTouched(true); } }; @@ -413,12 +403,8 @@ export function DiscountSimulator() { setShippingTiers([]); setMerchantFeePercent(DEFAULT_MERCHANT_FEE); setFixedCostPerOrder(DEFAULT_FIXED_COST); - setPointsConfig({ - pointsPerDollar: 0, - redemptionRate: 0, - pointDollarValue: DEFAULT_POINT_VALUE, - }); - setPointsTouched(false); + setPointDollarValue(DEFAULT_POINT_VALUE); + setPointDollarTouched(false); setSimulationResult(undefined); if (typeof window !== 'undefined') { @@ -463,16 +449,16 @@ export function DiscountSimulator() { onMerchantFeeChange={setMerchantFeePercent} fixedCostPerOrder={fixedCostPerOrder} onFixedCostChange={setFixedCostPerOrder} - pointsPerDollar={pointsConfig.pointsPerDollar} - redemptionRate={pointsConfig.redemptionRate} - pointDollarValue={pointsConfig.pointDollarValue} - onPointsChange={handlePointsChange} + pointsPerDollar={currentPointsPerDollar} + redemptionRate={currentRedemptionRate} + pointDollarValue={pointDollarValue} + onPointDollarValueChange={handlePointDollarValueChange} onConfigInputChange={handleConfigInputChange} onResetConfig={resetConfig} onRunSimulation={() => runSimulation()} isRunning={isSimulating} - recommendedPoints={recommendedPoints} - onApplyRecommendedPoints={handleApplyRecommendedPoints} + recommendedPointDollarValue={recommendedPointDollarValue} + onApplyRecommendedPointDollarValue={handleApplyRecommendedPointValue} result={simulationResult} />
diff --git a/inventory/src/types/discount-simulator.ts b/inventory/src/types/discount-simulator.ts index 46ace22..76edc4e 100644 --- a/inventory/src/types/discount-simulator.ts +++ b/inventory/src/types/discount-simulator.ts @@ -90,8 +90,8 @@ export interface DiscountSimulationRequest { merchantFeePercent: number; fixedCostPerOrder: number; pointsConfig: { - pointsPerDollar: number; - redemptionRate: number; - pointDollarValue: number; + pointsPerDollar: number | null; + redemptionRate: number | null; + pointDollarValue: number | null; }; }