Limit RFV history query to period buyers
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m2s

This commit is contained in:
Cauê Faleiros
2026-06-17 15:26:24 -03:00
parent 3a907b0743
commit 7764933fd9
3 changed files with 202 additions and 142 deletions

View File

@@ -1,5 +1,6 @@
const { pool } = require('../db');
const RFM_QUERY_TIMEOUT_MS = 15000;
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 PRODUCT_NAME_SQL = `
CASE
@@ -263,11 +264,13 @@ const getClientAnalytics = async (range = {}) => {
const getRfmAnalytics = async (range = {}) => {
const { params, whereClause } = buildDateFilter(range);
const normalizedEnd = normalizeDateParam(range.end);
const recencyReferenceDate = normalizedEnd ? '$1::date' : 'CURRENT_DATE';
const historyParams = normalizedEnd ? [normalizedEnd] : [];
const client = await pool.connect();
const [periodResult, historyResult] = await Promise.all([
pool.query(`
try {
await client.query('BEGIN');
await client.query(`SET LOCAL statement_timeout = '${RFM_QUERY_TIMEOUT_MS}ms'`);
const periodResult = await client.query(`
SELECT
${CUSTOMER_KEY_SQL} as customer_key,
MAX(COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido')) as name,
@@ -280,8 +283,37 @@ const getRfmAnalytics = async (range = {}) => {
${whereClause}
GROUP BY customer_key
ORDER BY monetary DESC;
`, params),
pool.query(`
`, params);
if (!periodResult.rows.length) {
await client.query('COMMIT');
return {
range: {
start: normalizeDateParam(range.start),
end: normalizeDateParam(range.end)
},
clients: [],
segments: buildRfmSegments([]),
matrix: {
recencyScores: [3, 2, 1],
valueScores: [1, 2, 3]
}
};
}
const periodPhones = [...new Set(periodResult.rows.map(row => row.phone).filter(Boolean))];
const periodNamesWithoutPhone = [...new Set(periodResult.rows
.filter(row => !row.phone && String(row.customer_key || '').startsWith('name:'))
.map(row => String(row.customer_key).slice(5))
.filter(Boolean))];
const historyParams = [];
const recencyReferenceDate = normalizedEnd
? `$${historyParams.push(normalizedEnd)}::date`
: 'CURRENT_DATE';
const phoneListParam = `$${historyParams.push(periodPhones)}::text[]`;
const nameListParam = `$${historyParams.push(periodNamesWithoutPhone)}::text[]`;
const historyResult = await client.query(`
SELECT
${CUSTOMER_KEY_SQL} as customer_key,
MAX(COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido')) as name,
@@ -294,73 +326,87 @@ const getRfmAnalytics = async (range = {}) => {
FROM orders
WHERE data_pedido_date IS NOT NULL
AND data_pedido_date <= ${recencyReferenceDate}
AND (
NULLIF(cliente_fone, '') = ANY(${phoneListParam})
OR (
NULLIF(cliente_fone, '') IS NULL
AND COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido') = ANY(${nameListParam})
)
)
GROUP BY customer_key;
`, historyParams)
]);
`, historyParams);
const historyClients = buildRfmClients(historyResult.rows.map(row => ({
customerKey: row.customer_key,
name: row.name,
phone: row.phone || '',
monetary: toNumber(row.monetary),
frequency: toNumber(row.frequency),
quantityPurchased: toNumber(row.quantity_purchased),
lastPurchaseDate: row.last_purchase_date,
recencyDays: toNumber(row.recency_days)
})));
const tagsByCustomerKey = new Map(historyClients.map(client => [client.customerKey, client]));
const clients = periodResult.rows.map(row => {
const taggedClient = tagsByCustomerKey.get(row.customer_key);
if (!taggedClient) {
const newCustomerSegment = getRfmSegment(3, 1);
return {
customerKey: row.customer_key,
name: row.name,
phone: row.phone || '',
monetary: toNumber(row.monetary),
frequency: toNumber(row.frequency),
quantityPurchased: toNumber(row.quantity_purchased),
lastPurchaseDate: row.last_purchase_date,
recencyDays: 0,
recencyScore: 3,
frequencyScore: 1,
monetaryScore: 1,
valueScore: 1,
rfmScore: '311',
segmentKey: newCustomerSegment.key,
segmentLabel: newCustomerSegment.label
};
}
return {
...taggedClient,
const historyClients = buildRfmClients(historyResult.rows.map(row => ({
customerKey: row.customer_key,
name: row.name,
phone: row.phone || '',
monetary: toNumber(row.monetary),
frequency: toNumber(row.frequency),
quantityPurchased: toNumber(row.quantity_purchased),
lastPurchaseDate: row.last_purchase_date
};
}).sort((a, b) => {
if (b.recencyScore !== a.recencyScore) return b.recencyScore - a.recencyScore;
if (b.valueScore !== a.valueScore) return b.valueScore - a.valueScore;
return b.monetary - a.monetary;
});
lastPurchaseDate: row.last_purchase_date,
recencyDays: toNumber(row.recency_days)
})));
const tagsByCustomerKey = new Map(historyClients.map(historyClient => [historyClient.customerKey, historyClient]));
return {
range: {
start: normalizeDateParam(range.start),
end: normalizeDateParam(range.end)
},
clients,
segments: buildRfmSegments(clients),
matrix: {
recencyScores: [3, 2, 1],
valueScores: [1, 2, 3]
}
};
const clients = periodResult.rows.map(row => {
const taggedClient = tagsByCustomerKey.get(row.customer_key);
if (!taggedClient) {
const newCustomerSegment = getRfmSegment(3, 1);
return {
customerKey: row.customer_key,
name: row.name,
phone: row.phone || '',
monetary: toNumber(row.monetary),
frequency: toNumber(row.frequency),
quantityPurchased: toNumber(row.quantity_purchased),
lastPurchaseDate: row.last_purchase_date,
recencyDays: 0,
recencyScore: 3,
frequencyScore: 1,
monetaryScore: 1,
valueScore: 1,
rfmScore: '311',
segmentKey: newCustomerSegment.key,
segmentLabel: newCustomerSegment.label
};
}
return {
...taggedClient,
customerKey: row.customer_key,
name: row.name,
phone: row.phone || '',
monetary: toNumber(row.monetary),
frequency: toNumber(row.frequency),
quantityPurchased: toNumber(row.quantity_purchased),
lastPurchaseDate: row.last_purchase_date
};
}).sort((a, b) => {
if (b.recencyScore !== a.recencyScore) return b.recencyScore - a.recencyScore;
if (b.valueScore !== a.valueScore) return b.valueScore - a.valueScore;
return b.monetary - a.monetary;
});
await client.query('COMMIT');
return {
range: {
start: normalizeDateParam(range.start),
end: normalizeDateParam(range.end)
},
clients,
segments: buildRfmSegments(clients),
matrix: {
recencyScores: [3, 2, 1],
valueScores: [1, 2, 3]
}
};
} catch (error) {
await client.query('ROLLBACK').catch(() => {});
throw error;
} finally {
client.release();
}
};
module.exports = {