import { Product } from '@/types'; /** * Normaliza un string para comparación (minúsculas, sin acentos, sin caracteres especiales) */ export function normalizeText(text: string): string { if (!text) return ''; return text .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9\s]/g, ' ') .replace(/\s+/g, ' ') .trim(); } /** * Calcula la similitud de Jaccard / Coeficiente de Dados entre dos textos * Devuelve un valor entre 0 (totalmente diferente) y 1.0 (idéntico) */ export function calculateTextSimilarity(textA: string, textB: string): number { const normA = normalizeText(textA); const normB = normalizeText(textB); if (!normA || !normB) return 0; if (normA === normB) return 1.0; // Si uno contiene al otro por completo if (normA.includes(normB) || normB.includes(normA)) { const ratio = Math.min(normA.length, normB.length) / Math.max(normA.length, normB.length); if (ratio > 0.7) return 0.92; } const tokensA = new Set(normA.split(' ').filter((w) => w.length > 2)); const tokensB = new Set(normB.split(' ').filter((w) => w.length > 2)); if (tokensA.size === 0 || tokensB.size === 0) return 0; const intersection = new Set([...tokensA].filter((t) => tokensB.has(t))); const union = new Set([...tokensA, ...tokensB]); return intersection.size / union.size; } export interface DuplicateCheckResult { isDuplicate: boolean; similarityScore: number; existingProduct?: Product; recommendation: 'create_new' | 'update_stock_and_sizes' | 'skip_duplicate'; message: string; } /** * Verifica si un producto entrante ya existe en el catálogo para evitar duplicados */ export function checkProductDuplicate( incoming: { name: string; brand: string; sizes: string[]; supplierPrice: number; }, existingCatalog: Product[], threshold = 0.75 ): DuplicateCheckResult { let highestScore = 0; let bestMatch: Product | undefined; for (const product of existingCatalog) { // 1. Si la marca no coincide, penalizar const brandMatch = normalizeText(incoming.brand) === normalizeText(product.brand); const score = calculateTextSimilarity(incoming.name, product.name); const finalScore = brandMatch ? score : score * 0.5; if (finalScore > highestScore) { highestScore = finalScore; bestMatch = product; } } // Si la similitud supera el umbral (75%) if (highestScore >= threshold && bestMatch) { // Verificar si aporta nuevas tallas const incomingSizesSet = new Set(incoming.sizes); const existingSizesSet = new Set(bestMatch.sizes); const hasNewSizes = incoming.sizes.some((s) => !existingSizesSet.has(s)); return { isDuplicate: true, similarityScore: Math.round(highestScore * 100) / 100, existingProduct: bestMatch, recommendation: hasNewSizes ? 'update_stock_and_sizes' : 'skip_duplicate', message: hasNewSizes ? `Producto ya registrado ("${bestMatch.name}"). Se detectaron nuevas tallas para actualizar.` : `Producto ya registrado ("${bestMatch.name}") con similitud del ${Math.round(highestScore * 100)}%. Se previene duplicación en la tienda y Facebook.`, }; } return { isDuplicate: false, similarityScore: Math.round(highestScore * 100) / 100, recommendation: 'create_new', message: 'Producto nuevo verificado sin duplicados. Listo para publicar.', }; }