Score RFV clients without phone
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m46s

This commit is contained in:
Cauê Faleiros
2026-06-17 09:59:57 -03:00
parent ed1f129b07
commit ce72231560
4 changed files with 29 additions and 20 deletions

View File

@@ -7,6 +7,7 @@ const PRODUCT_NAME_SQL = `
ELSE NULLIF(TRIM(regexp_replace(split_part(COALESCE(produto_descricao, 'Unknown'), ' TAMANHO', 1), '${SIZE_SUFFIX_SQL_PATTERN}', '', 'i')), '') ELSE NULLIF(TRIM(regexp_replace(split_part(COALESCE(produto_descricao, 'Unknown'), ' TAMANHO', 1), '${SIZE_SUFFIX_SQL_PATTERN}', '', 'i')), '')
END END
`; `;
const CUSTOMER_KEY_SQL = "COALESCE(NULLIF(cliente_fone, ''), 'name:' || COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido'))";
const normalizeDateParam = (value) => { const normalizeDateParam = (value) => {
if (!value) return null; if (!value) return null;
@@ -235,7 +236,8 @@ const getClientAnalytics = async (range = {}) => {
const { params, whereClause } = buildDateFilter(range); const { params, whereClause } = buildDateFilter(range);
const result = await pool.query(` const result = await pool.query(`
SELECT SELECT
COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido') as name, ${CUSTOMER_KEY_SQL} as customer_key,
MAX(COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido')) as name,
MAX(NULLIF(cliente_fone, '')) as phone, MAX(NULLIF(cliente_fone, '')) as phone,
COALESCE(SUM(quantidade), 0) as quantity_purchased, COALESCE(SUM(quantidade), 0) as quantity_purchased,
COALESCE(SUM(quantidade * valor_unitario), 0) as total_spent, COALESCE(SUM(quantidade * valor_unitario), 0) as total_spent,
@@ -243,11 +245,12 @@ const getClientAnalytics = async (range = {}) => {
MAX(data_pedido_date) as last_purchase_date MAX(data_pedido_date) as last_purchase_date
FROM orders FROM orders
${whereClause} ${whereClause}
GROUP BY COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido') GROUP BY customer_key
ORDER BY total_spent DESC; ORDER BY total_spent DESC;
`, params); `, params);
return result.rows.map(row => ({ return result.rows.map(row => ({
customerKey: row.customer_key,
name: row.name, name: row.name,
phone: row.phone || '', phone: row.phone || '',
quantityPurchased: toNumber(row.quantity_purchased), quantityPurchased: toNumber(row.quantity_purchased),
@@ -268,23 +271,23 @@ const getRfmAnalytics = async (range = {}) => {
const [periodResult, historyResult] = await Promise.all([ const [periodResult, historyResult] = await Promise.all([
pool.query(` pool.query(`
SELECT SELECT
${CUSTOMER_KEY_SQL} as customer_key,
MAX(cliente_nome) as name, MAX(cliente_nome) as name,
cliente_fone as phone, MAX(NULLIF(cliente_fone, '')) as phone,
COALESCE(SUM(quantidade * valor_unitario), 0) as monetary, COALESCE(SUM(quantidade * valor_unitario), 0) as monetary,
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency, COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency,
COALESCE(SUM(quantidade), 0) as quantity_purchased, COALESCE(SUM(quantidade), 0) as quantity_purchased,
MAX(data_pedido_date) as last_purchase_date MAX(data_pedido_date) as last_purchase_date
FROM orders FROM orders
${whereClause} ${whereClause}
AND cliente_fone IS NOT NULL GROUP BY customer_key
AND cliente_fone != ''
GROUP BY cliente_fone
ORDER BY monetary DESC; ORDER BY monetary DESC;
`, params), `, params),
pool.query(` pool.query(`
SELECT SELECT
${CUSTOMER_KEY_SQL} as customer_key,
MAX(cliente_nome) as name, MAX(cliente_nome) as name,
cliente_fone as phone, MAX(NULLIF(cliente_fone, '')) as phone,
COALESCE(SUM(quantidade * valor_unitario), 0) as monetary, COALESCE(SUM(quantidade * valor_unitario), 0) as monetary,
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency, COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency,
COALESCE(SUM(quantidade), 0) as quantity_purchased, COALESCE(SUM(quantidade), 0) as quantity_purchased,
@@ -293,30 +296,30 @@ const getRfmAnalytics = async (range = {}) => {
FROM orders FROM orders
WHERE data_pedido_date IS NOT NULL WHERE data_pedido_date IS NOT NULL
AND data_pedido_date <= ${recencyReferenceDate} AND data_pedido_date <= ${recencyReferenceDate}
AND cliente_fone IS NOT NULL GROUP BY customer_key;
AND cliente_fone != ''
GROUP BY cliente_fone;
`, historyParams) `, historyParams)
]); ]);
const historyClients = buildRfmClients(historyResult.rows.map(row => ({ const historyClients = buildRfmClients(historyResult.rows.map(row => ({
customerKey: row.customer_key,
name: row.name, name: row.name,
phone: row.phone, phone: row.phone || '',
monetary: toNumber(row.monetary), monetary: toNumber(row.monetary),
frequency: toNumber(row.frequency), frequency: toNumber(row.frequency),
quantityPurchased: toNumber(row.quantity_purchased), quantityPurchased: toNumber(row.quantity_purchased),
lastPurchaseDate: row.last_purchase_date, lastPurchaseDate: row.last_purchase_date,
recencyDays: toNumber(row.recency_days) recencyDays: toNumber(row.recency_days)
}))); })));
const tagsByPhone = new Map(historyClients.map(client => [client.phone, client])); const tagsByCustomerKey = new Map(historyClients.map(client => [client.customerKey, client]));
const clients = periodResult.rows.map(row => { const clients = periodResult.rows.map(row => {
const taggedClient = tagsByPhone.get(row.phone); const taggedClient = tagsByCustomerKey.get(row.customer_key);
if (!taggedClient) { if (!taggedClient) {
const newCustomerSegment = getRfmSegment(3, 1); const newCustomerSegment = getRfmSegment(3, 1);
return { return {
customerKey: row.customer_key,
name: row.name, name: row.name,
phone: row.phone, phone: row.phone || '',
monetary: toNumber(row.monetary), monetary: toNumber(row.monetary),
frequency: toNumber(row.frequency), frequency: toNumber(row.frequency),
quantityPurchased: toNumber(row.quantity_purchased), quantityPurchased: toNumber(row.quantity_purchased),
@@ -334,8 +337,9 @@ const getRfmAnalytics = async (range = {}) => {
return { return {
...taggedClient, ...taggedClient,
customerKey: row.customer_key,
name: row.name, name: row.name,
phone: row.phone, phone: row.phone || '',
monetary: toNumber(row.monetary), monetary: toNumber(row.monetary),
frequency: toNumber(row.frequency), frequency: toNumber(row.frequency),
quantityPurchased: toNumber(row.quantity_purchased), quantityPurchased: toNumber(row.quantity_purchased),

View File

@@ -204,6 +204,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected
return { return {
rows: [ rows: [
{ {
customer_key: '1',
name: 'Cliente Ontem', name: 'Cliente Ontem',
phone: '1', phone: '1',
monetary: 5000, monetary: 5000,
@@ -213,6 +214,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected
recency_days: endDate === '2026-06-13' ? 0 : 1 recency_days: endDate === '2026-06-13' ? 0 : 1
}, },
{ {
customer_key: '2',
name: 'Cliente Antigo', name: 'Cliente Antigo',
phone: '2', phone: '2',
monetary: 50, monetary: 50,
@@ -222,6 +224,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected
recency_days: 164 recency_days: 164
}, },
{ {
customer_key: '3',
name: 'Cliente Medio', name: 'Cliente Medio',
phone: '3', phone: '3',
monetary: 100, monetary: 100,
@@ -237,6 +240,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected
return { return {
rows: [ rows: [
{ {
customer_key: '1',
name: 'Cliente Ontem', name: 'Cliente Ontem',
phone: '1', phone: '1',
monetary: 100, monetary: 100,
@@ -245,6 +249,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected
last_purchase_date: '2026-06-14' last_purchase_date: '2026-06-14'
}, },
{ {
customer_key: '4',
name: 'Cliente Novo no Periodo', name: 'Cliente Novo no Periodo',
phone: '4', phone: '4',
monetary: 25, monetary: 25,

View File

@@ -7,7 +7,6 @@ import DateRangePicker from '../components/DateRangePicker';
import type { ClientSortOption, ClientSummary } from '../analytics/clients'; import type { ClientSortOption, ClientSummary } from '../analytics/clients';
const clientTypeStyles: Record<string, string> = { const clientTypeStyles: Record<string, string> = {
'Sem análise': 'border-zinc-600/30 bg-zinc-600/15 text-zinc-300',
'Campeão': 'border-emerald-500/30 bg-emerald-500/15 text-emerald-300', 'Campeão': 'border-emerald-500/30 bg-emerald-500/15 text-emerald-300',
'Potencial Leal': 'border-sky-500/30 bg-sky-500/15 text-sky-300', 'Potencial Leal': 'border-sky-500/30 bg-sky-500/15 text-sky-300',
'Novo Cliente': 'border-cyan-500/30 bg-cyan-500/15 text-cyan-300', 'Novo Cliente': 'border-cyan-500/30 bg-cyan-500/15 text-cyan-300',
@@ -20,7 +19,6 @@ const clientTypeStyles: Record<string, string> = {
}; };
const clientTypes = [ const clientTypes = [
'Sem análise',
'Campeão', 'Campeão',
'Potencial Leal', 'Potencial Leal',
'Novo Cliente', 'Novo Cliente',
@@ -102,9 +100,9 @@ const Clients = () => {
const allClientsData = useMemo(() => { const allClientsData = useMemo(() => {
const normalizedSearch = searchTerm.trim().toLowerCase(); const normalizedSearch = searchTerm.trim().toLowerCase();
const rfmByPhone = new Map((rfmAnalytics?.clients || []).map(client => [client.phone, client])); const rfmByCustomerKey = new Map((rfmAnalytics?.clients || []).map(client => [client.customerKey, client]));
const clients = clientAnalytics.map((client): ClientSummary => { const clients = clientAnalytics.map((client): ClientSummary => {
const rfmClient = client.phone ? rfmByPhone.get(client.phone) : undefined; const rfmClient = rfmByCustomerKey.get(client.customerKey);
return { return {
name: client.name, name: client.name,
phone: client.phone, phone: client.phone,
@@ -113,7 +111,7 @@ const Clients = () => {
totalItems: client.quantityPurchased, totalItems: client.quantityPurchased,
orderCount: client.orderCount, orderCount: client.orderCount,
lastPurchase: client.lastPurchaseDate ? new Date(client.lastPurchaseDate).getTime() : 0, lastPurchase: client.lastPurchaseDate ? new Date(client.lastPurchaseDate).getTime() : 0,
clientType: rfmClient ? (backendSegmentToClientType[rfmClient.segmentKey] || rfmClient.segmentLabel) : 'Sem análise', clientType: rfmClient ? (backendSegmentToClientType[rfmClient.segmentKey] || rfmClient.segmentLabel) : 'Perdido',
rfmScore: rfmClient?.rfmScore || '000', rfmScore: rfmClient?.rfmScore || '000',
rfmPriority: rfmClient ? getRfmPriority(rfmClient) : 0 rfmPriority: rfmClient ? getRfmPriority(rfmClient) : 0
}; };

View File

@@ -64,6 +64,7 @@ export interface DashboardAnalytics {
} }
export interface ClientAnalyticsItem { export interface ClientAnalyticsItem {
customerKey: string;
name: string; name: string;
phone: string; phone: string;
quantityPurchased: number; quantityPurchased: number;
@@ -73,6 +74,7 @@ export interface ClientAnalyticsItem {
} }
export interface RfmClient { export interface RfmClient {
customerKey: string;
name: string; name: string;
phone: string; phone: string;
monetary: number; monetary: number;