From 4131bf9eb8ebcbbfd512b7f8ed874b123496b2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Wed, 24 Jun 2026 13:37:11 -0300 Subject: [PATCH] Update RFV lifecycle to monthly windows --- backend/services/analyticsService.js | 6 +++--- backend/test/analyticsService.test.js | 26 +++++++++++++------------- src/pages/Rfm.tsx | 14 +++++++------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/backend/services/analyticsService.js b/backend/services/analyticsService.js index 54430c6..2a3ff78 100644 --- a/backend/services/analyticsService.js +++ b/backend/services/analyticsService.js @@ -2,9 +2,9 @@ const crypto = require('node:crypto'); const { pool } = require('../db'); const RFM_QUERY_TIMEOUT_MS = 15000; -const RECENT_MAX_DAYS = 60; -const COOLING_MAX_DAYS = 180; -const LOST_MIN_DAYS = 366; +const RECENT_MAX_DAYS = 7; +const COOLING_MAX_DAYS = 15; +const LOST_MIN_DAYS = 30; const FREQUENCY_MEDIUM_MAX_ORDERS = 4; const CLIENT_TOKEN_VERSION = 'v1'; const SIZE_SUFFIX_SQL_PATTERN = '\\s+-\\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2})(?:/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2}))*)$'; diff --git a/backend/test/analyticsService.test.js b/backend/test/analyticsService.test.js index ca4a40c..a1e10fe 100644 --- a/backend/test/analyticsService.test.js +++ b/backend/test/analyticsService.test.js @@ -236,12 +236,12 @@ test('scoreTertile treats equal recency as high recency', () => { test('getRecencyScore uses fixed lifecycle boundaries', () => { assert.equal(getRecencyScore(0), 3); - assert.equal(getRecencyScore(60), 3); - assert.equal(getRecencyScore(61), 2); - assert.equal(getRecencyScore(180), 2); - assert.equal(getRecencyScore(181), 1); - assert.equal(getRecencyScore(365), 1); - assert.equal(getRecencyScore(366), 1); + assert.equal(getRecencyScore(7), 3); + assert.equal(getRecencyScore(8), 2); + assert.equal(getRecencyScore(15), 2); + assert.equal(getRecencyScore(16), 1); + assert.equal(getRecencyScore(29), 1); + assert.equal(getRecencyScore(30), 1); }); test('getFrequencyScore uses fixed historical order thresholds', () => { @@ -296,7 +296,7 @@ test('buildRfmClients requires five orders and top value for a recent Champion', name: 'Potencial', monetary: 5000, frequency: 4, - recencyDays: 10, + recencyDays: 7, rfmFrequency: 4, rfmMonetary: 5000, rfmMonetaryScore: 3 @@ -306,7 +306,7 @@ test('buildRfmClients requires five orders and top value for a recent Champion', name: 'Champion', monetary: 5000, frequency: 5, - recencyDays: 10, + recencyDays: 7, rfmFrequency: 5, rfmMonetary: 5000, rfmMonetaryScore: 3 @@ -373,7 +373,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk name: 'Novo', monetary: 50, frequency: 1, - recencyDays: 60, + recencyDays: 7, rfmFrequency: 1, rfmMonetary: 50, rfmFrequencyScore: 1, @@ -384,7 +384,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk name: 'Não é mais novo', monetary: 50, frequency: 1, - recencyDays: 61, + recencyDays: 8, rfmFrequency: 1, rfmMonetary: 50, rfmFrequencyScore: 1, @@ -395,7 +395,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk name: 'Hibernando', monetary: 50, frequency: 1, - recencyDays: 250, + recencyDays: 16, rfmFrequency: 1, rfmMonetary: 50, rfmFrequencyScore: 1, @@ -406,7 +406,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk name: 'Em Risco', monetary: 100, frequency: 1, - recencyDays: 250, + recencyDays: 16, rfmFrequency: 10, rfmMonetary: 1000, rfmFrequencyScore: 3, @@ -417,7 +417,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk name: 'Perdido', monetary: 2000, frequency: 1, - recencyDays: 366, + recencyDays: 30, rfmFrequency: 1, rfmMonetary: 2000, rfmFrequencyScore: 1, diff --git a/src/pages/Rfm.tsx b/src/pages/Rfm.tsx index ef999b5..fbdc4ba 100644 --- a/src/pages/Rfm.tsx +++ b/src/pages/Rfm.tsx @@ -36,12 +36,12 @@ const segmentDescriptions: Record = { champions: 'Recentes, recorrentes e valiosos', potential_loyalists: 'Recentes e em evolução', new_customers: 'Primeira compra recente', - loyal_customers: 'Bom histórico, compra entre 61 e 180 dias', - need_attention: 'Perfil médio, compra entre 61 e 180 dias', - about_to_sleep: 'Perfil baixo, compra entre 61 e 180 dias', - at_risk: 'Histórico forte, sem compra entre 181 e 365 dias', - hibernating: 'Sem compra entre 181 e 365 dias', - lost: 'Sem compra há 366 dias ou mais' + loyal_customers: 'Bom histórico, compra entre 8 e 15 dias', + need_attention: 'Perfil médio, compra entre 8 e 15 dias', + about_to_sleep: 'Perfil baixo, compra entre 8 e 15 dias', + at_risk: 'Histórico forte, sem compra entre 16 e 29 dias', + hibernating: 'Sem compra entre 16 e 29 dias', + lost: 'Sem compra há 30 dias ou mais' }; const segmentActions: Record = { @@ -351,7 +351,7 @@ const Rfm = () => {

Rec. {recencyScore}

{scoreLabel(recencyScore)}

- {recencyScore === 3 ? '0 a 60 dias' : recencyScore === 2 ? '61 a 180 dias' : '181+ dias'} + {recencyScore === 3 ? '0 a 7 dias' : recencyScore === 2 ? '8 a 15 dias' : '16+ dias'}