# Feature Specification: Funil de Onboarding Completo (004)

**Feature Branch**: `004-onboard-funnel-events`

**Created**: 2026-06-08

**Status**: Draft

**Input**: Completar a integração VM112 → VM122 com todos os marcos do wizard de onboarding, timeline por sessão e widget de funil no Dashboard Ops — continuando a feature 001 (Phase D).

**Depends on**: `001-webhook-vm112-integration` (MVP `account.created` entregue)

**Anexos técnicos:**

| Documento | Conteúdo |
|-----------|----------|
| [cloudflare-zone-provision.md](./cloudflare-zone-provision.md) | Inserir domínio na conta Cloudflare Ligbox — código VM112, funções, endpoints, fluxo UI |
| [research.md](./research.md) | Hook points `onboarding.py` |
| [contracts/webhook-funnel-events.md](./contracts/webhook-funnel-events.md) | Contrato eventos funil |

---

### User Story 1 — Visibilidade do funil activo (Priority: P1)

A equipa ops vê no Dashboard quantos onboardings estão em cada fase (domínio validado, DNS aplicado, conta criada, concluído) sem abrir o portal VM112.

**Why this priority**: Responde directamente à expectativa de um painel com informação útil — mostra onde clientes travam.

**Independent Test**: Emitir eventos simulados para 3 sessões em fases diferentes e confirmar contadores correctos no widget funil.

**Acceptance Scenarios**:

1. **Given** 2 sessões com `domain.validated` e 1 com `onboarding.completed`, **When** o dashboard carrega, **Then** o widget mostra contagens por fase actual.
2. **Given** sessão sem eventos há > 24h, **When** listada no funil, **Then** aparece marcada como `stale` (inactiva).

---

### User Story 2 — Timeline por sessão no ticket (Priority: P1)

Ao abrir um ticket de onboarding, a equipa vê a sequência cronológica de eventos da mesma `session_id` (validação, DNS, conta, infra, conclusão).

**Why this priority**: Diagnóstico rápido quando onboarding falha a meio — sem cruzar logs manualmente.

**Independent Test**: Enviar 5 eventos com mesmo `session_id` e confirmar timeline ordenada no detalhe do ticket.

**Acceptance Scenarios**:

1. **Given** ticket criado por `account.created`, **When** chegam `domain.validated` e `dns.applied` com mesma sessão, **Then** aparecem na timeline do ticket (notas/eventos).
2. **Given** `onboarding.completed` para sessão existente, **When** processado, **Then** actualiza ticket (nota ou tag `ready`) sem criar ticket duplicado.

---

### User Story 3 — Portal emite marcos do wizard (Priority: P1)

O portal VM112 emite webhooks para cada marco importante do funil, de forma não-bloqueante (mesmo padrão `ops_webhook.py`).

**Why this priority**: Sem emissor completo, o Ops não tem dados para funil nem timeline.

**Independent Test**: Percorrer wizard em staging e confirmar eventos no registo Ops por ordem.

**Acceptance Scenarios**:

1. **Given** `POST /validate-domain` com sucesso, **When** resposta 200, **Then** emite `domain.validated`.
2. **Given** `POST /dns/cloudflare/apply` com registos aplicados, **When** sucesso, **Then** emite `dns.applied`.
3. **Given** provision infra concluído após criar conta, **When** sucesso, **Then** emite `infra.synced`.
4. **Given** fluxo completo sem erro, **When** resposta final ao cliente, **Then** emite `onboarding.completed`.
5. **Given** falha crítica (ex. CarbonioError em create account), **When** HTTP 400, **Then** emite `onboarding.failed` com motivo em `data.error`.

---

### User Story 4 — Sessão iniciada (Priority: P2)

Quando o cliente escolhe/valida domínio pela primeira vez na sessão, o Ops regista `onboarding.started` para contagem de funil desde o início.

**Why this priority**: Permite taxa de conversão início → conclusão; não bloqueia MVP do funil.

**Independent Test**: Primeira validação de domínio numa sessão nova gera evento `onboarding.started` uma única vez.

**Acceptance Scenarios**:

