# Plan de implementación — Integración multitenant con Google Ads

**Estado:** Propuesto, pendiente de aprobación  
**Fecha:** 2026-07-12  
**Regla:** no implementar código ni migraciones de este módulo hasta aprobación explícita.

## 1. Diagnóstico de la arquitectura actual

### Base disponible

- PHP 8.3 fijado en Composer, `strict_types`, PSR-4 y arquitectura modular sin framework.
- Punto de entrada público único en `public/index.php`; el document root debe apuntar a `public/`.
- `config.ini` externo obligatorio mediante `CCD_CONFIG_PATH`; el loader rechaza rutas relativas y archivos dentro del árbol del proyecto/publicación.
- PDO con prepared statements y configuración validada.
- `setup.php` exclusivamente CLI, migraciones SQL con checksum, registro de versión y bloqueo MySQL.
- Esquema inicial con tenants, licencias, usuarios, membresías, RBAC, auditoría y outbox.
- PHPUnit, PHPStan y PSR-12 configurados; el host local actual no permite ejecutar la suite completa porque tiene PHP 8.0 y no Composer.

### Vacíos que afectan al módulo

1. Aún no existe autenticación, sesión segura, `TenantContext`, middleware CSRF, autorización RBAC ni rate limiting. Los endpoints tenant no pueden publicarse hasta completar esta base de Sprint 1.
2. No existe router/controlador HTTP real ni renderizado Bootstrap. El dispatcher mínimo actual no debe crecer como una cadena de condicionales.
3. No existe un almacén de estados OAuth de un solo uso.
4. No existe abstracción criptográfica ni keyring con rotación.
5. No existe cliente Google Ads, cola de jobs ni worker cron.
6. Las tablas de dominios y detecciones todavía no existen; las FK opcionales de exclusiones hacia dichas entidades deben agregarse sólo cuando existan o incorporarse en la misma fase.
7. `audit_events` transversal ya existe. Se recomienda usarlo como auditoría canónica y crear un read model `google_ads_audit_logs` sólo si el panel necesita campos/retención específicos; duplicar logs sin contrato produciría inconsistencias.

### Decisión arquitectónica

El módulo será `GoogleAds` dentro del monolito modular, dividido en Domain, Application, Infrastructure e Interface. Los controladores sólo traducen HTTP, invocan casos de uso y devuelven respuestas. Ninguna clase de dominio dependerá del SDK de Google. El SDK quedará detrás de puertos/adaptadores para permitir pruebas sin cuentas reales.

## 2. Correcciones y precisiones a la especificación

- Una clave pública o `tenant_id` del navegador nunca autoriza el módulo. El tenant procede exclusivamente de `TenantContext` derivado de una sesión autenticada.
- `ListAccessibleCustomers` devuelve cuentas con acceso directo del usuario, no necesariamente todos los descendientes. El descubrimiento debe consultar esas raíces y recorrer `customer_client` para construir jerarquías MCC.
- `login_customer_id` identifica el manager usado para acceder al customer objetivo; no siempre coincide con el manager raíz ni con `manager_customer_id` descriptivo.
- El scope de Ads es `https://www.googleapis.com/auth/adwords`. Para mostrar correo se solicitarán también `openid` y `email`, con consentimiento y política de privacidad adecuados.
- El access token no se persistirá normalmente: el SDK obtendrá uno en memoria a partir del refresh token. Sólo el refresh token requiere persistencia cifrada.
- `config.ini` necesita un keyring, no una única clave sin mecanismo de rotación: `token_key_active_version` y claves versionadas.
- `setup.php` no puede quedar permanentemente deshabilitado tras la primera instalación porque debe aplicar actualizaciones. En producción seguirá siendo CLI, exigirá clave de instalación y quedará protegido por permisos del sistema operativo; las migraciones seguirán siendo reejecutables.
- MySQL ejecuta DDL con commits implícitos. La idempotencia se basará en migraciones forward-only, checksums y comprobación previa; no se prometerá rollback transaccional completo de DDL.
- Las exclusiones son criterios negativos por campaña. La compatibilidad y límites deben derivarse de la versión oficial vigente y de validación real de la API, no de una lista permanente codificada sin versionar.

