Add top campaign clients endpoint
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m39s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m39s
This commit is contained in:
@@ -10,8 +10,99 @@ const {
|
||||
} = require('./campaignFormatter');
|
||||
|
||||
const TOP_BUYERS_LIMIT = 100;
|
||||
const TOP_CLIENTS_DEFAULT_DAYS = 30;
|
||||
const TOP_CLIENTS_DEFAULT_LIMIT = 1000;
|
||||
const TOP_CLIENTS_MAX_LIMIT = 5000;
|
||||
const MAX_CAMPAIGN_ATTEMPTS = 3;
|
||||
const CAMPAIGN_DELTA_THRESHOLD = 100;
|
||||
const SAO_PAULO_TIME_ZONE = 'America/Sao_Paulo';
|
||||
const NORMALIZED_CUSTOMER_NAME_SQL = "NULLIF(LOWER(TRIM(regexp_replace(COALESCE(cliente_nome, ''), '\\s+', ' ', 'g'))), '')";
|
||||
const CUSTOMER_IDENTITY_CTE = `
|
||||
WITH customer_phone_by_name AS (
|
||||
SELECT
|
||||
${NORMALIZED_CUSTOMER_NAME_SQL} as normalized_customer_name,
|
||||
(ARRAY_AGG(NULLIF(cliente_fone, '') ORDER BY data_pedido_date DESC NULLS LAST, id DESC)
|
||||
)[1] as canonical_phone
|
||||
FROM orders
|
||||
WHERE NULLIF(cliente_fone, '') IS NOT NULL
|
||||
AND ${NORMALIZED_CUSTOMER_NAME_SQL} IS NOT NULL
|
||||
GROUP BY normalized_customer_name
|
||||
),
|
||||
identity_orders AS (
|
||||
SELECT
|
||||
orders.*,
|
||||
${NORMALIZED_CUSTOMER_NAME_SQL} as normalized_customer_name,
|
||||
COALESCE(
|
||||
NULLIF(orders.cliente_fone, ''),
|
||||
customer_phone_by_name.canonical_phone,
|
||||
'name:' || COALESCE(NULLIF(orders.cliente_nome, ''), 'Cliente Desconhecido')
|
||||
) as customer_key
|
||||
FROM orders
|
||||
LEFT JOIN customer_phone_by_name
|
||||
ON customer_phone_by_name.normalized_customer_name = ${NORMALIZED_CUSTOMER_NAME_SQL}
|
||||
)
|
||||
`;
|
||||
|
||||
const normalizeDateParam = (value) => {
|
||||
if (!value) return null;
|
||||
|
||||
const match = String(value).trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) return null;
|
||||
|
||||
const [, yearValue, monthValue, dayValue] = match;
|
||||
const year = Number(yearValue);
|
||||
const month = Number(monthValue);
|
||||
const day = Number(dayValue);
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
|
||||
if (
|
||||
date.getUTCFullYear() !== year ||
|
||||
date.getUTCMonth() !== month - 1 ||
|
||||
date.getUTCDate() !== day
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${yearValue}-${monthValue}-${dayValue}`;
|
||||
};
|
||||
|
||||
const parsePositiveInteger = (value, defaultValue, maxValue) => {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 1) return defaultValue;
|
||||
return Math.min(parsed, maxValue);
|
||||
};
|
||||
|
||||
const getDateStringInTimeZone = (date = new Date(), timeZone = SAO_PAULO_TIME_ZONE) => {
|
||||
const parts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}).formatToParts(date);
|
||||
const partMap = Object.fromEntries(parts.map(part => [part.type, part.value]));
|
||||
|
||||
return `${partMap.year}-${partMap.month}-${partMap.day}`;
|
||||
};
|
||||
|
||||
const subtractDaysFromDateString = (dateString, daysToSubtract) => {
|
||||
const [year, month, day] = dateString.split('-').map(Number);
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
date.setUTCDate(date.getUTCDate() - daysToSubtract);
|
||||
|
||||
return date.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
const getTopClientsDateRange = ({ days = TOP_CLIENTS_DEFAULT_DAYS, start, end } = {}) => {
|
||||
const normalizedDays = parsePositiveInteger(days, TOP_CLIENTS_DEFAULT_DAYS, 3650);
|
||||
const normalizedEnd = normalizeDateParam(end) || getDateStringInTimeZone();
|
||||
const normalizedStart = normalizeDateParam(start) || subtractDaysFromDateString(normalizedEnd, normalizedDays - 1);
|
||||
|
||||
return {
|
||||
days: normalizedDays,
|
||||
start: normalizedStart,
|
||||
end: normalizedEnd
|
||||
};
|
||||
};
|
||||
|
||||
const enqueueStockCampaignItem = async (client, item) => {
|
||||
if (!isCampaignEligibleProductName(item.baseProductName || item.nome)) {
|
||||
@@ -53,6 +144,48 @@ const getTopBuyersAllTime = async () => {
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
const getTopClientsForCampaign = async ({ days, limit, start, end } = {}) => {
|
||||
const range = getTopClientsDateRange({ days, start, end });
|
||||
const normalizedLimit = parsePositiveInteger(limit, TOP_CLIENTS_DEFAULT_LIMIT, TOP_CLIENTS_MAX_LIMIT);
|
||||
const result = await pool.query(`
|
||||
${CUSTOMER_IDENTITY_CTE}
|
||||
SELECT
|
||||
MAX(COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido')) as nome,
|
||||
customer_key as fone,
|
||||
COALESCE(SUM(quantidade * valor_unitario), 0) as total_gasto,
|
||||
COALESCE(SUM(quantidade), 0) as total_comprado,
|
||||
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as total_pedidos,
|
||||
MAX(data_pedido_date) as ultima_compra
|
||||
FROM identity_orders
|
||||
WHERE data_pedido_date >= $1::date
|
||||
AND data_pedido_date <= $2::date
|
||||
AND customer_key NOT LIKE 'name:%'
|
||||
GROUP BY customer_key
|
||||
ORDER BY total_gasto DESC
|
||||
LIMIT $3;
|
||||
`, [range.start, range.end, normalizedLimit]);
|
||||
|
||||
const customers = result.rows.map(row => ({
|
||||
nome: row.nome,
|
||||
fone: row.fone,
|
||||
total_gasto: Number(row.total_gasto || 0),
|
||||
total_comprado: Number(row.total_comprado || 0),
|
||||
total_pedidos: Number(row.total_pedidos || 0),
|
||||
ultima_compra: row.ultima_compra
|
||||
}));
|
||||
|
||||
return {
|
||||
campaign: 'top_clients',
|
||||
days: range.days,
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
limit: normalizedLimit,
|
||||
count: customers.length,
|
||||
generated_at: new Date().toISOString(),
|
||||
customers
|
||||
};
|
||||
};
|
||||
|
||||
const claimReadyCampaignItems = async () => {
|
||||
const client = await pool.connect();
|
||||
|
||||
@@ -285,6 +418,7 @@ module.exports = {
|
||||
enqueueStockCampaignItem,
|
||||
getCampaignPreview,
|
||||
getCampaignQueueSummary,
|
||||
getTopClientsForCampaign,
|
||||
retryCampaignItems,
|
||||
processPendingStockCampaigns
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user