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.organizacionde 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.
| Plan | Créditos/mes | Precio |
|---|---|---|
| Incluido | 5 | Gratis (gancho) |
| Starter | 50 | 0,5 UF/mes |
| Pro | 200 | 1 UF/mes |
| Pack adicional | 100 (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ón | Tipo | Descripción |
|---|---|---|
getTopOrganismos | onCall | Top ~20 organismos por cobertura de contactos (listado por defecto). Cache global 24h en org_contacts_top/default. Sin PII. |
searchOrganismos | onCall | Búsqueda de organismos por nombre (tokenizada, multi-palabra). Devuelve nombre + RUT + nº de contactos. Sin PII. |
searchOrgContacts | onCall | Contactos de un organismo por RUT. Cache 24h en org_contacts/{rut}. Aplica enmascarado server-side y merge con los unlocks de la empresa. |
unlockContact | onCall | Desbloqueo transaccional de 1 contacto (debita 1 crédito, idempotente). |
getCreditsBalance | onCall | Lectura del saldo de créditos de la empresa. |
renewMonthlyCredits | scheduled | Cron 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 / Documento | Acceso | Contenido |
|---|---|---|
org_contacts/{rut} | Solo Admin SDK | Cache del organismo: institucion, cachedAt, contactCount, schemaVersion. |
org_contacts/{rut}/contacts/{contactId} | Solo Admin SDK | Contacto deduplicado (contactId = MD5 de LOWER(email)): encargado, email, telefono, unidad, ultimaFecha, n_oportunidades, origen. |
org_contacts_top/default | Solo Admin SDK | Cache global del listado de organismos destacados. |
companies/{companyId}/credits/balance | Admin / miembros de la company | balance (número), monthlyCredits (tope mensual, default 5), updatedAt, lastRenewalAt. |
companies/{companyId}/unlocks/{contactId} | Admin / miembros de la company | Desbloqueos 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
searchOrgContactscachea por RUT con TTL de 24h. El camposchemaVersioninvalida cache viejo cuando cambia la forma de los documentos (p.ej. al agregarunidad/ultimaFecha).getTopOrganismoscachea 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 setearmonthlyCredits: 50/200en 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) sobrecompanies/{companyId}/credits/balance. - Protegido por reglas Firestore (
isAdmin()): solo admins de plataforma.
Frontend
| Componente | Ubicación | Rol |
|---|---|---|
InteligenciaContactosComponent | pages/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. |
InteligenciaContactosService | shared/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.
Seguridad y legal
- Multi-tenancy: toda operación usa el
companyIdde 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.