--- 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: `. 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:** cache `glpi_plugin_redmine_users` (sync) + auto-match por **e-mail** + overrides manuais (`glpi_plugin_redmine_usermap`). - **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). ## 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.