knowledge-base/records/plugin-dev/KB-PLUGIN-037-redmine-plugin-api-contract.md
2026-07-03 09:18:29 -03:00

148 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: KB-PLUGIN-037
title: "Plugin redmine — Contrato de API GLPI → Redmine (o quê, como e por quê)"
domain: plugin-dev
tags:
- redmine
- glpi11
- plugin
- api
- rest
- integration
- impersonation
- custom-fields
- time-tracking
- producao
status: active
severity: high
created_at: 2026-07-02
updated_at: 2026-07-02
applies_to:
- plugin redmine (GLPI 11.0.4+ prod / 11.0.7 dev)
- Redmine 5.x (redmine.mindtek.com.br / 10.10.100.6:8081)
related_records:
- KB-PLUGIN-031
- KB-PLUGIN-036
- KB-INFRA-004
---
# Plugin redmine — Contrato de API GLPI → Redmine
Registro de desenvolvedor do plugin `redmine` (marketplace Mindplace). Resume **o quê**
sai do GLPI, **como** entra no Redmine e **por quê** cada decisão existe. A referência
completa (payloads, exemplos HTTP) vive no repo do plugin: `docs/API.md`
(`servicedesk.mindtek.com.br/git/rodolpho.lopes/redmine`, espelho dev em
`onmind.mindtek.com.br/git/rodolpho.lopes/Redmine`).
## O quê (modelo)
| GLPI | Redmine | Quando |
|---|---|---|
| Projeto ou Entidade (vínculo manual na aba Redmine) | Projeto | vínculo 1:1 salvo em `glpi_plugin_redmine_projects` / `_entities` |
| Chamado | **1 issue** | criada no 1º lançamento de tempo (lazy) |
| TicketTask **Feito** com duração > 0 | **1 time entry** na issue | ao salvar a task (1× por task) |
| Status do chamado | status da issue | mapeado (`status_map` do formconfig) |
| Técnico atribuído / autor da task | autor da issue / do time entry | via impersonation |
**Por quê:** o chamado nasce e morre no GLPI; para o Redmine importam as horas e o
andamento. Tarefas não concluídas ficam só no GLPI (gate no `state = Feito`).
## Como (mecanismos-chave)
- **Auth:** header `X-Redmine-API-Key` com chave de **admin** (cifrada no banco via GLPIKey).
- **Autoria — impersonation:** header `X-Redmine-Switch-User: <login>`. Só funciona se o
usuário for **membro do projeto** com papel que permita a ação → o plugin faz
auto-membership (`POST /projects/:id/memberships.json`, papel = `default_role_id`).
Falhou (403/412)? Refaz como admin e loga — nunca perde o lançamento.
- **Mapeamento de usuários:** overrides manuais (`glpi_plugin_redmine_usermap`) →
auto-match por **e-mail** no cache `glpi_plugin_redmine_users`**lookup ao vivo**
no Redmine (`/users.json?name=<email>`) com upsert no cache (self-healing, 1.6.0).
Sem par: lança como admin **com log** ("Sem par Redmine para o usuário GLPI #N").
Incidente real (2026-07-02): o cache ficou vazio em prod e os lançamentos saíram
como admin em silêncio — motivou o self-healing e o `syncUsers` por upsert
(nunca esvazia a tabela).
- **Idempotência:** `glpi_plugin_redmine_tickets` (1 issue por chamado) e
`glpi_plugin_redmine_tasks` (tempo 1× por task). Se a issue vinculada foi apagada no
Redmine, o plugin detecta (GET 404) e **recria** removendo o vínculo órfão (1.5.5+).
- **Tempo:** GLPI grava segundos → Redmine quer horas decimais → `actiontime/3600`,
2 casas (0h05 → 0.08 h; o Redmine exibe "0:05").
- **Sincronizar Metadados:** cacheia projects, trackers, statuses, priorities,
activities, roles, users e **project_custom_fields** em
`glpi_plugin_redmine_configs.metadata_cache`. Os formulários leem SÓ do cache — após
mudanças no Redmine (novos campos, projetos), é preciso re-sincronizar.
## Por quê — regras do Redmine de PRODUÇÃO que moldaram o contrato
Aprendidas em produção (2026-06-30 a 07-02); o dev era permissivo, o prod não:
1. **Custom fields de projeto OBRIGATÓRIOS**: "Tipo de Projeto" (cf id 4) e
"Tipo de Faturamento" (cf id 5) não podem ficar vazios na criação de projeto →
`POST /projects.json` leva `custom_field_values: {"4": "...", "5": "..."}`.
O form das abas Redmine renderiza esses campos **dinamicamente** a partir de
`metadata_cache.project_custom_fields` (dropdown p/ `field_format=list`) — se o
Redmine ganhar novos campos obrigatórios, basta re-sincronizar metadados.
2. **Identificador**: só minúsculas/números/traços → `createProjectFull()` normaliza
("GNA" → "gna"); senão 422 "Identificador não é válido".
3. **Atividade do time entry é POR PROJETO**: a `default_activity_id` do formconfig
precisa estar habilitada no projeto de destino, senão 422 "não está incluso na lista".
4. **Comentário do time entry é obrigatório**: task sem descrição → fallback
"Tempo lançado via GLPI (chamado #N)" (1.5.4+).
5. **Projeto de destino precisa estar ATIVO** (status 1): fechado/arquivado → 422/403.
O dropdown de vínculo filtra só ativos (1.5.3+).
6. **Tracker é por projeto**: o `default_tracker_id` global precisa existir no projeto;
vazio = o Redmine usa o default do projeto (mais seguro em multi-cliente).
## Endpoints usados (resumo)
Leitura (sync): `/projects.json`, `/trackers.json`, `/issue_statuses.json`,
`/enumerations/issue_priorities.json`, `/enumerations/time_entry_activities.json`,
`/roles.json`, `/users.json` (paginado), `/projects/:id/issue_categories.json`,
`/custom_fields.json` (filtra `customized_type=project`).
Escrita: `POST /projects.json` (criar projeto c/ custom fields), `POST /issues.json`
(issue do chamado), `PUT /issues/:id.json` (status), `POST /time_entries.json` (tempo),
`POST /projects/:id/memberships.json` (auto-membership).
## Motor de Templates de Payload (2.0)
A partir da **2.0** o corpo de cada operação (`create_issue`, `log_time`,
`update_status`, `create_project`) pode vir de um **template editável** em vez de
ser montado no código — o objetivo é o plugin virar **produto ambiente-agnóstico**
(cada Redmine tem seus campos obrigatórios/personalizados). Guia do usuário:
`docs/TEMPLATES.md`; contrato de dev: `docs/API.md`.
- **`PluginRedmineTemplateengine`** (`inc/templateengine.class.php`): tabela
`glpi_plugin_redmine_templates`, renderizador **mustache-like** (`{{ var | filtro }}`;
tag ocupando o valor inteiro → **tipo nativo**; lookup indexado `map.status[chamado.status]`;
filtros `truncar/sem_html/minusculo/identificador/padrao`; `prune` de nulos), seeds
equivalentes ao 1.x, descoberta de custom fields e os contextos (`buildContext`,
`buildFormContext`, `sampleContext`).
- **Precedência**: template **ativo** → renderiza e envia; erro de **render** → log +
**fallback 1.x**; **sem** template ativo → comportamento **1.x idêntico**. Ou seja,
atualizar da 1.x para a 2.0 **não muda nada** até ativar um template.
- **UI** (config): editor linha-a-linha "DE → PARA" (paleta de variáveis GLPI via
autocomplete + dicionário dos campos que o Redmine espera, com obrigatoriedade e
valores), botão "Gerar template" (local, sem reload) e **testador**
(`ajax/template_test.php`): preview do render + dry-run que cria/apaga objeto de
teste e **traduz os 422** em dicas.
- **NÃO virou template** (é motor, não payload): impersonation, idempotência,
membership, gates (Feito/duração), recuperação de issue órfã.
- Decisão de escopo (2026-07-03): **não** implementar override por vínculo nem perfis
multi-Redmine sem caso real (YAGNI) — o template global cobre o cenário atual.
## Gotchas de manutenção
- Dev = GLPI 11.0.7, prod = **11.0.4**: desenvolver mirando 11.0.0+ (sem
`fields.csrfField()`, sem `Toolbox::logError()`). Ver [KB-PLUGIN-036].
- Ambiente em `production`: mudou `.twig``cache:clear` obrigatório (o cache do GLPI
não é invalidado por atualização de plugin).
- **Twig `merge` renumera chaves inteiras**: nunca montar opções `{id: nome}` no Twig —
montar no PHP (bug real: vínculo gravava o índice em vez do ID do projeto, 1.5.3).
- Release ZIP: wrapper `redmine/` como **primeira entrada** (entrada de diretório),
senão o instalador do Mindplace extrai aninhado.
## Classificação
- Tipo: contrato de integração + lições de produção.
- Reutilização: obrigatória para quem for manter/evoluir o plugin redmine; útil para
qualquer integração GLPI → API externa.