# Tarea 67 — Deduplicación de Entidades, Solicitud de Asociación y Código Único

**Proyecto:** LPDI SGR (eco.lpdi.co) · **Fecha:** 23 de julio de 2026 · **Estado:** ANÁLISIS Y PLAN — pendiente de tu aprobación antes de ejecutar

---

## 1. Entendimiento del problema (verificado en el código)

**Cómo se crea hoy una entidad:** el usuario entra a `/registro-startup`, `/registro-icg` (público) o al dashboard; el servidor inserta la fila en estado DRAFT; al "Registrar y continuar" se formaliza y queda activa. **No hay flujo de aprobación:** nadie revisa si la entidad es legítima o duplicada.

**Qué protección contra duplicados existe hoy:**

| Mecanismo | Qué hace | Qué NO hace |
|---|---|---|
| Índice único `(registrador, nombre)` excluyendo borradores | Impide que **el mismo usuario** registre dos veces el mismo nombre | **No impide que dos usuarios distintos registren la misma empresa** — este es el hueco que describes |
| `/api/check-company-name` | Busca coincidencia exacta (sensible a tildes) | No busca en el nombre legal, no cruza startups contra ICGs, no detecta variantes ("Acme SAS" vs "Acme S.A.S.") |
| Aviso ámbar en los formularios | "Ya existe una empresa con este nombre" | Es puramente informativo: no bloquea, no muestra cuál es, no ofrece alternativa. Se puede ignorar |

**Diagnóstico:** el sistema está expuesto hoy. A nivel de base de datos nada impide que dos usuarios creen "Acme" dos veces; a nivel de interfaz solo hay un aviso ignorable. Consecuencias: fichas fragmentadas de la misma empresa, métricas infladas, y confusión en perks y eventos. A favor: ya existen columnas normalizadas (`lower + unaccent`) que podemos reutilizar para el matching.

---

## 2. Evaluación de tu propuesta

**¿Mitiga el problema? Sí, sustancialmente.** Convierte el aviso pasivo en una **decisión consciente** (crear nueva vs. solicitar asociación), y la aprobación por el admin de la entidad existente es una verificación humana de pertenencia más robusta que validar dominios de correo (muchas startups tempranas usan Gmail).

**Lo que hay que aceptar o complementar:**

1. **El usuario siempre puede elegir "crear nueva de todas formas".** No recomiendo bloquear (hay casos legítimos, abajo), pero sí dejar rastro en auditoría cuando se ignora una coincidencia exacta.
2. **Casos borde legítimos** que impiden un bloqueo duro: mismo nombre en países distintos, filiales de un grupo, homónimos genuinos. Por eso el match muestra el país y no bloquea.
3. **Riesgo de fuga de datos en la tarjeta.** Pediste mostrar "correo del admin y del CEO". Ojo: esto choca con la regla que tú mismo fijaste el 14 de julio (evitar que por esta vía se extraiga información del sistema). Si mostramos correos completos a cualquiera que escriba un nombre, creamos un cosechador de correos. **Propuesta: nombre completo visible, pero correo enmascarado** (`j***@acme.com`, dominio visible) — basta para confirmar "sí, es mi empresa" sin regalar el correo. (Decisión 1 abajo.)

**Veredicto: la propuesta es correcta y ejecutable.** Se recomienda aprobarla con dos ajustes: correos enmascarados en la tarjeta, y auditoría de los "crear de todas formas" sobre coincidencia exacta.

---

## 3. Diseño de la solución

**Detección de coincidencias** (nuevo endpoint): busca contra startups **e** ICGs (una startup nueva puede chocar con un ICG y viceversa), en dos niveles: (1) coincidencia exacta normalizada (sin tildes, "Ácme S.A.S" ≡ "acme s.a.s"); (2) coincidencia aproximada por similitud de texto (captura "Acme SAS" vs "Acme S.A.S.", errores de tipeo). Compara nombre comercial, nombre legal y nombre de ICG, tal como pediste.

**UI en el registro:** al salir del campo de nombre, si hay coincidencias se abre un panel con tarjetas:

```
Encontramos entidades parecidas a "Acme"
┌────────────────────────────────────────────┐
│ ACME S.A.S. (Startup) · Colombia           │
│ Nombre legal: Acme Tecnología S.A.S.       │
│ Admin: Juan Pérez (j***@acme.com)          │
│ CEO: María Gómez (m***@acme.com)           │
│ Web: acme.com   ·  Coincidencia exacta     │
│ [Es mi empresa, solicitar asociación]      │
└────────────────────────────────────────────┘
[No es ninguna, crear entidad nueva]
```

