Filter campaign products to apparel

This commit is contained in:
Cauê Faleiros
2026-07-22 11:22:49 -03:00
parent 9c77475ce5
commit c07264fd69
3 changed files with 80 additions and 4 deletions

View File

@@ -6,6 +6,25 @@ const applyProductDisplayAlias = (name) => {
.replace(/^BASE LISA MOLETOM CANGURU\b/i, 'MOLETOM CANGURU PREMIUM');
};
const normalizeCampaignProductName = (name) => String(name || '')
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.replace(/\s+/g, ' ')
.trim()
.toUpperCase();
const isCampaignEligibleProductName = (name) => {
const normalizedName = normalizeCampaignProductName(name);
if (!normalizedName) return false;
if (/^(?:\d+(?:\.\d+)?\s+)?(?:MALHA|RIBANA)\b/.test(normalizedName)) return false;
if (/\b(?:MALHA|RIBANA|ATACADOR|ILHOS|ETIQUETA|TAG|FITA|LINHA PARA COSTURA|FIO|TECIDO|RETALHO|RESIDUO)\b/.test(normalizedName)) return false;
if (/\b(?:SALDO ESTOQUE|FRETE|SERVICO|TRANSPORTE|TECELAGEM|TINTURARIA|DTF|IMPRESSAO|PERSONALIZADO)\b/.test(normalizedName)) return false;
if (/\b(?:PRENSA|MAQUINA|OVERLOCK|OVERLOK|GALONEIRA|PRATELEIRA|CABO FLAT|WIPPER|PRIMER|FLUIDO|SENSOR|FILTRO)\b/.test(normalizedName)) return false;
return /\b(?:CAMISETA|CAMISA|MOLETOM|CANGURU|REGATA|OVERSIZE|OVER SIZE)\b/.test(normalizedName);
};
const formatProductNameForDisplay = (name) => {
return applyProductDisplayAlias(name)
.toLocaleLowerCase('pt-BR')
@@ -103,5 +122,6 @@ module.exports = {
formatProductList,
groupCampaignRows,
groupCampaignRowsByBaseProduct,
isCampaignEligibleProductName,
mapCampaignProducts
};

View File

@@ -5,6 +5,7 @@ const {
formatProductList,
groupCampaignRows,
groupCampaignRowsByBaseProduct,
isCampaignEligibleProductName,
mapCampaignProducts
} = require('./campaignFormatter');
@@ -13,6 +14,10 @@ const MAX_CAMPAIGN_ATTEMPTS = 3;
const CAMPAIGN_DELTA_THRESHOLD = 100;
const enqueueStockCampaignItem = async (client, item) => {
if (!isCampaignEligibleProductName(item.baseProductName || item.nome)) {
return false;
}
const query = `
INSERT INTO stock_campaign_queue (
base_product_name, produto_id, nome, saldo, delta_estoque
@@ -26,6 +31,8 @@ const enqueueStockCampaignItem = async (client, item) => {
item.saldo,
item.deltaEstoque
]);
return true;
};
const getTopBuyersAllTime = async () => {
@@ -117,7 +124,8 @@ const getCampaignQueueRows = async () => {
};
const getCampaignQueueSummary = async () => {
const rows = await getCampaignQueueRows();
const rows = (await getCampaignQueueRows())
.filter(row => isCampaignEligibleProductName(row.base_product_name || row.nome));
return {
threshold: CAMPAIGN_DELTA_THRESHOLD,
maxAttempts: MAX_CAMPAIGN_ATTEMPTS,
@@ -134,7 +142,8 @@ const getCampaignPreview = async () => {
AND attempts < $1
ORDER BY created_at ASC, id ASC;
`, [MAX_CAMPAIGN_ATTEMPTS]);
const groups = groupCampaignRowsByBaseProduct(result.rows);
const eligibleRows = result.rows.filter(row => isCampaignEligibleProductName(row.base_product_name || row.nome));
const groups = groupCampaignRowsByBaseProduct(eligibleRows);
const readyGroups = {};
const belowThresholdGroups = {};
@@ -205,6 +214,15 @@ const updateCampaignItemsStatus = async (ids, status, errorMessage = null) => {
`, [status, errorMessage, ids]);
};
const skipIneligibleCampaignItems = async (rows) => {
const ineligibleRows = rows.filter(row => !isCampaignEligibleProductName(row.base_product_name || row.nome));
const ids = ineligibleRows.map(row => row.id);
await updateCampaignItemsStatus(ids, 'skipped', 'Produto não elegível para campanha de cliente.');
return new Set(ineligibleRows.map(row => row.base_product_name)).size;
};
const sendWhatsappCampaign = async (products, customers) => {
const response = await fetch(N8N_WHATSAPP_TRIGGER_URL, {
method: 'POST',
@@ -231,14 +249,21 @@ const processPendingStockCampaigns = async () => {
return summary;
}
const groups = groupCampaignRowsByBaseProduct(rows);
summary.skippedGroups += await skipIneligibleCampaignItems(rows);
const eligibleRows = rows.filter(row => isCampaignEligibleProductName(row.base_product_name || row.nome));
const groups = groupCampaignRowsByBaseProduct(eligibleRows);
const products = mapCampaignProducts(groups);
const ids = products.flatMap(product => product.itemIds);
const customers = await getTopBuyersAllTime();
if (!products.length) {
return summary;
}
if (!customers.length) {
await updateCampaignItemsStatus(ids, 'skipped', 'No customers with valid phone numbers found.');
summary.skippedGroups = products.length;
summary.skippedGroups += products.length;
return summary;
}