diff --git a/backend/services/analyticsService.js b/backend/services/analyticsService.js index a96eefb..f4c6dbd 100644 --- a/backend/services/analyticsService.js +++ b/backend/services/analyticsService.js @@ -103,13 +103,15 @@ const buildRfmSegments = (clients) => { const buildRfmClients = (baseClients) => { const recencyValues = baseClients.map(client => client.recencyDays); - const frequencyValues = baseClients.map(client => client.frequency); - const monetaryValues = baseClients.map(client => client.monetary); + const frequencyValues = baseClients.map(client => client.rfmFrequency ?? client.frequency); + const monetaryValues = baseClients.map(client => client.rfmMonetary ?? client.monetary); return baseClients.map(client => { + const frequencyForScore = client.rfmFrequency ?? client.frequency; + const monetaryForScore = client.rfmMonetary ?? client.monetary; const recencyScore = scoreTertile(client.recencyDays, recencyValues, false); - const frequencyScore = scoreTertile(client.frequency, frequencyValues, true); - const monetaryScore = scoreTertile(client.monetary, monetaryValues, true); + const frequencyScore = scoreTertile(frequencyForScore, frequencyValues, true); + const monetaryScore = scoreTertile(monetaryForScore, monetaryValues, true); const valueScore = Math.min(3, Math.max(1, Math.round((frequencyScore + monetaryScore) / 2))); const segment = getRfmSegment(recencyScore, valueScore); @@ -260,19 +262,45 @@ const getRfmAnalytics = async (range = {}) => { } const result = await pool.query(` + WITH period_clients AS ( + SELECT + MAX(cliente_nome) as name, + cliente_fone as phone, + COALESCE(SUM(quantidade * valor_unitario), 0) as monetary, + COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency, + COALESCE(SUM(quantidade), 0) as quantity_purchased, + MAX(data_pedido_date) as last_purchase_date + FROM orders + ${whereClause} + AND cliente_fone IS NOT NULL + AND cliente_fone != '' + GROUP BY cliente_fone + ), + rfm_clients AS ( + SELECT + cliente_fone as phone, + COALESCE(SUM(quantidade * valor_unitario), 0) as rfm_monetary, + COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as rfm_frequency, + GREATEST((${recencyReferenceDate} - MAX(data_pedido_date))::int, 0) as recency_days + FROM orders + WHERE data_pedido_date IS NOT NULL + AND data_pedido_date <= ${recencyReferenceDate} + AND cliente_fone IS NOT NULL + AND cliente_fone != '' + GROUP BY cliente_fone + ) SELECT - MAX(cliente_nome) as name, - cliente_fone as phone, - COALESCE(SUM(quantidade * valor_unitario), 0) as monetary, - COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency, - COALESCE(SUM(quantidade), 0) as quantity_purchased, - MAX(data_pedido_date) as last_purchase_date, - GREATEST((${recencyReferenceDate} - MAX(data_pedido_date))::int, 0) as recency_days - FROM orders - ${whereClause} - AND cliente_fone IS NOT NULL - AND cliente_fone != '' - GROUP BY cliente_fone + period_clients.name, + period_clients.phone, + period_clients.monetary, + period_clients.frequency, + period_clients.quantity_purchased, + period_clients.last_purchase_date, + rfm_clients.rfm_monetary, + rfm_clients.rfm_frequency, + rfm_clients.recency_days + FROM period_clients + INNER JOIN rfm_clients ON rfm_clients.phone = period_clients.phone ORDER BY monetary DESC LIMIT 1000; `, queryParams); @@ -284,7 +312,9 @@ const getRfmAnalytics = async (range = {}) => { frequency: toNumber(row.frequency), quantityPurchased: toNumber(row.quantity_purchased), lastPurchaseDate: row.last_purchase_date, - recencyDays: toNumber(row.recency_days) + recencyDays: toNumber(row.recency_days), + rfmFrequency: toNumber(row.rfm_frequency), + rfmMonetary: toNumber(row.rfm_monetary) })); const clients = buildRfmClients(baseClients); diff --git a/backend/test/analyticsService.test.js b/backend/test/analyticsService.test.js index 4ff6b95..721bc7f 100644 --- a/backend/test/analyticsService.test.js +++ b/backend/test/analyticsService.test.js @@ -134,6 +134,52 @@ test('buildRfmClients keeps a yesterday buyer in the same high-recency segment w assert.equal(sevenDayClients[0].phone, yesterdayClients[0].phone); }); +test('buildRfmClients scores segments from RFM history when period totals are smaller', () => { + const clients = buildRfmClients([ + { + name: 'Champion que comprou ontem', + phone: '1', + monetary: 100, + frequency: 1, + quantityPurchased: 1, + lastPurchaseDate: '2026-06-14', + recencyDays: 0, + rfmFrequency: 20, + rfmMonetary: 10000 + }, + { + name: 'Compra alta no periodo', + phone: '2', + monetary: 1000, + frequency: 5, + quantityPurchased: 5, + lastPurchaseDate: '2026-06-10', + recencyDays: 4, + rfmFrequency: 1, + rfmMonetary: 100 + }, + { + name: 'Compra media no periodo', + phone: '3', + monetary: 900, + frequency: 4, + quantityPurchased: 4, + lastPurchaseDate: '2026-06-05', + recencyDays: 9, + rfmFrequency: 1, + rfmMonetary: 50 + } + ]); + + const yesterdayBuyer = clients.find(client => client.phone === '1'); + + assert.equal(yesterdayBuyer.segmentKey, 'champions'); + assert.equal(yesterdayBuyer.frequency, 1); + assert.equal(yesterdayBuyer.monetary, 100); + assert.equal(yesterdayBuyer.frequencyScore, 3); + assert.equal(yesterdayBuyer.monetaryScore, 3); +}); + test('getRfmAnalytics calculates recency against selected range end date', async () => { const originalQuery = pool.query; const calls = []; @@ -160,6 +206,7 @@ test('getRfmAnalytics calculates recency against selected range end date', async const yesterday = await getRfmAnalytics({ start: '2026-06-14', end: '2026-06-14' }); assert.match(calls[0].sql, /\(\$3::date - MAX\(data_pedido_date\)\)::int/); + assert.match(calls[0].sql, /data_pedido_date <= \$3::date/); assert.deepEqual(calls[0].params, ['2026-06-09', '2026-06-15', '2026-06-15']); assert.deepEqual(calls[1].params, ['2026-06-14', '2026-06-14', '2026-06-14']); assert.equal(sevenDays.clients[0].segmentKey, 'champions');