Files
dtf-system/API.md
Cauê Faleiros 98c951d374
Some checks failed
Validate, publish and deploy / validate (push) Successful in 2m2s
Validate, publish and deploy / publish-and-deploy (push) Failing after 8s
first commit
2026-09-15 16:42:34 -03:00

329 lines
9.5 KiB
Markdown

# API — contratos
> Historical prototype contracts only. The active local API is documented at
> [localhost:8000/docs](http://localhost:8000/docs) and in [LOCAL_SETUP.md](LOCAL_SETUP.md).
> `CONTEXT.md` overrides the legacy endpoints and workflows below.
> Todos os endpoints do módulo DTF, com requisição e resposta.
> Base: `portal/main.py` e `kanban/main.py`.
---
## Autenticação
| Quem chama | Como se autentica |
|---|---|
| Tiny (webhook) | header `x-token` com `WEBHOOK_TOKEN` |
| Cliente | token no path da URL — sem login |
| Agente | header `x-token` com `AGENTE_TOKEN` |
| Kanban (aba do PCP) | sessão do PCP — **rede interna, sem porta para fora** |
---
# PORTAL · na nuvem
## `POST /webhook/tiny`
Chamado quando nasce um pedido de DTF. **É o gatilho de tudo.**
```json
{ "numero": "48213", "cliente": "Estamparia Vitória",
"telefone": "5516999998888", "metros": 2.4, "cliente_novo": false }
```
**200**
```json
{ "ok": true, "link": "https://arte.dropstaratacado.com.br/arte/a7f3k9..." }
```
Cria o pedido, gera token de 7 dias e enfileira o WhatsApp com o link.
Se o número já existir, devolve `{"ok":true,"ja_existia":true}` sem duplicar.
**Latência importa.** O link só sai depois que o pedido existe no Tiny — se a
integração VNDA → Tiny demorar, o cliente espera. Medir antes de subir.
---
## `GET /arte/{token}`
Página de upload. HTML. O token já sabe o pedido — **o cliente não digita nada.**
## `GET /api/arte/{token}`
```json
{ "pedido": "48213", "cliente": "Estamparia Vitória", "metros": 2.4,
"area_util_cm": 97, "largura_max_cm": 57,
"limite_arquivo_m": 20, "max_gb": 5, "max_arquivos": 10 }
```
**410** se o token expirou.
## `POST /api/arte/{token}/url`
```json
{ "nome": "estampa.png", "bytes": 84000000 }
```
```json
{ "url": "https://r2.../48213/ab12_estampa.png?X-Amz-...",
"chave": "48213/ab12_estampa.png", "expira_em": 3600 }
```
**Upload direto para o storage.** Arquivo de 5 GB passando pelo VPS derrubaria
o processo. Em partes, para arquivos grandes.
## `POST /api/arte/{token}/pronto`
```json
{ "chave": "48213/ab12_estampa.png", "repeticoes": 20 }
```
```json
{ "arte_id": 991, "status": "processando" }
```
Dispara o pré-flight em background. O cliente acompanha por polling em
`GET /api/arte/{token}/status`.
## `GET /api/arte/{token}/status`
**Aprovada:**
```json
{ "status": "aprovada", "dpi_efetivo": 300, "qualidade_pct": 100,
"metros_totais": 48.0, "minutos_maquina": 144,
"partes": [{"ordem":1,"metros":20,"nome":"48213_p1.png"},
{"ordem":2,"metros":20,"nome":"48213_p2.png"},
{"ordem":3,"metros":8,"nome":"48213_p3_carimbado.png"}],
"avisos": [], "previsao_saida": "2026-09-02T18:00:00-03:00" }
```
**Recusada:**
```json
{ "status": "recusada",
"motivo": "A resolução real da arte é de 72 DPI no tamanho que você comprou. Precisamos de pelo menos 150 DPI — o ideal é 300. Reenvie em maior resolução." }
```
**O relógio do prazo só começa quando `status = aprovada`.** Arquivo ruim
enviado às 17h não faz as horas correrem contra a casa.
---
## `GET /api/artes` · o agente busca
Header `x-token`. Devolve o que está aprovado, com vírus liberado e ainda não baixado.
```json
[{ "arte_id": 991, "pedido": "48213", "cliente": "Estamparia Vitória",
"metros": 48.0, "minutos_maquina": 144, "conferir_manual": false,
"partes": [{"ordem":1,"metros":20,"nome":"48213_p1.png",
"url":"https://r2.../...","sha256":"a3f9..."}] }]
```
## `POST /api/artes/{id}/baixada` · confirma o download
## `POST /api/agente/heartbeat` · a cada 5 min
**Sem sinal por 15 minutos, alertar o TI.** Serviço silencioso parado é pior que
erro barulhento: ninguém percebe até o cliente cobrar.
---
## `GET /cliente/{token_cliente}` · minhas artes
Link permanente do cliente, não do pedido. Lista as artes tratadas dos últimos
12 meses.
## `POST /cliente/{token_cliente}/reimprimir`
```json
{ "arte_id": 812, "metros": 20 }
```
Abre pedido novo no Tiny com a arte já pronta. **Recompra sem atrito.**
---
# KANBAN · rede interna, aba do PCP
## `GET /api/quadro`
```json
{ "colunas": [{"id":"rec","nome":"Arte recebida"}, ...],
"cards": {
"fil": [{ "arte_id": 991, "pedido": "48213", "cliente": "Estamparia Vitória",
"metros": 48.0, "minutos_maquina": 144, "partes": 3,
"maquina": null, "desde": "2026-09-02T14:02:00",
"parado_seg": 4320, "conferir_manual": false }]
},
"maquinas": [{ "n": 1, "ocupada": true, "pedido": "48190",
"metros": 9.0, "rodando_seg": 720 },
{ "n": 3, "ocupada": false }] }
```
`maquinas` é o que pinta o **círculo vermelho**. Com seis máquinas vendo o mesmo
quadro, é o que evita dois operadores no mesmo arquivo.
---
## `POST /api/puxar`
```json
{ "maquina": 4, "usuario": "alexandre" }
```
```json
{ "maquina": "Maq 4", "pedido": "48197", "metros": 18.0,
"minutos": 60, "reserva_expira_em": 3 }
```
**Um pedido por vez** e **reserva de 3 minutos** — ajustes pedidos pela sala.
Pega sempre o mais antigo da fila. Se não entrar em `imp` em 3 min, volta.
**409** se a máquina já estiver ocupada. **404** se a fila estiver vazia.
## `POST /api/devolver`
```json
{ "arte_id": 991, "usuario": "thales", "motivo": "travou no meio" }
```
Volta ao **topo** da fila, não ao fim — o pedido já esperou uma vez.
## `POST /api/mover`
```json
{ "arte_id": 991, "para": "fin", "usuario": "poliana", "maquina": 4 }
```
Grava o movimento **antes** de qualquer integração externa. Se o Tiny estiver
fora, o job fica na fila e tenta de novo em 1, 5 e 30 min — **mas a medição de
tempo já está salva.**
Move o arquivo entre as pastas e enfileira marcador e WhatsApp quando a coluna
pedir. Ao ir para `apc`, pergunta o motivo da prova de cor.
## `PATCH /api/card/{arte_id}`
```json
{ "minutos_maquina": 90 }
```
Ajuste da estimativa. **O sistema sugere por `metros/20*60`; quem trata a arte
corrige só quando foge do normal.** É esse número que soma a fila contra a
capacidade do dia.
## `GET /p/{pedido}`
O QR do carimbo aponta para cá. Redireciona para o card. **A revisão bipa e cai
na tela do pedido** em vez de procurar na lista.
---
## Retrabalho
### `POST /api/retrabalho`
```json
{ "pedido_origem": "48184", "metros": 2.0, "causa": "impressao",
"aberto_por": "mayana", "evidencia": "print-whatsapp.jpg" }
```
```json
{ "id": 44, "alcada": "sala", "vez": 1, "conta_na_meta": true }
```
**A Mayana abre e classifica** — conhece o cliente e a reclamação.
**A causa fica travada.** Quem discorda contesta, não altera.
### `POST /api/retrabalho/{id}/autorizar`
```json
{ "usuario": "thales" }
```
**Thales ou Alexandre autorizam**, porque o retrabalho da casa entra na meta
deles. Eles confirmaram achar justo.
**403** se a alçada exigir financeiro ou diretoria.
### `POST /api/retrabalho/{id}/contestar`
```json
{ "usuario": "alexandre", "texto": "não foi impressão, a arte veio em CMYK" }
```
Registra a discordância sem mudar a causa. Fica com quem pediu e quem decidiu.
---
## Relatórios
### `GET /api/relatorio/etapas?dias=7`
```json
[{ "coluna": "tra", "nome": "Arte tratada", "movimentos": 84,
"media_min": 94, "pior_min": 380 }]
```
**Responde a pergunta que decide o terceiro turno:** quanto do tempo é fila e
quanto é trabalho. Se a arte demora 6h e a máquina imprime em 40 min, rodar 24h
só faz a fila esperar de madrugada.
### `GET /api/relatorio/impressao?dias=7`
```json
[{ "maquina": "Maq 1", "pedidos": 42, "media_min": 34,
"metros": 310.5, "m_por_hora": 20.4 }]
```
**`m_por_hora` valida ou derruba os 20 m/h.** Todo o cálculo de capacidade —
60.480 metros/mês, R$ 148 mil de ganho — depende desse número. Se for 14, a
conta muda inteira.
### `GET /api/relatorio/aproveitamento?dias=30`
```json
{ "metros_faturados": 11605, "metros_de_filme": 12693,
"aproveitamento_pct": 91.4, "valor_do_ponto_mes": 980,
"fonte": "transferências para o depósito Sala DTF no Tiny" }
```
**Não exige apontamento novo.** O consumo já é registrado quando o insumo é
transferido para o depósito de impressão — a Altus faz isso porque também
revende insumo.
### `GET /api/relatorio/retrabalho?dias=30`
```json
{ "por_causa": [{ "causa": "impressao", "descricao": "Falha de impressão",
"responsavel": "Thales e Alexandre", "pedidos": 9,
"metros": 24.5, "pct": 1.1, "meta": 0.4, "bate": false }],
"total_casa_pct": 2.9, "meta_casa_pct": 1.6 }
```
**⚠ A meta ainda não está decidida.** A proposta era 0,4% por causa; a sala
respondeu que "meta geral seria melhor". `META_POR_CAUSA` está isolado no código
para trocar sem mexer no resto.
---
## Códigos de erro
| Código | Quando |
|---|---|
| 400 | payload inválido, coluna inexistente, mais de 10 arquivos |
| 401 | token do webhook ou do agente errado |
| 403 | alçada insuficiente para autorizar retrabalho |
| 404 | token não existe, card não encontrado, fila vazia |
| 409 | máquina já ocupada, pedido já reservado |
| 410 | token do link expirou |
| 413 | arquivo acima de 5 GB |
| 429 | rate limit — 20 req/min por token |
---
## O que testar antes de dar por pronto
- [ ] Upload de arquivo de 5 GB sem derrubar o VPS
- [ ] Dois operadores clicando `puxar` ao mesmo tempo — só um pega
- [ ] Reserva expirando: puxar e não mover em 3 min devolve à fila
- [ ] Tiny fora do ar: o movimento grava e o job fica na fila
- [ ] Agente sem internet: para, e ao voltar baixa o acumulado
- [ ] Arquivo baixado pela metade não aparece no kanban
- [ ] Token expirado devolve 410 com mensagem clara ao cliente