Compare commits

...

2 Commits

Author SHA1 Message Date
Cauê Faleiros
0d6ef40c8e fix: preserve etiqueta product variants
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 42s
2026-06-01 09:54:30 -03:00
Cauê Faleiros
fce7bbf975 fix: title case campaign product names 2026-06-01 09:47:35 -03:00
7 changed files with 62 additions and 11 deletions

View File

@@ -108,6 +108,16 @@ const initDB = async () => {
console.error('Notice: Could not normalize queued campaign product names:', err.message); console.error('Notice: Could not normalize queued campaign product names:', err.message);
}); });
await pool.query(`
UPDATE stock_campaign_queue
SET base_product_name = nome
WHERE status IN ('pending', 'failed', 'processing')
AND nome ILIKE 'ETIQUETA%'
AND base_product_name != nome;
`).catch(err => {
console.error('Notice: Could not restore queued etiqueta product names:', err.message);
});
await pool.query(`CREATE INDEX IF NOT EXISTS idx_stock_campaign_queue_status ON stock_campaign_queue (status);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_stock_campaign_queue_status ON stock_campaign_queue (status);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_cliente_fone ON orders (cliente_fone);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_cliente_fone ON orders (cliente_fone);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_produto_id ON orders (produto_id);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_produto_id ON orders (produto_id);`);

View File