## 3. Diagrama textual del flujo OAuth

```text
Usuario autenticado
  -> POST /client/google-ads/connect [CSRF + permiso google_ads.manage]
  -> GoogleOAuthController
  -> GoogleOAuthService crea state_id aleatorio + PKCE verifier/challenge
  -> persiste hash(state_id), tenant_id, user_id, redirect interno,
     PKCE cifrado, expiración y used_at=null
  -> firma envelope state con HMAC y genera URL Google
  -> navegador redirige a Google OAuth
  -> Google autentica/solicita consentimiento (nunca entrega contraseña a CCD)
  -> GET /oauth/google-ads/callback?code=...&state=...
  -> valida firma, issuer lógico, expiración, sesión y state persistido
  -> bloqueo de fila; rechaza state usado/reutilizado
  -> canjea code + PKCE por tokens usando redirect_uri exacta
  -> exige refresh_token; obtiene identidad verificada para el correo
  -> cifra refresh_token con clave activa y persiste conexión tenant
  -> marca state usado antes de continuar
  -> encola discover_customers y redirige a pantalla de selección
  -> worker usa refresh token sólo en memoria
  -> ListAccessibleCustomers obtiene raíces directas
  -> recorre CustomerClient para jerarquías MCC y rutas login_customer_id
  -> guarda snapshot de cuentas accesibles
  -> usuario selecciona explícitamente una cuenta cliente
  -> servicio revalida acceso/permisos con una consulta real
  -> guarda customer_id + login_customer_id + manager_customer_id
  -> encola sincronización de campañas
```

Callbacks con `error`, estado ausente/alterado/vencido/usado, sesión distinta, refresh token ausente o scope incompleto fallan de forma segura, sin incluir códigos/tokens en logs ni respuesta.

## 4. Diseño de datos

Todos los IDs externos de Google se almacenan sin guiones como `VARCHAR(32)`, no como enteros PHP. Todas las tablas tenant llevan `tenant_id`, índices compuestos y FK que impiden referencias cruzadas cuando MySQL lo permite. IP/CIDR se conserva cifrada para recuperación y como HMAC normalizado para deduplicar/buscar; no se almacena en claro como propone el borrador original.

### `google_oauth_states`

- `id`, `state_hash` unique, `tenant_id`, `user_id`, `pkce_verifier_ciphertext`, `key_version`.
- `return_path`, `expires_at`, `used_at`, `created_at`.
- Unique `(tenant_id, state_hash)`; índices de expiración y usuario.

### `google_ads_connections`

- Campos solicitados, más `public_id`, `token_ciphertext`, `token_nonce`, `token_key_version`, `token_fingerprint`, `scope_json`, `row_version`.
- Estado: `connecting`, `connected`, `token_expired`, `token_revoked`, `error`, `insufficient_permissions`, `disconnected`.
- `authorized_user_id` referencia membresía/usuario validado.
- Unique para una conexión activa por `(tenant_id, customer_id)`; la política permitirá historial desconectado sin perder auditoría.
- Los errores se almacenan sanitizados y limitados; nunca stack traces ni tokens.

### `google_ads_accessible_customers`

- Campos solicitados, más `public_id`, `direct_access`, `access_depth`, `access_path_json`, `effective_login_customer_id`, `can_manage_campaigns`, `last_verified_at`.
- Unique `(tenant_id, connection_id, customer_id, effective_login_customer_id)`.
- FK compuesta `(tenant_id, connection_id)` para aislamiento.

### `google_ads_campaigns`

- Campos solicitados, más `public_id`, `advertising_channel_type`, `advertising_channel_sub_type`, `serving_status`, `compatibility_policy_version`.
- Unique `(tenant_id, connection_id, customer_id, campaign_id)`.
- Índices `(tenant_id, customer_id, campaign_status)` y compatibilidad.

### `google_ads_ip_exclusions`

- Campos solicitados, reemplazando `ip_address/cidr_block` en claro por:
  - `target_ciphertext`, `target_nonce`, `target_key_version`.
  - `target_hmac`, `target_family` (`ipv4`, `ipv6`), `prefix_length`.
