Saltar al contenido principal

Inteligencia de Contactos de Adquisiciones

Módulo tipo SaaS de venta de contactos B2B (estilo Apollo / Lusha / RocketReach) que permite a los proveedores encontrar a los encargados de compras de organismos públicos y desbloquear su email y teléfono pagando créditos.

Descripción

El módulo expone la base de contactos de quienes cursan las compras del Estado (datos provenientes de Mercado Público / Compra Ágil, ya disponibles en BigQuery). El proveedor busca un organismo, ve la lista de encargados de compras con su nombre, unidad y actividad, y el email y el teléfono aparecen enmascarados (s••••@maipu.cl, +56 2 2677 ••••). Para revelarlos, desbloquea el contacto gastando 1 crédito.

Modelo de negocio (igual a los SaaS de contactos):

  • Buscar y ver es gratis e ilimitado — es el gancho.
  • Se paga por desbloquear (1 crédito = email + teléfono de 1 contacto).
  • Un contacto desbloqueado queda disponible para toda la empresa, sin volver a cobrar.

Características Principales

🔍 Búsqueda de organismos

  • Por RUT exacto (formato XX.XXX.XXX-X) → va directo a los contactos del organismo.
  • Por nombre (texto libre, mín. 3 caracteres) → muestra una lista de organismos coincidentes para elegir. La búsqueda es multi-palabra (tokenizada): "municipalidad maipu" matchea "I MUNICIPALIDAD DE MAIPU".
  • Listado por defecto: al abrir el módulo se muestran automáticamente los organismos con más contactos disponibles (mayor cobertura), sin necesidad de buscar.

⚠️ Siempre se filtra por institucion.rut (RUT exacto), nunca por nombre del organismo a nivel de contactos: buscar "maip" mezclaría 13 organismos distintos (Isla de Maipo, San José de Maipo, CRS Maipú…).

👤 Listado de contactos

Por cada encargado de compras (deduplicado por LOWER(email)):

  • Nombre del encargado.
  • Unidad / Área desde la que postula (institucion.organizacion de su oportunidad más reciente).
  • Actividad: nº de oportunidades que maneja (proxy de relevancia) + última actividad (fecha de su oportunidad más reciente, proxy de vigencia del contacto).
  • Email y teléfono enmascarados (o completos si ya fueron desbloqueados por la empresa).
  • Filtro en memoria por nombre del contacto o unidad sobre la lista ya cargada.

🔓 Desbloqueo por créditos

  • Botón "Desblopquear (1 crédito)" en cada contacto no desbloqueado.
  • Transacción atómica: verifica saldo → debita 1 crédito → registra el unlock → devuelve email/teléfono completos.
  • Idempotente: un contacto ya desbloqueado por la empresa no se vuelve a cobrar.
  • El saldo del header se actualiza en tiempo real.

💳 Créditos y planes

Los créditos son por empresa (pool compartido por todos los usuarios de esa company). El saldo se muestra en el header y el badge es clickeable para reabrir el modal explicativo / de planes.

PlanCréditos/mesPrecio
Incluido5Gratis (gancho)
Starter500,5 UF/mes
Pro2001 UF/mes
Pack adicional100 (no expiran)0,75 UF

La compra self-service con pasarela de pago es Fase 2. Hoy los créditos se cargan manualmente (ver más abajo).

Arquitectura

Cloud Functions (functions/src/contacts_intelligence/)

FunciónTipoDescripción
getTopOrganismosonCallTop ~20 organismos por cobertura de contactos (listado por defecto). Cache global 24h en org_contacts_top/default. Sin PII.
searchOrganismosonCallBúsqueda de organismos por nombre (tokenizada, multi-palabra). Devuelve nombre + RUT + nº de contactos. Sin PII.
searchOrgContactsonCallContactos de un organismo por RUT. Cache 24h en org_contacts/{rut}. Aplica enmascarado server-side y merge con los unlocks de la empresa.
unlockContactonCallDesbloqueo transaccional de 1 contacto (debita 1 crédito, idempotente).
getCreditsBalanceonCallLectura del saldo de créditos de la empresa.
renewMonthlyCreditsscheduledCron mensual de renovación de créditos (ver abajo).

Seguridad común: validación de auth + rol (Admin/Manager/Executive/Executive Avanzado leídos de accounts/{uid}.roles[companyId], que es un array) + tope anti-abuso de 300 búsquedas/empresa/hora (no es monetización; la búsqueda es libre, el cobro está en el unlock).

