From dc774862a739149d9a0991dc3677c3896bcd8d10 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Sep 2025 22:41:44 -0400 Subject: [PATCH] Fix redemption rate part 2 --- .../dashboard/acot-server/routes/discounts.js | 120 +++++++++++------- 1 file changed, 72 insertions(+), 48 deletions(-) diff --git a/inventory-server/dashboard/acot-server/routes/discounts.js b/inventory-server/dashboard/acot-server/routes/discounts.js index 50c4395..e237b13 100644 --- a/inventory-server/dashboard/acot-server/routes/discounts.js +++ b/inventory-server/dashboard/acot-server/routes/discounts.js @@ -235,7 +235,7 @@ router.post('/simulate', async (req, res) => { connection = dbConn.connection; release = dbConn.release; - const params = [ + const filteredOrdersParams = [ shipCountry, formatDateForSql(startDt), formatDateForSql(endDt) @@ -248,14 +248,13 @@ router.post('/simulate', async (req, res) => { if (promoCodes.length > 0) { const placeholders = promoCodes.map(() => '?').join(','); promoFilterClause = `AND od.discount_code IN (${placeholders})`; - params.push(...promoCodes); + filteredOrdersParams.push(...promoCodes); } - params.push(formatDateForSql(startDt), formatDateForSql(endDt)); - const filteredOrdersQuery = ` SELECT o.order_id, + o.order_cid, o.summary_subtotal, o.summary_discount_subtotal, o.summary_shipping, @@ -274,6 +273,12 @@ router.post('/simulate', async (req, res) => { ${promoFilterClause} `; + const bucketParams = [ + ...filteredOrdersParams, + formatDateForSql(startDt), + formatDateForSql(endDt) + ]; + const bucketQuery = ` SELECT f.bucket_key, @@ -310,7 +315,7 @@ router.post('/simulate', async (req, res) => { GROUP BY f.bucket_key `; - const [rows] = await connection.execute(bucketQuery, params); + const [rows] = await connection.execute(bucketQuery, bucketParams); const totals = { orders: 0, @@ -366,56 +371,75 @@ router.post('/simulate', async (req, res) => { ? totals.pointsAwarded / totals.subtotal : 0; - // Calculate redemption rate with extended lookback to account for redemption lag + const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE; + + // Calculate redemption rate using aggregated award vs redemption pairing per customer 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 - ) + } 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 `; - - 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 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 redemptionRate = calculatedRedemptionRate; - const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE; + const redemptionRate = calculatedRedemptionRate; const bucketResults = []; let weightedProfitAmount = 0;