- `idempotency_key`, `operation_version`, `criterion_resource_name`, estado y errores sanitizados.
- Unique activo `(tenant_id, connection_id, customer_id, campaign_id, target_hmac, removed_at)` no es suficiente en MySQL por semántica NULL; se usará una columna generada/estado activo o una tabla de intención con unique determinístico.
- FK `domain_id` y `detection_id` se agregan cuando sus tablas existan; no se crearán FKs inválidas.

### `google_ads_sync_jobs`

- Campos solicitados, más `public_id`, `idempotency_key`, `locked_by`, `locked_at`, `heartbeat_at`, `timeout_at`, `max_attempts` y `next_attempt_at`.
- Unique `(tenant_id, idempotency_key)`.
- Índice de claim `(status, available_at, locked_at)` y por conexión.
- `payload_json` sólo contiene IDs internos y parámetros no secretos.

### Auditoría

- Fuente canónica: `audit_events`, ampliada si es necesario con `connection_id` en metadata segura.
- Si se aprueba tabla especializada `google_ads_audit_logs`, contendrá contexto consultable y una referencia única a `audit_events`, no una segunda historia independiente.
- La IP del actor se cifrará/HMAC o se aplicará política de retención; user agent se normaliza/trunca.

### Tablas adicionales

- `google_ads_token_events`: no contiene tokens; registra refresh/revocación/rotación y resultado.
- `google_ads_compatibility_policies`: versión de API, tipo/subtipo, soporte y fuente/fecha; permite actualizar compatibilidad sin desplegar lógica dispersa.

## 5. Contratos y clases

### Dominio y aplicación

- `GoogleOAuthService`: inicia y completa autorización; depende de state store, OAuth provider, cifrado, reloj y auditoría.
- `GoogleAdsConnectionService`: selecciona cuenta, prueba, desconecta localmente y revoca remotamente.
- `GoogleAdsCustomerDiscoveryService`: descubre raíces y jerarquías sin elegir automáticamente.
- `GoogleAdsCampaignService`: consulta/sincroniza campañas.
- `GoogleAdsIpExclusionService`: valida intención, compatibilidad, límite, idempotencia y crea jobs.
- `GoogleAdsCompatibilityService`: política versionada + validación real.
- `GoogleAdsTokenEncryptionService`: secretbox/keyring, rotación y zeroization razonable.
- `GoogleAdsTokenRefreshService`: crea credencial temporal y clasifica revocación/expiración.
- `GoogleAdsSyncQueueService`: enqueue, claim, heartbeat, retry, complete/cancel.
- `GoogleAdsAuditService`: allowlist de contexto seguro y redacción defensiva.
- `GoogleAdsClientFactory`: puerto que produce un gateway autenticado por operación.

### Repositorios

- Los seis repositorios solicitados más `GoogleOAuthStateRepository` y `GoogleAdsCompatibilityPolicyRepository`.
- Cada método tenant-owned recibe `TenantId` no opcional y usa `WHERE tenant_id = :tenant_id`.
- No habrá repositorio genérico ni métodos `find(id)` sin tenant.

### Gateways y DTO

- `GoogleOAuthGateway`, `GoogleAdsGateway`, `TokenCipher`, `Clock`, `TransactionManager`.
- DTO inmutables para URL OAuth, callback, cuenta accesible, campaña, exclusión, job y resultado del proveedor.
- Excepciones tipadas: OAuth inválido, token ausente/revocado, permiso insuficiente, cuenta inaccesible, incompatibilidad, duplicado, límite, error transitorio/permanente.

## 6. Archivos previstos

### Modificar

- `composer.json`: SDK oficial y extensiones requeridas.
- `config/config.ini.example`: sección `google_ads`, keyring e installation key hash de ejemplo.
- `src/Shared/Configuration/*`: configuración Ads validada y comprobación de permisos Linux.
- `setup.php`: clave requerida en producción, resumen, exit codes y migraciones.
- `public/index.php`: delegación al router/composición, sin lógica Google directa.
- `README.md`, `docs/ARQUITECTURA.md`, ADR y documentación operativa.

### Crear por fases