1. **Given** nova `session_id`, **When** primeiro `validate-domain` OK, **Then** emite `onboarding.started` (idempotente — não repete na mesma sessão).

---

### Edge Cases

- Eventos fora de ordem (ex. `onboarding.completed` antes de `dns.applied`): Ops aceita e ordena por timestamp na timeline.
- Mesmo evento repetido (retry portal): idempotência `event + session_id + domain` — sem duplicar timeline.
- Sessão sem `session_id`: evento aceite, funil agrupa em `unknown`.
- Domínio com múltiplas sessões: funil mostra sessões separadas; tickets ligados por `session_id`.
- Ops offline: portal continua; activity log regista falha webhook.

## Requirements *(mandatory)*

### Functional Requirements

- **FR-001**: Portal DEVE emitir: `onboarding.started`, `domain.validated`, `dns.applied`, `infra.synced`, `onboarding.completed`, `onboarding.failed` via `ops_webhook.emit_event`.
- **FR-002**: Emissão DEVE ser non-blocking (`BackgroundTasks` ou equivalente) — nunca alterar resposta HTTP ao cliente.
- **FR-003**: Ops DEVE persistir todos os eventos em `webhook_events` (já existente).
- **FR-004**: Ops DEVE expor `GET /api/v1/onboard/funnel` com contagens por fase e lista de sessões activas (últimas 48h).
- **FR-005**: Ops DEVE expor `GET /api/v1/onboard/sessions/{session_id}/timeline` com eventos ordenados.
- **FR-006**: Ticket existente (mesma `session_id`) DEVE enriquecer-se com novos eventos — notas internas ou campo `timeline` no detalhe.
- **FR-007**: Apenas `account.created` e `onboarding.failed` criam ticket novo; restantes eventos actualizam contexto.
- **FR-008**: Idempotência OBRIGATÓRIA: `(event_type, session_id, domain)` — já implementada em 001, manter.
- **FR-009**: UI Dashboard DEVE incluir widget **Funil Onboarding** (contadores + link sessões).
- **FR-010**: UI Ticket DEVE mostrar timeline de eventos da sessão.
- **FR-011**: Comunicação LAN-only (`10.10.10.112` → `10.10.10.122`).

### Key Entities

- **Funnel Session**: `session_id`, `domain`, `current_stage`, `last_event_at`, `events[]`.
- **Funnel Stage**: ordered enum — `started` → `domain_validated` → `dns_applied` → `account_created` → `infra_synced` → `completed` | `failed`.
- **Timeline Entry**: event_type, timestamp, data snapshot, source `vm112-onboard`.

## Success Criteria *(mandatory)*

- **SC-001**: 100% dos marcos do wizard (6 tipos) geram evento Ops quando portal e Ops disponíveis.
- **SC-002**: Equipa identifica fase actual de um onboarding em < 10s via Dashboard (sem SSH no portal).
- **SC-003**: Timeline completa visível no ticket para qualquer sessão com ≥ 2 eventos.
- **SC-004**: Zero tickets duplicados por sessão além de `account.created` + opcional `onboarding.failed`.
- **SC-005**: Falha Ops não afecta taxa de sucesso do wizard (0 regressões em testes E2E portal).

## Assumptions

- Receptor `/api/v1/webhooks/onboard` e `ops_webhook.py` já funcionais (001).
- `session_id` disponível via `bind_onboarding_session` em todos os routers de onboarding.
- Fases do funil mapeiam 1:1 aos passos actuais do wizard ibytera-mail-portal.
- `onboarding.completed` dispara no fim de `create_account` após infra (mesmo request).
- UI Desk v2 existente — extensão de dashboard e ticket detail, não rebuild.

## Dependencies

- Constitution v1.0.0 (separação VM112/122, LAN-only).
- Feature 001 deployada em VM112 + VM122.
- Feature 002 não conflita (origens diferentes).

## Out of Scope

- Notificações push (ntfy) em mudança de fase.
- Auth/RBAC no Desk (feature 003 futura).
- Sincronização Ops → Portal.
- Expor webhook onboard na internet pública.
- Kanban / SLA (feature desk futura).
