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'); .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) => { const formatProductNameForDisplay = (name) => {
return applyProductDisplayAlias(name) return applyProductDisplayAlias(name)
.toLocaleLowerCase('pt-BR') .toLocaleLowerCase('pt-BR')
@@ -103,5 +122,6 @@ module.exports = {
formatProductList, formatProductList,
groupCampaignRows, groupCampaignRows,
groupCampaignRowsByBaseProduct, groupCampaignRowsByBaseProduct,
isCampaignEligibleProductName,
mapCampaignProducts mapCampaignProducts
}; };

View File

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

View File

@@ -6,6 +6,7 @@ const {
formatProductNameForDisplay, formatProductNameForDisplay,
groupCampaignRows, groupCampaignRows,
groupCampaignRowsByBaseProduct, groupCampaignRowsByBaseProduct,
isCampaignEligibleProductName,
mapCampaignProducts mapCampaignProducts
} = require('../services/campaignFormatter'); } = require('../services/campaignFormatter');
@@ -58,6 +59,36 @@ test('formatProductNameForDisplay applies customer-facing campaign aliases', ()
); );
}); });
test('isCampaignEligibleProductName allows only customer-facing apparel campaigns', () => {
[
'Camiseta Premium Cor Bordo',
'Camiseta Premium Cor Preto',
'Moletom Canguru Premium Cor Preto',
'BASE LISA CAMISETA COR PRETO TAMANHO - G',
'BASE LISA MOLETOM CANGURU COR PRETO'
].forEach(name => {
assert.equal(isCampaignEligibleProductName(name), true, name);
});
[
'Ilhos Com Arruela',
'Atacador 001 Chato Preto 1,20 Mt',
'2099 Ribana 2x1 Cor Bordo',
'2001.09 Ribana 2x1 Cor Cinza',
'2001.09 Malha Camiseta 30oe Cor Cinza',
'201 Malha Camiseta 30oe Cor Preto',
'4006 Malha Camiseta 30oe Cor Marinho',
'IMPRESSÃO DTF PERSONALIZADO 57X100 (1 METRO)',
'SALDO ESTOQUE CAMISETA'
].forEach(name => {
assert.equal(isCampaignEligibleProductName(name), false, name);
});
});
test('isCampaignEligibleProductName keeps accessories out of WhatsApp apparel campaigns', () => {
assert.equal(isCampaignEligibleProductName('BONÉ - PRETO'), false);
});
test('mapCampaignProducts accumulates split deltas by base product', () => { test('mapCampaignProducts accumulates split deltas by base product', () => {
const groups = groupCampaignRowsByBaseProduct([ const groups = groupCampaignRowsByBaseProduct([
row({ id: 1, delta_estoque: 10, produto_id: 'SKU-P', nome: 'Produto Split TAMANHO - P' }), row({ id: 1, delta_estoque: 10, produto_id: 'SKU-P', nome: 'Produto Split TAMANHO - P' }),