**Flujo de solicitud de asociación (invitación inversa):** reutiliza los patrones ya probados del sistema (invitaciones de equipo con token y correos, transferencias de entidad). Nueva tabla `entity_join_requests`. **Quién aprueba** = exactamente quien ya puede editar la entidad: en startup, el registrador, el CEO o un miembro verificado con edición; en ICG, solo el registrador (los contactos ICG nunca editan, regla dura tuya). Ciclo: el solicitante pide → correo a los aprobadores con su tarjeta → el aprobador acepta o rechaza. Si acepta, entra como miembro verificado (ambas partes consintieron); el aprobador decide si con permiso de edición (default: sin). Expira a 10 días con recordatorio al día 5. El solicitante debe estar verificado (Opción B) para poder enviar la solicitud.

---

## 4. Código único de entidad

Imitando el generador de códigos de eventos (atómico, sin colisiones): **formato `TT-PPP-AA-NNNN`** → `ST-170-26-0042` (startup #42, Colombia, 2026), `IC-604-26-0007` (ICG, Perú).

- `TT` = tipo (ST startup, IC ICG) · `PPP` = país ISO numérico · `AA` = año de formalización · `NNNN` = consecutivo por tipo+año.
- Se asigna **al formalizar** (los borradores abandonados no queman números), es **inmutable** (no se regenera aunque cambie el nombre), y se hace **backfill** a las entidades ya formalizadas (las más antiguas reciben los números más bajos).
- Se muestra en la ficha, el listado, la consulta admin, los correos de la entidad y la tarjeta de coincidencias.

---

## 5. Plan de implementación (10 tareas, 4 olas)

Todas las migraciones: **aditivas, idempotentes y reversibles**, con snapshot previo (Supabase sin PITR). Zonas de alto riesgo (migraciones + RLS recursiva) con manos Opus y verificación con evidencia.

| Ola | Tareas | Contenido |
|-----|--------|-----------|
| **0** (alto riesgo) | 1–3 | Migraciones: matching de similares, tabla de solicitudes de asociación, código único + backfill |
| **1** | 4–6 | Backend: endpoint de similares (con correos enmascarados), lib de solicitudes de asociación + correos, asignación de código al formalizar |
| **2** | 7–9 | UI: modal de coincidencias en los 4 formularios, pantalla de gestión de solicitudes, mostrar el código único |
| **3** | 10 | Integración: flujo REAL completo en preview + smoke + deploy verificado por SHA |

Cada tarea lleva su "Done when" verificable (tests, idempotencia probada con psql, capturas Playwright, deploy por commit).

---

## 6. Necesito que decidas (antes de arrancar)

1. **Correos en la tarjeta:** ¿completos como pediste, o enmascarados (`j***@acme.com`)? Recomiendo enmascarados, por tu propia regla anti-extracción del 14-jul. El nombre completo sí se muestra.
2. **¿"Crear nueva de todas formas" siempre permitido?** Recomiendo que sí (homónimos legítimos entre países), dejando rastro en auditoría en coincidencia exacta. Alternativa dura: bloquear coincidencia exacta en el mismo país.
3. **Cuándo se asigna el código:** al formalizar (recomendado) o al crear el borrador.
4. **Formato del código:** ¿apruebas `TT-PPP-AA-NNNN` (ej. `ST-170-26-0042`)? ¿O sin país (`ST-26-0042`), o consecutivo global compartido entre startups e ICGs?
5. **Permisos del miembro aprobado:** ¿el aprobador elige si puede editar (default: sin), o siempre entra sin edición y el permiso se da después?
6. **Vigencia de la solicitud:** ¿10 días con recordatorio al día 5 (igual que las transferencias)?
7. **Duplicados ya existentes:** además de prevenir los futuros, ¿quieres un barrido único sobre la base actual (informe de posibles duplicados, solo lectura) para resolver caso por caso?

---

**Resumen:** hoy nada impide que dos usuarios registren la misma empresa; solo hay un aviso ignorable. Tu propuesta la convierte en una decisión consciente con verificación humana, y la consideramos correcta. La implementamos reutilizando infraestructura ya probada del sistema, en 10 tareas y 4 olas, con migraciones solo aditivas y reversibles. Con tu OK a las 7 decisiones arrancamos por la Ola 0.
