# Arquitectura — ONEtoONE Inteligencia de Red

## Capas de abstracción

```
┌──────────────────────────────────────────────────────────────────┐
│              INGESTA EXTERNA (proceso externo / cron)            │
│                                                                  │
│  Gmail API + Calendar API  →  Claude / Gemini  →  BD relacional  │
│  (Google Workspace)           (clasificación,     interactions   │
│                                normalización,     table)         │
│                                extracción                        │
│                                de contacto)                      │
└──────────────────────────────────┬───────────────────────────────┘
                                   │ alimenta
┌──────────────────────────────────▼───────────────────────────────┐
│                         DATA (arrays PHP / BD real)              │
│  companies.php · users.php · interactions.php · web_content.php  │
└──────────────────────────────────┬───────────────────────────────┘
                                   │ lee
┌──────────────────────────────────▼───────────────────────────────┐
│                           REPOSITORIES                           │
│  CompanyRepository · UserRepository                              │
│  InteractionRepository · WebContentRepository                    │
└──────────┬────────────────────────────────────────────────────── ┘
           │ instancia / inyecta
┌──────────▼──────────┐           ┌──────────────────────────────┐
│      SERVICES        │           │           HELPERS            │
│  ScoreCalculator     │           │  DataProvider (static)       │
│  CompanyMatcher      │           │  StageHelper  (static)       │
│  ContactExtractor    │           └──────────────────────────────┘
│  NetworkStatsService │
└──────────┬──────────┘
           │ orquesta
┌──────────▼──────────────────────────────────────────────────────┐
│                         CONTROLLERS                             │
│        CompanyInsightController · UserInsightController         │
└──────────────────────────────────┬──────────────────────────────┘
                                   │ instancia
┌──────────────────────────────────▼──────────────────────────────┐
│                      ENTRY POINTS (public/)                     │
│          api.php · index.php · view.php · users.php             │
└─────────────────────────────────────────────────────────────────┘
```

---

## Entry points

| Archivo | Responsabilidad |
|---|---|
| `public/api.php` | Endpoint JSON. Enruta por query string a `CompanyInsightController` o `NetworkStatsService` |
| `public/index.php` | Landing con listado de empresas. Usa `DataProvider` (estático) |
| `public/view.php` | Vista de detalle de empresa. Instancia `CompanyInsightController` |
| `public/users.php` | Vista de detalle de advisor. Instancia `UserInsightController` |
| `public/scoring.php` | Página explicativa del sistema de puntuación. Sin lógica de negocio |
| `public/api-docs.php` | Documentación interactiva de la API. Sin lógica de negocio |

---

## Controllers

### `CompanyInsightController`
Orquesta la consulta de relaciones de red para una empresa dada.

**Atributos privados (instanciados en constructor)**
| Atributo | Tipo |
|---|---|
| `$companyRepo` | `CompanyRepository` |
| `$interactionRepo` | `InteractionRepository` |
| `$userRepo` | `UserRepository` |
| `$matcher` | `CompanyMatcherService` |
| `$scoreCalculator` | `ScoreCalculator` |
| `$contactExtractor` | `ContactExtractor` ← recibe `WebContentRepository` |

**Métodos**
| Visibilidad | Firma | Descripción |
|---|---|---|
| `public` | `handle(array $params): array` | Punto de entrada. Devuelve array con empresa, advisors, score y contacto |
| `private` | `resolveCompany(array $params): ?array` | Resuelve la empresa por `domain` o `id` |
| `private` | `errorResponse(string $message): array` | Formatea errores de respuesta |

---

### `UserInsightController`
Orquesta la consulta del historial de interacciones de un advisor.

**Atributos privados**
| Atributo | Tipo |
|---|---|
| `$userRepo` | `UserRepository` |
| `$interactionRepo` | `InteractionRepository` |
| `$companyRepo` | `CompanyRepository` |
| `$scoreCalculator` | `ScoreCalculator` |
| `$contactExtractor` | `ContactExtractor` ← recibe `WebContentRepository` |

**Métodos**
| Visibilidad | Firma | Descripción |
|---|---|---|
| `public` | `handle(array $params): array` | Devuelve historial de empresas del advisor con score |
| `private` | `extractRawKey(array $interaction): ?string` | Extrae dominio o nombre de empresa de la interacción |
| `private` | `buildMeta(array $company): array` | Construye metadatos de empresa para la respuesta |
| `private` | `errorResponse(string $message): array` | Formatea errores |

---

## Services

### `ScoreCalculator`
Calcula la puntuación de la relación advisor–empresa. Sin dependencias externas.

**Constantes de scoring**
| Constante | Valor |
|---|---|
| `SCORE_MEETING` | 15 |
| `SCORE_CALL` | 10 |
| `SCORE_EMAIL` | 7 |
| `SCORE_EVENT` | 5 |
| `BONUS_ACTIVE` | +20 (últimos 12 meses) |
| `BONUS_SUSTAINED` | +15 (varios años) |
| `BONUS_REPEATED` | +10 (2+ contactos) |
| `BONUS_CONTACT_IDENTIFIED` | +5 |

**Métodos**
| Visibilidad | Firma | Descripción |
|---|---|---|
| `public` | `calculate(array $interactions): array` | Devuelve `{score, label, breakdown}` |
| `private` | `label(int $score): string` | `low` / `medium` / `high` |

---

### `CompanyMatcherService`
Determina si una interacción pertenece a una empresa. Sin dependencias externas.

