Add client purchase pattern analytics
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 59s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 59s
This commit is contained in:
@@ -4,6 +4,7 @@ const {
|
||||
getClientAnalytics,
|
||||
getClientDetailsAnalytics,
|
||||
getClientFilterOptions,
|
||||
getClientPurchasePatternAnalytics,
|
||||
getDashboardAnalytics,
|
||||
getProductAnalytics,
|
||||
getProductDetailsAnalytics,
|
||||
@@ -75,6 +76,15 @@ router.get('/analytics/clients/filters', verifyToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/analytics/clients/purchase-pattern', verifyToken, async (req, res) => {
|
||||
try {
|
||||
res.json(await getClientPurchasePatternAnalytics());
|
||||
} catch (error) {
|
||||
console.error('Error fetching client purchase pattern analytics:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/analytics/clients/:clientToken/details', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const details = await getClientDetailsAnalytics(req.params.clientToken, getRange(req.query));
|
||||
|
||||
@@ -942,6 +942,58 @@ const getClientFilterOptions = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
const getClientPurchasePatternAnalytics = async () => {
|
||||
const result = await pool.query(`
|
||||
${CUSTOMER_IDENTITY_CTE},
|
||||
order_events AS (
|
||||
SELECT DISTINCT ON (
|
||||
${CUSTOMER_KEY_SQL},
|
||||
COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text)
|
||||
)
|
||||
data_pedido_date,
|
||||
created_at
|
||||
FROM identity_orders
|
||||
WHERE data_pedido_date IS NOT NULL
|
||||
ORDER BY
|
||||
${CUSTOMER_KEY_SQL},
|
||||
COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text),
|
||||
created_at ASC NULLS LAST
|
||||
)
|
||||
SELECT
|
||||
EXTRACT(DOW FROM data_pedido_date)::int as weekday,
|
||||
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;
|
||||
`);
|
||||
|
||||
const purchaseWeekdays = WEEKDAY_LABELS.map(label => ({ label, value: 0 }));
|
||||
const purchaseHours = Array.from({ length: 24 }, (_, hour) => ({
|
||||
label: `${String(hour).padStart(2, '0')}h`,
|
||||
value: 0
|
||||
}));
|
||||
|
||||
result.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;
|
||||
}
|
||||
|
||||
if (hour !== null && Number.isInteger(hour) && purchaseHours[hour]) {
|
||||
purchaseHours[hour].value += orderCount;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
purchaseWeekdays,
|
||||
purchaseHours
|
||||
};
|
||||
};
|
||||
|
||||
const getOrderGroupKey = (row) => (
|
||||
row.pedido_id ||
|
||||
`${row.data_pedido || getDateOnly(row.data_pedido_date) || ''}_${row.valor_pedido || 0}`
|
||||
@@ -1353,6 +1405,7 @@ module.exports = {
|
||||
getFrequencyScore,
|
||||
getClientDetailsAnalytics,
|
||||
getClientFilterOptions,
|
||||
getClientPurchasePatternAnalytics,
|
||||
getPreviousDate,
|
||||
getRecencyScore,
|
||||
getRfmAnalytics,
|
||||
|
||||
@@ -11,6 +11,7 @@ const {
|
||||
getClientAnalytics,
|
||||
getClientDetailsAnalytics,
|
||||
getClientFilterOptions,
|
||||
getClientPurchasePatternAnalytics,
|
||||
getDashboardAnalytics,
|
||||
getPreviousDate,
|
||||
getProductAnalytics,
|
||||
@@ -791,6 +792,40 @@ test('getClientFilterOptions returns all distinct order metadata options', async
|
||||
}
|
||||
});
|
||||
|
||||
test('getClientPurchasePatternAnalytics returns all-time weekday and hour counts', async () => {
|
||||
const originalQuery = pool.query;
|
||||
|
||||
pool.query = async (sql, params = []) => {
|
||||
assert.deepEqual(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/);
|
||||
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 }
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const pattern = await getClientPurchasePatternAnalytics();
|
||||
|
||||
assert.equal(pattern.purchaseWeekdays.length, 7);
|
||||
assert.equal(pattern.purchaseHours.length, 24);
|
||||
assert.deepEqual(pattern.purchaseWeekdays[1], { label: 'Seg', value: 2 });
|
||||
assert.deepEqual(pattern.purchaseWeekdays[5], { label: 'Sex', value: 2 });
|
||||
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 });
|
||||
} finally {
|
||||
pool.query = originalQuery;
|
||||
}
|
||||
});
|
||||
|
||||
test('getClientDetailsAnalytics resolves legacy name tokens to the canonical phone client', async () => {
|
||||
const originalQuery = pool.query;
|
||||
const calls = [];
|
||||
|
||||
Reference in New Issue
Block a user