Pular para conteúdo

5 · Resultado do exame (Bundle transaction)

Sistema: Lab+ (Laboratório) · Recursos: Observation ×4, DiagnosticReport, ServiceRequest (atualização)

O laudo do hemograma envolve seis operações: criar as 4 Observation (uma por analito), criar o DiagnosticReport que as referencia e concluir o ServiceRequest do pedido. O Lab+ envia tudo em um único POST com um Bundle do tipo transaction: o servidor aplica as seis operações atomicamente — ou tudo entra, ou nada entra.

Gravação — POST / (raiz) com Bundle transaction

curl -X POST 'http://localhost:8080/fhir' \
  -H 'Content-Type: application/fhir+json' \
  -d @resultado.json

Pontos-chave do bundle:

  • Cada Observation recebe um fullUrl temporário urn:uuid:…;
  • O DiagnosticReport.result referencia esses urn:uuid — o servidor reescreve as referências para os ids definitivos (Observation/91, …);
  • O basedOn aponta o ServiceRequest já existente, por referência literal;
  • A última entry é um PUT que conclui o pedido (status: completed), preservando a meta.tag original do iSUS APS (quem atualiza recurso alheio não troca a tag do criador).
resultado.json (valores da demonstração — dengue grave)
{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:c1f4a6d0-93b1-4f52-8a41-000000000001",
      "resource": {
        "resourceType": "Observation",
        "status": "final",
        "category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory", "display": "Laboratory"}]}],
        "code": {
          "coding": [{"system": "http://loinc.org", "code": "718-7", "display": "Hemoglobina"}],
          "text": "Hemoglobina"
        },
        "subject": {"reference": "Patient/42"},
        "effectiveDateTime": "2026-09-10T10:12:00-03:00",
        "valueQuantity": {"value": 14.2, "unit": "g/dL", "system": "http://unitsofmeasure.org", "code": "g/dL"},
        "referenceRange": [{
          "low": {"value": 13.2, "unit": "g/dL", "system": "http://unitsofmeasure.org", "code": "g/dL"},
          "high": {"value": 16.6, "unit": "g/dL", "system": "http://unitsofmeasure.org", "code": "g/dL"}
        }],
        "meta": {"tag": [{"system": "https://demo-cbis.example/fhir/source-system", "code": "labmais", "display": "Lab+"}]}
      },
      "request": {"method": "POST", "url": "Observation"}
    },
    {
      "fullUrl": "urn:uuid:c1f4a6d0-93b1-4f52-8a41-000000000002",
      "resource": {
        "resourceType": "Observation",
        "status": "final",
        "category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory", "display": "Laboratory"}]}],
        "code": {
          "coding": [{"system": "http://loinc.org", "code": "4544-3", "display": "Hematócrito"}],
          "text": "Hematócrito"
        },
        "subject": {"reference": "Patient/42"},
        "effectiveDateTime": "2026-09-10T10:12:00-03:00",
        "valueQuantity": {"value": 48.0, "unit": "%", "system": "http://unitsofmeasure.org", "code": "%"},
        "referenceRange": [{
          "low": {"value": 38.8, "unit": "%", "system": "http://unitsofmeasure.org", "code": "%"},
          "high": {"value": 50.0, "unit": "%", "system": "http://unitsofmeasure.org", "code": "%"}
        }],
        "meta": {"tag": [{"system": "https://demo-cbis.example/fhir/source-system", "code": "labmais", "display": "Lab+"}]}
      },
      "request": {"method": "POST", "url": "Observation"}
    },
    {
      "fullUrl": "urn:uuid:c1f4a6d0-93b1-4f52-8a41-000000000003",
      "resource": {
        "resourceType": "Observation",
        "status": "final",
        "category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory", "display": "Laboratory"}]}],
        "code": {
          "coding": [{"system": "http://loinc.org", "code": "6690-2", "display": "Leucócitos"}],
          "text": "Leucócitos"
        },
        "subject": {"reference": "Patient/42"},
        "effectiveDateTime": "2026-09-10T10:12:00-03:00",
        "valueQuantity": {"value": 2.1, "unit": "10*3/uL", "system": "http://unitsofmeasure.org", "code": "10*3/uL"},
        "referenceRange": [{
          "low": {"value": 4.0, "unit": "10*3/uL", "system": "http://unitsofmeasure.org", "code": "10*3/uL"},
          "high": {"value": 11.0, "unit": "10*3/uL", "system": "http://unitsofmeasure.org", "code": "10*3/uL"}
        }],
        "interpretation": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", "code": "L", "display": "Low"}]}],
        "meta": {"tag": [{"system": "https://demo-cbis.example/fhir/source-system", "code": "labmais", "display": "Lab+"}]}
      },
      "request": {"method": "POST", "url": "Observation"}
    },
    {
      "fullUrl": "urn:uuid:c1f4a6d0-93b1-4f52-8a41-000000000004",
      "resource": {
        "resourceType": "Observation",
        "status": "final",
        "category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory", "display": "Laboratory"}]}],
        "code": {
          "coding": [{"system": "http://loinc.org", "code": "777-3", "display": "Plaquetas"}],
          "text": "Plaquetas"
        },
        "subject": {"reference": "Patient/42"},
        "effectiveDateTime": "2026-09-10T10:12:00-03:00",
        "valueQuantity": {"value": 42.0, "unit": "10*3/uL", "system": "http://unitsofmeasure.org", "code": "10*3/uL"},
        "referenceRange": [{
          "low": {"value": 150.0, "unit": "10*3/uL", "system": "http://unitsofmeasure.org", "code": "10*3/uL"},
          "high": {"value": 450.0, "unit": "10*3/uL", "system": "http://unitsofmeasure.org", "code": "10*3/uL"}
        }],
        "interpretation": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", "code": "L", "display": "Low"}]}],
        "meta": {"tag": [{"system": "https://demo-cbis.example/fhir/source-system", "code": "labmais", "display": "Lab+"}]}
      },
      "request": {"method": "POST", "url": "Observation"}
    },
    {
      "fullUrl": "urn:uuid:c1f4a6d0-93b1-4f52-8a41-000000000005",
      "resource": {
        "resourceType": "DiagnosticReport",
        "status": "final",
        "category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v2-0074", "code": "LAB", "display": "Laboratory"}]}],
        "code": {
          "coding": [{"system": "http://loinc.org", "code": "58410-2", "display": "Hemograma completo"}],
          "text": "Hemograma completo"
        },
        "subject": {"reference": "Patient/42"},
        "basedOn": [{"reference": "ServiceRequest/10"}],
        "effectiveDateTime": "2026-09-10T10:12:00-03:00",
        "issued": "2026-09-10T10:12:00-03:00",
        "performer": [{"display": "Lab+ Análises Clínicas"}],
        "result": [
          {"reference": "urn:uuid:c1f4a6d0-93b1-4f52-8a41-000000000001", "display": "Hemoglobina"},
          {"reference": "urn:uuid:c1f4a6d0-93b1-4f52-8a41-000000000002", "display": "Hematócrito"},
          {"reference": "urn:uuid:c1f4a6d0-93b1-4f52-8a41-000000000003", "display": "Leucócitos"},
          {"reference": "urn:uuid:c1f4a6d0-93b1-4f52-8a41-000000000004", "display": "Plaquetas"}
        ],
        "conclusion": "Plaquetopenia acentuada, leucopenia e hemoconcentração — achados compatíveis com dengue grave.",
        "meta": {"tag": [{"system": "https://demo-cbis.example/fhir/source-system", "code": "labmais", "display": "Lab+"}]}
      },
      "request": {"method": "POST", "url": "DiagnosticReport"}
    },
    {
      "resource": {
        "resourceType": "ServiceRequest",
        "id": "10",
        "status": "completed",
        "intent": "order",
        "category": [{"coding": [{"system": "https://demo-cbis.example/fhir/category", "code": "laboratorio", "display": "Exame laboratorial"}]}],
        "code": {
          "coding": [{"system": "http://loinc.org", "code": "58410-2", "display": "Hemograma completo"}],
          "text": "Hemograma completo"
        },
        "subject": {"reference": "Patient/42"},
        "encounter": {"reference": "Encounter/7"},
        "authoredOn": "2026-09-10T09:35:00-03:00",
        "requester": {"display": "UBS Vila Esperança — iSUS APS"},
        "meta": {"tag": [{"system": "https://demo-cbis.example/fhir/source-system", "code": "isus-aps", "display": "iSUS APS"}]}
      },
      "request": {"method": "PUT", "url": "ServiceRequest/10"}
    }
  ]
}

