From d8e020371cc7d61d7c73be4adec9562c80616e0c Mon Sep 17 00:00:00 2001 From: AI Bot Date: Thu, 3 Sep 2026 11:02:23 -0500 Subject: [PATCH] feat(sanitizer): excluir rigurosamente el costo del producto en titulos y textos de publicaciones --- .../src/app/api/whatsapp-feed/route.ts | 2 + dripzone-app/src/lib/attractive-copy.ts | 2 +- dripzone-app/src/lib/data-service.ts | 4 +- dripzone-app/src/lib/gemini-parser.ts | 9 +- dripzone-app/src/lib/sanitizer.ts | 59 ++++++++++--- dripzone-app/src/lib/server-appwrite.ts | 4 +- src/lib/gemini.ts | 6 +- zapply-app/src/lib/attractive-copy.ts | 2 +- zapply-app/src/lib/sanitizer.ts | 86 ++++++++++++++++++- 9 files changed, 149 insertions(+), 25 deletions(-) diff --git a/dripzone-app/src/app/api/whatsapp-feed/route.ts b/dripzone-app/src/app/api/whatsapp-feed/route.ts index 56d3b63..b87e636 100644 --- a/dripzone-app/src/app/api/whatsapp-feed/route.ts +++ b/dripzone-app/src/app/api/whatsapp-feed/route.ts @@ -263,6 +263,7 @@ export async function POST(request: Request) { name: prodName, brand: prodBrand, retailPrice, + supplierPrice: post.extractedProduct?.supplierPrice || 75000, forbiddenTerms: forbiddenContext, }); @@ -323,6 +324,7 @@ export async function POST(request: Request) { name: prodName, brand: prodBrand, retailPrice, + supplierPrice: p.extractedProduct?.supplierPrice || 75000, forbiddenTerms: forbiddenContext, }); diff --git a/dripzone-app/src/lib/attractive-copy.ts b/dripzone-app/src/lib/attractive-copy.ts index 48938e9..9802fbd 100644 --- a/dripzone-app/src/lib/attractive-copy.ts +++ b/dripzone-app/src/lib/attractive-copy.ts @@ -45,5 +45,5 @@ export function generateAttractiveProductCopy(product: { ⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles por talla en inventario nacional. ¡Asegura tu par ahora antes de que se agoten!`; - return sanitizePublicationText(rawCopy, product.forbiddenTerms); + return sanitizePublicationText(rawCopy, product.forbiddenTerms, product.supplierPrice); } diff --git a/dripzone-app/src/lib/data-service.ts b/dripzone-app/src/lib/data-service.ts index 7796d50..711778a 100644 --- a/dripzone-app/src/lib/data-service.ts +++ b/dripzone-app/src/lib/data-service.ts @@ -215,9 +215,11 @@ class DataService { name: cleanName, brand, retailPrice: product.retailPrice, + supplierPrice: product.supplierPrice, forbiddenTerms: [product.supplierName], }), - [product.supplierName] + [product.supplierName], + product.supplierPrice ); product.name = cleanName; diff --git a/dripzone-app/src/lib/gemini-parser.ts b/dripzone-app/src/lib/gemini-parser.ts index bd36041..9ae9bdb 100644 --- a/dripzone-app/src/lib/gemini-parser.ts +++ b/dripzone-app/src/lib/gemini-parser.ts @@ -618,11 +618,12 @@ export async function parseSupplierMessage( const systemPrompt = `Eres un experto mundial en reconocimiento de calzado deportivo y zapatillas urbanas para e-commerce en Colombia. Tu misión es identificar con MÁXIMA PRECISIÓN la marca y el modelo exacto del calzado. -⚠️ REGLA CRÍTICA DE CERO PROVEEDORES / CERO GRUPOS (ESTRICTO): -- NI EN EL TÍTULO ("name") NI EN NINGÚN TEXTO COMERCIAL DE LA PUBLICACIÓN SE DEBE INCLUIR EL NOMBRE DEL GRUPO DE WHATSAPP, CANAL, PROVEEDOR, BODEGA MAYORISTA, DISTRIBUIDOR O REMITENTE. +⚠️ REGLA CRÍTICA DE CERO PROVEEDORES / CERO GRUPOS / CERO COSTOS (ESTRICTO): +- NI EN EL TÍTULO ("name") NI EN NINGÚN TEXTO COMERCIAL DE LA PUBLICACIÓN SE DEBE INCLUIR EL NOMBRE DEL GRUPO DE WHATSAPP, CANAL, PROVEEDOR, BODEGA MAYORISTA, DISTRIBUIDOR, NI EL COSTO DEL PRODUCTO (PRECIO DE COMPRA / MAYORISTA). +- El costo del producto SOLO debe extraerse en el campo numérico interno "supplierPrice" para el cálculo de margen, pero JAMÁS debe figurar en el título ("name") ni en ninguna descripción de la publicación. - El campo "name" debe ser ÚNICAMENTE el modelo limpio y comercial del calzado (ej: "Adidas Samba OG Blanco Negro", "Nike Air Force 1 07 All White", "New Balance 9060 Sea Salt", "On Cloudmonster Triple Black", "Puma Palermo Special Verde"). -- Si el mensaje contiene nombres de grupos o proveedores (${forbiddenClause} o palabras como "Bodega", "Mayorista", "Distribuidora", "Proveedor", "Jovic", "CC Mío", "Team Emporio", etc.), DEBES OMITIRLOS Y ELIMINARLOS POR COMPLETO DEL TÍTULO Y DE TODO TEXTO. -- Si la zapatilla no tiene modelo exacto identificable, asigna un nombre comercial elegante (ej: "Sneakers Urban Edition"), pero JAMÁS inventes ni coloques el nombre de la bodega o del grupo. +- Si el mensaje contiene nombres de grupos o proveedores (${forbiddenClause} o palabras como "Bodega", "Mayorista", "Distribuidora", "Proveedor", "Jovic", "CC Mío", "Team Emporio", etc.), o menciones de costo/precios de bodega, DEBES OMITIRLOS Y ELIMINARLOS POR COMPLETO DEL TÍTULO Y DE TODO TEXTO. +- Si la zapatilla no tiene modelo exacto identificable, asigna un nombre comercial elegante (ej: "Sneakers Urban Edition"), pero JAMÁS inventes ni coloques el nombre de la bodega, del grupo ni el costo del producto. ${ imagePayload diff --git a/dripzone-app/src/lib/sanitizer.ts b/dripzone-app/src/lib/sanitizer.ts index 7e6d51d..5b17dd4 100644 --- a/dripzone-app/src/lib/sanitizer.ts +++ b/dripzone-app/src/lib/sanitizer.ts @@ -50,6 +50,15 @@ const SUPPLIER_PREFIX_PATTERNS = [ const GENERIC_FORBIDDEN_WORDS_REGEX = /\b(proveedor(?:es)?|bodega(?:s)?(?:\s+mayorista)?|distribuidor(?:a)?(?:es)?|mayorista(?:s)?|canal(?:\s+mayorista)?|grupo(?:\s+de)?\s+whatsapp)\b/gi; +// Regex para eliminar menciones de costo, precios mayoristas y precios de compra +const COST_AND_PRICE_PATTERNS = [ + /(?:costo|precio\s*mayorista|precio\s*bodega|precio\s*distribuidor|precio\s*compra|al\s*por\s*mayor|valor\s*mayorista|mayorista)\s*[:\-–—]?\s*(?:\$?\s*[\d.,]+\s*(?:mil|k)?|\d+k|\$\s*\d+)?/gi, + /(?:^|\n|\s*)(?:costo|precio\s*costo|precio\s*proveedor|precio\s*bodega)\s*[:\-–—]?\s*[^\n]+/gi, + /(?:💸|💰|💵|💲)\s*[:\-–—]?\s*(?:\$?\s*[\d.,]+\s*(?:mil|k)?|\d+k|\$\s*\d+)/gi, + /(?:^|\n|\s*)(?:precio|valor)\s*[:\-–—]\s*(?:\$?\s*[\d.,]+\s*(?:mil|k)?|\d+k|\$\s*\d+)/gi, + /\b\d{2,3}[\.,]\d{3}\b|\b\d{2,3}k\b|\b\d{2,3}\s*mil\b/gi, +]; + /** * Escapa caracteres especiales para usar en expresiones regulares */ @@ -95,7 +104,7 @@ function buildForbiddenList(extraForbidden: (string | undefined | null)[] = []): * Sanitiza el TÍTULO / NOMBRE comercial de la publicación * - Elimina nombres de grupos de WhatsApp * - Elimina nombres de proveedores o bodegas - * - Elimina menciones como "Bodega", "Mayorista", "Proveedor", etc. + * - Elimina costos, precios y menciones a intermediarios * - Asegura que el producto tenga un título comercial atractivo y limpio */ export function sanitizePublicationTitle( @@ -124,23 +133,29 @@ export function sanitizePublicationTitle( // 3. Quitar palabras genéricas de intermediarios cleaned = cleaned.replace(GENERIC_FORBIDDEN_WORDS_REGEX, ' ').trim(); - // 4. Limpiar emojis excesivos de inicio, códigos numéricos, precios pegados + // 4. Quitar estrictamente costos, cifras de precios y menciones monetarias + for (const pattern of COST_AND_PRICE_PATTERNS) { + cleaned = cleaned.replace(pattern, ' ').trim(); + } + + // 5. Limpiar emojis excesivos de inicio, códigos numéricos, precios pegados cleaned = cleaned .replace(/^[#*_\-•—–~:;,\s]+/, '') .replace(/🔥|🔴|‼️|✨|🌸|👑|💎|🛍️|🌟|💸|💰|💵|💲|\*/g, '') .replace(/\b\d{2,3}[\.,]\d{3}\b|\b\d{2,3}k\b|\$\s*\d+/gi, '') .replace(/dama\s*y\s*caballero|caballero|dama|unisex|recarga|nueva\s*mercanc[ií]a|[uú]ltima\s*unidad/gi, '') + .replace(/\b(costo|precio|valor)\b/gi, '') .replace(/\s+/g, ' ') .trim(); - // 5. Si quedó vacío o demasiado corto o numérico, asignar nombre por defecto limpio + // 6. Si quedó vacío o demasiado corto o numérico, asignar nombre por defecto limpio if (cleaned.length < 3 || /^\d+$/.test(cleaned) || /^[-,.:;\s]+$/.test(cleaned)) { return fallbackBrand && fallbackBrand !== 'Urbano / Variado' ? `${fallbackBrand} Urban Edition` : 'Zapatillas Urbanas Streetwear'; } - // 6. Formatear en Capital Case limpio + // 7. Formatear en Capital Case limpio return cleaned .split(' ') .filter(Boolean) @@ -157,12 +172,14 @@ export function sanitizePublicationTitle( /** * Sanitiza el TEXTO / DESCRIPCIÓN de la publicación * - Elimina menciones a grupos de WhatsApp, enlaces a grupos y canales - * - Elimina nombres de proveedores, bodegas o precios de compra mayorista + * - Elimina nombres de proveedores, bodegas o intermediarios + * - Elimina rigurosamente el COSTO del producto, precio mayorista o valor de compra * - Elimina contactos directos del proveedor o advertencias dirigidas a revendedores */ export function sanitizePublicationText( rawText: string, - extraForbidden: (string | undefined | null)[] = [] + extraForbidden: (string | undefined | null)[] = [], + knownSupplierPrice?: number ): string { if (!rawText || typeof rawText !== 'string') return ''; @@ -181,7 +198,22 @@ export function sanitizePublicationText( cleaned = cleaned.replace(termRegex, ' '); } - // 4. Quitar líneas que sean exclusivamente identificadores de proveedor o grupo + // 4. Si se conoce el precio de costo numérico, eliminar cualquier aparición de dicha cifra + if (knownSupplierPrice && knownSupplierPrice > 0) { + const costStr = knownSupplierPrice.toString(); + const costFormatted = knownSupplierPrice.toLocaleString('es-CO'); + const costInK = `${Math.round(knownSupplierPrice / 1000)}k`; + const costInMil = `${Math.round(knownSupplierPrice / 1000)}\\s*mil`; + const costRegex = new RegExp(`\\$?\\s*(?:${escapeRegExp(costStr)}|${escapeRegExp(costFormatted)}|${costInK}|${costInMil})\\b`, 'gi'); + cleaned = cleaned.replace(costRegex, ' '); + } + + // 5. Quitar patrones de costos, precios mayoristas y frases de distribuidor + for (const pattern of COST_AND_PRICE_PATTERNS) { + cleaned = cleaned.replace(pattern, ' '); + } + + // 6. Quitar líneas que sean identificadores de proveedor, grupo o costos const lines = cleaned.split('\n'); const cleanLines = lines.filter((line) => { const lower = line.toLowerCase().trim(); @@ -190,11 +222,18 @@ export function sanitizePublicationText( lower.startsWith('bodega:') || lower.startsWith('canal:') || lower.startsWith('grupo:') || + lower.startsWith('costo:') || + lower.startsWith('costo') || + lower.startsWith('precio mayorista') || + lower.startsWith('precio distribuidor') || + lower.startsWith('mayorista:') || lower.includes('grupo mayorista') || lower.includes('canal de difusión') || lower.includes('canal de difusion') || lower.includes('difusion jovic') || - lower.includes('cc mío 3b') + lower.includes('cc mío 3b') || + lower.includes('precio al por mayor') || + lower.includes('precio de compra') ) { return false; } @@ -203,10 +242,10 @@ export function sanitizePublicationText( cleaned = cleanLines.join('\n'); - // 5. Quitar palabras clave genéricas de intermediarios sueltas + // 7. Quitar palabras clave genéricas de intermediarios sueltas cleaned = cleaned.replace(GENERIC_FORBIDDEN_WORDS_REGEX, ' '); - // 6. Normalizar espaciado + // 8. Normalizar espaciado cleaned = cleaned .replace(/[ \t]+/g, ' ') .replace(/\n{3,}/g, '\n\n') diff --git a/dripzone-app/src/lib/server-appwrite.ts b/dripzone-app/src/lib/server-appwrite.ts index 59cef09..ac6de28 100644 --- a/dripzone-app/src/lib/server-appwrite.ts +++ b/dripzone-app/src/lib/server-appwrite.ts @@ -523,7 +523,7 @@ export async function getProductsFromAppwrite(): Promise { const brand = d.brand || 'Sneakers'; const cleanName = sanitizePublicationTitle(d.name || 'Zapatillas', [d.supplierName], brand); - const cleanDesc = sanitizePublicationText(d.description || '', [d.supplierName]); + const cleanDesc = sanitizePublicationText(d.description || '', [d.supplierName], d.supplierPrice); return { id: d.$id, @@ -555,7 +555,7 @@ export async function getProductsFromAppwrite(): Promise { export async function saveProductToAppwrite(product: Product): Promise { const brand = product.brand || 'Sneakers'; const cleanName = sanitizePublicationTitle(product.name, [product.supplierName], brand); - const cleanDesc = sanitizePublicationText(product.description || '', [product.supplierName]); + const cleanDesc = sanitizePublicationText(product.description || '', [product.supplierName], product.supplierPrice); const payload = { name: cleanName, diff --git a/src/lib/gemini.ts b/src/lib/gemini.ts index 735ea3f..33baa04 100644 --- a/src/lib/gemini.ts +++ b/src/lib/gemini.ts @@ -96,11 +96,11 @@ export async function processWhatsAppMessage( Eres un asistente editorial experto y editor gráfico de Almeira Web. Analiza los mensajes y recursos multimedia recibidos desde WhatsApp para estructurarlos como publicaciones del portal. ${explicitTypeDirective} - REGLA CRÍTICA OBLIGATORIA (CERO NOMBRES DE GRUPO / PROVEEDOR): - Ni en el título ('titulo') ni en el texto o contenido ('contenido', 'seo_descripcion') de la publicación se debe incluir el nombre del grupo de WhatsApp ni el proveedor o remitente del producto o servicio. El título y el texto deben enfocarse en el hecho noticioso o producto sin mencionar nombres de grupos, canales de difusión ni proveedores mayoristas. + REGLA CRÍTICA OBLIGATORIA (CERO NOMBRES DE GRUPO / PROVEEDOR / COSTOS): + Ni en el título ('titulo') ni en el texto o contenido ('contenido', 'seo_descripcion') de la publicación se debe incluir el nombre del grupo de WhatsApp, el proveedor o remitente del producto o servicio, ni el costo del producto (precio mayorista o de compra). El título y el texto deben enfocarse en el hecho noticioso o producto con redacción profesional limpia, sin citar nombres de grupos, canales de difusión, proveedores ni precios de costo. Debes estructurar el mensaje en base a lo siguiente: - 1. titulo: Asigna un título claro, profesional y conciso (máx. 70 caracteres). ¡Cero nombres de grupos o proveedores! + 1. titulo: Asigna un título claro, profesional y conciso (máx. 70 caracteres). ¡Cero nombres de grupos, proveedores o costos! 2. tipo: Determina la categoría exacta ${explicitType ? `(Usa obligatoriamente "${explicitType}")` : ''}: - "noticia": Noticias generales, política, actualidad, comunicados y reportajes (dimensiones recomendadas de imagen: 640x360 px, relación 16:9). - "evento": Invitaciones a actividades, jornadas, ferias, foros, congresos, festivales o convocatorias con fecha (dimensiones de imagen: 640x640 px, relación 1:1). diff --git a/zapply-app/src/lib/attractive-copy.ts b/zapply-app/src/lib/attractive-copy.ts index 48938e9..9802fbd 100644 --- a/zapply-app/src/lib/attractive-copy.ts +++ b/zapply-app/src/lib/attractive-copy.ts @@ -45,5 +45,5 @@ export function generateAttractiveProductCopy(product: { ⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles por talla en inventario nacional. ¡Asegura tu par ahora antes de que se agoten!`; - return sanitizePublicationText(rawCopy, product.forbiddenTerms); + return sanitizePublicationText(rawCopy, product.forbiddenTerms, product.supplierPrice); } diff --git a/zapply-app/src/lib/sanitizer.ts b/zapply-app/src/lib/sanitizer.ts index b3d0d02..5b17dd4 100644 --- a/zapply-app/src/lib/sanitizer.ts +++ b/zapply-app/src/lib/sanitizer.ts @@ -1,9 +1,10 @@ /** - * Módulo de Sanitización para Títulos y Textos de Publicaciones (Zapply) + * Módulo de Sanitización para Títulos y Textos de Publicaciones * Garantiza estrictamente que NI en el título NI en el texto de la publicación * se incluya el nombre del grupo de WhatsApp, canal, proveedor o términos mayoristas. */ +// Términos y palabras clave prohibidas por defecto (grupos, bodegas, proveedores y mayoristas) export const DEFAULT_FORBIDDEN_TERMS = [ 'difusión jovic store colombia', 'difusion jovic store colombia', @@ -39,18 +40,35 @@ export const DEFAULT_FORBIDDEN_TERMS = [ 'bodegas aliadas', ]; +// Regex para detectar patrones de prefijos de proveedor / grupo const SUPPLIER_PREFIX_PATTERNS = [ /(?:^|\n|\s*)(?:proveedor|bodega|distribuidor[a]?|mayorista[s]?|canal|grupo\s*(?:de\s*whatsapp)?)\s*[:\-–—]\s*([^\n,.]+)/gi, /(?:^|\n|\s*)(?:de|por|remitente|fuente)\s*[:\-–—]\s*([^\n,.]+)/gi, ]; +// Regex para eliminar palabras y conceptos de intermediarios en publicaciones comerciales const GENERIC_FORBIDDEN_WORDS_REGEX = /\b(proveedor(?:es)?|bodega(?:s)?(?:\s+mayorista)?|distribuidor(?:a)?(?:es)?|mayorista(?:s)?|canal(?:\s+mayorista)?|grupo(?:\s+de)?\s+whatsapp)\b/gi; +// Regex para eliminar menciones de costo, precios mayoristas y precios de compra +const COST_AND_PRICE_PATTERNS = [ + /(?:costo|precio\s*mayorista|precio\s*bodega|precio\s*distribuidor|precio\s*compra|al\s*por\s*mayor|valor\s*mayorista|mayorista)\s*[:\-–—]?\s*(?:\$?\s*[\d.,]+\s*(?:mil|k)?|\d+k|\$\s*\d+)?/gi, + /(?:^|\n|\s*)(?:costo|precio\s*costo|precio\s*proveedor|precio\s*bodega)\s*[:\-–—]?\s*[^\n]+/gi, + /(?:💸|💰|💵|💲)\s*[:\-–—]?\s*(?:\$?\s*[\d.,]+\s*(?:mil|k)?|\d+k|\$\s*\d+)/gi, + /(?:^|\n|\s*)(?:precio|valor)\s*[:\-–—]\s*(?:\$?\s*[\d.,]+\s*(?:mil|k)?|\d+k|\$\s*\d+)/gi, + /\b\d{2,3}[\.,]\d{3}\b|\b\d{2,3}k\b|\b\d{2,3}\s*mil\b/gi, +]; + +/** + * Escapa caracteres especiales para usar en expresiones regulares + */ function escapeRegExp(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } +/** + * Limpia emojis y símbolos decorativos de nombres de grupos + */ export function cleanGroupName(raw: string): string { if (!raw) return ''; return raw @@ -59,8 +77,12 @@ export function cleanGroupName(raw: string): string { .trim(); } +/** + * Normaliza y compila lista de términos prohibidos + */ function buildForbiddenList(extraForbidden: (string | undefined | null)[] = []): string[] { const terms = new Set(); + DEFAULT_FORBIDDEN_TERMS.forEach((t) => terms.add(t.toLowerCase().trim())); extraForbidden.forEach((item) => { @@ -78,6 +100,13 @@ function buildForbiddenList(extraForbidden: (string | undefined | null)[] = []): return Array.from(terms).filter((t) => t.length >= 3); } +/** + * Sanitiza el TÍTULO / NOMBRE comercial de la publicación + * - Elimina nombres de grupos de WhatsApp + * - Elimina nombres de proveedores o bodegas + * - Elimina costos, precios y menciones a intermediarios + * - Asegura que el producto tenga un título comercial atractivo y limpio + */ export function sanitizePublicationTitle( rawTitle: string, extraForbidden: (string | undefined | null)[] = [], @@ -89,36 +118,49 @@ export function sanitizePublicationTitle( let cleaned = rawTitle.trim(); + // 1. Quitar prefijos de proveedor (ej: "Proveedor: Nike Dunk" -> "Nike Dunk") for (const pattern of SUPPLIER_PREFIX_PATTERNS) { cleaned = cleaned.replace(pattern, '').trim(); } + // 2. Quitar nombres de grupos y proveedores específicos const forbiddenList = buildForbiddenList(extraForbidden); for (const term of forbiddenList) { const termRegex = new RegExp(`(^|[\\s,;:\\-–—(]|\\[)${escapeRegExp(term)}([\\s,;:\\-–—)\\]]|$)`, 'gi'); cleaned = cleaned.replace(termRegex, ' ').trim(); } + // 3. Quitar palabras genéricas de intermediarios cleaned = cleaned.replace(GENERIC_FORBIDDEN_WORDS_REGEX, ' ').trim(); + // 4. Quitar estrictamente costos, cifras de precios y menciones monetarias + for (const pattern of COST_AND_PRICE_PATTERNS) { + cleaned = cleaned.replace(pattern, ' ').trim(); + } + + // 5. Limpiar emojis excesivos de inicio, códigos numéricos, precios pegados cleaned = cleaned .replace(/^[#*_\-•—–~:;,\s]+/, '') .replace(/🔥|🔴|‼️|✨|🌸|👑|💎|🛍️|🌟|💸|💰|💵|💲|\*/g, '') .replace(/\b\d{2,3}[\.,]\d{3}\b|\b\d{2,3}k\b|\$\s*\d+/gi, '') .replace(/dama\s*y\s*caballero|caballero|dama|unisex|recarga|nueva\s*mercanc[ií]a|[uú]ltima\s*unidad/gi, '') + .replace(/\b(costo|precio|valor)\b/gi, '') .replace(/\s+/g, ' ') .trim(); + // 6. Si quedó vacío o demasiado corto o numérico, asignar nombre por defecto limpio if (cleaned.length < 3 || /^\d+$/.test(cleaned) || /^[-,.:;\s]+$/.test(cleaned)) { return fallbackBrand && fallbackBrand !== 'Urbano / Variado' ? `${fallbackBrand} Urban Edition` : 'Zapatillas Urbanas Streetwear'; } + // 7. Formatear en Capital Case limpio return cleaned .split(' ') .filter(Boolean) .map((w) => { + // Conservar siglas reconocidas en mayúsculas completas (OG, SE, SB, TN, CL, RS-X, GT, etc.) if (/^(OG|SE|SB|TN|CL|GT|RS-X|BB|LV|NB|XT-6|9060|550|530|1906R|2002R)$/i.test(w)) { return w.toUpperCase(); } @@ -127,23 +169,51 @@ export function sanitizePublicationTitle( .join(' '); } +/** + * Sanitiza el TEXTO / DESCRIPCIÓN de la publicación + * - Elimina menciones a grupos de WhatsApp, enlaces a grupos y canales + * - Elimina nombres de proveedores, bodegas o intermediarios + * - Elimina rigurosamente el COSTO del producto, precio mayorista o valor de compra + * - Elimina contactos directos del proveedor o advertencias dirigidas a revendedores + */ export function sanitizePublicationText( rawText: string, - extraForbidden: (string | undefined | null)[] = [] + extraForbidden: (string | undefined | null)[] = [], + knownSupplierPrice?: number ): string { if (!rawText || typeof rawText !== 'string') return ''; let cleaned = rawText; + // 1. Quitar enlaces de invitaciones a grupos de WhatsApp cleaned = cleaned.replace(/https?:\/\/chat\.whatsapp\.com\/[A-Za-z0-9]+/gi, ''); + + // 2. Quitar teléfonos típicos de proveedores mencionados en el texto cleaned = cleaned.replace(/(?:tel[eé]fono|contacto|pedidos\s*proveedor|asesor|bodega)\s*[:\-–—]?\s*(?:\+?57\s*)?3\d{2}[\s.-]?\d{3}[\s.-]?\d{4}/gi, ''); + // 3. Quitar nombres de grupos y proveedores específicos const forbiddenList = buildForbiddenList(extraForbidden); for (const term of forbiddenList) { const termRegex = new RegExp(`(^|[\\s,;:\\-–—(]|\\[)${escapeRegExp(term)}([\\s,;:\\-–—)\\]]|$)`, 'gi'); cleaned = cleaned.replace(termRegex, ' '); } + // 4. Si se conoce el precio de costo numérico, eliminar cualquier aparición de dicha cifra + if (knownSupplierPrice && knownSupplierPrice > 0) { + const costStr = knownSupplierPrice.toString(); + const costFormatted = knownSupplierPrice.toLocaleString('es-CO'); + const costInK = `${Math.round(knownSupplierPrice / 1000)}k`; + const costInMil = `${Math.round(knownSupplierPrice / 1000)}\\s*mil`; + const costRegex = new RegExp(`\\$?\\s*(?:${escapeRegExp(costStr)}|${escapeRegExp(costFormatted)}|${costInK}|${costInMil})\\b`, 'gi'); + cleaned = cleaned.replace(costRegex, ' '); + } + + // 5. Quitar patrones de costos, precios mayoristas y frases de distribuidor + for (const pattern of COST_AND_PRICE_PATTERNS) { + cleaned = cleaned.replace(pattern, ' '); + } + + // 6. Quitar líneas que sean identificadores de proveedor, grupo o costos const lines = cleaned.split('\n'); const cleanLines = lines.filter((line) => { const lower = line.toLowerCase().trim(); @@ -152,11 +222,18 @@ export function sanitizePublicationText( lower.startsWith('bodega:') || lower.startsWith('canal:') || lower.startsWith('grupo:') || + lower.startsWith('costo:') || + lower.startsWith('costo') || + lower.startsWith('precio mayorista') || + lower.startsWith('precio distribuidor') || + lower.startsWith('mayorista:') || lower.includes('grupo mayorista') || lower.includes('canal de difusión') || lower.includes('canal de difusion') || lower.includes('difusion jovic') || - lower.includes('cc mío 3b') + lower.includes('cc mío 3b') || + lower.includes('precio al por mayor') || + lower.includes('precio de compra') ) { return false; } @@ -164,8 +241,11 @@ export function sanitizePublicationText( }); cleaned = cleanLines.join('\n'); + + // 7. Quitar palabras clave genéricas de intermediarios sueltas cleaned = cleaned.replace(GENERIC_FORBIDDEN_WORDS_REGEX, ' '); + // 8. Normalizar espaciado cleaned = cleaned .replace(/[ \t]+/g, ' ') .replace(/\n{3,}/g, '\n\n')