Fix redemption rate part 2
This commit is contained in:
@@ -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
|
);
|
||||||
|
|
||||||
|
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
|
FROM order_discounts od
|
||||||
JOIN _order o ON od.order_id = o.order_id
|
JOIN _order o ON od.order_id = o.order_id
|
||||||
WHERE od.discount_type = 20 AND od.discount_active = 1
|
WHERE od.discount_type = 20 AND od.discount_active = 1
|
||||||
AND o.order_status NOT IN (15)
|
AND o.order_status NOT IN (15)
|
||||||
AND o.ship_country = ?
|
AND o.ship_country = ?
|
||||||
AND o.date_placed BETWEEN ? AND ?
|
AND o.date_placed BETWEEN ? AND ?
|
||||||
AND o.order_cid IN (
|
GROUP BY o.order_cid
|
||||||
SELECT DISTINCT order_cid
|
) AS redemptions ON redemptions.order_cid = awards.order_cid
|
||||||
FROM _order
|
|
||||||
WHERE date_placed BETWEEN ? AND ?
|
|
||||||
AND order_status NOT IN (15)
|
|
||||||
AND summary_points > 0
|
|
||||||
)
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
const redemptionStatsParams = [
|
||||||
const [extendedRows] = await connection.execute(extendedRedemptionQuery, [
|
pointDollarValue,
|
||||||
|
...filteredOrdersParams,
|
||||||
shipCountry,
|
shipCountry,
|
||||||
formatDateForSql(extendedStartDt),
|
|
||||||
formatDateForSql(endDt),
|
|
||||||
formatDateForSql(startDt),
|
formatDateForSql(startDt),
|
||||||
formatDateForSql(endDt)
|
formatDateForSql(extendedEndDt)
|
||||||
]);
|
];
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user