# Segunor Intel Grid — API v1 (Integração)

Guia prático para consumir a API pública do Intel Grid a partir de
outros serviços internos (HR, CRM, etc.).

## Overview

O Intel Grid expõe em `/api/v1/*` um conjunto reduzido de endpoints
para **cruzamento de dados de candidatos com o grafo judicial e
corporativo** que a aplicação mantém. Cenário típico:

> Recebes uma candidatura de emprego. Antes de avançar, verificas se o
> candidato aparece como parte em processos judiciais onde uma das
> tuas **empresas concorrentes** é contraparte.

A API devolve *matches* fuzzy por nome, cada um com a lista de
processos relevantes, o papel do candidato vs. concorrente e um
`alert_level` pré-calculado (`high` / `medium` / `low`).

**Base URL** (intranet): `http://192.168.10.47/api/v1`
**OpenAPI / Swagger UI**: `http://192.168.10.47/api/v1/docs`
**OpenAPI JSON**: `http://192.168.10.47/api/v1/openapi.json`

## Autenticação

### Obter uma key

Um administrador no Intel Grid deve:

1. Entrar em **`/admin/api-keys`** no UI web.
2. **"Nova key"** → dar um *label* (ex: `hr-app`) → guardar.
3. O dialog mostra a **chave raw uma só vez**. Copiar e guardar
   **imediatamente** num vault / secret manager do serviço
   consumidor. Depois de fechar o dialog a key não volta a ser
   exposta — se for perdida, é preciso revogar e gerar outra.

### Apresentar a key em cada request

Header obrigatório em **todos** os endpoints v1:

```
X-API-Key: ig_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Sem o header, ou com uma key inválida/revogada, o servidor devolve
`401 Unauthorized`.

## Endpoints

### `GET /api/v1/health`

Ping autenticado. Responde 200 quando a key é válida, com o label da
key — útil como smoke test inicial.

**Resposta**:
```json
{ "ok": true, "label": "hr-app", "version": "v1" }
```

### `GET /api/v1/candidates/check`

**O endpoint principal.** Pesquisa fuzzy por nome em `process_parties`,
cruza com processos cujo `monitoring_type` da empresa associada é
`competitor`, e devolve matches agrupados por identidade
(nome + NIF quando disponível).

| Query param | Tipo | Default | Descrição |
|---|---|---|---|
| `name` | string, 3..200 chars | (obrig.) | Nome a pesquisar (fuzzy trigram) |
| `min_similarity` | float, 0.1..1.0 | `0.4` | Corte mínimo de similaridade pg_trgm. Valores mais baixos apanham mais matches, ao custo de falsos positivos. |
| `limit` | int, 1..200 | `50` | Máximo de rows processadas pela query SQL |

**Resposta** (exemplo abreviado):
```json
{
  "query": {"name": "Bernardo Almeida", "min_similarity": 0.4, "limit": 50},
  "searched_at": "2026-04-21T20:00:00Z",
  "total_matches": 1,
  "matches": [
    {
      "party_name": "BERNARDO CRISTIANO TEIXEIRA DE ALMEIDA",
      "party_nif": "234567890",
      "confidence": 0.85,
      "processes": [
        {
          "process_number": "123/24.0TBLSB",
          "tribunal": "Tribunal Judicial de Lisboa",
          "source": "distribuicao",
          "date_filed": "2024-03-10",
          "candidate_role": "autor",
          "monitored_company": {
            "nif": "509835333",
            "legal_name": "BLACKELIT SECURITY LDA",
            "monitoring_type": "competitor",
            "role": "réu"
          },
          "alert_level": "high",
          "alert_reason": "candidate_sues_competitor"
        }
      ],
      "summary": {
        "processes_total": 1, "high": 1, "medium": 0, "low": 0,
        "unique_competitors": 1
      },
      "top_alert": "high"
    }
  ]
}
```

### `GET /api/v1/candidates/check-by-nif`

Variante exacta quando o CV tem NIF conhecido. Mesmo payload de
resposta mas com `confidence = 1.0` sempre.

| Query param | Tipo | Default | Descrição |
|---|---|---|---|
| `nif` | string, regex `^\d{9}$` | (obrig.) | NIF do candidato (9 dígitos) |
| `limit` | int, 1..200 | `50` | Máximo de rows |

## Alert levels

Cada processo é classificado server-side por combinação de
(`source`, papel do candidato, papel do concorrente):

| `alert_level` | Quando | `alert_reason` | Interpretação |
|---|---|---|---|
| **high** | `distribuicao` + candidato autor/requerente/exequente + concorrente réu/requerido/executado | `candidate_sues_competitor` | Candidato litiga activamente contra um concorrente — forte sinal |
| **medium** | `distribuicao` + candidato réu/requerido/executado + concorrente autor/requerente/exequente | `competitor_sues_candidate` | Concorrente processa o candidato — pode indicar problema de integridade |
| **low** | `cire` + candidato `credor` | `candidate_creditor_in_competitor_insolvency` | Candidato é credor na insolvência de um concorrente — sinal marginal |

Processos que não caibam em nenhum destes padrões **não** são
retornados (filtro feito em Python depois da query SQL).

Cada grupo de match também traz um `top_alert` — o alerta mais grave
entre os seus processos — para permitir decisões rápidas no cliente.

## Confidence score

O `confidence` do match vem do operador `similarity()` do pg_trgm
(Postgres trigram extension). Vai de 0 a 1 onde:

- `≥ 0.7` — muito provável ser a mesma pessoa (nome quase idêntico)
- `0.4 – 0.7` — match plausível; validar por NIF ou data de nascimento
  se precisares de certeza
- `< 0.4` — cortado por default

O default de `min_similarity=0.4` equilibra recall vs. precisão; pode
ser afinado por query se o teu sistema RH quiser exigir mais certeza
(ex: `?min_similarity=0.55`).

## Rate limits

Cada key é limitada a **60 requests/minuto**. Ao exceder, resposta:

```
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{"detail": "rate limit exceeded: 60 per 1 minute"}
```

O limite aplica-se por key, não globalmente — várias apps com keys
distintas têm janelas independentes.

## Erros

| Status | Quando |
|---|---|
| `200 OK` | Sucesso; payload conforme descrito |
| `401 Unauthorized` | Key em falta, inválida, ou revogada |
| `422 Unprocessable Entity` | Parâmetros inválidos (nome < 3 chars, NIF mal formatado, etc.) |
| `429 Too Many Requests` | Rate limit por key excedido |
| `500 Internal Server Error` | Bug no Intel Grid — reportar |

Respostas de erro seguem o formato FastAPI:
```json
{"detail": "mensagem de erro"}
```

## Exemplos

### curl

```bash
export IG_KEY="ig_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# Health check
curl -H "X-API-Key: $IG_KEY" http://192.168.10.47/api/v1/health

