# Deprecación y Migración — Implementaciones Completas

## Feature Flags para Migraciones Graduales

```python
# feature_flags.py — flags para migrar gradualmente
from enum import Enum


class FeatureFlag(str, Enum):
    MOTOR_BUSQUEDA_OPENSEARCH = "motor_busqueda_opensearch"
    API_V2_RESPUESTA = "api_v2_respuesta"
    SESIONES_POSTGRES = "sesiones_postgres"


class ServicioFeatureFlags:
    """Lee feature flags desde la base de datos con caché local."""

    def esta_activo(self, flag: FeatureFlag, contexto: dict | None = None) -> bool:
        configuracion = self._obtener_config(flag)

        if not configuracion.activo:
            return False

        if configuracion.porcentaje_rollout < 100:
            if contexto and "usuario_id" in contexto:
                hash_usuario = hash(contexto["usuario_id"] + flag.value) % 100
                return hash_usuario < configuracion.porcentaje_rollout

        return True


# Uso en servicio de búsqueda
class ServicioBusqueda:
    def __init__(self, flags: ServicioFeatureFlags):
        self.flags = flags
        self.es_cliente = ElasticSearchCliente()
        self.os_cliente = OpenSearchCliente()

    def buscar(self, query: str, usuario_id: str) -> list[Resultado]:
        if self.flags.esta_activo(
            FeatureFlag.MOTOR_BUSQUEDA_OPENSEARCH,
            contexto={"usuario_id": usuario_id},
        ):
            return self.os_cliente.buscar(query)
        else:
            return self.es_cliente.buscar(query)
```

---

## Patrón Strangler Fig — Migración de Monolito a Microservicios

```
Antes:
  Cliente -> [Monolito] -> BD monolito

Proceso (reemplazar módulo por módulo):
  Paso 1: Cliente -> [Proxy/API Gateway] -> [Monolito]
  Paso 2: Cliente -> [Proxy] -> { [Monolito], [Nuevo Servicio Pagos] }
  Paso 3: Cliente -> [Proxy] -> { [Monolito sin pagos], [Servicio Pagos] }
  ...
  Final:  Cliente -> [API Gateway] -> { [Svc Pedidos], [Svc Pagos], [Svc Usuarios] }
```

```python
# proxy_router.py — Strangler Fig con feature flags
class ProxyEstrangulador:
    """
    Enruta peticiones al monolito o al nuevo servicio según el feature flag.
    Permite migración gradual sin cambios en el cliente.
    """

    def __init__(
        self,
        monolito_url: str,
        flags: ServicioFeatureFlags,
    ):
        self.monolito_url = monolito_url
        self.flags = flags
        self.servicios_nuevos: dict[str, str] = {}

    def registrar_servicio(self, patron_ruta: str, url_servicio: str) -> None:
        self.servicios_nuevos[patron_ruta] = url_servicio

    async def enrutar(self, request: Request) -> Response:
        ruta = request.url.path

        for patron, url_servicio in self.servicios_nuevos.items():
            if ruta.startswith(patron):
                flag_nombre = f"nuevo_servicio_{patron.strip('/').replace('/', '_')}"
                usuario_id = request.headers.get("X-User-ID")

                if self.flags.esta_activo(
                    FeatureFlag(flag_nombre),
                    contexto={"usuario_id": usuario_id},
                ):
                    return await self._proxy(request, url_servicio)

        return await self._proxy(request, self.monolito_url)

    async def _proxy(self, request: Request, destino_url: str) -> Response:
        async with httpx.AsyncClient() as cliente:
            respuesta = await cliente.request(
                method=request.method,
                url=f"{destino_url}{request.url.path}",
                headers=dict(request.headers),
                content=await request.body(),
                timeout=30.0,
            )
        return Response(
            content=respuesta.content,
            status_code=respuesta.status_code,
            headers=dict(respuesta.headers),
        )
```

---

## Guía de Migración — Plantilla para Usuarios

```markdown
# Guía de Migración: v1 -> v2 del Endpoint de Usuarios

## Resumen
El endpoint `/v1/usuarios` será eliminado el **2026-09-01**.
Esta guía explica cómo migrar a `/v2/usuarios`.

## Qué cambió?

| Aspecto | v1 | v2 |
|---------|----|----|
| Campo nombre | `nombre_completo` (string) | `nombre` + `apellidos` (strings separados) |
| Dirección | Objeto plano | Objeto anidado con `calle`, `ciudad`, `cp` |
| Fechas | `"2024-01-15T10:00:00"` (sin TZ) | `"2024-01-15T10:00:00-06:00"` (con TZ) |

## Antes (v1)
```json
GET /v1/usuarios/abc123
{
  "id": "abc123",
  "nombre_completo": "Ana García López",
  "direccion": "Insurgentes Sur 123, CDMX, 06600"
}
```

## Después (v2)
```json
GET /v2/usuarios/abc123
{
  "id": "abc123",
  "nombre": "Ana",
  "apellidos": "García López",
  "direccion": {
    "calle": "Insurgentes Sur 123",
    "ciudad": "Ciudad de México",
    "cp": "06600"
  }
}
```

## Script de Migración (TypeScript)
```typescript
function adaptarUsuarioV1aV2(v1: UsuarioV1): UsuarioV2 {
  const [nombre, ...apellidosParts] = v1.nombre_completo.split(" ");
  return {
    id: v1.id,
    nombre,
    apellidos: apellidosParts.join(" "),
    direccion: parsearDireccion(v1.direccion),
  };
}
```

## Línea de Tiempo
- **2026-03-01**: v2 disponible en staging
- **2026-04-01**: v2 disponible en producción
- **2026-06-01**: v1 retorna header `Deprecation: true`
- **2026-09-01**: v1 eliminado (retorna 410 Gone)
```

---

## Gestión de Breaking Changes en APIs Públicas

```python
# Detectar clientes que aún usan endpoints deprecados
class MonitorDeprecados:
    def registrar_uso(
        self,
        endpoint: str,
        version: str,
        cliente_id: str,
        user_agent: str,
    ) -> None:
        db.execute("""
            INSERT INTO uso_endpoints_deprecados
                (endpoint, version, cliente_id, user_agent, timestamp)
            VALUES (:ep, :v, :cid, :ua, now())
            ON CONFLICT (endpoint, version, cliente_id)
            DO UPDATE SET
                ultimo_uso = now(),
                total_llamadas = uso_endpoints_deprecados.total_llamadas + 1
        """, {"ep": endpoint, "v": version, "cid": cliente_id, "ua": user_agent})

    def clientes_activos_deprecados(
        self, endpoint: str, dias: int = 30
    ) -> list[dict]:
        return db.execute("""
            SELECT
                cliente_id,
                MAX(ultimo_uso) AS ultimo_uso,
                SUM(total_llamadas) AS llamadas_totales
            FROM uso_endpoints_deprecados
            WHERE endpoint = :ep
              AND ultimo_uso > now() - INTERVAL ':dias days'
            GROUP BY cliente_id
            ORDER BY llamadas_totales DESC
        """, {"ep": endpoint, "dias": dias}).fetchall()
```