```text
database/migrations/0002_google_ads_core.sql
database/migrations/0003_google_ads_campaigns.sql
database/migrations/0004_google_ads_exclusions.sql
database/migrations/0005_google_ads_jobs.sql
src/GoogleAds/Domain/**
src/GoogleAds/Application/**
src/GoogleAds/Infrastructure/Google/**
src/GoogleAds/Infrastructure/Persistence/**
src/GoogleAds/Interface/Http/**
src/GoogleAds/Interface/Cli/**
bin/google_ads_worker.php
templates/client/google_ads/**
public/assets/js/google-ads.js
tests/Unit/GoogleAds/**
tests/Integration/GoogleAds/**
tests/Contract/GoogleAds/**
docs/GOOGLE_ADS_OPERATIONS.md
docs/adr/0006-google-ads-sdk-version.md
docs/adr/0007-token-encryption-keyring.md
docs/adr/0008-database-sync-queue.md
```

No se crearán archivos ejecutables dentro de `public/`, salvo el front controller existente.

## 7. Dependencias Composer y extensiones

### Requeridas

- `googleads/google-ads-php:^33.3`: cliente oficial compatible con Google Ads API v24 al momento de este plan.
- `google/auth`: dependencia transitiva del SDK; se fijará en lockfile, no se duplicará salvo necesidad explícita.
- `ext-sodium`: cifrado autenticado `sodium_crypto_secretbox`.
- `ext-pdo`, `ext-json`, `ext-openssl`, `ext-curl`, `ext-mbstring` y `ext-grpc`/transporte admitido por la versión del SDK según el hosting.

No se agregará framework. Antes de fijar versiones se ejecutarán `composer show`/auditoría en PHP 8.3 y se verificará la matriz oficial; actualmente la librería PHP oficial requiere PHP 8.1+ y la versión mínima de cliente para API v24 es 33.3.0.

## 8. Configuración externa requerida

```ini
[google_ads]
developer_token = "..."
oauth_client_id = "...apps.googleusercontent.com"
oauth_client_secret = "..."
oauth_redirect_uri = "https://dominio.example/oauth/google-ads/callback"
api_version = "v24"
token_key_active_version = 1
token_key_v1 = "base64-de-32-bytes-aleatorios"
state_hmac_key = "base64-de-32-bytes-aleatorios"
installation_key_hash = "hash-argon2id"
```

Base64 sólo transporta material aleatorio en el INI; no cifra tokens. El loader decodifica y exige 32 bytes, claves distintas para cifrado/state, HTTPS en producción y permisos sin lectura de grupo/otros en Linux. Nunca imprime valores.

## 9. Google Cloud Console

1. Un único proyecto de Central Click Defender, asociado coherentemente al developer token.
2. Habilitar Google Ads API.
3. Configurar OAuth consent screen, datos de empresa, dominio verificado, privacy policy y términos.
4. Crear un único OAuth Client ID tipo Web application.
5. Registrar exactamente el redirect URI productivo y URIs separados para staging; no wildcards.
6. Scopes: `https://www.googleapis.com/auth/adwords`, `openid`, `email`.
7. Publicar/verificar la aplicación cuando corresponda; usuarios de prueba mientras esté en testing.
8. Restringir acceso administrativo al proyecto, habilitar 2SV, alertas y rotación de secreto.

Google exige 2-Step Verification para generar refresh tokens nuevos en este flujo; debe incluirse en onboarding y soporte.

## 10. Google Ads Manager / API Center

1. Crear o usar un MCC corporativo de Central Click Defender para solicitar el developer token; normalmente una empresa reutiliza un único token.
2. Verificar nivel vigente: Test sólo accede a cuentas test; Explorer/Basic/Standard tienen límites y permisos distintos.
3. Solicitar uso permisible de **Ad creation / management**, no sólo reporting, porque se mutarán criterios de campaña.
4. Para producción multiusuario, planificar Basic y luego Standard según volumen; mantener correo API supervisado.
5. Revisar Required Minimum Functionality si se solicita Standard Access y preparar acceso demo para revisión.
6. Las cuentas del cliente no tienen que vincularse obligatoriamente al MCC de CCD si el OAuth autorizado y una ruta manager válida conceden acceso; no se debe forzar una relación comercial innecesaria.
7. Crear cuentas test y campañas representativas para pruebas de mutación antes de usar producción.