Resposta — Bundle transaction-response

O servidor responde uma entry por operação, na mesma ordem do envio (exigência do FHIR R4), com o location de cada recurso:

{
  "resourceType": "Bundle",
  "type": "transaction-response",
  "entry": [
    {"response": {"status": "201 Created", "location": "Observation/91/_history/1"}},
    {"response": {"status": "201 Created", "location": "Observation/92/_history/1"}},
    {"response": {"status": "201 Created", "location": "Observation/93/_history/1"}},
    {"response": {"status": "201 Created", "location": "Observation/94/_history/1"}},
    {"response": {"status": "201 Created", "location": "DiagnosticReport/95/_history/1"}},
    {"response": {"status": "200 OK", "location": "ServiceRequest/10/_history/2"}}
  ]
}

Ao gravar, o servidor reescreveu o result do laudo: quem ler DiagnosticReport/95 encontrará {"reference": "Observation/91", "display": "Hemoglobina"}, e assim por diante. Se qualquer entry falhar (validação, conflito), nenhum recurso é gravado — o pedido segue active na worklist e o laboratório pode tentar de novo.

Leitura — laudos emitidos

O painel "Laudos emitidos" do Lab+ filtra pela tag de origem:

curl 'http://localhost:8080/fhir/DiagnosticReport?_tag=https://demo-cbis.example/fhir/source-system|labmais&_include=DiagnosticReport:subject&_sort=-issued'

