Deduplicate top campaign clients by name
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m54s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m54s
This commit is contained in:
@@ -17,6 +17,32 @@ const MAX_CAMPAIGN_ATTEMPTS = 3;
|
|||||||
const CAMPAIGN_DELTA_THRESHOLD = 100;
|
const CAMPAIGN_DELTA_THRESHOLD = 100;
|
||||||
const SAO_PAULO_TIME_ZONE = 'America/Sao_Paulo';
|
const SAO_PAULO_TIME_ZONE = 'America/Sao_Paulo';
|
||||||
const NORMALIZED_CUSTOMER_NAME_SQL = "NULLIF(LOWER(TRIM(regexp_replace(COALESCE(cliente_nome, ''), '\\s+', ' ', 'g'))), '')";
|
const NORMALIZED_CUSTOMER_NAME_SQL = "NULLIF(LOWER(TRIM(regexp_replace(COALESCE(cliente_nome, ''), '\\s+', ' ', 'g'))), '')";
|
||||||
|
const NORMALIZED_CUSTOMER_PHONE_SQL = "NULLIF(regexp_replace(COALESCE(cliente_fone, ''), '\\D', '', 'g'), '')";
|
||||||
|
const WHATSAPP_CUSTOMER_PHONE_SQL = `
|
||||||
|
CASE
|
||||||
|
WHEN ${NORMALIZED_CUSTOMER_PHONE_SQL} LIKE '55%' THEN ${NORMALIZED_CUSTOMER_PHONE_SQL}
|
||||||
|
WHEN length(${NORMALIZED_CUSTOMER_PHONE_SQL}) IN (10, 11) THEN '55' || ${NORMALIZED_CUSTOMER_PHONE_SQL}
|
||||||
|
ELSE ${NORMALIZED_CUSTOMER_PHONE_SQL}
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
const CANONICAL_CAMPAIGN_CUSTOMER_NAME_SQL = `
|
||||||
|
NULLIF(TRIM(regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(LOWER(COALESCE(cliente_nome, '')), '[^[:alnum:][:space:]]+', ' ', 'g'),
|
||||||
|
'(^|[[:space:]])[0-9]{2,14}([[:space:]]|$)',
|
||||||
|
' ',
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
'(^|[[:space:]])(ltda|me|eireli|epp)([[:space:]]|$)',
|
||||||
|
' ',
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
'[[:space:]]+',
|
||||||
|
' ',
|
||||||
|
'g'
|
||||||
|
)), '')
|
||||||
|
`;
|
||||||
const CUSTOMER_IDENTITY_CTE = `
|
const CUSTOMER_IDENTITY_CTE = `
|
||||||
WITH customer_phone_by_name AS (
|
WITH customer_phone_by_name AS (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -148,19 +174,32 @@ const getTopClientsForCampaign = async ({ days, limit, start, end } = {}) => {
|
|||||||
const range = getTopClientsDateRange({ days, start, end });
|
const range = getTopClientsDateRange({ days, start, end });
|
||||||
const normalizedLimit = parsePositiveInteger(limit, TOP_CLIENTS_DEFAULT_LIMIT, TOP_CLIENTS_MAX_LIMIT);
|
const normalizedLimit = parsePositiveInteger(limit, TOP_CLIENTS_DEFAULT_LIMIT, TOP_CLIENTS_MAX_LIMIT);
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
${CUSTOMER_IDENTITY_CTE}
|
WITH campaign_orders AS (
|
||||||
|
SELECT
|
||||||
|
orders.*,
|
||||||
|
${CANONICAL_CAMPAIGN_CUSTOMER_NAME_SQL} as canonical_customer_name,
|
||||||
|
${WHATSAPP_CUSTOMER_PHONE_SQL} as whatsapp_phone
|
||||||
|
FROM orders
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
MAX(COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido')) as nome,
|
(
|
||||||
customer_key as fone,
|
ARRAY_AGG(COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido')
|
||||||
|
ORDER BY data_pedido_date DESC NULLS LAST, id DESC)
|
||||||
|
)[1] as nome,
|
||||||
|
(
|
||||||
|
ARRAY_AGG(whatsapp_phone ORDER BY data_pedido_date DESC NULLS LAST, id DESC)
|
||||||
|
FILTER (WHERE whatsapp_phone IS NOT NULL)
|
||||||
|
)[1] as fone,
|
||||||
COALESCE(SUM(quantidade * valor_unitario), 0) as total_gasto,
|
COALESCE(SUM(quantidade * valor_unitario), 0) as total_gasto,
|
||||||
COALESCE(SUM(quantidade), 0) as total_comprado,
|
COALESCE(SUM(quantidade), 0) as total_comprado,
|
||||||
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as total_pedidos,
|
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as total_pedidos,
|
||||||
MAX(data_pedido_date) as ultima_compra
|
MAX(data_pedido_date) as ultima_compra,
|
||||||
FROM identity_orders
|
ARRAY_REMOVE(ARRAY_AGG(DISTINCT whatsapp_phone), NULL) as telefones
|
||||||
|
FROM campaign_orders
|
||||||
WHERE data_pedido_date >= $1::date
|
WHERE data_pedido_date >= $1::date
|
||||||
AND data_pedido_date <= $2::date
|
AND data_pedido_date <= $2::date
|
||||||
AND customer_key NOT LIKE 'name:%'
|
AND whatsapp_phone IS NOT NULL
|
||||||
GROUP BY customer_key
|
GROUP BY COALESCE(canonical_customer_name, whatsapp_phone)
|
||||||
ORDER BY total_gasto DESC
|
ORDER BY total_gasto DESC
|
||||||
LIMIT $3;
|
LIMIT $3;
|
||||||
`, [range.start, range.end, normalizedLimit]);
|
`, [range.start, range.end, normalizedLimit]);
|
||||||
@@ -171,7 +210,8 @@ const getTopClientsForCampaign = async ({ days, limit, start, end } = {}) => {
|
|||||||
total_gasto: Number(row.total_gasto || 0),
|
total_gasto: Number(row.total_gasto || 0),
|
||||||
total_comprado: Number(row.total_comprado || 0),
|
total_comprado: Number(row.total_comprado || 0),
|
||||||
total_pedidos: Number(row.total_pedidos || 0),
|
total_pedidos: Number(row.total_pedidos || 0),
|
||||||
ultima_compra: row.ultima_compra
|
ultima_compra: row.ultima_compra,
|
||||||
|
telefones: Array.isArray(row.telefones) ? row.telefones : []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ test('getTopClientsForCampaign returns top clients for an explicit date range',
|
|||||||
total_gasto: '1234.50',
|
total_gasto: '1234.50',
|
||||||
total_comprado: '18',
|
total_comprado: '18',
|
||||||
total_pedidos: 4,
|
total_pedidos: 4,
|
||||||
ultima_compra: '2026-07-27'
|
ultima_compra: '2026-07-27',
|
||||||
|
telefones: ['5516999999901', '5516999999902']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}), async ({ getTopClientsForCampaign }, queries) => {
|
}), async ({ getTopClientsForCampaign }, queries) => {
|
||||||
@@ -68,12 +69,14 @@ test('getTopClientsForCampaign returns top clients for an explicit date range',
|
|||||||
total_gasto: 1234.5,
|
total_gasto: 1234.5,
|
||||||
total_comprado: 18,
|
total_comprado: 18,
|
||||||
total_pedidos: 4,
|
total_pedidos: 4,
|
||||||
ultima_compra: '2026-07-27'
|
ultima_compra: '2026-07-27',
|
||||||
|
telefones: ['5516999999901', '5516999999902']
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(queries.length, 1);
|
assert.equal(queries.length, 1);
|
||||||
assert.deepEqual(queries[0].params, ['2026-06-28', '2026-07-27', 1000]);
|
assert.deepEqual(queries[0].params, ['2026-06-28', '2026-07-27', 1000]);
|
||||||
assert.match(queries[0].sql, /customer_key NOT LIKE 'name:%'/);
|
assert.match(queries[0].sql, /GROUP BY COALESCE\(canonical_customer_name, whatsapp_phone\)/);
|
||||||
|
assert.match(queries[0].sql, /ARRAY_AGG\(DISTINCT whatsapp_phone\)/);
|
||||||
assert.match(queries[0].sql, /ORDER BY total_gasto DESC/);
|
assert.match(queries[0].sql, /ORDER BY total_gasto DESC/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -91,3 +94,17 @@ test('getTopClientsForCampaign derives an inclusive 30 day range from the end da
|
|||||||
assert.deepEqual(queries[0].params, ['2026-06-28', '2026-07-27', 1000]);
|
assert.deepEqual(queries[0].params, ['2026-06-28', '2026-07-27', 1000]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getTopClientsForCampaign normalizes phones and ranks one row per canonical client name', async () => {
|
||||||
|
await withCampaignService(async () => ({ rows: [] }), async ({ getTopClientsForCampaign }, queries) => {
|
||||||
|
await getTopClientsForCampaign({
|
||||||
|
days: '30',
|
||||||
|
end: '2026-07-27'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.match(queries[0].sql, /regexp_replace\(COALESCE\(cliente_fone, ''\), '\\D', '', 'g'\)/);
|
||||||
|
assert.match(queries[0].sql, /WHEN length\(/);
|
||||||
|
assert.match(queries[0].sql, /'55' \|\|/);
|
||||||
|
assert.match(queries[0].sql, /canonical_customer_name/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user