Compare commits
3 Commits
eaa0a5010d
...
615e07ad32
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
615e07ad32 | ||
|
|
c07264fd69 | ||
|
|
9c77475ce5 |
@@ -5,6 +5,29 @@ const pool = new Pool({
|
||||
connectionString: DATABASE_URL
|
||||
});
|
||||
|
||||
const defaultCatalogCategories = [
|
||||
['Camiseta regular', 'Bases lisas e camisetas adultas.'],
|
||||
['Camiseta infantil', 'Produtos infantis por cor e tamanho.'],
|
||||
['Moletom', 'Moletons, cangurus e produtos de frio.'],
|
||||
['Oversized', 'Modelagens oversized e variações relacionadas.'],
|
||||
['Acessórios', 'Bonés, itens complementares e produtos não têxteis.'],
|
||||
['DTF', 'Insumos e serviços relacionados a impressão DTF.'],
|
||||
['Malha', 'Tecidos e malhas usados como matéria-prima.'],
|
||||
['Aviamentos', 'Ribanas, linhas, ilhós e componentes de costura.'],
|
||||
['Embalagens', 'Sacos, etiquetas, tags e materiais de expedição.'],
|
||||
['Insumos gerais', 'Materiais de apoio sem família operacional específica.']
|
||||
];
|
||||
|
||||
const seedDefaultCatalogCategories = async () => {
|
||||
for (const [name, description] of defaultCatalogCategories) {
|
||||
await pool.query(`
|
||||
INSERT INTO catalog_categories (name, description, updated_at)
|
||||
VALUES ($1, $2, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
`, [name, description]);
|
||||
}
|
||||
};
|
||||
|
||||
const initDB = async () => {
|
||||
try {
|
||||
await pool.query(`SET TIME ZONE 'America/Sao_Paulo';`);
|
||||
@@ -302,6 +325,8 @@ const initDB = async () => {
|
||||
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
|
||||
`).catch(() => {});
|
||||
|
||||
await seedDefaultCatalogCategories();
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE supply_receipts
|
||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
|
||||
@@ -6,6 +6,26 @@ 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(?:TINTA|FILME|POLIAMIDA|PO PARA DTF|ROLO DTF|FLUIDO|PRIMER|CABECA|SENSOR|FILTRO|BOMBA)\b.*\bDTF\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)\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(?:DTF|CAMISETA|CAMISA|MOLETOM|CANGURU|REGATA|OVERSIZE|OVER SIZE)\b/.test(normalizedName);
|
||||
};
|
||||
|
||||
const formatProductNameForDisplay = (name) => {
|
||||
return applyProductDisplayAlias(name)
|
||||
.toLocaleLowerCase('pt-BR')
|
||||
@@ -103,5 +123,6 @@ module.exports = {
|
||||
formatProductList,
|
||||
groupCampaignRows,
|
||||
groupCampaignRowsByBaseProduct,
|
||||
isCampaignEligibleProductName,
|
||||
mapCampaignProducts
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const {
|
||||
formatProductNameForDisplay,
|
||||
groupCampaignRows,
|
||||
groupCampaignRowsByBaseProduct,
|
||||
isCampaignEligibleProductName,
|
||||
mapCampaignProducts
|
||||
} = require('../services/campaignFormatter');
|
||||
|
||||
@@ -58,6 +59,39 @@ 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',
|
||||
'IMPRESSÃO DTF PERSONALIZADO 57X100 (1 METRO)',
|
||||
'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',
|
||||
'TINTA DTF 1 LITRO - BRANCO',
|
||||
'FILME DTF ROLO 60CM',
|
||||
'POLIAMIDA EM PÓ PARA DTF - 1 KG',
|
||||
'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', () => {
|
||||
const groups = groupCampaignRowsByBaseProduct([
|
||||
row({ id: 1, delta_estoque: 10, produto_id: 'SKU-P', nome: 'Produto Split TAMANHO - P' }),
|
||||
|
||||
Reference in New Issue
Block a user