Use recent data for purchase hour pattern
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m29s

This commit is contained in:
Cauê Faleiros
2026-07-01 13:17:05 -03:00
parent 57fd48ccc5
commit 27d2980b32
5 changed files with 82 additions and 21 deletions

View File

@@ -2,6 +2,7 @@ const crypto = require('node:crypto');
const { pool } = require('../db');
const RFM_QUERY_TIMEOUT_MS = 15000;
const CLIENT_PURCHASE_PATTERN_HOUR_LOOKBACK_DAYS = 60;
const RECENT_MAX_DAYS = 7;
const COOLING_MAX_DAYS = 15;
const LOST_MIN_DAYS = 30;
@@ -1005,7 +1006,8 @@ const getClientFilterOptions = async () => {
};
const getClientPurchasePatternAnalytics = async () => {
const result = await pool.query(`
const [weekdayResult, hourResult] = await Promise.all([
pool.query(`
${CUSTOMER_IDENTITY_CTE},
order_events AS (
SELECT DISTINCT ON (
@@ -1023,12 +1025,35 @@ const getClientPurchasePatternAnalytics = async () => {
)
SELECT
EXTRACT(DOW FROM data_pedido_date)::int as weekday,
COUNT(*)::int as order_count
FROM order_events
GROUP BY weekday
ORDER BY weekday ASC;
`),
pool.query(`
${CUSTOMER_IDENTITY_CTE},
order_events AS (
SELECT DISTINCT ON (
${CUSTOMER_KEY_SQL},
COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text)
)
created_at
FROM identity_orders
WHERE data_pedido_date IS NOT NULL
AND created_at >= NOW() - ($1::int * INTERVAL '1 day')
ORDER BY
${CUSTOMER_KEY_SQL},
COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text),
created_at ASC NULLS LAST
)
SELECT
EXTRACT(HOUR FROM created_at AT TIME ZONE 'America/Sao_Paulo')::int as hour,
COUNT(*)::int as order_count
FROM order_events
GROUP BY weekday, hour
ORDER BY weekday ASC, hour ASC;
`);
GROUP BY hour
ORDER BY hour ASC;
`, [CLIENT_PURCHASE_PATTERN_HOUR_LOOKBACK_DAYS])
]);
const purchaseWeekdays = WEEKDAY_LABELS.map(label => ({ label, value: 0 }));
const purchaseHours = Array.from({ length: 24 }, (_, hour) => ({
@@ -1036,14 +1061,18 @@ const getClientPurchasePatternAnalytics = async () => {
value: 0
}));
result.rows.forEach(row => {
weekdayResult.rows.forEach(row => {
const weekday = row.weekday === null || row.weekday === undefined ? null : Number(row.weekday);
const hour = row.hour === null || row.hour === undefined ? null : Number(row.hour);
const orderCount = toNumber(row.order_count);
if (weekday !== null && Number.isInteger(weekday) && purchaseWeekdays[weekday]) {
purchaseWeekdays[weekday].value += orderCount;
}
});
hourResult.rows.forEach(row => {
const hour = row.hour === null || row.hour === undefined ? null : Number(row.hour);
const orderCount = toNumber(row.order_count);
if (hour !== null && Number.isInteger(hour) && purchaseHours[hour]) {
purchaseHours[hour].value += orderCount;
@@ -1052,7 +1081,9 @@ const getClientPurchasePatternAnalytics = async () => {
return {
purchaseWeekdays,
purchaseHours
purchaseHours,
weekdayRangeLabel: 'Todo período',
hourRangeLabel: `Últimos ${CLIENT_PURCHASE_PATTERN_HOUR_LOOKBACK_DAYS} dias`
};
};

View File

@@ -831,21 +831,35 @@ test('getClientFilterOptions returns all distinct order metadata options', async
}
});
test('getClientPurchasePatternAnalytics returns all-time weekday and hour counts', async () => {
test('getClientPurchasePatternAnalytics returns all-time weekday and recent hour counts', async () => {
const originalQuery = pool.query;
const calls = [];
pool.query = async (sql, params = []) => {
assert.deepEqual(params, []);
calls.push({ sql, params });
assert.match(sql, /order_events AS/);
assert.match(sql, /DISTINCT ON \(\s+customer_key,\s+COALESCE\(NULLIF\(pedido_id, ''\), data_pedido \|\| '_' \|\| valor_pedido::text\)\s+\)/);
assert.match(sql, /EXTRACT\(DOW FROM data_pedido_date\)::int as weekday/);
if (sql.includes('EXTRACT(DOW FROM data_pedido_date)')) {
assert.deepEqual(params, []);
assert.doesNotMatch(sql, /created_at >= NOW\(\)/);
return {
rows: [
{ weekday: 1, order_count: 2 },
{ weekday: 5, order_count: 2 }
]
};
}
assert.deepEqual(params, [60]);
assert.match(sql, /created_at >= NOW\(\) - \(\$1::int \* INTERVAL '1 day'\)/);
assert.match(sql, /EXTRACT\(HOUR FROM created_at AT TIME ZONE 'America\/Sao_Paulo'\)::int as hour/);
return {
rows: [
{ weekday: 1, hour: 9, order_count: 2 },
{ weekday: 5, hour: 18, order_count: 1 },
{ weekday: 5, hour: null, order_count: 1 }
{ hour: 9, order_count: 2 },
{ hour: 18, order_count: 1 },
{ hour: null, order_count: 1 }
]
};
};
@@ -853,6 +867,7 @@ test('getClientPurchasePatternAnalytics returns all-time weekday and hour counts
try {
const pattern = await getClientPurchasePatternAnalytics();
assert.equal(calls.length, 2);
assert.equal(pattern.purchaseWeekdays.length, 7);
assert.equal(pattern.purchaseHours.length, 24);
assert.deepEqual(pattern.purchaseWeekdays[1], { label: 'Seg', value: 2 });
@@ -860,6 +875,8 @@ test('getClientPurchasePatternAnalytics returns all-time weekday and hour counts
assert.deepEqual(pattern.purchaseHours[9], { label: '09h', value: 2 });
assert.deepEqual(pattern.purchaseHours[18], { label: '18h', value: 1 });
assert.deepEqual(pattern.purchaseHours[0], { label: '00h', value: 0 });
assert.equal(pattern.weekdayRangeLabel, 'Todo período');
assert.equal(pattern.hourRangeLabel, 'Últimos 60 dias');
} finally {
pool.query = originalQuery;
}