Consultar, listar e gerenciar o ciclo de vida do tenant — API
Cobre o cadastro administrativo do tenant (organização) já existente: consultar e editar o
perfil completo, listar e localizar tenants conforme o que o ator pode enxergar, e o ciclo
inativar / reativar. Não existe rota HTTP para criar um tenant neste módulo — o
provisionamento inicial é feito por um serviço interno (CreateOrganizationTenantService),
consumido pelo fluxo de registration/onboarding (fora do escopo desta documentação); aqui só
descrevemos as regras de validação do perfil porque elas são as mesmas usadas na criação.
Funcionamento
Perfil (GET/PATCH .../profile): o perfil é o conjunto completo de dados administrativos do
tenant — nome, slug, e-mail do admin, documento (CPF/CNPJ/estrangeiro), telefone, descrição,
timezone, links de app (Android/iOS) e configurações de entrega (delivery). GET exige apenas
permissão de leitura no tenant; PATCH exige permissão de gestão e é uma atualização parcial:
qualquer campo omitido mantém seu valor atual, e null explícito limpa o campo (quando o campo
aceita null). Pelo menos um campo precisa ser enviado. document e identificationType são um
par atômico: só podem mudar juntos.
Versionamento otimista: o PATCH aceita um campo opcional expectedVersion no corpo. Se
enviado e diferente da versão atual do tenant, a atualização é recusada com 409 — sem isso, a
gravação é "o último que salvar, vale" (last-write-wins). A versão só é incrementada por este
PATCH; inativar e reativar não mudam a versão.
Listar (GET /tenants): devolve só tenants ativos, filtrados pelo que o ator pode ver — um
administrador de plataforma vê todos; qualquer outro ator só vê os tenants em que tem permissão
direta, ou dos quais é dono de alguma unidade em que tem acesso. Aceita filtros de texto
("contains", sem diferenciar maiúsculas/minúsculas) por nome, slug, documento e e-mail do admin, e
ordenação por nome ou slug.
Buscar um tenant (GET /tenants/:tenantId): igual à listagem, mas para um único id — um tenant
fora do que o ator pode ver é reportado como 404, do mesmo jeito que um tenant inexistente (não
revela se ele existe fora do alcance do ator).
Inativar (POST .../inactivate): soft-delete lógico (marca deletedAt), sem cascata — inativar
um tenant não desliga automaticamente suas unidades, vínculos de PACS ou módulos; apenas grava
um evento de auditoria (organization.tenant.inactivated) para quem for consumir de forma
assíncrona. Um tenant já inativo (ou inexistente) responde 404 — a rota não é idempotente nesse
sentido, é preciso conferir o estado antes de chamar de novo.
Reativar (POST .../reactivate): usa um guard de autorização dedicado (não o guard genérico),
porque reativar por definição parte de um tenant inativo, e o mecanismo padrão de escopo
"tenant" do IAM exige tenant ativo para resolver o acesso. Reativar um tenant já ativo é idempotente
(não erra, não faz nada). Reativar pode falhar com 409 se, enquanto o tenant estava inativo,
outro tenant ativo passou a usar o mesmo slug ou document — a unicidade é só entre tenants
ativos, então isso é possível.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| GET | /v1/tenants/:tenantId/profile | Perfil administrativo completo do tenant |
| PATCH | /v1/tenants/:tenantId/profile | Atualiza parcialmente o perfil (com versionamento otimista opcional) |
| GET | /v1/tenants | Lista tenants ativos visíveis ao ator, paginada e filtrável |
| GET | /v1/tenants/:tenantId | Resumo de um tenant ativo e visível ao ator |
| POST | /v1/tenants/:tenantId/inactivate | Inativa o tenant (soft-delete) |
| POST | /v1/tenants/:tenantId/reactivate | Reativa um tenant inativo |
Versão: v1
Swagger: Organization — Tenant directory
Rota (Dev): http://localhost:3000/v1/tenants
Lógica de decisão de PATCH .../profile:
Lógica de decisão de POST .../inactivate e POST .../reactivate:
Permissões
| Rota | Guards | Perfil / escopo exigido |
|---|---|---|
GET .../profile | JwtAuthenticationGuard, AuthorizationGuard | tenant:read no tenant do path |
PATCH .../profile | idem | permission:manage no tenant do path (checado duas vezes: guard HTTP + serviço) |
GET /tenants | idem | unit:read, escopo organization-list — administrador de plataforma vê todos; os demais só o que a política de escopo resolver |
GET /tenants/:tenantId | idem | unit:read no tenant do path — tenant fora do alcance do ator responde 404, não 403 |
POST .../inactivate | idem | permission:manage no tenant do path |
POST .../reactivate | JwtAuthenticationGuard, OrganizationInactiveTenantManagementGuard (guard dedicado, não o genérico) | platform-admin ou permission:manage no tenant do path — reimplementado neste guard porque o guard genérico exige tenant ativo para resolver o escopo tenant |
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Authorization | Sim | Bearer <access_token> |
Content-Type | Sim em PATCH | application/json |
Path parameters
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
tenantId | UUID | Sim | Identificador do tenant |
Query parameters
GET /tenants:
| Nome | Tipo | Obrigatório | Default | Descrição |
|---|---|---|---|---|
name | string | Não | — | Filtro "contains" (case-insensitive) pelo nome |
slug | string | Não | — | Filtro "contains" pelo slug |
document | string | Não | — | Filtro "contains" pelo documento |
adminEmail | string | Não | — | Filtro "contains" pelo e-mail do admin |
sort | string | Não | name ASC | Ex.: "name ASC,slug DESC"; só aceita os campos name e slug |
page | number | Não | 1 | Mínimo 1 |
limit | number | Não | 10 | Entre 1 e 100 |
Body
PATCH .../profile — UpdateOrganizationTenantProfileRestRequest (todos os campos opcionais; enviar null limpa o campo quando ele aceita null; o corpo rejeita campos não listados):
json{"name": "Hospital Exemplo","slug": "hospital-exemplo","adminEmail": "admin@hospital-exemplo.com","document": "12.345.678/0001-90","identificationType": "CNPJ","phone": "+55 11 99999-0000","description": "Rede de diagnóstico por imagem","timezoneId": "America/Sao_Paulo","clientLabel": "Hospital Exemplo","deliveryUrl": "https://entrega.hospital-exemplo.com","externalViewerHost": "viewer.hospital-exemplo.com","androidAppUrl": "https://play.google.com/store/apps/details?id=com.exemplo","iosAppUrl": "https://apps.apple.com/app/id0000000000","expectedVersion": 3}
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
name | string | Não | 1–255 caracteres |
slug | string | Não | 3–100 caracteres; minúsculo, alfanumérico, hífens internos, não pode começar/terminar com hífen |
adminEmail | string | Não | e-mail válido, até 255 caracteres |
document | string | null | Não | par atômico com identificationType (os dois ou nenhum); dígito verificador validado para CPF/CNPJ; até 50 caracteres para FOREIGN |
identificationType | CNPJ | CPF | FOREIGN | null | Não | ver acima |
phone | string | null | Não | até 20 caracteres, formato de telefone |
description | string | null | Não | até 2000 caracteres |
timezoneId | string | null | Não | identificador IANA (ex. America/Sao_Paulo); precisa existir e estar ativo no catálogo de timezones |
clientLabel | string | null | Não | até 255 caracteres |
deliveryUrl | string | null | Não | URL absoluta http/https, até 500 caracteres |
externalViewerHost | string | null | Não | hostname canônico, até 255 caracteres |
androidAppUrl / iosAppUrl | string | null | Não | URL absoluta http/https, até 500 caracteres |
expectedVersion | number | Não | inteiro ≥ 1 — versão esperada para o lock otimista |
GET, POST .../inactivate e POST .../reactivate não recebem corpo.
Response
200 — perfil (OrganizationTenantProfileEnvelopeRestResponse), devolvido por GET/PATCH .../profile:
json{"data": {"tenantId": "8f2a...-uuid","name": "Hospital Exemplo","slug": "hospital-exemplo","adminEmail": "admin@hospital-exemplo.com","document": "12.345.678/0001-90","identificationType": "CNPJ","phone": "+55 11 99999-0000","description": "Rede de diagnóstico por imagem","timezoneId": "America/Sao_Paulo","clientLabel": "Hospital Exemplo","deliveryUrl": "https://entrega.hospital-exemplo.com","externalViewerHost": "viewer.hospital-exemplo.com","androidAppUrl": "https://play.google.com/store/apps/details?id=com.exemplo","iosAppUrl": "https://apps.apple.com/app/id0000000000","version": 4}}
200 — item de listagem/busca (OrganizationTenantDirectoryItemResponse), devolvido por GET /tenants (paginado) e GET /tenants/:tenantId:
json{"data": {"tenantId": "8f2a...-uuid","name": "Hospital Exemplo","slug": "hospital-exemplo","adminEmail": "admin@hospital-exemplo.com","document": "12.345.678/0001-90","identificationType": "CNPJ","deletedAt": null}}
POST .../inactivate e POST .../reactivate devolvem 204 sem corpo.
Erros
| Classe de erro | errorCode | Status | Quando ocorre |
|---|---|---|---|
| Erro de validação do Nest | — | 400 | tenantId inválido; corpo com campo extra ou fora do formato dos decorators |
ValidationError | ORGANIZATION_TENANT_PROFILE_UPDATE_INVALID | 400 | PATCH sem nenhum campo, ou document/identificationType enviados sem o par |
ValidationError | ORGANIZATION_TENANT_PROFILE_INVALID | 400 | name/slug/document/phone/description/timezoneId em formato inválido |
ValidationError | ORGANIZATION_DELIVERY_SETTINGS_INVALID | 400 | clientLabel/deliveryUrl/externalViewerHost inválidos |
ValidationError | ORGANIZATION_TENANT_APP_LINKS_INVALID | 400 | androidAppUrl/iosAppUrl não são URL absoluta válida |
ForbiddenAction | ORGANIZATION_TENANT_MANAGE_FORBIDDEN | 403 | ator sem permission:manage no tenant e sem ser platform-admin (PATCH, inactivate, reactivate) |
ForbiddenAction (guard genérico) | — | 403 | ator sem tenant:read/unit:read no tenant (rotas de leitura) |
NotFoundException (Nest) | — | 404 | tenant não existe, está inativo, ou está fora do alcance do ator (GET .../profile, GET /tenants/:tenantId) |
OrganizationTenantNotFoundError | ORGANIZATION_TENANT_NOT_FOUND | 404 | tenant não existe (ou já está inativo, no caso de inactivate) — PATCH, inactivate, reactivate |
OrganizationTimezoneNotFoundError | ORGANIZATION_TIMEZONE_NOT_FOUND | 404 | timezoneId novo não existe ou está inativo |
OrganizationTenantAlreadyExistsError | ORGANIZATION_TENANT_SLUG_ALREADY_EXISTS | 409 | novo slug já usado por outro tenant ativo (PATCH, reactivate) |
OrganizationTenantAlreadyExistsError | ORGANIZATION_TENANT_DOCUMENT_ALREADY_EXISTS | 409 | novo document já usado por outro tenant ativo (PATCH, reactivate) |
OrganizationTenantVersionConflictError | ORGANIZATION_TENANT_VERSION_CONFLICT | 409 | expectedVersion enviado é diferente da versão atual (PATCH) |
Regras de negócio
| ID | Regra | Comportamento esperado |
|---|---|---|
| RN-01 | Perfil e listagem só enxergam tenants ativos | Tenant inativo é tratado como inexistente nas rotas de leitura |
| RN-02 | Versionamento otimista é opcional e vive no corpo (expectedVersion), não em header | Sem esse campo, PATCH é last-write-wins |
| RN-03 | version só é incrementada por PATCH .../profile | Inativar e reativar não mudam a versão |
| RN-04 | Unicidade de slug/document é só entre tenants ativos | Um tenant inativo "libera" seu slug/document para outro tenant ativo usar |
| RN-05 | document e identificationType formam um par atômico | Só podem ser alterados juntos: ambos presentes ou ambos null |
| RN-06 | Validação do document depende do identificationType | CPF/CNPJ têm dígito verificador conferido; FOREIGN é texto livre (1–50 caracteres, sem formato fixo) |
| RN-07 | Campos de delivery/app-links aceitam null para limpar e undefined(omitido) para manter | Não confundir "não enviei" com "quero apagar" |
| RN-08 | Um tenant inacessível ao ator é reportado como 404, nunca 403 | GET /tenants/:tenantId não revela a existência de tenants fora do escopo |
| RN-09 | Um ator com acesso irrestrito (ex. platform-admin) não sofre filtro de tenant na listagem | Vê todos os tenants ativos |
| RN-10 | Inativar/reativar não tem cascata sobre unidades, PACS ou módulos do tenant | Só o próprio tenant muda de estado; um evento de auditoria é publicado para quem consumir de forma assíncrona |
| RN-11 | Reativar um tenant já ativo é idempotente | Não erra, não altera nada |
| RN-12 | Reativar pode reintroduzir conflito de unicidade | Se slug/document foram "roubados" por outro tenant ativo enquanto este estava inativo, a reativação falha com 409 até o conflito ser resolvido |
| RN-13 | Criar um tenant não é uma operação exposta por este módulo via HTTP | O provisionamento é interno, chamado pelo fluxo de registration/onboarding; exige ser platform-admin (assertCanManageTenants, sem alternativa de permission:manage, porque o tenant ainda não existe) |
Variáveis de ambiente
Nenhuma variável de ambiente controla estas rotas diretamente.
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 | PATCH: não (mesmo payload gera novo version, mas sem efeito colateral extra). inactivate: não (chamar duas vezes na 2ª dá 404). reactivate: sim |
| Paginação | GET /tenants: sim, page/limit (máx. 100 por página) |
| Rate limit | Não observado |
| Cache | Não |
| Auditoria | Sim — evento de outbox em PATCH (antes/depois só dos campos alterados), inactivate e reactivate |
Relacionado
- 🖥️ Tela:
A confirmar — responsável: time de frontend; data: 24/09/2026.(não mapeada neste levantamento) - 📂 Módulo: Tenant / Organização
- 🔗 Unidades: Unidades — cada tenant agrupa suas unidades