Skip to main content

Comentar exame — API

Permite a um usuário autorizado registrar um comentário de texto livre em um exame, listar os comentários existentes, ler um comentário específico e apagar um comentário. O comentário não carrega intenção clínica: ele não lê nem altera as flags de achado crítico ou suspeita técnica do exame — isso é feito por rotas próprias, ver Achado crítico e suspeita técnica.

Funcionamento​

Criar (POST /exams/:examId/comments):

  1. Autentica o access token (JwtAuthenticationGuard) e autoriza a permissão exam:add-comment com escopo RLS sobre o examId do path — sem acesso ao exame, a resposta é 404 (o recurso é ocultado, não apenas negado).
  2. Valida o corpo: description é obrigatória, string, sem campos extras aceitos (o DTO rejeita qualquer campo além de description, incluindo tentativas de mandar isCriticalFinding ou isTechnicalSuspicion — confirmado no teste de ponta a ponta).
  3. Normaliza a descrição (trim) e valida o tamanho (1 a 4096 caracteres após o trim); uma descrição vazia ou maior que o limite é rejeitada.
  4. Busca o exame para obter a unidade (unitId) e grava o comentário com o autor sendo sempre o usuário autenticado do token — o cliente nunca informa o autor.
  5. Registra um evento de auditoria (diagnosis.exam.comment-added) e agenda a reprojeção do item de worklist do exame (o contador de comentários do worklist é recalculado de forma assíncrona, não fica nesta resposta).

Listar (GET /exams/:examId/comments): devolve os comentários ativos do exame, mais recentes primeiro, paginados, cada um já com o perfil do autor resolvido (nome, usuario_oauth_id e URL da foto, quando disponíveis).

Ler um comentário (GET /exam-comments/:id): devolve um único comentário pelo seu próprio identificador (não pelo exame).

Apagar (DELETE /exam-comments/:id): soft delete. Só o autor do comentário ou um administrador de plataforma pode apagar; qualquer outro usuário recebe 403, mesmo tendo permissão de exclusão de comentários na unidade. Apagar registra diagnosis.exam.comment-removed e também agenda a reprojeção do worklist.

Endpoints​

MétodoRotaDescrição
POST/v1/exams/:examId/commentsCria um comentário no exame
GET/v1/exams/:examId/commentsLista os comentários do exame (paginado)
GET/v1/exam-comments/:idLê um comentário específico
DELETE/v1/exam-comments/:idApaga (soft delete) um comentário

Versão: v1

Swagger: tag Diagnosis — Exam comments · Rota (Dev): http://localhost:3000/v1/exams/{examId}/comments

Lógica de decisão das quatro rotas:

Permissões​

RotaGuardsPermissão / regra adicional
POST /exams/:examId/commentsJwtAuthenticationGuard, AuthorizationGuardexam:add-comment, escopo RLS pelo examId do path
GET /exams/:examId/commentsJwtAuthenticationGuard, AuthorizationGuardexam:list-comment, escopo RLS pelo examId do path
GET /exam-comments/:idJwtAuthenticationGuard, AuthorizationGuardexam:list-comment, escopo RLS pelo id do comentário (resolve a unidade do comentário)
DELETE /exam-comments/:idJwtAuthenticationGuard, AuthorizationGuardexam:delete-comment, escopo RLS pelo id e checagem adicional na camada de serviço: só o autor (userId do comentário == usuário do token) ou um usuário com isPlatformAdmin pode apagar — ter a permissão exam:delete-comment na unidade não é suficiente sozinho

Um usuário sem acesso à unidade do exame ou do comentário recebe 404 (não 403): o escopo RLS oculta a existência do recurso, confirmado no teste de ponta a ponta das flags clínicas (diagnosis-exam-clinical-flags.e2e.spec.ts, mesmo mecanismo de guard usado aqui).

Headers​

HeaderObrigatórioDescrição
AuthorizationSimBearer <access_token>
Content-TypeSim (em POST)application/json

Path parameters​

NomeTipoObrigatórioDescrição
examIduuidSim (POST/GET de listagem)Identificador do exame
iduuidSim (GET/DELETE de item)Identificador do comentário

Query parameters​

GET /exams/:examId/comments:

NomeTipoObrigatórioDefaultDescrição
pageintegerNão1Página (mínimo 1)
limitintegerNão20Itens por página (1 a 100)

Body​

POST /exams/:examId/comments — CreateDiagnosisExamCommentRequest:

json
{ "description": "Paciente com dor torácica, revisar contraste." }
CampoTipoObrigatórioValidação
descriptionstringSimtrim, não vazia, @MaxLength(4096). Qualquer outro campo no corpo é rejeitado pelo validador (whitelist estrita)

GET/DELETE não recebem corpo.

Response​

201 / 200 — DiagnosisExamCommentEnvelopeResponse:

json
{
"data": {
"id": "0196cf9a-9e32-7d69-a38d-7360192a811c",
"examId": "0196cf9a-9e32-7d69-a38d-7360192a811d",
"unitId": "0196cf9a-9e32-7d69-a38d-7360192a811e",
"userId": "0196cf9a-9e32-7d69-a38d-7360192a811f",
"description": "Paciente com dor torácica, revisar contraste.",
"isNew": true,
"createdAt": "2026-09-24T12:00:00.000Z",
"authorName": "Maria Souza",
"usuarioOauthId": "0196cf9a-9e32-7d69-a38d-7360192a811f",
"linkFotoPath": null
}
}

200 — GET /exams/:examId/comments (DiagnosisExamCommentListEnvelopeResponse): mesmo formato de item, dentro de data: [...].