# Candidate check por nome
curl -H "X-API-Key: $IG_KEY" \
  "http://192.168.10.47/api/v1/candidates/check?name=Bernardo%20Almeida&min_similarity=0.5"

# Candidate check por NIF
curl -H "X-API-Key: $IG_KEY" \
  "http://192.168.10.47/api/v1/candidates/check-by-nif?nif=234567890"
```

### Python (httpx)

```python
import httpx

IG_BASE = "http://192.168.10.47/api/v1"
IG_KEY  = "ig_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

def check_candidate(name: str, min_sim: float = 0.4) -> dict:
    r = httpx.get(
        f"{IG_BASE}/candidates/check",
        params={"name": name, "min_similarity": min_sim},
        headers={"X-API-Key": IG_KEY},
        timeout=10.0,
    )
    r.raise_for_status()
    return r.json()

result = check_candidate("Bernardo Almeida")
for m in result["matches"]:
    if m["top_alert"] in ("high", "medium"):
        print(f"⚠ {m['party_name']} ({m['confidence']:.2f}): {m['top_alert']}")
```

### Node.js (fetch)

```javascript
const IG_BASE = "http://192.168.10.47/api/v1";
const IG_KEY  = process.env.IG_KEY;

async function checkCandidate(name) {
  const url = new URL(`${IG_BASE}/candidates/check`);
  url.searchParams.set("name", name);
  const r = await fetch(url, { headers: { "X-API-Key": IG_KEY } });
  if (!r.ok) throw new Error(`Intel Grid ${r.status}`);
  return r.json();
}

const result = await checkCandidate("Bernardo Almeida");
result.matches
  .filter(m => ["high", "medium"].includes(m.top_alert))
  .forEach(m => console.log(`⚠ ${m.party_name}: ${m.top_alert}`));
```

### PHP (Guzzle)

```php
<?php
use GuzzleHttp\Client;

$ig = new Client([
    'base_uri' => 'http://192.168.10.47/api/v1/',
    'headers' => ['X-API-Key' => getenv('IG_KEY')],
    'timeout' => 10.0,
]);

