Filter campaign products to apparel
This commit is contained in:
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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' }),
|
||||||
|
|||||||
Reference in New Issue
Block a user