204 — DELETE /exam-comments/:id: sem corpo.

O campo isNew é sempre gravado como true na criação; nenhum ponto do código deste pacote o altera para false depois (não existe uma rota de "marcar como lido"). A confirmar — responsável: time de Diagnóstico; data: 24/09/2026. — se isNew é consumido por outro lugar do front ou é um campo legado sem uso funcional atual.

Erros​

Classe de erroerrorCodeStatusQuando ocorre
(guard de autenticação)UNAUTHENTICATED401token ausente, inválido ou expirado
ForbiddenAction (autorização)FORBIDDEN_ACTION403sem a permissão exigida no escopo da unidade
ForbiddenAction (regra de exclusão)—403tentativa de apagar comentário de outro autor sem ser admin de plataforma
(validação de payload)BAD_REQUEST400corpo ausente, description vazia/maior que 4096 ou com campo não reconhecido
DiagnosisExamCommentInputInvalidErrorEXAM_COMMENT_INPUT_INVALID400entrada inválida detectada na camada de domínio (defesa redundante à validação do DTO)
DiagnosisExamNotFoundErrorEXAM_NOT_FOUND404exame do path não existe ou está fora do escopo do ator
DiagnosisExamCommentNotFoundErrorEXAM_COMMENT_NOT_FOUND404comentário não existe, já foi apagado, ou está fora do escopo do ator

Regras de negócio​

IDRegraComportamento esperado
RN-01Autor é sempre o usuário autenticadoo cliente não informa o autor; o servidor usa o userId do token
RN-02Comentário não carrega intenção clínicacriar um comentário nunca altera critical-finding nem technical-suspicion do exame (confirmado em teste unitário e em teste de ponta a ponta)
RN-03Descrição é normalizada e limitadatrim() primeiro, depois valida 1 a 4096 caracteres; só espaços em branco é tratado como vazio
RN-04Apenas o autor ou um admin de plataforma apagater a permissão exam:delete-comment na unidade não basta; a checagem de dono é feita na camada de domínio (DiagnosisExamCommentRecord.assertMayBeDeletedBy)
RN-05Exclusão é lógica (soft delete)o registro deixa de aparecer em listagens e buscas, mas não é removido fisicamente
RN-06Listagem vem ordenada por data de criação decrescentemais recente primeiro, sem opção de ordenação alternativa
RN-07Criar ou apagar um comentário reprojeta o worklist do exameo contador commentCount do item de worklist (GET /worklist) é recalculado de forma assíncrona; não é devolvido nesta resposta
RN-08Um comentário pode liberar um requisito de preparo do exameo fluxo de status do exame (workflow-policy) confere se existe ao menos um comentário ativo antes de permitir a transição de status configurada como exigindo comentário; detalhe de implementação fora do escopo desta página (ver assert-diagnosis-exam-preparation-complete.service.ts)

Compliance​

Órgão / normaExigênciaComo a rota atende
LGPDminimização; base legal de tratamento por profissional de saúdeo corpo aceita só a descrição; autor e datas são atribuídos pelo servidor, nunca pelo cliente
HIPAAtrilha de quem escreveu/apagou o quê e quandoeventos de auditoria diagnosis.exam.comment-added e diagnosis.exam.comment-removed, consultáveis via Audit
ANVISA (indireto)rastreabilidade de anotações clínicas no prontuárioexclusão é lógica (soft delete), preservando o histórico subjacente

Variáveis de ambiente​

Nenhuma variável de ambiente específica destas rotas foi identificada neste levantamento.

Tempo médio de resposta​

A confirmar — responsável: time de Diagnóstico; data: 24/09/2026. Não há medição publicada; não foi executado neste levantamento.

Requisitos não funcionais​

RequisitoDefinição
IdempotênciaNão — cada POST cria um novo comentário; DELETE de um comentário já apagado responde 404
PaginaçãoSim, em GET /exams/:examId/comments (page/limit, limite máximo 100 por página)
Rate limitNão identificado @Throttle específico nestas rotas neste levantamento
CacheNão
AuditoriaSim — diagnosis.exam.comment-added, diagnosis.exam.comment-removed

Divergências e lacunas confirmadas​

  • Tamanho máximo mudou. O levantamento do sistema legado (Bitrix, card F-001) registra o limite antigo de descrição como 512 caracteres (BR-CMT-001); o código atual valida até 4096 caracteres (CreateDiagnosisExamCommentRequest, CreateDiagnosisExamCommentInput). Trate a regra legada como superada pelo código.
  • Log em tabela própria de exame não existe mais. O legado gravava uma entrada em tb_exame_log a cada comentário (BR-CMT-020); o mecanismo atual equivalente é o evento de auditoria via outbox (diagnosis.exam.comment-added), com destino e formato diferentes.
  • Foto padrão do autor não confirmada. O legado preenchia uma foto padrão quando o autor não tinha foto (BR-CMT-083). No código atual, linkFotoPath é simplesmente null quando a foto não está disponível (DiagnosisExamCommentResponse); não há fallback visível nesta camada. A confirmar — responsável: time de Diagnóstico/Frontend; data: 24/09/2026.

Relacionado​

  • 🩺 Achado crítico e suspeita técnica — sinalização clínica independente do comentário
  • 📋 Audit — consultar logs — como ler diagnosis.exam.comment-added/comment-removed
  • 📂 Módulo: Comment
  • 🖥️ Tela: A confirmar — responsável: time de frontend; data: 24/09/2026. (fora do escopo deste levantamento, que cobriu apenas o backend)