Redmine/docs/API.md
Gemini 27ee6addeb docs: API.md — criação de projeto com custom_field_values e regras por instância
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 10:42:11 -03:00

11 KiB
Raw Blame History

Documentação de API — Plugin Redmine Integration

Documento de desenvolvedor. Descreve o que sai do GLPI e como entra no Redmine: gatilhos, chamadas à API REST do Redmine, payloads, cabeçalhos e o mapeamento de campos. Base de código: inc/redmineapi.class.php (cliente HTTP) e hook.php (orquestração).

Índice

  1. Visão geral
  2. Autenticação e cabeçalhos
  3. Gatilhos no GLPI
  4. Resolução do projeto Redmine
  5. Endpoints do Redmine usados
  6. Mapeamento de campos GLPI → Redmine
  7. Impersonation (autoria)
  8. Idempotência e tabelas de estado
  9. Tratamento de erros
  10. Exemplos completos

1. Visão geral

O plugin é um cliente da API REST do Redmine. Ele reage a eventos do GLPI (hooks) e traduz esses eventos em chamadas HTTP ao Redmine.

                 hooks GLPI                       HTTP (REST/JSON)
GLPI (chamados) ───────────► Plugin (PHP/cURL) ──────────────────► Redmine
                             inc/redmineapi.class.php
  • Sentido único (GLPI → Redmine) no fluxo automático. O GLPI é a fonte; o Redmine recebe issues e lançamentos de tempo.
  • A única exceção de leitura é a Sincronização de Metadados (Redmine → cache local).

2. Autenticação e cabeçalhos

Todas as chamadas usam a chave de API de um administrador do Redmine (armazenada cifrada com GLPIKey em glpi_plugin_redmine_configs.api_key).

Cabeçalhos padrão:

Cabeçalho Valor Quando
X-Redmine-API-Key <api_key do admin> sempre
Content-Type application/json POST/PUT
X-Redmine-Switch-User <login do usuário Redmine> impersonation (ver §7)
  • Timeout: 5s por requisição (evita 504).
  • Sucesso = HTTP 2xx. O corpo é JSON.

3. Gatilhos no GLPI

Evento GLPI (hook) Função Ação no Redmine
item_add / item_update em TicketTask plugin_redmine_tickettask_* cria a issue (1ª vez) + lança o tempo
item_update em Ticket plugin_redmine_ticket_update atualiza o status da issue vinculada
Aba Redmine em Projeto/Entidade (form) front/project.form.php, front/entity.form.php cria/vincula projeto, define categoria, sincroniza metadados

Gate do lançamento de tempo: o processamento da TicketTask só ocorre quando task.state == Planning::DONE (Feito) e actiontime > 0. Tarefas não concluídas permanecem apenas no GLPI.


4. Resolução do projeto Redmine

Antes de criar a issue, o plugin descobre em qual projeto Redmine ela entra, a partir do chamado:

Ticket
 ├─ vinculado a um Projeto GLPI? ──► glpi_plugin_redmine_projects ─► redmine_project_id
 │      (projeto via glpi_projecttasks_tickets ou glpi_itils_projects)
 └─ senão ──► entities_id do ticket ─► glpi_plugin_redmine_entities ─► redmine_project_id
                (match EXATO da entidade, sem herança de árvore)

Sem vínculo (nem projeto nem entidade) → nada é enviado (log e retorno).


5. Endpoints do Redmine usados

Leitura (Sincronizar Metadados)

Método Endpoint Uso
GET /projects.json?limit=100 lista de projetos (cache + dropdowns)
GET /trackers.json tipos de tarefa
GET /issue_statuses.json status (alvo do mapa de status)
GET /enumerations/issue_priorities.json prioridades
GET /enumerations/time_entry_activities.json atividades de tempo
GET /roles.json papéis (para auto-membership)
GET /users.json?limit=100&offset=N usuários (cache p/ mapeamento; paginado)
GET /projects/{id}/issue_categories.json categorias (por projeto)
GET /custom_fields.json definições de campos personalizados (filtra customized_type=project)

Escrita

Método Endpoint Uso
POST /projects.json criar projeto Redmine (aba Projeto/Entidade) — inclui custom_field_values
POST /issues.json criar a issue a partir do chamado
PUT /issues/{id}.json sincronizar status da issue
POST /time_entries.json lançar tempo da TicketTask
POST /projects/{id}/memberships.json adicionar técnico como membro (auto-membership)

6. Mapeamento de campos GLPI → Redmine

Issue (POST /issues.json)

Campo Redmine (issue.*) Origem no GLPI
project_id projeto Redmine resolvido (§4)
subject Ticket.name (truncado a 255, HTML removido)
description Ticket.content (HTML removido)
tracker_id config.default_tracker_id
priority_id config.default_priority_id
status_id config.status_map[Ticket.status]
category_id default_category_id do vínculo (projeto/entidade)
watcher_user_ids config.default_watcher_ids (array)
autor técnico atribuído ao chamado → X-Redmine-Switch-User (§7)

Lançamento de tempo (POST /time_entries.json)

Campo Redmine (time_entry.*) Origem no GLPI
issue_id issue da issue do chamado
hours TicketTask.actiontime / 3600 (segundos → horas decimais)
comments TicketTask.content (HTML removido)
activity_id config.default_activity_id
autor quem lançou a task (TicketTask.users_id) → X-Redmine-Switch-User

Status (PUT /issues/{id}.json)