## 11. Endpoints y autorización

Las rutas solicitadas se mantienen. Se añade `POST /client/google-ads/revoke` para diferenciar revocación Google de desconexión local. El callback es público en routing pero sólo acepta state válido, de un solo uso y asociado a una sesión compatible.

Cada mutación exige sesión, TenantContext, permiso, CSRF y rate limit. Admin global usa endpoint/policy separados e impersonación auditable; `{client}` nunca determina autorización. Respuestas usan Problem Details y IDs públicos opacos.

## 12. Cola y worker

- Job persistente por base de datos conforme a la petición; no reutilizar `outbox_messages` como cola de trabajo.
- Claim atómico mediante transacción y `SELECT ... FOR UPDATE SKIP LOCKED` en MySQL 8.
- Lease con `locked_at`, `heartbeat_at`, `timeout_at`, worker UUID y recuperación de jobs abandonados.
- Idempotency key determinística por intención/campaña/versión.
- Backoff exponencial con jitter, máximo configurable, clasificación estricta transitorio/permanente.
- Estados: pending, processing, completed, retry, failed, cancelled.
- Cron ejecuta `php /ruta/privada/bin/google_ads_worker.php --max-runtime=50` cada minuto con lock de proceso.
- Graceful shutdown, batch acotado, métricas y exit codes; ningún token en argumentos o logs.

## 13. Interfaz

Pantalla responsive Bootstrap 5 con los estados y campos indicados. El correo se muestra parcialmente enmascarado salvo necesidad/permiso. Mensajes obligatorios:

> Al conectar Google Ads, Central Click Defender podrá consultar las campañas seleccionadas y administrar exclusiones de IP en su nombre. La plataforma nunca tendrá acceso a su contraseña de Google.

> Las exclusiones se aplican por campaña y solo en campañas compatibles.

La desconexión abre modal que explica las dos opciones:

- **Desconectar localmente:** detiene jobs, elimina token local de forma criptográfica/lógica según retención y conserva auditoría; no revoca otros usos del consentimiento en Google.
- **Revocar en Google:** llama al endpoint de revocación, elimina ciphertext/nonce, cancela pendientes y registra resultado. Si Google no responde, la conexión queda `revocation_pending` y se reintenta de forma segura.

## 14. Estrategia de pruebas

- Unitarias: URL/state/PKCE, expiración/replay, cifrado/key version, redactor, IP/CIDR, compatibilidad, idempotencia, retry classifier.
- Integración MySQL: repositorios con tenant obligatorio, constraints cruzados, claim concurrente, checksums y cancelación.
- Contrato con fakes grabados/sanitizados: discovery directo/MCC, campañas, mutate/remove, códigos transitorios/permanentes.
- E2E con cuenta test: OAuth manual controlado, selección explícita, sincronización y exclusión reversible.
- Matriz completa solicitada: callback inválido/reutilizado, refresh ausente/revocado, cero/múltiples cuentas, manager/cliente, inaccesible/permisos, campaña compatible/incompatible, IP/CIDR, duplicado/límite, creación/eliminación, errores y aislamiento.
- Seguridad: comprobar que exception/log/audit/JSON no contienen refresh/access token, client secret, developer token, verifier PKCE ni clave.

Cada test cross-tenant intentará leer y mutar por ID conocido de otro tenant y verificará `404`/denegación sin revelar existencia.

## 15. Riesgos técnicos

