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({