Campo Origem
status_id config.status_map[Ticket.status] (no item_update do Ticket)

Criação de projeto (POST /projects.json — abas Redmine de Projeto/Entidade)

Campo Redmine (project.*) Origem
name, description, is_public formulário da aba
identifier formulário, normalizado para minúsculas (regra do Redmine)
parent_id dropdown "Subprojeto de"
enabled_module_names multiselect "Módulos"
custom_field_values { "<cf_id>": "<valor>", ... } — campos personalizados de projeto

Os campos personalizados são renderizados dinamicamente a partir de metadata_cache.project_custom_fields (populado pelo "Sincronizar Metadados" via GET /custom_fields.json). Em instâncias onde eles são obrigatórios (ex.: produção Mindtek: "Tipo de Projeto" e "Tipo de Faturamento"), a criação falha com 422 se não forem enviados.

Conversões importantes

  • Tempo: GLPI grava actiontime em segundos (int); Redmine espera horas decimais. 5400 → 1.5.
  • Subject: limite de 255 chars no Redmine → truncado.
  • HTML: descrições do GLPI (TinyMCE) passam por RichText::getTextFromHtml.
  • Identificador de projeto: minúsculas/números/traços — normalizado em createProjectFull().

Regras que variam por instância/projeto do Redmine (validar em produção)

  • Custom fields de projeto podem ser obrigatórios na criação (ver acima).
  • Atividade do time entry é por projeto: a default_activity_id precisa estar habilitada no projeto de destino (senão 422 "não está incluso na lista").
  • Comentário do time entry pode ser obrigatório: task sem descrição usa o fallback "Tempo lançado via GLPI (chamado #N)".
  • Tracker é por projeto: default global vazio = Redmine usa o default do projeto.
  • Issues só podem ser criadas em projetos ativos (o dropdown de vínculo já filtra).

7. Impersonation (autoria)

Por padrão, tudo criado via API nasce como o admin (dono da chave). Para atribuir o autor correto, o plugin usa o cabeçalho X-Redmine-Switch-User: <login>.

Pré-requisito: o usuário impersonado precisa ser membro do projeto Redmine com um papel — senão o Redmine retorna 403. Por isso, antes de impersonar, o plugin chama ensureProjectMembership() (adiciona o técnico com config.default_role_id).

Mapeamento GLPI → usuário Redmine (resolveRedmineUser):

  1. Override manual (glpi_plugin_redmine_usermap), se houver.
  2. Auto-match por e-mail (e-mail do usuário GLPI = mail do usuário Redmine, do cache).

Sem correspondência → cai no admin (fallback, §9).


8. Idempotência e tabelas de estado

Tabela Garante
glpi_plugin_redmine_tickets (tickets_id único) 1 issue por chamado (reuso)
glpi_plugin_redmine_tasks (tickettasks_id único) tempo lançado 1× por task (sem duplicar)
glpi_plugin_redmine_projects / _entities vínculo GLPI ↔ projeto Redmine
glpi_plugin_redmine_users / _usermap cache de usuários + overrides de mapeamento

Editar horas de uma task já lançada não re-sincroniza. Reverter FeitoA fazer não apaga o time entry.


9. Tratamento de erros

  • Impersonation falha (403/412 — usuário sem par no Redmine ou não-membro): o plugin refaz a chamada como admin (não perde a issue/tempo) e registra aviso em files/_log/redmine*.
  • API inacessível / chave inválida: syncMetadata() retorna false; criação de issue/tempo é abortada com log.
  • Todo erro HTTP >= 400 é logado via Toolbox::logInFile('redmine', ...) (em files/_log/redmine*).

10. Exemplos completos

Criar issue (com impersonation)

POST /issues.json HTTP/1.1
X-Redmine-API-Key: <admin_key>
X-Redmine-Switch-User: joao.silva
Content-Type: application/json

{
  "issue": {
    "project_id": 3,
    "subject": "Erro no servidor de e-mail",
    "description": "Cliente relata falha no envio.",
    "tracker_id": 1,
    "priority_id": 2,
    "status_id": 1,
    "category_id": 7,
    "watcher_user_ids": [5]
  }
}

Resposta 201: { "issue": { "id": 42, "author": { "id": 9, "name": "João Silva" }, ... } }

Lançar tempo (com impersonation)

POST /time_entries.json HTTP/1.1
X-Redmine-API-Key: <admin_key>
X-Redmine-Switch-User: joao.silva
Content-Type: application/json

{
  "time_entry": {
    "issue_id": 42,
    "hours": 1.5,
    "activity_id": 1,
    "comments": "Reinício do serviço e validação."
  }
}

Sincronizar status da issue

PUT /issues/42.json
X-Redmine-API-Key: <admin_key>
Content-Type: application/json

{ "issue": { "status_id": 5 } }

Auto-membership (antes de impersonar)

POST /projects/3/memberships.json
X-Redmine-API-Key: <admin_key>
Content-Type: application/json

{ "membership": { "user_id": 9, "role_ids": [3] } }

Resumo do fluxo end-to-end (suporte recorrente / entidade)

  1. Técnico marca a TicketTask como Feito no GLPI.
  2. Hook resolve o projeto Redmine pela entidade do chamado.
  3. (1ª vez) POST /issues.json cria a issue — autor = técnico atribuído (impersonation + auto-membership).
  4. POST /time_entries.json lança as horas — autor = quem lançou a task.
  5. Ao mudar o status do chamado, PUT /issues/{id}.json reflete no Redmine.