# API v1 — resumo

Base: `https://api.tvmabassa.co.mz/api/v1` · JSON · autenticação `Authorization: Bearer <token>`.
Erros: `{"error":{"code":"validation_failed","message":"…","fields":{…}}}` (401, 403, 404, 422, 429).
Listas paginadas: `{"data":[…],"links":{…},"meta":{…}}` (`?page=`, `?per_page=`).
Cabeçalhos úteis: `X-Device-Id` (UUID v4 gerado pela app, para gostos/visualizações anónimos) e `Idempotency-Key` (UUID, pagamentos).

## Público
| Método e caminho | Descrição |
|---|---|
| `GET /categories` | secções |
| `GET /plans` | planos activos (preço em string `"150.00"`) |
| `GET /articles?category=&q=&featured=1&slugs[]=&sort=recent\|popular&per_page=` | lista (sem corpo). `slugs[]` devolve só esses artigos (usado nos Guardados) |
| `GET /articles/{slug}` | detalhe. `locked:true` e só 1 parágrafo se for premium e o leitor não tiver acesso |
| `GET /articles/{slug}/related` | relacionadas |
| `POST /articles/{slug}/view` | conta visualização (1/hora por leitor) → `{ok,counted,views}` |
| `PUT\|DELETE /articles/{slug}/like` | gosto (idempotente). Com token → conta; sem token → `X-Device-Id` |
| `GET /breves?category=` | breves |
| `POST /contact` | `name,email,subject,message,privacy_accepted,website(vazio)` |

## Conta
| | |
|---|---|
| `POST /auth/register` | `name,email,phone?,password,password_confirmation` → `{token,user}` |
| `POST /auth/login` | leitor → `{token,user}` · staff → `{requires_2fa,challenge_token}` ou `{requires_2fa_setup,setup_token}` |
| `POST /auth/2fa/setup` (token `setup`) | `{secret,otpauth_uri}` (a app mostra a chave e a ligação `otpauth://`; QR opcional) |
| `POST /auth/2fa/confirm` (token `setup`) | `{code}` → token admin + `recovery_codes` (mostrar uma vez) |
| `POST /auth/2fa/verify` (token `challenge`) | `{code}` ou `{recovery_code}` → token admin |
| `GET /auth/me` · `POST /auth/logout` · `POST /auth/password` | |
| `GET /me/bookmarks` · `PUT\|DELETE /me/bookmarks/{slug}` | guardados |
| `GET /me/subscription` · `DELETE /me/subscription` | estado / terminar |
| `GET /me/payments` | histórico |
| `PUT /me/profile` | `{name, phone?}` (o email e o papel não se mudam) |

## Pagamentos (token de leitor)
`POST /payments` com `Idempotency-Key`:
```json
{ "plan": "mensal", "method": "mpesa", "phone": "84 123 4567" }
```
- `method`: `mpesa|emola|mkesh|card`. `phone` só para mobile money.
- Resposta `201` → `data.status`: `pending` (confirmar no telemóvel / abrir `checkout_url` no cartão; o gateway devolve o leitor a `/#/subscrever?ref=<referência>`), `paid` ou `failed`.
- A app consulta `GET /payments/{reference}` de 3 em 3 s (máx. ~100 s; depois a app mostra «Verificar pagamento») até `paid|failed|expired`; `premium:true` indica acesso activo.
- **Nunca** enviar número de cartão/CVV a esta API.

## Webhook (só agregador)
`POST /webhooks/payments` com `X-Signature: t=<unix>,v1=<hmac_sha256_hex("<t>.<corpo>")>`
```json
{"id":"evt_1","type":"payment.succeeded|payment.failed|payment.cancelled",
 "data":{"reference":"TVM-261028-AB3XK","provider_reference":"…","amount":"150.00","currency":"MZN"}}
```
Formato assumido — ajustar ao agregador escolhido.

## Admin (token `admin`; editores sem as rotas marcadas †)
`GET /admin/dashboard` (inclui `views_by_category`) · `GET|POST /admin/articles` (`?status=&category=&q=`) · `GET|PUT|DELETE† /admin/articles/{id}` ·
`PATCH /admin/articles/{id}/status` (`draft|published`) · `GET /admin/authors` · `GET|POST /admin/breves` ·
`PUT|DELETE† /admin/breves/{id}` · `POST /admin/uploads` (multipart `image`) ·
† só admin: `GET|POST /admin/plans`, `PUT /admin/plans/{id}` (planos não se apagam: desactivam-se), `GET /admin/payments` (+ `summary`), `GET /admin/payments/export` (CSV),
`GET /admin/subscribers`, `GET /admin/messages`, `PATCH /admin/messages/{id}`, `GET /admin/audit`.

**Perfil (qualquer membro da equipa, também com palavra-passe temporária):** `GET|PUT /admin/profile` (`name, phone?, job_title?, bio?`) ·
`POST /admin/profile/recovery-codes` (`{password}` → novos códigos, mostrados uma vez). A palavra-passe muda-se com `POST /auth/password`.

**Equipa (só administrador):** `GET /admin/team?q=` · `POST /admin/team` (`name,email,phone?,job_title?,bio?,password` → cria **editor** com palavra-passe temporária; o papel não se escolhe) ·
`PUT /admin/team/{id}` · `POST /admin/team/{id}/disable|enable|reset-password|reset-2fa` · `DELETE /admin/team/{id}`.
Administradores só se criam por linha de comandos (`php artisan app:create-admin`) e não aparecem como geríveis (`managed:false`; 404 nas acções).
Com `must_change_password:true` toda a API do painel responde `403 password_change_required`, excepto `/admin/profile*` e `/auth/password`.

**Editores/jornalistas** só vêem e mexem nas **suas** notícias (as do seu perfil de autor): lista, detalhe, edição e estado das de outros dão 404; o autor é sempre o próprio (`author_id` do pedido é ignorado); `/admin/authors` e `/admin/dashboard` só mostram os seus dados. Administradores vêem tudo. As Breves continuam partilhadas pela redacção.

Corpo de notícia: `[{"type":"p","text":"…"},{"type":"quote","text":"…","by":"…"},{"type":"h2","text":"…"}]` — só texto.
