# Conectores multi-fontes — contrato v0.1 (P2-008)

> **APROVADO v0.1 para implementação** (gate S3).  
> Issue `P2-008` no plano P2 (`docs/pesa-p2-plano-executavel.html`).  
> Desbloqueia **P2-009** (primeiro conector incremental com job + log + alerta de falha).  
> Este documento **não** implementa jobs; define o contrato que o código deve respeitar.

---

## 0. Princípios

1. **Uma fonte, um conector** — LMS, ERP e CRM não misturam payloads no mesmo envelope.
2. **Camada canônica** — cada conector normaliza para o modelo PESA (`estudante`, `periodo_calendario`, `evento_engajamento`, `evento_financeiro`); a UI e o score consomem só a camada canônica.
3. **Idempotência** — reprocessar o mesmo lote não duplica entidades (`chave_natural` + `fonte` + `versao_payload`).
4. **Dry-run antes do commit** — toda sync tem modo `dry_run` com contagens e amostra de erros.
5. **Falha visível** — timeout, HTTP≠2xx, schema inválido ou queda de volume geram registro de run + alerta; não falhar em silêncio.
6. **Segredos mascarados** — tokens/senhas nunca voltam completos no `GET` de config (mesmo padrão do CRM de calendário).

---

## 1. Objetivo (P2-008)

Contratar a ingestão multi-fonte para reduzir dependência de planilha manual, com:

| Dimensão | Entrega deste documento |
|----------|-------------------------|
| **Payload** | Envelope comum + schemas por fonte (LMS / ERP / CRM) |
| **Frequência** | Políticas de schedule, incremental e backfill |
| **Reconciliação** | Chaves, upsert, soft-delete, conflitos, limiares |
| **Alertas de falha** | Eventos, canais, severidade e retenção de logs |

**Fora de escopo v0.1:** ML sobre o stream; webhooks push (só pull agendado); multi-tenant SaaS.

---

## 2. Fontes oficiais v0.1

| Código | Fonte | Domínio canônico | Estado atual no PESA |
|--------|-------|------------------|----------------------|
| `crm_calendario` | CRM / SIS calendário | Períodos e marcos acadêmicos | **Parcial** — REST + CSV em `calendario_academico` (`origem: crm_api` / `crm_arquivo`) |
| `lms` | AVA / LMS | Engajamento (acessos, fórum, atividades, último acesso) | **Conector P2-009** (`ava.*`, `interacoes.*`) |
| `erp_academico` | ERP acadêmico | Matrícula, notas, presença, curso/turma | **Conector P2-009** (N1–N3, `presenca.*`) |
| `erp_financeiro` | ERP financeiro | Mensalidade / inadimplência | **Conector P2-009** (`mensalidade.*`) |
| `sis_estudantes` | Cadastro institucional | Identidade e demografia (UC, nome, curso, campus…) | **Conector P2-009** (merge parcial) |

A primeira implementação P2-009 deve escolher **uma** fonte além do calendário (recomendado: `lms` **ou** `erp_financeiro`), reutilizando o envelope e o runner deste contrato.

**Status:** P2-009 entregue com **LMS**, **ERP financeiro**, **ERP acadêmico** e **SIS estudantes** no mesmo runner (`admin-conectores.html`).

---

## 3. Envelope comum de payload

Todo lote (arquivo ou resposta de API) é interpretado como:

```json
{
  "versao_payload": "conectores_v0.1",
  "fonte": "lms",
  "instituicao_id": "default",
  "gerado_em": "2026-08-05T12:00:00Z",
  "cursor": {
    "modo": "incremental",
    "since": "2026-08-04T12:00:00Z",
    "until": null,
    "page": 1,
    "page_size": 500
  },
  "itens": [ ],
  "meta": {
    "total_declarado": null,
    "checksum": null
  }
}
```

