Ir al contenido

Matching Automático

Sistema de detección inteligente de proveedores desde OCR y transacciones bancarias.

Cuando subes un ticket, facturas o sincronizas transacciones bancarias, el sistema busca automáticamente:

  1. Coincidencia por CIF/NIF (100% confianza)
  2. Coincidencia por alias (85-95% confianza)
  3. Similitud de nombre (50-95% confianza)

Hook: useOCRVendorMatch()

Prioridades:

  1. CIF/NIF exacto

    • Busca en vendors.tax_id
    • Confianza: 100%
    • Matching inmediato
  2. Alias registrado

    • Busca en vendor_aliases
    • Usa RPC find_vendor_by_alias
    • Confianza: 85-95% (según tipo de alias)
  3. Nombre similar

    • Busca en vendors.name y vendors.legal_name
    • Calcula scoring de similitud
    • Confianza: 50-95% (según similitud)

Hook: useBankTransactionVendorMatch()

Prioridades:

  1. Alias bancario

    • Busca en vendor_aliases con source = 'bank'
    • Confianza: alta/media/baja (90% o más / 70-89% / menos de 70%)
  2. Nombre parcial

    • Busca en vendors.name con ILIKE
    • Confianza: media
RangoNivelAcción sugerida
100%ExactoAuto-asignar
85-99%AltoMostrar como sugerencia principal
70-84%MedioMostrar en lista de sugerencias
50-69%BajoMostrar como posible coincidencia
menos de 50%Sin matchPermitir crear nuevo

Cuando el usuario confirma un proveedor para un gasto:

  1. Se extrae el nombre original (del OCR o transacción bancaria)
  2. Se compara con el nombre guardado en el proveedor
  3. Si difieren, se crea un alias automático:
    • vendor_aliases.alias = nombre original
    • vendor_aliases.source = ‘ocr’ o ‘bank’ o ‘learned’
    • vendor_aliases.match_type = ‘contains’
  4. Próximas detecciones usarán ese alias para matching automático
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ática

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

1. useVendorMatching()

Archivo: src/features/vendors/hooks/useVendorMatching.ts

Funciones:

  • findVendorMatch(businessId, merchantName) → busca por alias y nombre
  • createAliasIfNeeded(vendorId, businessId, newAlias, source) → crea alias si no existe
  • getVendorSuggestion(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 match
if (nameLower === searchLower) return 0.95;
// Contains
if (nameLower.includes(searchLower) || searchLower.includes(nameLower)) {
const lengthRatio = min / max;
return 0.7 + (lengthRatio * 0.2); // 0.70 - 0.90
}
// Starts with
if (starts_with(first_4_chars)) return 0.6;
// Default
return 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;
}

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;

Archivo: src/features/vendors/api/vendor-adapter.ts

// Buscar por alias
async 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 alias
async 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;
}
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);

Ver todos los aliases de un proveedor:

SELECT va.*, v.name as vendor_name
FROM vendor_aliases va
JOIN vendors v ON va.vendor_id = v.id
WHERE 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_name
FROM vendor_aliases va
JOIN vendors v ON va.vendor_id = v.id
WHERE va.business_id = '<business_id>'
ORDER BY va.times_matched DESC
LIMIT 20;

Proveedores sin aliases:

SELECT v.id, v.name
FROM vendors v
LEFT JOIN vendor_aliases va ON va.vendor_id = v.id
WHERE v.business_id = '<business_id>'
AND va.id IS NULL
ORDER BY v.name;

Matching fallido (sin alias y sin CIF):

-- Gastos sin proveedor asignado
SELECT e.id, e.ocr_vendor_name, e.amount
FROM expenses e
WHERE e.business_id = '<business_id>'
AND e.vendor_id IS NULL
AND e.ocr_vendor_name IS NOT NULL
ORDER BY e.created_at DESC;

Probar matching manualmente:

-- Simula el RPC find_vendor_by_alias
SELECT 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 confidence
FROM vendor_aliases va
WHERE 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;

Para depurar matching:

// En useOCRVendorMatch.ts
console.log('🔍 OCR Vendor Match:', {
ocrVendorName,
nifCif,
matchResult: { vendor, matchType, confidence }
});
// En QuickVendorModal.tsx
console.log('🔗 Alias OCR creado:', {
initialName,
savedName,
vendorId
});
// En useBankTransactionVendorMatch.ts
console.log('🏦 Bank Match:', {
merchantName,
matchResult: { vendor, confidence, matchedBy }
});

< Volver a Proveedores | < Volver a Panel Negocio