O iSUS APS enxerga o mesmo laudo pela busca por paciente (passo 3), casando basedOn → ServiceRequest/10. A vigilância (CIMAN, passo 6) busca o laudo mais recente do paciente.

Destino alternativo — barramento do Showcase SBIS/IPES

Além de gravar neste showcase, o Lab+ pode publicar o mesmo laudo estruturado no barramento do Showcase de Interoperabilidade 2026 (SBIS/IPES) — um ambiente externo, mantido pela SBIS/IPES, que valida o Bundle contra o perfil Laboratory do IG do IPES. O campo Destino do laudo do formulário (as três opções ficam sempre habilitadas) decide o que acontece:

Destino O que é gravado Situação do pedido
Este showcase (padrão) Bundle transaction no HAPI local, como nas seções acima Conclui (status: completed)
Barramento SBIS/IPES Bundle transaction só no barramento SBIS/IPES — nada é gravado no HAPI local Continua active na worklist
Ambos Envia ao SBIS/IPES e, confirmado, grava também neste showcase Conclui se as duas gravações derem certo; se só o SBIS for confirmado, continua active

O destino Este showcase nunca chama o barramento SBIS.

Antes de enviar, o Lab+ obtém um token OAuth2 e confirma que o barramento reconhece o paciente:

# 1. Token OAuth2 (client_credentials, Basic com clientId:clientSecret)
curl -X POST 'https://showcase-interop.sbis.org.br/token' \
  -H 'Authorization: Basic <base64(clientId:clientSecret)>' \
  -d 'grant_type=client_credentials'

# 2. Pré-checagem: o MPI do barramento conhece este CPF?
curl 'https://showcase-interop.sbis.org.br/mpi/fhir/1.0.1/Patient/2.16.840.1.113883.13.237-<cpf>' \
  -H 'Authorization: Bearer <token>'

A pré-checagem é uma regra do próprio barramento, não do Lab+: o MPI do SBIS recusa CPFs que não reconhece — 404 com OperationOutcome, o caso do paciente de demonstração desta jornada (CPF 00000000000) — mas cria na hora o Patient de qualquer CPF válido (200), mesmo que ele nunca tenha sido cadastrado antes. Quando o MPI recusa, o Lab+ interrompe o envio (nada é gravado) e pede para refazer com um paciente de CPF válido, ou publicar só neste showcase.

Use somente o CPF de teste — nunca o de uma pessoa real

Para demonstrar este destino, cadastre no iSUS um paciente com o CPF de teste do participante, 75124076100 (veja o README.md da raiz do repositório). Nunca use o CPF de uma pessoa real: como o MPI materializa qualquer CPF válido na hora, o laudo fictício ficaria associado a essa pessoa real no ambiente do SBIS, com as credenciais da SES-GO.

Confirmado o paciente, o Lab+ envia o Bundle:

curl -X POST 'https://showcase-interop.sbis.org.br/ehrposter/fhir/1.0.0' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/fhir+json' \
  -d @resultado-sbis.json

Esse Bundle transaction, marcado com o perfil Laboratory do IG do IPES (meta.profile), leva o mesmo conteúdo clínico do bundle gravado neste showcase: os dois saem do mesmo Laudo (apps/labmais/backend/app/laudo.py), montado uma vez por publicação. Valores, unidades, faixas de referência, conclusão, a data do pedido (authoredOn) e a do resultado (effectiveDateTime e issued, o mesmo instante nos dois lados) são idênticos — o teste tests/test_pareamento.py compara os dois bundles campo a campo.

O que muda é só o envelope, por exigência de cada barramento:

Aspecto Este showcase (HAPI local) Barramento SBIS/IPES
ServiceRequest PUT no SR já existente, criado pelo iSUS APS (conclui o pedido) POST de um SR próprio do Lab+ — categoria SADT e código TUSS 403043018, exigidos pelo modelo validado
Motivo do pedido fica no SR original copiado do pedido original: o texto e, quando o solicitante informa, o CID-10 dele. Sem motivo no pedido, o campo não é enviado
Quem pediu fica no SR original requester copiado como veio — pelo nome ou pelo CNS, no formato dos pedidos que chegam do IPES
Código do exame LOINC 58410-2 (Hemograma completo) TUSS 403043018, no SR e no DiagnosticReport
Paciente Patient/{id} local Patient/2.16.840.1.113883.13.237-<cpf> — referência lógica; o recurso Patient não é enviado, só um identifier de CPF
Identificadores nenhum além da meta.tag de origem identifier enviado com system urn:oid:<OID> e value <valor> (ex.: labmais-10-20260916103000) no DiagnosticReport, e <valor>-sr no ServiceRequest; o barramento atribui ao recurso o id <OID>.<valor> — é esse id que aparece no location da resposta e no toast do Lab+; sem meta.tag
Faixa de referência referenceRange nas Observations igual — referenceRange nas Observations
Flag do resultado interpretation (L/H) nas Observations sem flag: com interpretation o barramento responde HTTP 500 ao montar o laudo — "Exception evaluating OGNL expression: result.resource.interpretation.coding[0].display (template: diagnosticreport)". O template do IPES lê interpretation como objeto, mas no FHIR R4 é lista. Sondado em 21/09/2026 (integracao/respostas/20260921-14331[56]-labstrc-*.json); a faixa de referência supre a leitura
Origem do registro meta.tag source-system = labmais sem meta.tag: lá a origem é o participante autenticado
Ordem da resposta preserva a ordem das entries enviadas não preserva a ordem — o laudo é localizado pelo tipo de recurso (DiagnosticReport) no location, não pela posição

Por que o authoredOn importa

Sem ServiceRequest.authoredOn, a solicitação aparece como "Data não informada" na linha do tempo do barramento SBIS. O Lab+ envia ali a data do pedido (a que o iSUS APS gravou), não a hora em que o resultado saiu.

O perfil Laboratory descreve o laudo em PDF

O Laboratory do IG exige exatamente três entries (ServiceRequest, DiagnosticReport e Binary), e o DiagnosticReportPresentedForm proíbe result, conclusion e effective[x]; o ObservationBRIPES proíbe referenceRange e interpretation. O modelo estruturado não tem perfil publicado, e o barramento o aceita assim mesmo — a validação do ehrposter checa o formato do identifier, não o StructureDefinition. O que vale aqui é o que foi sondado e aceito; se o IPES apertar a validação, o caminho é pedir o perfil do modelo estruturado ao helpdesk.

Quando o destino é Ambos, o Lab+ envia primeiro ao barramento SBIS e só grava neste showcase depois de confirmado — a única ordem em que uma falha parcial é recuperável: se este showcase fosse gravado primeiro, o pedido sairia da worklist (completed) e não haveria mais como reenviar ao SBIS.

Situação × HTTP × o que foi gravado

Situação HTTP O que foi gravado
Entrada inválida (limites dos analitos, conclusão vazia ou acima de 1000 caracteres, destino desconhecido) 422 Nada
Pedido que não é hemograma completo 422 Nada
Paciente sem CPF, ou CPF não reconhecido pelo MPI do SBIS 422 Nada
Recusa do SBIS (4xx com OperationOutcome) 502 Nada
SBIS inacessível, sem resposta, erro 5xx ou falha ao obter o token 503 Nada neste showcase — a mensagem inclui "confira no Portal Clínico" quando o envio pode ter chegado ao barramento
Destino Ambos, com o SBIS confirmado e este showcase indisponível 200, com aviso Só no SBIS; o pedido continua pendente nesta worklist
SBIS aceitou o envio mas não devolveu o id do laudo na resposta 200, com aviso para conferir Conforme o destino (SBIS e, se Ambos, também este showcase)

Laudo só no SBIS não aparece neste showcase

Um laudo publicado apenas no destino Barramento SBIS/IPES não passa pelo HAPI local desta demonstração — por isso ele não aparece no painel "Laudos emitidos" do Lab+, na linha do tempo do Hub Saúde nem no histórico do Meu PEP (todos leem exclusivamente o HAPI local). Ele fica disponível no Portal Clínico do Showcase SBIS/IPES.