@@ -1,7 +1,12 @@
const SIZE_SUFFIX_PATTERN = /\s+-\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\d{2})(?:\/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\d{2}))*)$/i; const SIZE_SUFFIX_PATTERN = /\s+-\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\d{2})(?:\/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\d{2}))*)$/i;
const getBaseProductName = (name) => { const getBaseProductName = (name) => {
return String(name || 'Unknown') const productName = String(name || 'Unknown').trim();
if (productName.toLocaleUpperCase('pt-BR').startsWith('ETIQUETA')) {
return productName;
}
return productName
.split(' TAMANHO')[0] .split(' TAMANHO')[0]
.replace(SIZE_SUFFIX_PATTERN, '') .replace(SIZE_SUFFIX_PATTERN, '')
.trim(); .trim();

View File

@@ -1,7 +1,12 @@
const { pool } = require('../db'); const { pool } = require('../db');
const SIZE_SUFFIX_SQL_PATTERN = '\\s+-\\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2})(?:/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2}))*)$'; const SIZE_SUFFIX_SQL_PATTERN = '\\s+-\\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2})(?:/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2}))*)$';
const PRODUCT_NAME_SQL = `NULLIF(TRIM(regexp_replace(split_part(COALESCE(produto_descricao, 'Unknown'), ' TAMANHO', 1), '${SIZE_SUFFIX_SQL_PATTERN}', '', 'i')), '')`; const PRODUCT_NAME_SQL = `
CASE
WHEN COALESCE(produto_descricao, 'Unknown') ILIKE 'ETIQUETA%' THEN COALESCE(produto_descricao, 'Unknown')
ELSE NULLIF(TRIM(regexp_replace(split_part(COALESCE(produto_descricao, 'Unknown'), ' TAMANHO', 1), '${SIZE_SUFFIX_SQL_PATTERN}', '', 'i')), '')
END
`;
const normalizeDateParam = (value) => { const normalizeDateParam = (value) => {
if (!value) return null; if (!value) return null;

View File

@@ -1,9 +1,19 @@
const formatProductNameForDisplay = (name) => {
return String(name || '')
.toLocaleLowerCase('pt-BR')
.replace(/(^|[\s/-])(\p{L})/gu, (_, separator, letter) => {
return `${separator}${letter.toLocaleUpperCase('pt-BR')}`;
});
};
const formatProductList = (productNames) => { const formatProductList = (productNames) => {
if (productNames.length <= 2) { const displayNames = productNames.map(formatProductNameForDisplay);
return productNames.join(' e ');
if (displayNames.length <= 2) {
return displayNames.join(' e ');
} }
return `${productNames.slice(0, -1).join(', ')} e ${productNames[productNames.length - 1]}`; return `${displayNames.slice(0, -1).join(', ')} e ${displayNames[displayNames.length - 1]}`;
}; };
const groupCampaignRowsByBaseProduct = (rows) => { const groupCampaignRowsByBaseProduct = (rows) => {
@@ -23,7 +33,7 @@ const mapCampaignProducts = (groups) => {
const sortedItems = [...items].sort((a, b) => String(a.nome).localeCompare(String(b.nome), 'pt-BR')); const sortedItems = [...items].sort((a, b) => String(a.nome).localeCompare(String(b.nome), 'pt-BR'));
return { return {
baseProduct: baseProductName, baseProduct: formatProductNameForDisplay(baseProductName),
total_delta: sortedItems.reduce((sum, item) => sum + Number(item.delta_estoque || 0), 0), total_delta: sortedItems.reduce((sum, item) => sum + Number(item.delta_estoque || 0), 0),
sizes: sortedItems.map(item => ({ sizes: sortedItems.map(item => ({
id: item.produto_id, id: item.produto_id,
@@ -85,6 +95,7 @@ const groupCampaignRows = (rows) => {
module.exports = { module.exports = {
buildWhatsappCampaignPayload, buildWhatsappCampaignPayload,
formatProductNameForDisplay,
formatProductList, formatProductList,
groupCampaignRows, groupCampaignRows,
groupCampaignRowsByBaseProduct, groupCampaignRowsByBaseProduct,

View File

@@ -3,6 +3,7 @@ const assert = require('node:assert/strict');
const { const {
buildWhatsappCampaignPayload, buildWhatsappCampaignPayload,
formatProductList, formatProductList,
formatProductNameForDisplay,
groupCampaignRows, groupCampaignRows,
groupCampaignRowsByBaseProduct, groupCampaignRowsByBaseProduct,
mapCampaignProducts mapCampaignProducts
@@ -26,14 +27,22 @@ const row = (overrides) => ({
test('formatProductList uses Portuguese list joining', () => { test('formatProductList uses Portuguese list joining', () => {
assert.equal(formatProductList([]), ''); assert.equal(formatProductList([]), '');
assert.equal(formatProductList(['BONÉ - PRETO']), 'BONÉ - PRETO'); assert.equal(formatProductList(['BONÉ - PRETO']), 'Boné - Preto');
assert.equal(formatProductList(['BONÉ - PRETO', 'BASE BRANCA']), 'BONÉ - PRETO e BASE BRANCA'); assert.equal(formatProductList(['BONÉ - PRETO', 'BASE BRANCA']), 'Boné - Preto e Base Branca');
assert.equal( assert.equal(
formatProductList(['BONÉ - PRETO', 'BASE BRANCA', 'BASE PRETA']), formatProductList(['BONÉ - PRETO', 'BASE BRANCA', 'BASE PRETA']),
'BONÉ - PRETO, BASE BRANCA e BASE PRETA' 'Boné - Preto, Base Branca e Base Preta'
); );
}); });
test('formatProductNameForDisplay converts campaign product names to title case', () => {
assert.equal(
formatProductNameForDisplay('BASE LISA MOLETOM CANGURU COR PRETO'),
'Base Lisa Moletom Canguru Cor Preto'
);
assert.equal(formatProductNameForDisplay('BONÉ - BRANCO'), 'Boné - Branco');
});
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' }),
@@ -70,7 +79,7 @@ test('buildWhatsappCampaignPayload combines multiple ready products into one mes
])); ]));
const payload = buildWhatsappCampaignPayload(products, [{ nome: 'Cliente', fone: '5511999999999' }]); const payload = buildWhatsappCampaignPayload(products, [{ nome: 'Cliente', fone: '5511999999999' }]);
assert.equal(payload.productsText, 'BONÉ - PRETO e BASE LISA CAMISETA COR BRANCO'); assert.equal(payload.productsText, 'Boné - Preto e Base Lisa Camiseta Cor Branco');
assert.equal(payload.baseProduct, payload.productsText); assert.equal(payload.baseProduct, payload.productsText);
assert.equal(payload.total_delta, 200); assert.equal(payload.total_delta, 200);
assert.equal(payload.products.length, 2); assert.equal(payload.products.length, 2);

View File

@@ -21,3 +21,9 @@ test('getBaseProductName strips trailing size suffixes without removing colors',
); );
assert.equal(getBaseProductName('BONÉ - BRANCO'), 'BONÉ - BRANCO'); assert.equal(getBaseProductName('BONÉ - BRANCO'), 'BONÉ - BRANCO');
}); });
test('getBaseProductName preserves etiqueta product variants', () => {
assert.equal(getBaseProductName('ETIQUETA 10X5 851UN'), 'ETIQUETA 10X5 851UN');
assert.equal(getBaseProductName('ETIQUETA BRANCA TAMANHO 08'), 'ETIQUETA BRANCA TAMANHO 08');
assert.equal(getBaseProductName('ETIQUETA BRANCA TAMANHO GG'), 'ETIQUETA BRANCA TAMANHO GG');
});

View File

@@ -4,7 +4,12 @@ import { parseOrderDate } from '../dataService';
const SIZE_SUFFIX_PATTERN = /\s+-\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\d{2})(?:\/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\d{2}))*)$/i; const SIZE_SUFFIX_PATTERN = /\s+-\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\d{2})(?:\/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\d{2}))*)$/i;
export const getBaseProductName = (description: string): string => { export const getBaseProductName = (description: string): string => {
return description.split(' TAMANHO')[0].replace(SIZE_SUFFIX_PATTERN, '').trim(); const productName = description.trim();
if (productName.toLocaleUpperCase('pt-BR').startsWith('ETIQUETA')) {
return productName;
}
return productName.split(' TAMANHO')[0].replace(SIZE_SUFFIX_PATTERN, '').trim();
}; };
export const getClientDisplayName = (order: OrderData): string => { export const getClientDisplayName = (order: OrderData): string => {