function checkCandidate(Client $ig, string $name): array {
    $resp = $ig->get('candidates/check', [
        'query' => ['name' => $name, 'min_similarity' => 0.4],
    ]);
    return json_decode($resp->getBody(), true);
}

$result = checkCandidate($ig, 'Bernardo Almeida');
foreach ($result['matches'] as $m) {
    if (in_array($m['top_alert'], ['high', 'medium'])) {
        fwrite(STDERR, "⚠ {$m['party_name']}: {$m['top_alert']}\n");
    }
}
```

## FAQ

**Múltiplos matches para o mesmo candidato** — acontece quando há
pessoas com nomes parecidos em `process_parties` (ex: "João Silva").
Cada identidade distinta (agrupada por `(party_name, party_nif)`)
aparece como um bloco em `matches[]`. O teu cliente deve:

1. Se tiver o NIF do candidato, filtrar por `party_nif == cv_nif`.
2. Caso contrário, escolher o match com `confidence` mais alta E
   validar manualmente antes de tomar acção sobre um candidato.

**Limiar de similaridade** — começar com `0.4` (default). Se vires
muitos falsos positivos para nomes comuns ("João Silva", "Ana Ferreira")
subir para `0.55` ou `0.6`. Se vires falsos negativos em nomes com
acentos/grafias variantes, descer para `0.3`.

**Caching client-side** — os dados da API mudam com scrapes
overnight (distribuição + CIRE). Cachear resultados no cliente por
≤24h é geralmente seguro. Não caches por mais de isso: as empresas
que pertencem ao conjunto *competitor* podem mudar a qualquer momento
no UI do Intel Grid.

**Privacidade / GDPR** — a API devolve apenas dados já públicos
(registos judiciais e corporativos publicados oficialmente). Guardar
logs de consultas no teu lado é aconselhável para auditoria; o Intel
Grid também incrementa `use_count` e `last_used_at` na tabela
`api_keys` por key.

**Revogação** — um admin pode revogar uma key a qualquer momento em
`/admin/api-keys` (botão 🗑). Uma key revogada devolve `401` na
chamada seguinte.

---

## Watchlist de insolvências (v1)

`POST /api/v1/watchlist` — regista entidades a vigiar nos anúncios de
insolvência do CITIUS. Usado pelo Sabichão, que empurra o NIF ao criar um
cliente.

```bash
curl -X POST http://10.0.30.108/api/v1/watchlist \
  -H "X-API-Key: ig_..." -H "Content-Type: application/json" \
  -d '{"items":[{"nif":"517 607 891","name":"Empresa Lda","external_ref":"086/2026"}]}'
```

```json
{"recebidos": 1, "aceites": 1, "ignorados": [], "hits_retroactivos": 2}
```

- **Idempotente**: repetir o mesmo NIF actualiza, não duplica.
- **O NIF aceita espaços e pontuação** — é normalizado a 9 dígitos deste lado.
  (O Sabichão guarda-os como `517 607 891`.)
- **Nunca remove.** Não há endpoint de remoção de propósito: um cliente
  desactivado continua a ser um devedor cujo processo interessa acompanhar.
  Para suspender avisos, desactivar em `/admin/watchlist`.
- `hits_retroactivos` diz quantos anúncios **já arquivados** passaram a dar
  match com os NIFs enviados. É o que dá sentido a varrer o país inteiro: um
  cliente adicionado hoje pode já ter sido declarado insolvente na semana
  passada.
- Aceita lotes. O rate limit é de 60 pedidos/minuto por chave, portanto uma
  carga inicial deve ir em lotes (~25 itens) e não um pedido por entidade.

## Dossiê consolidado (v1)

`GET /api/v1/dossier?nif=...` — tudo o que o Intel Grid sabe sobre uma
entidade, numa só chamada. Alimenta a tab "Intel" da ficha de cliente do
Sabichão.

```bash
curl -H "X-API-Key: ig_..." "http://10.0.30.108/api/v1/dossier?nif=500243719"
```

Devolve `empresa` (firma oficial, natureza jurídica, sede desdobrada, CAE,
alvará, objecto, capital), `prazos_reclamacao`, `anuncios_insolvencia`,
`processos` + `processos_contagem`, `publicacoes`, `pessoas` e
`contratos_publicos`.

**`monitorizada: false` não significa "sem ocorrências"** — significa que a
entidade nunca foi registada no Intel Grid, portanto nada foi recolhido para
ela. Quem consome tem de distinguir os dois casos, senão um dossiê vazio
lê-se como "está tudo bem".