**Métodos**
| Visibilidad | Firma | Descripción |
|---|---|---|
| `public` | `matches(array $interaction, string $domain, string $companyName): bool` | Matching híbrido: primero por dominio de email, fallback por nombre |
| `private` | `extractDomainFromEmail(string $email): ?string` | Extrae dominio del campo email de la interacción |

---

### `ContactExtractor`
Identifica el contacto concreto de la empresa cruzando interacciones con contenido web.

**Constructor**
```
__construct(WebContentRepository $webContentRepository)
```

**Métodos**
| Visibilidad | Firma | Descripción |
|---|---|---|
| `public` | `extract(array $interactions, string $domain): array` | Devuelve `{name, role}` del contacto más probable |

---

### `NetworkStatsService`
Genera rankings de advisors por actividad de red.

**Atributos privados (instanciados en constructor)**
| Atributo | Tipo |
|---|---|
| `$interactionRepo` | `InteractionRepository` |
| `$userRepo` | `UserRepository` |

**Métodos**
| Visibilidad | Firma | Descripción |
|---|---|---|
| `public` | `topUsersByInteractions(int $limit = 5): array` | Top N advisors ordenados por número de interacciones |

---

## Helpers

### `DataProvider` *(clase estática)*
Caché de petición para datos agregados usados en vistas.

**Métodos estáticos**
| Firma | Descripción |
|---|---|
| `companies(): array` | Lista completa de empresas |
| `users(): array` | Lista completa de advisors |
| `topUsers(int $limit = 5): array` | Delega en `NetworkStatsService` |

---

### `StageHelper` *(clase estática)*
Traduce el estado de relación (`stage`) a etiqueta y clase CSS.

**Métodos estáticos**
| Firma | Descripción |
|---|---|
| `label(string $stage): string` | Texto legible del stage |
| `cssClass(string $stage): string` | Clase CSS asociada |

---

## Repositories

Todos comparten el mismo contrato: encapsulan el acceso a datos. Sustituir los arrays por PDO, una API REST o un ORM no requiere tocar ninguna otra capa.

| Clase | Métodos públicos |
|---|---|
| `CompanyRepository` | `findByDomain(string): ?array` · `findById(int): ?array` · `findByName(string): ?array` |
| `UserRepository` | `findById(int): ?array` · `all(): array` |
| `InteractionRepository` | `all(): array` |
| `WebContentRepository` | `findByDomain(string): ?array` |

---

## Grafo de dependencias completo

```
api.php
├── CompanyInsightController
│   ├── CompanyRepository
│   ├── InteractionRepository
│   ├── UserRepository
│   ├── CompanyMatcherService
│   ├── ScoreCalculator
│   └── ContactExtractor
│       └── WebContentRepository
└── NetworkStatsService
    ├── InteractionRepository
    └── UserRepository

view.php
└── CompanyInsightController (ídem arriba)

users.php
└── UserInsightController
    ├── UserRepository
    ├── InteractionRepository
    ├── CompanyRepository
    ├── ScoreCalculator
    └── ContactExtractor
        └── WebContentRepository

index.php
└── DataProvider::topUsers()
    └── NetworkStatsService
        ├── InteractionRepository
        └── UserRepository
```

---

## Regla de dependencias

```
Ingesta externa  →  Data  →  Repositories  →  Services  →  Controllers  →  Entry points
                                           →  Helpers
```

Ninguna capa inferior conoce a las superiores. Los repositorios no saben que existen controllers. Los services no saben que existen vistas. El flujo de dependencias es estrictamente descendente.

El proceso de ingesta es completamente independiente de la aplicación: alimenta la capa de datos y no conoce nada de controllers, services ni vistas. Sustituir los arrays PHP por una BD real solo requiere modificar los repositorios.

---

## Proceso de ingesta (externo a la aplicación)

El enunciado no especifica cómo se registran las interacciones. Dado que ONEtoONE opera con Google Workspace, se presupone que la fuente de verdad son Gmail y Google Calendar.

**Responsabilidades del proceso de ingesta:**

| Paso | Herramienta | Qué hace |
|---|---|---|
| Extracción | Gmail API | Obtiene emails de cada asesor con contactos de dominios empresariales |
| Extracción | Calendar API | Obtiene reuniones y llamadas con asistentes externos |
| Clasificación | Claude / Gemini | Determina el tipo de interacción (`reunion`, `llamada`, `email`, `evento`) a partir del asunto o cuerpo cuando no es evidente |
| Normalización | Claude / Gemini | Resuelve variantes del nombre de empresa ("Acme", "Acme Ltd", "ACME Corp") al nombre canónico de la BD relacional |
| Extracción de contacto | Claude / Gemini | Extrae nombre y cargo del interlocutor desde la firma del email o descripción del evento |
| Persistencia | BD relacional | Guarda la interacción normalizada en la tabla `interactions` |

**Punto de sustitución en el MVP:**

`InteractionRepository::all()` lee hoy `data/interactions.php`. En producción lee la tabla `interactions` que este proceso de ingesta rellena. Ningún otro fichero cambia.

**Prueba de concepto:**

`scripts/gmail_ingestion_example.php` muestra el flujo completo: autenticación con cuenta de servicio de Google Workspace, extracción de emails (Gmail API) y eventos (Calendar API), clasificación y normalización con Claude, y conversión al formato exacto que espera `InteractionRepository`.