| Riesgo | Mitigación |
|---|---|
| Developer token sin nivel/uso permisible para mutar producción | validar API Center antes de Fase 2; cuenta test y feature flag |
| Cambio/deprecación rápida de versión API | adapter + ADR/version pin + revisión trimestral de sunset |
| Hosting sin gRPC/extensiones/cron fiable | prueba de compatibilidad en Fase 1; evaluar transporte REST soportado o hosting apto |
| Refresh token no devuelto en reconexión | detectar, instruir/reconsentir sin sobrescribir token válido |
| Jerarquías MCC grandes/cíclicas lógicas | traversal con visited set, profundidad/batch/quotas |
| Falso supuesto de compatibilidad IP | policy versionada + validate-only/consulta real; no inferir sólo por tipo |
| Duplicados por workers concurrentes | unique/idempotency + row locks + reconciliación remota |
| Límites/cuotas/rate limits | throttling global/tenant/customer, backoff y budgets |
| Exposición de tokens en SDK/excepciones | logger nulo/sanitizado, redactor central, tests canary secrets |
| Pérdida de clave de cifrado | backup seguro/key escrow y rotación ensayada; sin clave no se recuperan tokens |
| Revocación fuera de CCD no detectada | clasificación `invalid_grant`, estado token_revoked; evaluar Cross-Account Protection |
| Setup DDL parcialmente aplicado | migraciones pequeñas forward-only, prechecks, backup y procedimiento de reparación |

## 16. Plan por fases y puertas de aprobación

### Fase 1 — Configuración, datos, cifrado y OAuth

- Completar prerequisitos de sesión/TenantContext/CSRF necesarios para OAuth.
- Config global validada, keyring, permisos de archivo y setup protegido.
- Migraciones connection/state; cipher secretbox y state+PKCE one-time.
- Inicio/callback OAuth, identidad, conexión y auditoría sanitizada.
- Salida: tests de state/replay/cipher/tenant y callback fake verdes; ningún secreto en salida.

### Fase 2 — Cuentas y campañas

- Discovery de accesos directos + jerarquías MCC.
- Selección explícita, validación de permisos y cálculo de login customer.
- Prueba de conexión y sincronización de campañas.
- Salida: casos cero/varias/manager/cliente/inaccesible verdes en fake y cuenta test.

### Fase 3 — Exclusiones y auditoría

- Política de compatibilidad versionada, normalización IP/CIDR y límites.
- Intención idempotente, mutate/remove de `IpBlockInfo`, reconciliación y auditoría.
- Salida: exclusión reversible en campaña test compatible y matriz de errores completa.

### Fase 4 — Cola y cron

- Job store, claim/lease/heartbeat, retries, DLQ lógica, cancelación y worker.
- Salida: concurrencia, crash recovery, error transitorio/permanente e idempotencia probados.

### Fase 5 — Interfaz, hardening y operación

- Panel Bootstrap, endpoints finales, modal desconectar/revocar, registros seguros.
- Rate limits, observabilidad, runbook, instalación, pentest focalizado y pruebas E2E.
- Salida: criterios funcionales completos, documentación y checklist productivo.

Al cerrar cada fase se entregará lista exacta de archivos, migraciones, comandos de prueba, resultados, limitaciones y se esperará aprobación antes de iniciar la siguiente.

## 17. Decisiones requeridas para aprobar Fase 1

1. Confirmar que primero se implementará el mínimo de IAM/sesión/TenantContext/CSRF necesario; hoy no existe y OAuth multitenant no puede ser seguro sin ello.
2. Confirmar `sodium_crypto_secretbox` y keyring versionado como cifrado.
3. Confirmar que IP/CIDR se almacenará cifrada + HMAC, no en claro.
4. Confirmar auditoría canónica en `audit_events` y read model especializado sólo si es necesario.
5. Proporcionar nivel/estado del developer token, versión de MySQL y extensiones disponibles del hosting.
6. Confirmar dominio/redirect URI de staging y producción.
7. Confirmar la política de desconexión: borrado criptográfico inmediato del refresh token y conservación de auditoría sin secretos.

## 18. Fuentes oficiales verificadas

- Google Ads API v24.1 es la versión vigente consultada; el cliente PHP mínimo para v24 es 33.3.0.
- Scope Ads oficial: `https://www.googleapis.com/auth/adwords`.
- `ListAccessibleCustomers` sólo refleja acceso directo y el árbol MCC se consulta mediante `CustomerClient`.
- Developer token, OAuth y `login-customer-id` cumplen funciones distintas.
- Los niveles del developer token, cuotas y permissible use condicionan mutaciones en producción.

Estas condiciones se volverán a verificar al comenzar cada fase que interactúe con Google, porque la API y sus políticas tienen calendario de cambios frecuente.