| Campo | Obrigatório | Regra |
|-------|-------------|--------|
| `versao_payload` | sim | Deve ser `conectores_v0.1` (ou lista de versões aceitas no runner) |
| `fonte` | sim | Um dos códigos da §2 |
| `gerado_em` | sim | ISO-8601 UTC |
| `cursor` | recomendado | Obrigatório em modo incremental |
| `itens` | sim | Array; vazio é válido mas dispara alerta `volume_zero` se inesperado |
| `meta.total_declarado` | não | Se presente e ≠ `len(itens)` (página única) → `volume_inconsistente` |

### 3.1 Configuração do conector (persistência)

Espelhar o padrão `app_config_instituicao` / `calendario_crm_api`:

| Chave sugerida | Uso |
|----------------|-----|
| `conector_lms` | URL, auth, mapeamento, schedule |
| `conector_erp_academico` | idem |
| `conector_erp_financeiro` | idem |
| `conector_sis_estudantes` | idem |
| `calendario_crm_api` | **já existe** — manter; alinhar campos ao envelope quando possível |

Campos mínimos de config:

```json
{
  "ativo": false,
  "fonte": "lms",
  "baseUrl": "https://lms.exemplo.edu",
  "path": "/api/v1/engagement",
  "method": "GET",
  "authTipo": "bearer",
  "authToken": "",
  "authHeader": "Authorization",
  "authUser": "",
  "authPassword": "",
  "jsonPath": "data",
  "timeoutSec": 30,
  "query": {},
  "mapeamento": {},
  "schedule": {
    "cron": "0 3 * * *",
    "timezone": "America/Sao_Paulo",
    "modo": "incremental",
    "janela_lookback_horas": 36
  },
  "reconciliacao": {
    "soft_delete": false,
    "queda_volume_pct": 40,
    "min_itens_ok": 1
  },
  "alerta": {
    "email": true,
    "funcoes": ["configurar_calendario", "upload_estudantes"]
  }
}
```

Auth: `none` | `bearer` | `api_key` | `basic` (igual ao CRM de calendário).

---

## 4. Schemas por fonte (campos canônicos)

Chave natural do estudante em todos os schemas: `estudante_id` normalizado (`uc:{UC}` ou `id:{id}`), alinhado a `score_rematricula.chave_estudante` / `normalizar_estudante_id`.

### 4.1 `lms` — engajamento

Cada item em `itens`:

| Campo canônico | Tipo | Obrigatório | Origem típica |
|----------------|------|-------------|---------------|
| `estudante_id` | string | sim | UC / login LMS |
| `ava.acessos` | number | não | contagem no período |
| `ava.ultimoAcesso` | string ISO | não | último hit |
| `interacoes.forum` | number | não | |
| `interacoes.materiais` | number | não | |
| `interacoes.atividades` | number | não | |
| `curso` | string | não | se a fonte segmentar |
| `observado_em` | string ISO | sim | timestamp do fato |

Merge na camada canônica: atualiza apenas chaves `ava.*` / `interacoes.*` do estudante; **não** apaga notas/financeiro.

### 4.2 `erp_academico` — desempenho e presença

| Campo canônico | Tipo | Obrigatório |
|----------------|------|-------------|
| `estudante_id` | string | sim |
| `curso` | string | recomendado |
| `disciplina` | string | se granularidade for por disciplina |
| `N1`, `N2`, `PI`, `N3` | number | não |
| `presenca.total` | number | não |
| `presenca.faltas` | number | não |
| `semestre` | string | recomendado |
| `observado_em` | string ISO | sim |

Alinhar ao modelo tabular oficial (`modelo-estudantes.json`).

### 4.3 `erp_financeiro`

| Campo canônico | Tipo | Obrigatório |
|----------------|------|-------------|
| `estudante_id` | string | sim |
| `mensalidade.situacao` | string | recomendado |
| `mensalidade.valorEmAberto` | number | não |
| `mensalidade.mesesEmAtraso` | number | não |
| `observado_em` | string ISO | sim |

### 4.4 `crm_calendario` (já implementado — referência)

Itens de período (não estudante):

