Fix redemption rate part 2

This commit is contained in:
2025-09-25 22:41:44 -04:00
parent d3e3cba087
commit dc774862a7

View File

@@ -235,7 +235,7 @@ router.post('/simulate', async (req, res) => {
connection = dbConn.connection; connection = dbConn.connection;
release = dbConn.release; release = dbConn.release;
const params = [ const filteredOrdersParams = [
shipCountry, shipCountry,
formatDateForSql(startDt), formatDateForSql(startDt),
formatDateForSql(endDt) formatDateForSql(endDt)
@@ -248,14 +248,13 @@ router.post('/simulate', async (req, res) => {
if (promoCodes.length > 0) { if (promoCodes.length > 0) {
const placeholders = promoCodes.map(() => '?').join(','); const placeholders = promoCodes.map(() => '?').join(',');
promoFilterClause = `AND od.discount_code IN (${placeholders})`; promoFilterClause = `AND od.discount_code IN (${placeholders})`;
params.push(...promoCodes); filteredOrdersParams.push(...promoCodes);
} }
params.push(formatDateForSql(startDt), formatDateForSql(endDt));
const filteredOrdersQuery = ` const filteredOrdersQuery = `
SELECT SELECT
o.order_id, o.order_id,
o.order_cid,
o.summary_subtotal, o.summary_subtotal,
o.summary_discount_subtotal, o.summary_discount_subtotal,
o.summary_shipping, o.summary_shipping,
@@ -274,6 +273,12 @@ router.post('/simulate', async (req, res) => {
${promoFilterClause} ${promoFilterClause}
`; `;
const bucketParams = [
...filteredOrdersParams,
formatDateForSql(startDt),
formatDateForSql(endDt)
];
const bucketQuery = ` const bucketQuery = `
SELECT SELECT
f.bucket_key, f.bucket_key,
@@ -310,7 +315,7 @@ router.post('/simulate', async (req, res) => {
GROUP BY f.bucket_key GROUP BY f.bucket_key
`; `;
const [rows] = await connection.execute(bucketQuery, params); const [rows] = await connection.execute(bucketQuery, bucketParams);
const totals = { const totals = {
orders: 0, orders: 0,
@@ -366,57 +371,76 @@ router.post('/simulate', async (req, res) => {
? totals.pointsAwarded / totals.subtotal ? totals.pointsAwarded / totals.subtotal
: 0; : 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; let calculatedRedemptionRate = 0;
if (config.points.redemptionRate != null) { if (config.points.redemptionRate != null) {
calculatedRedemptionRate = config.points.redemptionRate; calculatedRedemptionRate = config.points.redemptionRate;
} else if (totals.pointsAwarded > 0) { } else if (totals.pointsAwarded > 0 && pointDollarValue > 0) {
// Use a 12-month lookback to capture more realistic redemption patterns const extendedEndDt = DateTime.min(
const extendedStartDt = startDt.minus({ months: 12 }); endDt.plus({ months: 12 }),
const extendedRedemptionQuery = ` DateTime.now().endOf('day')
SELECT SUM(od.discount_amount) as extended_redemptions );
FROM order_discounts od
JOIN _order o ON od.order_id = o.order_id const redemptionStatsQuery = `
WHERE od.discount_type = 20 AND od.discount_active = 1 SELECT
AND o.order_status NOT IN (15) SUM(awards.points_awarded) AS total_awarded_points,
AND o.ship_country = ? SUM(
AND o.date_placed BETWEEN ? AND ? LEAST(
AND o.order_cid IN ( awards.points_awarded,
SELECT DISTINCT order_cid COALESCE(redemptions.redemption_amount, 0) / ?
FROM _order )
WHERE date_placed BETWEEN ? AND ? ) AS matched_redeemed_points
AND order_status NOT IN (15) FROM (
AND summary_points > 0 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 redemptionStatsParams = [
const [extendedRows] = await connection.execute(extendedRedemptionQuery, [ pointDollarValue,
shipCountry, ...filteredOrdersParams,
formatDateForSql(extendedStartDt), shipCountry,
formatDateForSql(endDt), formatDateForSql(startDt),
formatDateForSql(startDt), formatDateForSql(extendedEndDt)
formatDateForSql(endDt) ];
]);
const extendedRedemptions = Number(extendedRows[0]?.extended_redemptions || 0); const [redemptionStatsRows] = await connection.execute(redemptionStatsQuery, redemptionStatsParams);
// Convert dollar redemptions to points using the correct conversion rate (200 points = $1) const redemptionStats = redemptionStatsRows[0] || {};
const extendedRedemptionsInPoints = extendedRedemptions * 200; const totalAwardedPoints = Number(redemptionStats.total_awarded_points || 0);
if (extendedRedemptionsInPoints > 0) { const matchedRedeemedPoints = Number(redemptionStats.matched_redeemed_points || 0);
calculatedRedemptionRate = Math.min(1, extendedRedemptionsInPoints / totals.pointsAwarded);
} else { if (totalAwardedPoints > 0 && matchedRedeemedPoints > 0) {
throw new Error('Unable to calculate redemption rate: no redemption data found in extended lookback period'); calculatedRedemptionRate = Math.min(1, matchedRedeemedPoints / totalAwardedPoints);
}
} catch (error) {
console.error('Failed to calculate redemption rate:', error);
throw error; // Let it fail instead of using fallback
} }
} }
const redemptionRate = calculatedRedemptionRate; const redemptionRate = calculatedRedemptionRate;
const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE;
const bucketResults = []; const bucketResults = [];
let weightedProfitAmount = 0; let weightedProfitAmount = 0;
let weightedProfitPercent = 0; let weightedProfitPercent = 0;