-- =============================================================================
--  inv_invitado — CANDADO ESTRUCTURAL contra los invitados duplicados por réplica
-- =============================================================================
--  Base de datos : asturias_api  (servidor PÚBLICO, CL032BACK)
--  Archivo       : database/sql/inv_invitado_unique_replica.sql
--  Fecha         : 2026-08-17
--  Equivale a    : database/migrations/2026_08_17_120000_add_unique_replica_to_inv_invitado.php
--
--  MOTIVO
--  ------
--  La reconciliación ya adopta la fila del socio en vez de insertar otra (que es lo que
--  duplicaba los invitados de las solicitudes nacidas en el Portal). Este índice cubre
--  lo que el código no puede cubrir solo: la CARRERA.
--
--  El outbox de Tlalpan deduplica sus marcas con
--      WHERE replicacionEstado IN ('Pendiente','Error')
--  y 'Procesando' NO está en esa lista. Una marca EN VUELO no deduplica: si el operador
--  edita la solicitud mientras su réplica se envía, nace una segunda marca. Con
--  INVITADOS_REPLICACION_MODO=cola (un job por marca, varios workers) o tras un lease
--  vencido, esas dos marcas llegan como DOS POST /replica SIMULTÁNEOS.
--
--  Y ahí ninguna defensa del código alcanza:
--    · el corte por hash no protege — las dos leen el hash antes de que ninguna lo selle;
--    · la reconciliación tampoco   — las dos leen el mismo estado y las dos INSERTAN;
--    · la transacción tampoco      — son transacciones distintas y nada las serializa.
--
--  Con el índice, la segunda escritura choca, su transacción revierte entera, el
--  endpoint responde 500, Tlalpan lo trata como TRANSITORIO y reintenta. En el reintento
--  la fila ya existe y se ACTUALIZA. El duplicado nunca llega a existir.
--
--  ⚠️ NULL NO ESTORBA: en MySQL un UNIQUE admite tantos NULL como quiera, así que los
--  invitados que captura el socio —todos con idSolicitudInvitadoTlalpan = NULL hasta que
--  Tlalpan los adopta— siguen conviviendo sin restricción. El candado gobierna SOLO lo
--  replicado.
--
--  Sustituye al índice NO único idx_invitado_tlalpan (migración 2026_08_13_130000):
--  mismas columnas y mismo orden, así que las consultas siguen resolviéndose igual.
-- =============================================================================

-- -----------------------------------------------------------------------------
--  PASO 1 — VERIFICAR ANTES DE ALTERAR. Debe devolver 0 filas.
--  Si devuelve alguna, conserva la de idInvitado MÁS BAJO de cada grupo (es la que
--  tiene el documento en disco) y pon el resto en idSolicitudInvitadoTlalpan = NULL
--  con invitadoEstado = 'Eliminado'. NO borres filas: el disco indexa por idInvitado.
-- -----------------------------------------------------------------------------
SELECT idSolicitud, idSolicitudInvitadoTlalpan, COUNT(*) AS n,
       GROUP_CONCAT(idInvitado ORDER BY idInvitado) AS filas
  FROM inv_invitado
 WHERE idSolicitudInvitadoTlalpan IS NOT NULL
 GROUP BY idSolicitud, idSolicitudInvitadoTlalpan
HAVING COUNT(*) > 1;

-- -----------------------------------------------------------------------------
--  PASO 2 — EL CANDADO
--  Se crea el UNIQUE antes de soltar el otro: si el ALTER fallara, la tabla no se queda
--  ni un instante sin índice para (idSolicitud, puntero), que es por donde busca la
--  reconciliación en cada réplica.
-- -----------------------------------------------------------------------------
ALTER TABLE `inv_invitado`
  ADD UNIQUE KEY `uq_invitado_tlalpan` (`idSolicitud`, `idSolicitudInvitadoTlalpan`);

ALTER TABLE `inv_invitado`
  DROP INDEX `idx_invitado_tlalpan`;

-- -----------------------------------------------------------------------------
--  REVERSA
-- -----------------------------------------------------------------------------
-- ALTER TABLE `inv_invitado`
--   ADD INDEX `idx_invitado_tlalpan` (`idSolicitud`, `idSolicitudInvitadoTlalpan`);
-- ALTER TABLE `inv_invitado`
--   DROP INDEX `uq_invitado_tlalpan`;