| Campo | Obrigatório |
|-------|-------------|
| `nivel` | sim |
| `codigo` | sim |
| `inicio`, `fim` | sim (date) |
| `diagnostico_inicial`, `acompanhamento_1`, `acompanhamento_2`, `fechamento` | não |

Chave natural: `(nivel, codigo)`. Origem: `crm_api` / `crm_arquivo`. Auditoria: `app_calendario_importacao`.

### 4.5 `sis_estudantes`

Identidade + demografia (colunas de `modelo-estudantes.json` sem notas/AVA/financeiro obrigatórios).  
Upsert por `estudante_id`; campos ausentes no lote **não** devem zerar valores existentes (merge parcial), salvo flag explícita `substituir_registro: true`.

---

## 5. Frequência e modos de sync

| Modo | Uso | Cursor |
|------|-----|--------|
| `full` | Carga inicial / recuperação | Ignora `since`; pagina até esgotar |
| `incremental` | Rotina diária/horária | `since` = último `cursor_ok` − lookback |
| `backfill` | Janela histórica explícita | `since` + `until` obrigatórios |
| `manual` | Botão na UI (já usado no calendário) | Opcional |

**Frequência padrão sugerida v0.1**

| Fonte | Cron sugerido | Modo |
|-------|---------------|------|
| `lms` | Diário 03:00 America/Sao_Paulo | incremental |
| `erp_financeiro` | Diário 04:00 | incremental |
| `erp_academico` | Diário 05:00 (ou após fechamento de notas) | incremental |
| `crm_calendario` | Semanal + manual | full (volumes pequenos) |
| `sis_estudantes` | Semanal ou sob demanda | full / incremental se a API permitir |

O runner P2-009 **deve** persistir `ultimo_sucesso_em` e `cursor_ok` por fonte.

---

## 6. Reconciliação

### 6.1 Upsert

1. Mapear item bruto → canônico (`mapeamento` + validação de tipos).
2. Resolver `estudante_id` / chave de calendário.
3. Mesclar no destino (dataset / tabela) sem sobrescrever campos de **outras** fontes.
4. Registrar contagens: `recebidos`, `validos`, `upserted`, `ignorados`, `erros`.

### 6.2 Soft-delete (opt-in)

Se `reconciliacao.soft_delete = true` em carga `full`:

- Entidades daquela fonte **não** presentes no lote → marcar `inativo_em` / `ativo=false`.
- **Proibido** no modo incremental (evita apagar o que está fora da janela).

### 6.3 Conflitos

| Situação | Política v0.1 |
|----------|----------------|
| Dois itens no mesmo lote com a mesma chave | Último vence; contar `duplicatas_lote` |
| Campo canônico divergente entre fontes | Cada fonte escreve só seu namespace (`ava`, `mensalidade`, …); score lê o merge |
| Estudante inexistente no SIS | Criar stub mínimo (`estudante_id` + campos do lote) **ou** rejeitar com erro `estudante_desconhecido` — default: **criar stub** e alerta `stubs_criados` se > limiar |

### 6.4 Limiares de volume

Comparar `len(itens)` (ou `total_declarado`) com a mediana das últimas N runs OK (N=4):

- Queda ≥ `queda_volume_pct` (default 40%) → falha `volume_queda` (não commit, salvo `forcar=true`).
- `len(itens) < min_itens_ok` em horário esperado → `volume_zero`.

Calendário CRM existente: manter dry-run + resumo; adotar os mesmos códigos de alerta quando o runner unificado existir.

---

## 7. Alertas de falha

### 7.1 Eventos

| Código | Severidade | Quando |
|--------|------------|--------|
| `config_invalida` | alta | URL/auth/mapeamento inválidos |
| `http_erro` | alta | status ≠ 2xx |
| `timeout` | alta | excedeu `timeoutSec` |
| `schema_invalido` | alta | > X% itens inválidos (default 10%) |
| `volume_zero` | média | lote vazio inesperado |
| `volume_queda` | alta | queda vs baseline de runs |
| `volume_inconsistente` | média | `total_declarado` ≠ itens |
| `partial_ok` | baixa | sync OK com erros de linha < limiar |
| `sucesso` | info | run concluída com commit |

