Calibrate RFV customer lifecycle segments
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m30s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m30s
This commit is contained in:
@@ -6,6 +6,7 @@ const {
|
||||
buildRfmSegments,
|
||||
buildDateFilter,
|
||||
getPreviousDate,
|
||||
getRecencyScore,
|
||||
getRfmAnalytics,
|
||||
getRfmSegment,
|
||||
normalizeDateParam,
|
||||
@@ -109,6 +110,16 @@ test('scoreTertile treats equal recency as high recency', () => {
|
||||
assert.equal(scoreTertile(0, [0, 0, 0], false), 3);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
test('getRfmSegment maps the 3x3 RFM matrix', () => {
|
||||
assert.deepEqual(getRfmSegment(3, 3), { key: 'champions', label: 'Champions' });
|
||||
assert.deepEqual(getRfmSegment(2, 2), { key: 'need_attention', label: 'Precisam de Atenção' });
|
||||
@@ -191,6 +202,74 @@ test('buildRfmClients scores segments from RFM history when period totals are sm
|
||||
assert.equal(yesterdayBuyer.monetaryScore, 3);
|
||||
});
|
||||
|
||||
test('buildRfmClients applies lifecycle protections to new, hibernating, at-risk, and lost clients', () => {
|
||||
const clients = buildRfmClients([
|
||||
{
|
||||
customerKey: 'new',
|
||||
name: 'Novo',
|
||||
monetary: 50,
|
||||
frequency: 1,
|
||||
recencyDays: 60,
|
||||
rfmFrequency: 1,
|
||||
rfmMonetary: 50,
|
||||
rfmFrequencyScore: 1,
|
||||
rfmMonetaryScore: 1
|
||||
},
|
||||
{
|
||||
customerKey: 'not-new',
|
||||
name: 'Não é mais novo',
|
||||
monetary: 50,
|
||||
frequency: 1,
|
||||
recencyDays: 61,
|
||||
rfmFrequency: 1,
|
||||
rfmMonetary: 50,
|
||||
rfmFrequencyScore: 1,
|
||||
rfmMonetaryScore: 1
|
||||
},
|
||||
{
|
||||
customerKey: 'hibernating',
|
||||
name: 'Hibernando',
|
||||
monetary: 50,
|
||||
frequency: 1,
|
||||
recencyDays: 250,
|
||||
rfmFrequency: 1,
|
||||
rfmMonetary: 50,
|
||||
rfmFrequencyScore: 1,
|
||||
rfmMonetaryScore: 1
|
||||
},
|
||||
{
|
||||
customerKey: 'at-risk',
|
||||
name: 'Em Risco',
|
||||
monetary: 100,
|
||||
frequency: 1,
|
||||
recencyDays: 250,
|
||||
rfmFrequency: 10,
|
||||
rfmMonetary: 1000,
|
||||
rfmFrequencyScore: 3,
|
||||
rfmMonetaryScore: 2
|
||||
},
|
||||
{
|
||||
customerKey: 'lost',
|
||||
name: 'Perdido',
|
||||
monetary: 2000,
|
||||
frequency: 1,
|
||||
recencyDays: 366,
|
||||
rfmFrequency: 1,
|
||||
rfmMonetary: 2000,
|
||||
rfmFrequencyScore: 1,
|
||||
rfmMonetaryScore: 3
|
||||
}
|
||||
]);
|
||||
const byKey = new Map(clients.map(client => [client.customerKey, client]));
|
||||
|
||||
assert.equal(byKey.get('new').segmentKey, 'new_customers');
|
||||
assert.equal(byKey.get('not-new').segmentKey, 'about_to_sleep');
|
||||
assert.equal(byKey.get('hibernating').segmentKey, 'hibernating');
|
||||
assert.equal(byKey.get('at-risk').segmentKey, 'at_risk');
|
||||
assert.equal(byKey.get('lost').segmentKey, 'lost');
|
||||
assert.equal(byKey.get('lost').rfmScore, '113');
|
||||
});
|
||||
|
||||
test('getRfmAnalytics classifies period buyers by history through the selected range end', async () => {
|
||||
const originalConnect = pool.connect;
|
||||
const calls = [];
|
||||
@@ -216,27 +295,21 @@ test('getRfmAnalytics classifies period buyers by history through the selected r
|
||||
frequency: 20,
|
||||
quantity_purchased: 20,
|
||||
last_purchase_date: '2026-06-14',
|
||||
recency_days: referenceDate === '2026-06-14' ? 0 : 1
|
||||
recency_days: referenceDate === '2026-06-14' ? 0 : 1,
|
||||
frequency_score: 3,
|
||||
monetary_score: 3
|
||||
},
|
||||
{
|
||||
customer_key: '2',
|
||||
name: 'Cliente Antigo',
|
||||
phone: '2',
|
||||
monetary: 50,
|
||||
customer_key: '4',
|
||||
name: 'Cliente Novo no Periodo',
|
||||
phone: '4',
|
||||
monetary: 25,
|
||||
frequency: 1,
|
||||
quantity_purchased: 1,
|
||||
last_purchase_date: '2026-01-01',
|
||||
recency_days: 164
|
||||
},
|
||||
{
|
||||
customer_key: '3',
|
||||
name: 'Cliente Medio',
|
||||
phone: '3',
|
||||
monetary: 100,
|
||||
frequency: 2,
|
||||
quantity_purchased: 2,
|
||||
last_purchase_date: '2026-03-01',
|
||||
recency_days: 105
|
||||
last_purchase_date: '2026-06-14',
|
||||
recency_days: referenceDate === '2026-06-14' ? 0 : 1,
|
||||
frequency_score: 1,
|
||||
monetary_score: 1
|
||||
},
|
||||
{
|
||||
customer_key: 'name:Cliente Sem Fone',
|
||||
@@ -246,7 +319,9 @@ test('getRfmAnalytics classifies period buyers by history through the selected r
|
||||
frequency: 10,
|
||||
quantity_purchased: 10,
|
||||
last_purchase_date: '2026-06-14',
|
||||
recency_days: 1
|
||||
recency_days: referenceDate === '2026-06-14' ? 0 : 1,
|
||||
frequency_score: 3,
|
||||
monetary_score: 3
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -301,12 +376,13 @@ test('getRfmAnalytics classifies period buyers by history through the selected r
|
||||
assert.doesNotMatch(selectCalls[0].sql, /cliente_fone IS NOT NULL/);
|
||||
assert.match(selectCalls[1].sql, /\(\$1::date - MAX\(data_pedido_date\)\)::int/);
|
||||
assert.match(selectCalls[1].sql, /data_pedido_date <= \$1::date/);
|
||||
assert.match(selectCalls[1].sql, /NULLIF\(cliente_fone, ''\) = ANY\(\$2::text\[\]\)/);
|
||||
assert.match(selectCalls[1].sql, /COALESCE\(NULLIF\(cliente_nome, ''\), 'Cliente Desconhecido'\) = ANY\(\$3::text\[\]\)/);
|
||||
assert.match(selectCalls[1].sql, /PERCENT_RANK\(\) OVER \(ORDER BY frequency\)/);
|
||||
assert.match(selectCalls[1].sql, /PERCENT_RANK\(\) OVER \(ORDER BY monetary\)/);
|
||||
assert.match(selectCalls[1].sql, /customer_key = ANY\(\$2::text\[\]\)/);
|
||||
assert.doesNotMatch(selectCalls[1].sql, /cliente_fone IS NOT NULL/);
|
||||
assert.deepEqual(selectCalls[1].params, ['2026-06-15', ['1', '4'], ['Cliente Sem Fone']]);
|
||||
assert.deepEqual(selectCalls[1].params, ['2026-06-15', ['1', '4', 'name:Cliente Sem Fone']]);
|
||||
assert.deepEqual(selectCalls[2].params, ['2026-06-14', '2026-06-14']);
|
||||
assert.deepEqual(selectCalls[3].params, ['2026-06-14', ['1', '4'], ['Cliente Sem Fone']]);
|
||||
assert.deepEqual(selectCalls[3].params, ['2026-06-14', ['1', '4', 'name:Cliente Sem Fone']]);
|
||||
assert.equal(sevenDays.clients[0].segmentKey, 'champions');
|
||||
assert.equal(yesterday.clients[0].segmentKey, 'champions');
|
||||
assert.equal(yesterday.clients[0].frequency, 1);
|
||||
|
||||
Reference in New Issue
Block a user