Ligar e desligar módulos por tenant e unidade — API
Um módulo, neste contexto, é uma funcionalidade inteira do produto que pode ser ligada ou
desligada por tenant e, opcionalmente, sobrescrita por unidade — por exemplo peer_review
(segunda opinião de laudo), ai_report, command_center, worklist, messaging, entre outras 14
chaves fixas do catálogo. A rota não guarda "o estado atual" como uma coluna mutável: guarda um
histórico de controles (quem ligou/desligou e quando), e o estado exibido é sempre projetado a
partir do controle ativo mais recente.
Funcionamento
- Toda rota exige um access token válido (
JwtAuthenticationGuard). A permissão é checada porOrganizationModuleAuthorizationService: ler exigepermission:read/permission:manageno tenant (ou também na unidade, para rotas de unidade); escrever (PUT/DELETE) exigepermission:manageno tenant ou na unidade. Um administrador de plataforma sempre passa. - Listar/consultar no tenant (
GET .../modules,GET .../modules/:key) projeta as 14 chaves do catálogo a partir do controle de tenant ativo mais recente de cada uma.enabledreflete o último estado gravado;configureddiz se já existe algum controle de tenant para aquela chave — um módulo pode estarenabled: false, configured: true(foi desligado explicitamente) ouenabled: false, configured: false(nunca foi mexido), e essas duas situações são distintas. - Listar/consultar na unidade (
GET .../units/:unitId/modules[/:key]) aplica a mesma projeção, mas com precedência: se a unidade tem um override próprio, ele vence — mesmo que o valor sejafalse(desligar explicitamente na unidade sobrepõe um tenant com o módulo ligado). Sem override de unidade, herda o valor do tenant; sem nenhum dos dois, o módulo é considerado desligado (source: NONE). A resposta expõesource(UNIT/TENANT/NONE), o valor de tenant (tenantDefault) e o de unidade (unitOverride) separadamente, além doenabledjá resolvido. - Ligar/desligar (
PUT .../modules/:key, no tenant ou na unidade) grava um novo controle (histórico) com o status pedido. Pedir o status que já está vigente é tratado como no-op: nada é gravado, nenhum evento de auditoria é emitido. Toda escrita roda dentro de uma transação com lock pessimista no tenant (e na unidade, se aplicável) e publica um evento de auditoria (outbox) com o antes/depois. - Remover o controle (
DELETE .../modules/:key) faz um soft-delete do controle ativo — a linha de histórico permanece, só deixa de estar ativa. É idempotente: remover quando não há controle ativo (ou quando o módulo nunca foi configurado) também devolve204, sem erro. Depois de remover, o tenant volta a "não configurado"; a unidade volta a herdar o valor do tenant. - Um módulo pode ficar administrável (ligar/desligar via esta API) sem que nenhuma regra de
negócio no resto do backend realmente o aplique ainda — o catálogo interno de "consumidores"
marca cada chave como
ENFORCED(tem consumidor comprovado por código e teste — hoje sópeer_review),PROJECTED(o estado é calculado e exposto, mas nada no backend refatorado ainda consulta) ouBLOCKED(a chave está deliberadamente impedida de virarENFORCEDaté uma tarefa de migração específica). Isso é controlado por um gate de migração (script/teste), não por nenhuma rota HTTP — não afeta o comportamento observável desta API, é uma proteção interna de rollout.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| GET | /v1/tenants/:tenantId/modules | Lista o estado das 14 chaves no tenant |
| GET | /v1/tenants/:tenantId/modules/:key | Estado de uma chave no tenant |
| PUT | /v1/tenants/:tenantId/modules/:key | Liga/desliga o módulo no tenant |
| DELETE | /v1/tenants/:tenantId/modules/:key | Remove o controle de tenant (volta a "não configurado") |
| GET | /v1/tenants/:tenantId/units/:unitId/modules | Lista o estado das 14 chaves na unidade, com precedência já aplicada |
| GET | /v1/tenants/:tenantId/units/:unitId/modules/:key | Estado de uma chave na unidade |
| PUT | /v1/tenants/:tenantId/units/:unitId/modules/:key | Cria/atualiza um override de unidade |
| DELETE | /v1/tenants/:tenantId/units/:unitId/modules/:key | Remove o override de unidade (volta a herdar do tenant) |
| GET | /v1/operations/organization/tenants/:tenantId/modules/:key/history | Histórico completo de controles de tenant para uma chave (só platform-admin) |
Versão: v1
Swagger:
Organization — Modules(rotas de tenant)Organization — Unit modules(rotas de unidade)readOrganizationModuleControlHistory
Rota (Dev): http://localhost:3000/v1/tenants/:tenantId/modules
A rota de histórico está fisicamente implementada no controller de administração do submódulo
unit(OrganizationAdministrationController, tag "Organization — Administration"), não no controller de módulos — é o único consumidor HTTP do serviço de histórico deste submódulo.
Lógica de decisão de PUT .../modules/:key (tenant ou unidade):
Permissões
| Rota | Guards | Perfil / escopo exigido |
|---|---|---|
GET de tenant | JwtAuthenticationGuard | permission:read/permission:manage no tenant, ou platform-admin |
PUT/DELETE de tenant | JwtAuthenticationGuard | permission:manage no tenant, ou platform-admin |
GET de unidade | JwtAuthenticationGuard | permission:read/permission:manage no tenant ou na unidade, ou platform-admin |
PUT/DELETE de unidade | JwtAuthenticationGuard | permission:manage no tenant ou na unidade, ou platform-admin |
GET de histórico | JwtAuthenticationGuard, AuthorizationGuard (@Authorize({scope:{kind:'platform-admin'}})) + checagem interna assertCanManageTenants | Somente administrador de plataforma — gestor de tenant/unidade comum não acessa, mesmo com permission:manage |
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Authorization | Sim | Bearer <access_token> |
Content-Type | Sim em PUT | application/json |
Path parameters
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
tenantId | UUID | Sim | Identificador do tenant |
unitId | UUID | Sim (rotas de unidade) | Identificador da unidade |
key | string | Sim (exceto list) | Uma das 14 chaves do catálogo (OrganizationModuleKey); chave fora do enum → 404, não 400 |
Query parameters
Nenhum.
Body
PUT .../modules/:key — SetOrganizationModuleRestRequest:
json{ "enabled": true }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
enabled | boolean | Sim | @IsBoolean(); corpo aceita só este campo (whitelist + forbidNonWhitelisted) |
Response
200 — estado de tenant (OrganizationTenantModuleEnvelopeRestResponse / lista):
json{ "data": { "key": "peer_review", "enabled": true, "configured": true } }
200 — estado de unidade (OrganizationUnitModuleEnvelopeRestResponse / lista):
json{"data": {"key": "peer_review","enabled": false,"source": "UNIT","tenantDefault": true,"unitOverride": false}}
200 — histórico (OrganizationTenantModuleControlHistoryEnvelopeRestResponse):
json{"data": [{ "active": false, "createdAt": "2026-01-10T12:00:00.000Z", "deletedAt": "2026-02-01T09:00:00.000Z", "enabled": true },{ "active": true, "createdAt": "2026-02-01T09:05:00.000Z", "deletedAt": null, "enabled": false }]}
DELETE (tenant e unidade) devolve 204 sem corpo.
Erros
| Classe de erro | errorCode | Status | Quando ocorre |
|---|---|---|---|
| Erro de validação do Nest | — | 400 | tenantId/unitId não é UUID; enabled ausente/não-booleano; corpo com campo extra |
OrganizationModuleNotFoundError | ORGANIZATION_MODULE_NOT_FOUND | 404 | :key não pertence às 14 chaves do catálogo |
ForbiddenAction | ORGANIZATION_MODULE_ACCESS_FORBIDDEN | 403 | ator sem a permissão exigida no tenant/unidade |
OrganizationTenantNotFoundError | ORGANIZATION_TENANT_NOT_FOUND | 404 | tenant inexistente/inativo |
OrganizationUnitNotFoundError | ORGANIZATION_UNIT_NOT_FOUND | 404 | unidade inexistente/inativa, ou não pertence ao tenant do path |
OrganizationModuleCapabilityDisabledError | ORGANIZATION_MODULE_CAPABILITY_DISABLED | 403 | não é lançado por nenhuma rota deste controller — pertence à checagem interna que outros módulos do backend fazem (ex. laudo verifica peer_review) antes de agir; documentado aqui por ser o mesmo catálogo |
ForbiddenAction (histórico) | ORGANIZATION_TENANT_MANAGE_FORBIDDEN | 403 | ator autenticado mas não é administrador de plataforma, ao chamar a rota de histórico |
Regras de negócio
| ID | Regra | Comportamento esperado |
|---|---|---|
| RN-01 | Catálogo fixo de 14 chaves, cada uma suportando os dois escopos (tenant e unidade) | Chave fora do enum → 404, não 400 |
| RN-02 | Override de unidade sempre vence sobre o default do tenant quando presente, mesmo que o valor seja false | Desligar explicitamente na unidade sobrepõe um tenant ligado |
| RN-03 | "Configurado" é diferente de "desligado" no tenant | enabled: false, configured: true (desligado de propósito) ≠ enabled: false, configured: false (nunca configurado) |
| RN-04 | Pedir o status já vigente é no-op | Não grava linha nova, não emite evento de auditoria |
| RN-05 | Remover é idempotente e é soft-delete | Chamar DELETE duas vezes não falha; a linha de histórico é preservada (deletedAt), nunca apagada |
| RN-06 | Toda escrita roda em transação com lock pessimista no tenant (e unidade) | Evita condição de corrida entre duas trocas concorrentes do mesmo módulo |
| RN-07 | Toda escrita publica evento de auditoria via outbox, com antes/depois | Base para a trilha de "quem ligou/desligou o quê e quando" |
| RN-08 | Existe no máximo um controle ativo por (tenant, módulo) e por (unidade, módulo) | Garantido por índice único parcial no banco (WHERE deleted_at IS NULL) |
| RN-09 | Histórico de controle só existe para o escopo tenant, sem paginação, ordenado do mais antigo para o mais recente, incluindo registros removidos | Não há histórico equivalente por unidade nesta API |
| RN-10 | A rota de histórico é restrita a administrador de plataforma | Diferente de todas as outras rotas deste grupo, que aceitam gestor de tenant/unidade com permission:manage |
| RN-11 | Um módulo pode estar administrável sem ter efeito real no resto do sistema | O catálogo interno de consumidores (ENFORCED/PROJECTED/BLOCKED) não muda o comportamento da API, mas explica por que ligar um módulo PROJECTED (ex. power_bi) hoje não altera nada visível fora desta consulta |
Variáveis de ambiente
Nenhuma variável de ambiente controla estas rotas. O catálogo de 14 chaves e o mapeamento de consumidores são constantes no código.
Tempo médio de resposta
A confirmar — responsável: time de Organization; data: 24/09/2026. Não há medição publicada.
Requisitos não funcionais
| Requisito | Definição |
|---|---|
| Idempotência | PUT: não (pedir o mesmo status é no-op, mas pedir status diferente sempre cria novo histórico) — na prática o resultado final é idempotente. DELETE: sim |
| Paginação | Não — listagens sempre trazem as 14 chaves; histórico não pagina |
| Rate limit | Não observado |
| Cache | Não |
| Auditoria | Sim — evento de outbox em toda troca de status, mais o histórico completo consultável (só tenant, só platform-admin) |
Relacionado
- 🖥️ Tela:
A confirmar — responsável: time de frontend; data: 24/09/2026.(não mapeada neste levantamento) - 📂 Módulo: Tenant / Organização
- 🔗 Configuração: Configuração (tenant e unidade) — mecanismo irmão, com a mesma cascata tenant→unidade, mas para valores tipados em vez de features inteiras