### 7.2 Canais (v0.1)

1. **Persistência** — tabela/log de runs (obrigatório no P2-009).
2. **UI admin** — lista das últimas runs com status (obrigatório).
3. **E-mail** — via SMTP institucional já existente, se `alerta.email` e destinatários/funções configurados.
4. **Não** inventar chat/Slack nesta versão.

### 7.3 Retenção

- Metadados de run: **≥ 90 dias**.
- Amostra de erros de linha (máx. 50 por run): **≥ 30 dias**.
- Payloads brutos completos: opcional; se guardar, criptografar em repouso e TTL ≤ 7 dias.

---

## 8. Log de execução (contrato de run)

Cada tentativa (manual ou agendada) produz um registro:

```json
{
  "run_id": "uuid",
  "fonte": "lms",
  "modo": "incremental",
  "dry_run": false,
  "iniciado_em": "…",
  "finalizado_em": "…",
  "status": "sucesso|falha|parcial",
  "http_status": 200,
  "contagens": {
    "recebidos": 1200,
    "validos": 1190,
    "upserted": 1185,
    "erros": 10,
    "duplicatas_lote": 5
  },
  "cursor_entrada": { "since": "…" },
  "cursor_saida": { "since": "…" },
  "alerta_codigos": [],
  "mensagem": "",
  "usuario": "sistema|email"
}
```

API futura (P2-009), prefixo sugerido:

- `GET/PUT /api/conectores/{fonte}/config`
- `POST /api/conectores/{fonte}/test`
- `POST /api/conectores/{fonte}/sincronizar` body `{ "dry_run", "forcar", "modo" }`
- `GET /api/conectores/runs?fonte=&limit=`

Até existir, o calendário continua em `/api/calendario-academico/crm-api/*`.

---

## 9. RBAC

| Ação | Função sugerida |
|------|-----------------|
| Ver config / runs | `upload_estudantes` ou `configurar_calendario` |
| Editar config / sync commit | mesmas + perfil global ou admin |
| Filtro por curso | Não aplicável ao raw da fonte; aplicar **após** merge nas APIs analíticas (padrão P2-006) |

---

## 10. Relação com o que já existe

| Componente | Ação |
|------------|------|
| `calendario_crm_api` | Manter; documentar como implementação de referência de `crm_calendario` |
| `app_calendario_importacao` | Modelo de auditoria a generalizar → `app_conector_run` (P2-009) |
| `admin-importar.html` / CSV | Continua como fallback; conectores não removem o upload manual |
| `app_estudantes_dataset` | Destino canônico v0.1 do merge LMS/ERP/SIS (blob global); evolução futura: staging por fonte |
| SMTP | Canal de alerta reutilizado |

---

## 11. Definition of Done — P2-008

- [x] Payload envelope + schemas LMS / ERP / CRM / SIS documentados.
- [x] Frequência (cron, modos full/incremental/backfill/manual) documentada.
- [x] Regras de reconciliação e limiares de volume documentadas.
- [x] Catálogo de alertas + canais + retenção documentados.
- [x] Contrato de run e endpoints futuros esboçados.
- [x] Plano P2 e índice de docs atualizados; P2-009 liberado para implementação.

---

## 12. Próximo passo (P2-009 — entregue)

Implementado:

1. Tabela `app_conector_run` + configs `conector_lms` / `conector_erp_financeiro` / `conector_erp_academico` / `conector_sis_estudantes`.
2. Runner LMS: fetch → map → dry-run/commit → log → alerta e-mail.
3. Runner ERP financeiro: mesmo fluxo, merge só em `mensalidade.*`.
4. Runner ERP acadêmico: merge em notas/`presenca.*`/disciplinas.
5. Runner SIS: merge parcial de identidade/demografia (`substituir_registro` opcional).
6. API `/api/conectores/*` + job `POST /api/conectores/jobs/tick`.
7. UI `admin-conectores.html` (seletor de fonte).

Próximos: refinamentos operacionais e monitoramento.
