Update RFV lifecycle to monthly windows
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 41s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 41s
This commit is contained in:
@@ -2,9 +2,9 @@ const crypto = require('node:crypto');
|
|||||||
const { pool } = require('../db');
|
const { pool } = require('../db');
|
||||||
|
|
||||||
const RFM_QUERY_TIMEOUT_MS = 15000;
|
const RFM_QUERY_TIMEOUT_MS = 15000;
|
||||||
const RECENT_MAX_DAYS = 60;
|
const RECENT_MAX_DAYS = 7;
|
||||||
const COOLING_MAX_DAYS = 180;
|
const COOLING_MAX_DAYS = 15;
|
||||||
const LOST_MIN_DAYS = 366;
|
const LOST_MIN_DAYS = 30;
|
||||||
const FREQUENCY_MEDIUM_MAX_ORDERS = 4;
|
const FREQUENCY_MEDIUM_MAX_ORDERS = 4;
|
||||||
const CLIENT_TOKEN_VERSION = 'v1';
|
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}))*)$';
|
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}))*)$';
|
||||||
|
|||||||
@@ -236,12 +236,12 @@ test('scoreTertile treats equal recency as high recency', () => {
|
|||||||
|
|
||||||
test('getRecencyScore uses fixed lifecycle boundaries', () => {
|
test('getRecencyScore uses fixed lifecycle boundaries', () => {
|
||||||
assert.equal(getRecencyScore(0), 3);
|
assert.equal(getRecencyScore(0), 3);
|
||||||
assert.equal(getRecencyScore(60), 3);
|
assert.equal(getRecencyScore(7), 3);
|
||||||
assert.equal(getRecencyScore(61), 2);
|
assert.equal(getRecencyScore(8), 2);
|
||||||
assert.equal(getRecencyScore(180), 2);
|
assert.equal(getRecencyScore(15), 2);
|
||||||
assert.equal(getRecencyScore(181), 1);
|
assert.equal(getRecencyScore(16), 1);
|
||||||
assert.equal(getRecencyScore(365), 1);
|
assert.equal(getRecencyScore(29), 1);
|
||||||
assert.equal(getRecencyScore(366), 1);
|
assert.equal(getRecencyScore(30), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getFrequencyScore uses fixed historical order thresholds', () => {
|
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',
|
name: 'Potencial',
|
||||||
monetary: 5000,
|
monetary: 5000,
|
||||||
frequency: 4,
|
frequency: 4,
|
||||||
recencyDays: 10,
|
recencyDays: 7,
|
||||||
rfmFrequency: 4,
|
rfmFrequency: 4,
|
||||||
rfmMonetary: 5000,
|
rfmMonetary: 5000,
|
||||||
rfmMonetaryScore: 3
|
rfmMonetaryScore: 3
|
||||||
@@ -306,7 +306,7 @@ test('buildRfmClients requires five orders and top value for a recent Champion',
|
|||||||
name: 'Champion',
|
name: 'Champion',
|
||||||
monetary: 5000,
|
monetary: 5000,
|
||||||
frequency: 5,
|
frequency: 5,
|
||||||
recencyDays: 10,
|
recencyDays: 7,
|
||||||
rfmFrequency: 5,
|
rfmFrequency: 5,
|
||||||
rfmMonetary: 5000,
|
rfmMonetary: 5000,
|
||||||
rfmMonetaryScore: 3
|
rfmMonetaryScore: 3
|
||||||
@@ -373,7 +373,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk
|
|||||||
name: 'Novo',
|
name: 'Novo',
|
||||||
monetary: 50,
|
monetary: 50,
|
||||||
frequency: 1,
|
frequency: 1,
|
||||||
recencyDays: 60,
|
recencyDays: 7,
|
||||||
rfmFrequency: 1,
|
rfmFrequency: 1,
|
||||||
rfmMonetary: 50,
|
rfmMonetary: 50,
|
||||||
rfmFrequencyScore: 1,
|
rfmFrequencyScore: 1,
|
||||||
@@ -384,7 +384,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk
|
|||||||
name: 'Não é mais novo',
|
name: 'Não é mais novo',
|
||||||
monetary: 50,
|
monetary: 50,
|
||||||
frequency: 1,
|
frequency: 1,
|
||||||
recencyDays: 61,
|
recencyDays: 8,
|
||||||
rfmFrequency: 1,
|
rfmFrequency: 1,
|
||||||
rfmMonetary: 50,
|
rfmMonetary: 50,
|
||||||
rfmFrequencyScore: 1,
|
rfmFrequencyScore: 1,
|
||||||
@@ -395,7 +395,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk
|
|||||||
name: 'Hibernando',
|
name: 'Hibernando',
|
||||||
monetary: 50,
|
monetary: 50,
|
||||||
frequency: 1,
|
frequency: 1,
|
||||||
recencyDays: 250,
|
recencyDays: 16,
|
||||||
rfmFrequency: 1,
|
rfmFrequency: 1,
|
||||||
rfmMonetary: 50,
|
rfmMonetary: 50,
|
||||||
rfmFrequencyScore: 1,
|
rfmFrequencyScore: 1,
|
||||||
@@ -406,7 +406,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk
|
|||||||
name: 'Em Risco',
|
name: 'Em Risco',
|
||||||
monetary: 100,
|
monetary: 100,
|
||||||
frequency: 1,
|
frequency: 1,
|
||||||
recencyDays: 250,
|
recencyDays: 16,
|
||||||
rfmFrequency: 10,
|
rfmFrequency: 10,
|
||||||
rfmMonetary: 1000,
|
rfmMonetary: 1000,
|
||||||
rfmFrequencyScore: 3,
|
rfmFrequencyScore: 3,
|
||||||
@@ -417,7 +417,7 @@ test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk
|
|||||||
name: 'Perdido',
|
name: 'Perdido',
|
||||||
monetary: 2000,
|
monetary: 2000,
|
||||||
frequency: 1,
|
frequency: 1,
|
||||||
recencyDays: 366,
|
recencyDays: 30,
|
||||||
rfmFrequency: 1,
|
rfmFrequency: 1,
|
||||||
rfmMonetary: 2000,
|
rfmMonetary: 2000,
|
||||||
rfmFrequencyScore: 1,
|
rfmFrequencyScore: 1,
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ const segmentDescriptions: Record<string, string> = {
|
|||||||
champions: 'Recentes, recorrentes e valiosos',
|
champions: 'Recentes, recorrentes e valiosos',
|
||||||
potential_loyalists: 'Recentes e em evolução',
|
potential_loyalists: 'Recentes e em evolução',
|
||||||
new_customers: 'Primeira compra recente',
|
new_customers: 'Primeira compra recente',
|
||||||
loyal_customers: 'Bom histórico, compra entre 61 e 180 dias',
|
loyal_customers: 'Bom histórico, compra entre 8 e 15 dias',
|
||||||
need_attention: 'Perfil médio, compra entre 61 e 180 dias',
|
need_attention: 'Perfil médio, compra entre 8 e 15 dias',
|
||||||
about_to_sleep: 'Perfil baixo, compra entre 61 e 180 dias',
|
about_to_sleep: 'Perfil baixo, compra entre 8 e 15 dias',
|
||||||
at_risk: 'Histórico forte, sem compra entre 181 e 365 dias',
|
at_risk: 'Histórico forte, sem compra entre 16 e 29 dias',
|
||||||
hibernating: 'Sem compra entre 181 e 365 dias',
|
hibernating: 'Sem compra entre 16 e 29 dias',
|
||||||
lost: 'Sem compra há 366 dias ou mais'
|
lost: 'Sem compra há 30 dias ou mais'
|
||||||
};
|
};
|
||||||
|
|
||||||
const segmentActions: Record<string, string> = {
|
const segmentActions: Record<string, string> = {
|
||||||
@@ -351,7 +351,7 @@ const Rfm = () => {
|
|||||||
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Rec. {recencyScore}</p>
|
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Rec. {recencyScore}</p>
|
||||||
<p className="text-xs font-bold text-dark-text">{scoreLabel(recencyScore)}</p>
|
<p className="text-xs font-bold text-dark-text">{scoreLabel(recencyScore)}</p>
|
||||||
<p className="mt-1 text-[10px] font-semibold text-dark-muted">
|
<p className="mt-1 text-[10px] font-semibold text-dark-muted">
|
||||||
{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'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user