Skip to main content

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étodoRotaDescrição
GET/v1/tenants/:tenantId/profilePerfil administrativo completo do tenant
PATCH/v1/tenants/:tenantId/profileAtualiza parcialmente o perfil (com versionamento otimista opcional)
GET/v1/tenantsLista tenants ativos visíveis ao ator, paginada e filtrável
GET/v1/tenants/:tenantIdResumo de um tenant ativo e visível ao ator
POST/v1/tenants/:tenantId/inactivateInativa o tenant (soft-delete)
POST/v1/tenants/:tenantId/reactivateReativa 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​

RotaGuardsPerfil / escopo exigido
GET .../profileJwtAuthenticationGuard, AuthorizationGuardtenant:read no tenant do path
PATCH .../profileidempermission:manage no tenant do path (checado duas vezes: guard HTTP + serviço)
GET /tenantsidemunit:read, escopo organization-list — administrador de plataforma vê todos; os demais só o que a política de escopo resolver
GET /tenants/:tenantIdidemunit:read no tenant do path — tenant fora do alcance do ator responde 404, não 403
POST .../inactivateidempermission:manage no tenant do path
POST .../reactivateJwtAuthenticationGuard, 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​

HeaderObrigatórioDescrição
AuthorizationSimBearer <access_token>
Content-TypeSim em PATCHapplication/json

Path parameters​

NomeTipoObrigatórioDescrição
tenantIdUUIDSimIdentificador do tenant

Query parameters​

GET /tenants:

NomeTipoObrigatórioDefaultDescrição
namestringNão—Filtro "contains" (case-insensitive) pelo nome
slugstringNão—Filtro "contains" pelo slug
documentstringNão—Filtro "contains" pelo documento
adminEmailstringNão—Filtro "contains" pelo e-mail do admin
sortstringNãoname ASCEx.: "name ASC,slug DESC"; só aceita os campos name e slug
pagenumberNão1Mínimo 1
limitnumberNão10Entre 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
}
CampoTipoObrigatórioValidação
namestringNão1–255 caracteres
slugstringNão3–100 caracteres; minúsculo, alfanumérico, hífens internos, não pode começar/terminar com hífen
adminEmailstringNãoe-mail válido, até 255 caracteres
documentstring | nullNãopar atômico com identificationType (os dois ou nenhum); dígito verificador validado para CPF/CNPJ; até 50 caracteres para FOREIGN
identificationTypeCNPJ | CPF | FOREIGN | nullNãover acima
phonestring | nullNãoaté 20 caracteres, formato de telefone
descriptionstring | nullNãoaté 2000 caracteres
timezoneIdstring | nullNãoidentificador IANA (ex. America/Sao_Paulo); precisa existir e estar ativo no catálogo de timezones
clientLabelstring | nullNãoaté 255 caracteres
deliveryUrlstring | nullNãoURL absoluta http/https, até 500 caracteres
externalViewerHoststring | nullNãohostname canônico, até 255 caracteres
androidAppUrl / iosAppUrlstring | nullNãoURL absoluta http/https, até 500 caracteres
expectedVersionnumberNãointeiro ≥ 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 erroerrorCodeStatusQuando ocorre
Erro de validação do Nest—400tenantId inválido; corpo com campo extra ou fora do formato dos decorators
ValidationErrorORGANIZATION_TENANT_PROFILE_UPDATE_INVALID400PATCH sem nenhum campo, ou document/identificationType enviados sem o par
ValidationErrorORGANIZATION_TENANT_PROFILE_INVALID400name/slug/document/phone/description/timezoneId em formato inválido
ValidationErrorORGANIZATION_DELIVERY_SETTINGS_INVALID400clientLabel/deliveryUrl/externalViewerHost inválidos
ValidationErrorORGANIZATION_TENANT_APP_LINKS_INVALID400androidAppUrl/iosAppUrl não são URL absoluta válida
ForbiddenActionORGANIZATION_TENANT_MANAGE_FORBIDDEN403ator sem permission:manage no tenant e sem ser platform-admin (PATCH, inactivate, reactivate)
ForbiddenAction (guard genérico)—403ator sem tenant:read/unit:read no tenant (rotas de leitura)
NotFoundException (Nest)—404tenant não existe, está inativo, ou está fora do alcance do ator (GET .../profile, GET /tenants/:tenantId)
OrganizationTenantNotFoundErrorORGANIZATION_TENANT_NOT_FOUND404tenant não existe (ou já está inativo, no caso de inactivate) — PATCH, inactivate, reactivate
OrganizationTimezoneNotFoundErrorORGANIZATION_TIMEZONE_NOT_FOUND404timezoneId novo não existe ou está inativo
OrganizationTenantAlreadyExistsErrorORGANIZATION_TENANT_SLUG_ALREADY_EXISTS409novo slug já usado por outro tenant ativo (PATCH, reactivate)
OrganizationTenantAlreadyExistsErrorORGANIZATION_TENANT_DOCUMENT_ALREADY_EXISTS409novo document já usado por outro tenant ativo (PATCH, reactivate)
OrganizationTenantVersionConflictErrorORGANIZATION_TENANT_VERSION_CONFLICT409expectedVersion enviado é diferente da versão atual (PATCH)

Regras de negócio​

IDRegraComportamento esperado
RN-01Perfil e listagem só enxergam tenants ativosTenant inativo é tratado como inexistente nas rotas de leitura
RN-02Versionamento otimista é opcional e vive no corpo (expectedVersion), não em headerSem esse campo, PATCH é last-write-wins
RN-03version só é incrementada por PATCH .../profileInativar e reativar não mudam a versão
RN-04Unicidade de slug/document é só entre tenants ativosUm tenant inativo "libera" seu slug/document para outro tenant ativo usar
RN-05document e identificationType formam um par atômicoSó podem ser alterados juntos: ambos presentes ou ambos null
RN-06Validação do document depende do identificationTypeCPF/CNPJ têm dígito verificador conferido; FOREIGN é texto livre (1–50 caracteres, sem formato fixo)
RN-07Campos de delivery/app-links aceitam null para limpar e undefined(omitido) para manterNão confundir "não enviei" com "quero apagar"
RN-08Um tenant inacessível ao ator é reportado como 404, nunca 403GET /tenants/:tenantId não revela a existência de tenants fora do escopo
RN-09Um ator com acesso irrestrito (ex. platform-admin) não sofre filtro de tenant na listagemVê todos os tenants ativos
RN-10Inativar/reativar não tem cascata sobre unidades, PACS ou módulos do tenantSó o próprio tenant muda de estado; um evento de auditoria é publicado para quem consumir de forma assíncrona
RN-11Reativar um tenant já ativo é idempotenteNão erra, não altera nada
RN-12Reativar pode reintroduzir conflito de unicidadeSe slug/document foram "roubados" por outro tenant ativo enquanto este estava inativo, a reativação falha com 409 até o conflito ser resolvido
RN-13Criar um tenant não é uma operação exposta por este módulo via HTTPO 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​

RequisitoDefinição
IdempotênciaPATCH: 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çãoGET /tenants: sim, page/limit (máx. 100 por página)
Rate limitNão observado
CacheNão
AuditoriaSim — 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