From a1aa071e1dde5eaecdb6db2d345ee911343e28a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Mon, 1 Jun 2026 09:27:54 -0300 Subject: [PATCH] fix: normalize campaign product size suffixes --- backend/db.js | 14 ++++++++++++++ backend/mappers/stockMapper.js | 9 ++++++++- backend/services/analyticsService.js | 3 ++- backend/test/stockMapper.test.js | 23 +++++++++++++++++++++++ src/analytics/orders.ts | 4 +++- 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 backend/test/stockMapper.test.js diff --git a/backend/db.js b/backend/db.js index eb2d604..bae155e 100644 --- a/backend/db.js +++ b/backend/db.js @@ -94,6 +94,20 @@ const initDB = async () => { ALTER COLUMN sent_at TYPE TIMESTAMPTZ USING sent_at AT TIME ZONE 'America/Sao_Paulo'; `).catch(() => {}); + await pool.query(` + UPDATE stock_campaign_queue + SET base_product_name = TRIM(regexp_replace( + base_product_name, + '\\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' + )) + WHERE status IN ('pending', 'failed', 'processing') + AND base_product_name ~* '\\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}))*)$'; + `).catch(err => { + console.error('Notice: Could not normalize queued campaign 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_orders_cliente_fone ON orders (cliente_fone);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_produto_id ON orders (produto_id);`); diff --git a/backend/mappers/stockMapper.js b/backend/mappers/stockMapper.js index a3c233a..cf1a8ab 100644 --- a/backend/mappers/stockMapper.js +++ b/backend/mappers/stockMapper.js @@ -1,4 +1,11 @@ -const getBaseProductName = (name) => String(name || 'Unknown').split(' TAMANHO')[0].trim(); +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) => { + return String(name || 'Unknown') + .split(' TAMANHO')[0] + .replace(SIZE_SUFFIX_PATTERN, '') + .trim(); +}; const normalizeStockPayload = (item) => { const produtoId = item.idProduto || item.ID_Produto || ''; diff --git a/backend/services/analyticsService.js b/backend/services/analyticsService.js index 470a1f9..dc77c2e 100644 --- a/backend/services/analyticsService.js +++ b/backend/services/analyticsService.js @@ -1,6 +1,7 @@ const { pool } = require('../db'); -const PRODUCT_NAME_SQL = "NULLIF(TRIM(split_part(COALESCE(produto_descricao, 'Unknown'), ' TAMANHO', 1)), '')"; +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 normalizeDateParam = (value) => { if (!value) return null; diff --git a/backend/test/stockMapper.test.js b/backend/test/stockMapper.test.js new file mode 100644 index 0000000..90aa337 --- /dev/null +++ b/backend/test/stockMapper.test.js @@ -0,0 +1,23 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const { getBaseProductName } = require('../mappers/stockMapper'); + +test('getBaseProductName strips TAMANHO suffixes', () => { + assert.equal( + getBaseProductName('BASE LISA CAMISETA COR BRANCO TAMANHO - P'), + 'BASE LISA CAMISETA COR BRANCO' + ); +}); + +test('getBaseProductName strips trailing size suffixes without removing colors', () => { + assert.equal( + getBaseProductName('BASE LISA MOLETOM CANGURU COR PRETO - M'), + 'BASE LISA MOLETOM CANGURU COR PRETO' + ); + assert.equal( + getBaseProductName('BASE LISA MOLETOM CANGURU COR PRETO - M/G/GG'), + 'BASE LISA MOLETOM CANGURU COR PRETO' + ); + assert.equal(getBaseProductName('BONÉ - BRANCO'), 'BONÉ - BRANCO'); +}); diff --git a/src/analytics/orders.ts b/src/analytics/orders.ts index 89a0315..6e56dec 100644 --- a/src/analytics/orders.ts +++ b/src/analytics/orders.ts @@ -1,8 +1,10 @@ import type { DateRange, OrderData } from '../types'; 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; + export const getBaseProductName = (description: string): string => { - return description.split(' TAMANHO')[0]; + return description.split(' TAMANHO')[0].replace(SIZE_SUFFIX_PATTERN, '').trim(); }; export const getClientDisplayName = (order: OrderData): string => {