Matching Automático
Sistema de detección inteligente de proveedores desde OCR y transacciones bancarias.
Cómo funciona
Sección titulada «Cómo funciona»Cuando subes un ticket, facturas o sincronizas transacciones bancarias, el sistema busca automáticamente:
- Coincidencia por CIF/NIF (100% confianza)
- Coincidencia por alias (85-95% confianza)
- Similitud de nombre (50-95% confianza)
Matching desde OCR
Sección titulada «Matching desde OCR»Hook: useOCRVendorMatch()
Prioridades:
-
CIF/NIF exacto
- Busca en
vendors.tax_id - Confianza: 100%
- Matching inmediato
- Busca en
-
Alias registrado
- Busca en
vendor_aliases - Usa RPC
find_vendor_by_alias - Confianza: 85-95% (según tipo de alias)
- Busca en
-
Nombre similar
- Busca en
vendors.nameyvendors.legal_name - Calcula scoring de similitud
- Confianza: 50-95% (según similitud)
- Busca en
Matching desde transacciones bancarias
Sección titulada «Matching desde transacciones bancarias»Hook: useBankTransactionVendorMatch()
Prioridades:
-
Alias bancario
- Busca en
vendor_aliasesconsource = 'bank' - Confianza: alta/media/baja (90% o más / 70-89% / menos de 70%)
- Busca en
-
Nombre parcial
- Busca en
vendors.namecon ILIKE - Confianza: media
- Busca en
Scoring de confianza
Sección titulada «Scoring de confianza»| Rango | Nivel | Acción sugerida |
|---|---|---|
| 100% | Exacto | Auto-asignar |
| 85-99% | Alto | Mostrar como sugerencia principal |
| 70-84% | Medio | Mostrar en lista de sugerencias |
| 50-69% | Bajo | Mostrar como posible coincidencia |
| menos de 50% | Sin match | Permitir crear nuevo |
Auto-learning
Sección titulada «Auto-learning»Cuando el usuario confirma un proveedor para un gasto:
- Se extrae el nombre original (del OCR o transacción bancaria)
- Se compara con el nombre guardado en el proveedor
- Si difieren, se crea un alias automático:
vendor_aliases.alias= nombre originalvendor_aliases.source= ‘ocr’ o ‘bank’ o ‘learned’vendor_aliases.match_type= ‘contains’
- Próximas detecciones usarán ese alias para matching automático
Ejemplo completo
Sección titulada «Ejemplo completo»1. Usuario sube ticket OCR detecta: "MERCADONA VALENCIA 123" CIF: A46103834
2. Sistema busca matches: ✓ CIF encontrado → Proveedor "Mercadona" → Confianza: 100% → Auto-asignado
3. Usuario guarda el gasto Sistema detecta: nombre OCR ≠ nombre proveedor → Crea alias: "MERCADONA VALENCIA 123" → "Mercadona"
4. Próximo ticket con "MERCADONA VALENCIA 123" → Match por alias (confianza: 95%) → Sugerencia automáticaGestión de aliases
Sección titulada «Gestión de aliases»Los aliases se crean automáticamente, pero también puedes:
- Ver aliases: Abre el proveedor → verás la lista de nombres alternativos
- Eliminar alias: Si un alias causa matching incorrecto
- Ver estadísticas: Cuántas veces se ha usado cada alias
Detalles técnicos — Matching Automático
Detalles técnicos
Hooks principales
Sección titulada «Hooks principales»1. useVendorMatching()
Archivo: src/features/vendors/hooks/useVendorMatching.ts
Funciones:
findVendorMatch(businessId, merchantName)→ busca por alias y nombrecreateAliasIfNeeded(vendorId, businessId, newAlias, source)→ crea alias si no existegetVendorSuggestion(businessId, data)→ obtiene sugerencia desde OCR o transacción
2. useOCRVendorMatch()
Archivo: src/features/vendors/hooks/useOCRVendorMatch.ts
interface OCRVendorMatchResult { vendor: Vendor | null; matchType: 'tax_id' | 'name' | 'alias' | null; confidence: number; // 0.0 - 1.0 aliasMatched?: string;}Algoritmo de similitud de nombres:
// Exact matchif (nameLower === searchLower) return 0.95;
// Containsif (nameLower.includes(searchLower) || searchLower.includes(nameLower)) { const lengthRatio = min / max; return 0.7 + (lengthRatio * 0.2); // 0.70 - 0.90}
// Starts withif (starts_with(first_4_chars)) return 0.6;
// Defaultreturn 0.5;3. useBankTransactionVendorMatch()
Archivo: src/features/vendors/hooks/useBankTransactionVendorMatch.ts
interface VendorMatchResult { vendor: Vendor | null; confidence: 'high' | 'medium' | 'low' | null; matchedBy: 'alias' | 'name' | null;}RPC Functions (Postgres)
Sección titulada «RPC Functions (Postgres)»find_vendor_by_alias
CREATE OR REPLACE FUNCTION find_vendor_by_alias( p_business_id UUID, p_search_term TEXT)RETURNS TABLE ( vendor_id UUID, alias_matched TEXT, confidence INTEGER) AS $$BEGIN RETURN QUERY SELECT va.vendor_id, va.alias AS alias_matched, CASE va.match_type WHEN 'exact' THEN 100 WHEN 'starts_with' THEN 90 WHEN 'contains' THEN 85 WHEN 'regex' THEN 80 END AS confidence FROM vendor_aliases va WHERE va.business_id = p_business_id AND ( (va.match_type = 'exact' AND LOWER(va.alias) = LOWER(p_search_term)) OR (va.match_type = 'contains' AND LOWER(p_search_term) LIKE '%' || LOWER(va.alias) || '%') OR (va.match_type = 'starts_with' AND LOWER(p_search_term) LIKE LOWER(va.alias) || '%') ) ORDER BY confidence DESC, va.times_matched DESC LIMIT 5;END;$$ LANGUAGE plpgsql;increment_alias_match
CREATE OR REPLACE FUNCTION increment_alias_match(p_alias_id UUID)RETURNS VOID AS $$BEGIN UPDATE vendor_aliases SET times_matched = times_matched + 1, last_matched_at = NOW() WHERE id = p_alias_id;END;$$ LANGUAGE plpgsql;Adapter functions
Sección titulada «Adapter functions»Archivo: src/features/vendors/api/vendor-adapter.ts
// Buscar por aliasasync findByAlias(businessId: string, merchantName: string): Promise<Vendor | null> { const { data } = await supabase.rpc('find_vendor_by_alias', { p_business_id: businessId, p_search_term: merchantName, });
if (data && data.length > 0) { const bestMatch = data[0]; // Fetch full vendor const { data: vendor } = await supabase .from('vendors') .select('*') .eq('id', bestMatch.vendor_id) .single();
return { ...vendor, confidence: bestMatch.confidence }; } return null;}
// Crear aliasasync createAlias(aliasData): Promise<VendorAlias> { const { data } = await supabase .from('vendor_aliases') .insert([{ vendor_id: aliasData.vendorId, business_id: aliasData.businessId, alias: aliasData.alias, match_type: aliasData.matchType || 'contains', source: aliasData.source || 'manual', }]) .select() .single();
return data;}Tabla vendor_aliases
Sección titulada «Tabla vendor_aliases»CREATE TABLE vendor_aliases ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), vendor_id UUID NOT NULL REFERENCES vendors(id) ON DELETE CASCADE, business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, alias TEXT NOT NULL, match_type TEXT NOT NULL CHECK (match_type IN ('exact', 'contains', 'starts_with', 'regex')), source TEXT NOT NULL CHECK (source IN ('manual', 'bank', 'ocr', 'learned')), times_matched INTEGER DEFAULT 0, last_matched_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(business_id, alias));
CREATE INDEX idx_vendor_aliases_lookup ON vendor_aliases(business_id, alias);CREATE INDEX idx_vendor_aliases_vendor ON vendor_aliases(vendor_id);Queries de diagnóstico
Sección titulada «Queries de diagnóstico»Ver todos los aliases de un proveedor:
SELECT va.*, v.name as vendor_nameFROM vendor_aliases vaJOIN vendors v ON va.vendor_id = v.idWHERE va.vendor_id = '<vendor_id>'ORDER BY va.times_matched DESC;Aliases más usados del negocio:
SELECT va.alias, va.source, va.times_matched, v.name as vendor_nameFROM vendor_aliases vaJOIN vendors v ON va.vendor_id = v.idWHERE va.business_id = '<business_id>'ORDER BY va.times_matched DESCLIMIT 20;Proveedores sin aliases:
SELECT v.id, v.nameFROM vendors vLEFT JOIN vendor_aliases va ON va.vendor_id = v.idWHERE v.business_id = '<business_id>' AND va.id IS NULLORDER BY v.name;Matching fallido (sin alias y sin CIF):
-- Gastos sin proveedor asignadoSELECT e.id, e.ocr_vendor_name, e.amountFROM expenses eWHERE e.business_id = '<business_id>' AND e.vendor_id IS NULL AND e.ocr_vendor_name IS NOT NULLORDER BY e.created_at DESC;Probar matching manualmente:
-- Simula el RPC find_vendor_by_aliasSELECT va.vendor_id, va.alias, va.match_type, va.times_matched, CASE va.match_type WHEN 'exact' THEN 100 WHEN 'starts_with' THEN 90 WHEN 'contains' THEN 85 WHEN 'regex' THEN 80 END AS confidenceFROM vendor_aliases vaWHERE va.business_id = '<business_id>' AND ( (va.match_type = 'exact' AND LOWER(va.alias) = LOWER('<search_term>')) OR (va.match_type = 'contains' AND LOWER('<search_term>') LIKE '%' || LOWER(va.alias) || '%') OR (va.match_type = 'starts_with' AND LOWER('<search_term>') LIKE LOWER(va.alias) || '%') )ORDER BY confidence DESC, va.times_matched DESC;Logs de debugging
Sección titulada «Logs de debugging»Para depurar matching:
// En useOCRVendorMatch.tsconsole.log('🔍 OCR Vendor Match:', { ocrVendorName, nifCif, matchResult: { vendor, matchType, confidence }});
// En QuickVendorModal.tsxconsole.log('🔗 Alias OCR creado:', { initialName, savedName, vendorId});
// En useBankTransactionVendorMatch.tsconsole.log('🏦 Bank Match:', { merchantName, matchResult: { vendor, confidence, matchedBy }});