BigQuery: queries parametrizadas (el texto/RUT nunca se interpola en el SQL). No se fija location — BigQuery autodetecta la región del dataset.

Enmascarado server-side

Los campos email y telefono completos nunca salen de la Cloud Function si el contacto no está desbloqueado por la empresa. El frontend solo recibe los valores enmascarados + unlocked: boolean.

Modelo de datos (Firestore)

Colección / DocumentoAccesoContenido
org_contacts/{rut}Solo Admin SDKCache del organismo: institucion, cachedAt, contactCount, schemaVersion.
org_contacts/{rut}/contacts/{contactId}Solo Admin SDKContacto deduplicado (contactId = MD5 de LOWER(email)): encargado, email, telefono, unidad, ultimaFecha, n_oportunidades, origen.
org_contacts_top/defaultSolo Admin SDKCache global del listado de organismos destacados.
companies/{companyId}/credits/balanceAdmin / miembros de la companybalance (número), monthlyCredits (tope mensual, default 5), updatedAt, lastRenewalAt.
companies/{companyId}/unlocks/{contactId}Admin / miembros de la companyDesbloqueos de la empresa: contactId, rut, unlockedAt, unlockedBy.
companies/{companyId}/api-rate-limit/{windowKey}Contador de búsquedas por ventana horaria (compartido con la Public API).

Cache e invalidación

  • searchOrgContacts cachea por RUT con TTL de 24h. El campo schemaVersion invalida cache viejo cuando cambia la forma de los documentos (p.ej. al agregar unidad/ultimaFecha).
  • getTopOrganismos cachea globalmente 24h (el listado es idéntico para todas las empresas).

Renovación mensual de créditos (cron)

renewMonthlyCredits (scheduled, día 1 de cada mes, 06:00 America/Santiago):

  • Recorre todas las empresas y aplica balance = max(balance, tope).
  • Rellena hasta el tope, nunca reduce → respeta los créditos de packs comprados.
  • El tope se lee de companies/{id}/credits/balance.monthlyCredits (default 5 = plan "Incluido"). Para planes pagados basta con setear monthlyCredits: 50/200 en esa empresa y el mismo cron reparte.
  • Si una empresa no tiene doc de créditos, lo crea con el tope por defecto.

Carga manual de créditos (admin)

Mientras no exista compra self-service, un admin de plataforma carga créditos desde:

/company-settings → tab Empresas → menú "⋮" de la empresa → "Cargar créditos".

  • Muestra el saldo actual y pide la cantidad a agregar.
  • Escribe con incremento atómico (FieldValue.increment) sobre companies/{companyId}/credits/balance.
  • Protegido por reglas Firestore (isAdmin()): solo admins de plataforma.

Frontend

ComponenteUbicaciónRol
InteligenciaContactosComponentpages/commercial/inteligencia-contactos/Página principal: buscador, header con saldo, listado de organismos.
ContactosListaComponent.../components/contactos-lista/Tabla de contactos: filtro en memoria, columna Actividad, unlock.
IcInfoModalComponent.../components/info-modal/Modal explicativo + tabla de planes.
InteligenciaContactosServiceshared/services/Callables (searchOrganismos, searchOrgContacts, unlockContact, getTopOrganismos) + watchCreditsBalance (tiempo real).

Modal informativo: se auto-muestra la primera vez (persiste en localStorage con la key ic-adquisiciones:info-seen). El badge de créditos del header lo reabre al clic, sin alterar el flag.

  • Multi-tenancy: toda operación usa el companyId de la empresa activa.
  • Ley 19.628 (Chile): son datos de contacto profesional de funcionarios en procesos de compra del Estado (fuente pública), pero igual aplica la ley. Antes del lanzamiento comercial completo se requiere: revisión de los TdU de las APIs de ChileCompra/Mercado Público, cláusula en los ToS de Marko y un flujo de oposición/eliminación para funcionarios.

Fuera de alcance (Fase 2)

  • Job de enriquecimiento por documentos de postulación (subir la cobertura actual, ~7% promedio, mayor en organismos grandes).
  • Compra de créditos self-service con pasarela de pago.
  • Modelo de dos bolsas (créditos de plan que se resetean + packs que persisten).
  • Exportación CSV de contactos desbloqueados.