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):
- Autentica o access token (
JwtAuthenticationGuard) e autoriza a permissãoexam:add-commentcom escopo RLS sobre oexamIddo path — sem acesso ao exame, a resposta é 404 (o recurso é ocultado, não apenas negado). - Valida o corpo:
descriptioné obrigatória, string, sem campos extras aceitos (o DTO rejeita qualquer campo além dedescription, incluindo tentativas de mandarisCriticalFindingouisTechnicalSuspicion— confirmado no teste de ponta a ponta). - 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. - 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. - 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étodo | Rota | Descrição |
|---|---|---|
| POST | /v1/exams/:examId/comments | Cria um comentário no exame |
| GET | /v1/exams/:examId/comments | Lista os comentários do exame (paginado) |
| GET | /v1/exam-comments/:id | Lê um comentário específico |
| DELETE | /v1/exam-comments/:id | Apaga (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
| Rota | Guards | Permissão / regra adicional |
|---|---|---|
POST /exams/:examId/comments | JwtAuthenticationGuard, AuthorizationGuard | exam:add-comment, escopo RLS pelo examId do path |
GET /exams/:examId/comments | JwtAuthenticationGuard, AuthorizationGuard | exam:list-comment, escopo RLS pelo examId do path |
GET /exam-comments/:id | JwtAuthenticationGuard, AuthorizationGuard | exam:list-comment, escopo RLS pelo id do comentário (resolve a unidade do comentário) |
DELETE /exam-comments/:id | JwtAuthenticationGuard, AuthorizationGuard | exam: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
| Header | Obrigatório | Descrição |
|---|---|---|
Authorization | Sim | Bearer <access_token> |
Content-Type | Sim (em POST) | application/json |
Path parameters
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
examId | uuid | Sim (POST/GET de listagem) | Identificador do exame |
id | uuid | Sim (GET/DELETE de item) | Identificador do comentário |
Query parameters
GET /exams/:examId/comments:
| Nome | Tipo | Obrigatório | Default | Descrição |
|---|---|---|---|---|
page | integer | Não | 1 | Página (mínimo 1) |
limit | integer | Não | 20 | Itens por página (1 a 100) |
Body
POST /exams/:examId/comments — CreateDiagnosisExamCommentRequest:
json{ "description": "Paciente com dor torácica, revisar contraste." }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
description | string | Sim | trim, 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 erro | errorCode | Status | Quando ocorre |
|---|---|---|---|
| (guard de autenticação) | UNAUTHENTICATED | 401 | token ausente, inválido ou expirado |
ForbiddenAction (autorização) | FORBIDDEN_ACTION | 403 | sem a permissão exigida no escopo da unidade |
ForbiddenAction (regra de exclusão) | — | 403 | tentativa de apagar comentário de outro autor sem ser admin de plataforma |
| (validação de payload) | BAD_REQUEST | 400 | corpo ausente, description vazia/maior que 4096 ou com campo não reconhecido |
DiagnosisExamCommentInputInvalidError | EXAM_COMMENT_INPUT_INVALID | 400 | entrada inválida detectada na camada de domínio (defesa redundante à validação do DTO) |
DiagnosisExamNotFoundError | EXAM_NOT_FOUND | 404 | exame do path não existe ou está fora do escopo do ator |
DiagnosisExamCommentNotFoundError | EXAM_COMMENT_NOT_FOUND | 404 | comentário não existe, já foi apagado, ou está fora do escopo do ator |
Regras de negócio
| ID | Regra | Comportamento esperado |
|---|---|---|
| RN-01 | Autor é sempre o usuário autenticado | o cliente não informa o autor; o servidor usa o userId do token |
| RN-02 | Comentário não carrega intenção clínica | criar 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-03 | Descrição é normalizada e limitada | trim() primeiro, depois valida 1 a 4096 caracteres; só espaços em branco é tratado como vazio |
| RN-04 | Apenas o autor ou um admin de plataforma apaga | ter a permissão exam:delete-comment na unidade não basta; a checagem de dono é feita na camada de domínio (DiagnosisExamCommentRecord.assertMayBeDeletedBy) |
| RN-05 | Exclusão é lógica (soft delete) | o registro deixa de aparecer em listagens e buscas, mas não é removido fisicamente |
| RN-06 | Listagem vem ordenada por data de criação decrescente | mais recente primeiro, sem opção de ordenação alternativa |
| RN-07 | Criar ou apagar um comentário reprojeta o worklist do exame | o contador commentCount do item de worklist (GET /worklist) é recalculado de forma assíncrona; não é devolvido nesta resposta |
| RN-08 | Um comentário pode liberar um requisito de preparo do exame | o 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 / norma | Exigência | Como a rota atende |
|---|---|---|
| LGPD | minimização; base legal de tratamento por profissional de saúde | o corpo aceita só a descrição; autor e datas são atribuídos pelo servidor, nunca pelo cliente |
| HIPAA | trilha de quem escreveu/apagou o quê e quando | eventos 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ário | exclusã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
| Requisito | Definição |
|---|---|
| Idempotência | Não — cada POST cria um novo comentário; DELETE de um comentário já apagado responde 404 |
| Paginação | Sim, em GET /exams/:examId/comments (page/limit, limite máximo 100 por página) |
| Rate limit | Não identificado @Throttle específico nestas rotas neste levantamento |
| Cache | Não |
| Auditoria | Sim — 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_loga 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é simplesmentenullquando 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)