Use all-time client purchase patterns
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m25s

This commit is contained in:
Cauê Faleiros
2026-06-29 09:29:16 -03:00
parent 8b3ac60883
commit a0a1108f3a
4 changed files with 56 additions and 23 deletions

View File

@@ -910,7 +910,7 @@ const getClientDetailsAnalytics = async (clientToken, range = {}) => {
periodFilters.push(`data_pedido_date <= $${periodParams.length}::date`); periodFilters.push(`data_pedido_date <= $${periodParams.length}::date`);
} }
const [summaryResult, periodResult] = await Promise.all([ const [summaryResult, periodResult, patternResult] = await Promise.all([
pool.query(` pool.query(`
${CUSTOMER_IDENTITY_CTE} ${CUSTOMER_IDENTITY_CTE}
SELECT SELECT
@@ -944,7 +944,20 @@ const getClientDetailsAnalytics = async (clientToken, range = {}) => {
FROM identity_orders FROM identity_orders
WHERE ${periodFilters.join(' AND ')} WHERE ${periodFilters.join(' AND ')}
ORDER BY data_pedido_date DESC, COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text) DESC; ORDER BY data_pedido_date DESC, COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text) DESC;
`, periodParams) `, periodParams),
pool.query(`
${CUSTOMER_IDENTITY_CTE}
SELECT
data_pedido,
data_pedido_date,
created_at,
pedido_id,
valor_pedido
FROM identity_orders
WHERE ${CUSTOMER_KEY_SQL} = $1
AND data_pedido_date IS NOT NULL
ORDER BY data_pedido_date DESC, COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text) DESC;
`, [resolvedCustomerKey])
]); ]);
const summary = summaryResult.rows[0] || {}; const summary = summaryResult.rows[0] || {};
@@ -962,25 +975,29 @@ const getClientDetailsAnalytics = async (clientToken, range = {}) => {
let periodSpent = 0; let periodSpent = 0;
let periodItems = 0; let periodItems = 0;
patternResult.rows.forEach(row => {
const groupKey = getOrderGroupKey(row);
if (patternOrderKeys.has(groupKey)) return;
patternOrderKeys.add(groupKey);
const weekdayIndex = getWeekdayIndex(row.data_pedido_date) ?? getWeekdayIndex(row.data_pedido);
if (weekdayIndex !== null) {
weekdayCounts[weekdayIndex].value += 1;
}
const orderHour = getHourFromTimestamp(row.created_at) ?? getHourFromTimestamp(row.data_pedido);
if (orderHour !== null) {
hourCounts[orderHour].value += 1;
}
});
periodResult.rows.forEach(row => { periodResult.rows.forEach(row => {
const itemRevenue = toNumber(row.quantidade) * toNumber(row.valor_unitario); const itemRevenue = toNumber(row.quantidade) * toNumber(row.valor_unitario);
const dateKey = getDateOnly(row.data_pedido_date) || getDateOnly(row.data_pedido) || ''; const dateKey = getDateOnly(row.data_pedido_date) || getDateOnly(row.data_pedido) || '';
const dateLabel = row.data_pedido || dateKey; const dateLabel = row.data_pedido || dateKey;
const groupKey = getOrderGroupKey(row); const groupKey = getOrderGroupKey(row);
if (!patternOrderKeys.has(groupKey)) {
patternOrderKeys.add(groupKey);
const weekdayIndex = getWeekdayIndex(row.data_pedido_date) ?? getWeekdayIndex(row.data_pedido);
if (weekdayIndex !== null) {
weekdayCounts[weekdayIndex].value += 1;
}
const orderHour = getHourFromTimestamp(row.created_at) ?? getHourFromTimestamp(row.data_pedido);
if (orderHour !== null) {
hourCounts[orderHour].value += 1;
}
}
periodSpent += itemRevenue; periodSpent += itemRevenue;
periodItems += toNumber(row.quantidade); periodItems += toNumber(row.quantidade);

View File

@@ -805,7 +805,7 @@ test('getClientDetailsAnalytics resolves legacy name tokens to the canonical pho
try { try {
const details = await getClientDetailsAnalytics(clientToken, { start: '2026-06-01', end: '2026-06-15' }); const details = await getClientDetailsAnalytics(clientToken, { start: '2026-06-01', end: '2026-06-15' });
assert.equal(calls.length, 4); assert.equal(calls.length, 5);
assert.match(calls[0].sql, /FROM client_identity_tokens/); assert.match(calls[0].sql, /FROM client_identity_tokens/);
assert.deepEqual(calls[0].params, [clientToken]); assert.deepEqual(calls[0].params, [clientToken]);
assert.match(calls[1].sql, /SELECT customer_key/); assert.match(calls[1].sql, /SELECT customer_key/);
@@ -816,6 +816,10 @@ test('getClientDetailsAnalytics resolves legacy name tokens to the canonical pho
assert.match(calls[3].sql, /data_pedido_date >= \$2::date/); assert.match(calls[3].sql, /data_pedido_date >= \$2::date/);
assert.match(calls[3].sql, /data_pedido_date <= \$3::date/); assert.match(calls[3].sql, /data_pedido_date <= \$3::date/);
assert.deepEqual(calls[3].params, ['(16) 99999-9999', '2026-06-01', '2026-06-15']); assert.deepEqual(calls[3].params, ['(16) 99999-9999', '2026-06-01', '2026-06-15']);
assert.match(calls[4].sql, /created_at/);
assert.doesNotMatch(calls[4].sql, /data_pedido_date >=/);
assert.doesNotMatch(calls[4].sql, /data_pedido_date <=/);
assert.deepEqual(calls[4].params, ['(16) 99999-9999']);
assert.equal(details.clientName, 'Cliente Sem Fone'); assert.equal(details.clientName, 'Cliente Sem Fone');
assert.equal(details.clientPhone, '(16) 99999-9999'); assert.equal(details.clientPhone, '(16) 99999-9999');
assert.equal(details.allTimeOrderCount, 3); assert.equal(details.allTimeOrderCount, 3);

View File

@@ -260,14 +260,21 @@ export const buildClientDetailsMetrics = (ordersData: OrderData[], customerKey:
value: 0 value: 0
})); }));
groupedOrders.forEach(group => { const patternOrders = new Map<string, OrderData>();
const firstItem = group.items[0]; clientOrders.forEach(order => {
const orderDate = parseOrderDate(group.date); const key = order.ID_Pedido || `${order.Data_Pedido}_${order.Valor_Pedido}`;
if (!patternOrders.has(key)) {
patternOrders.set(key, order);
}
});
patternOrders.forEach(order => {
const orderDate = parseOrderDate(order.Data_Pedido);
if (!Number.isNaN(orderDate.getTime())) { if (!Number.isNaN(orderDate.getTime())) {
weekdayCounts[orderDate.getDay()].value += 1; weekdayCounts[orderDate.getDay()].value += 1;
} }
const orderHour = firstItem ? getOrderHour(firstItem) : null; const orderHour = getOrderHour(order);
if (orderHour !== null) { if (orderHour !== null) {
hourCounts[orderHour].value += 1; hourCounts[orderHour].value += 1;
} }

View File

@@ -291,9 +291,14 @@ const ClientDetails = () => {
</div> </div>
<section className="space-y-4"> <section className="space-y-4">
<div> <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<h3 className="text-lg font-bold text-zinc-900 dark:text-dark-text">Padrão de Compra</h3> <div>
<p className="mt-1 text-sm font-medium text-zinc-500 dark:text-dark-muted">Quando este cliente costuma comprar.</p> <h3 className="text-lg font-bold text-zinc-900 dark:text-dark-text">Padrão de Compra</h3>
<p className="mt-1 text-sm font-medium text-zinc-500 dark:text-dark-muted">Quando este cliente costuma comprar.</p>
</div>
<span className="w-fit rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-bold uppercase tracking-wide text-zinc-500 dark:border-dark-border dark:bg-dark-card dark:text-dark-muted">
Todo período
</span>
</div> </div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">